diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApi.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApi.java
index c132d629db1578cd4e0c1f75b912de0c393e7abc..80da12b7918488a23e321af4fe5e0f6888d2a70c 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApi.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApi.java
@@ -104,6 +104,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ExperimentSearchCrit
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.MaterialSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ProjectSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SampleSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SearchResult;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SpaceSearchCriterion;
 import ch.systemsx.cisd.openbis.common.spring.IInvocationLoggerContext;
 import ch.systemsx.cisd.openbis.generic.server.AbstractServer;
@@ -420,7 +421,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Override
     @Transactional(readOnly = true)
     @RolesAllowed({ RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<Space> searchSpaces(String sessionToken, SpaceSearchCriterion searchCriterion, SpaceFetchOptions fetchOptions)
+    public SearchResult<Space> searchSpaces(String sessionToken, SpaceSearchCriterion searchCriterion, SpaceFetchOptions fetchOptions)
     {
         return searchSpaceExecutor.search(sessionToken, searchCriterion, fetchOptions);
     }
@@ -428,7 +429,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Override
     @Transactional(readOnly = true)
     @RolesAllowed({ RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<Project> searchProjects(String sessionToken, ProjectSearchCriterion searchCriterion, ProjectFetchOptions fetchOptions)
+    public SearchResult<Project> searchProjects(String sessionToken, ProjectSearchCriterion searchCriterion, ProjectFetchOptions fetchOptions)
     {
         return searchProjectExecutor.search(sessionToken, searchCriterion, fetchOptions);
     }
@@ -436,7 +437,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Override
     @Transactional(readOnly = true)
     @RolesAllowed({ RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<Experiment> searchExperiments(String sessionToken, ExperimentSearchCriterion searchCriterion,
+    public SearchResult<Experiment> searchExperiments(String sessionToken, ExperimentSearchCriterion searchCriterion,
             ExperimentFetchOptions fetchOptions)
     {
         return searchExperimentExecutor.search(sessionToken, searchCriterion, fetchOptions);
@@ -445,7 +446,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Override
     @Transactional(readOnly = true)
     @RolesAllowed({ RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<Sample> searchSamples(String sessionToken, SampleSearchCriterion searchCriterion, SampleFetchOptions fetchOptions)
+    public SearchResult<Sample> searchSamples(String sessionToken, SampleSearchCriterion searchCriterion, SampleFetchOptions fetchOptions)
     {
         return searchSampleExecutor.search(sessionToken, searchCriterion, fetchOptions);
     }
@@ -453,7 +454,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Override
     @Transactional(readOnly = true)
     @RolesAllowed({ RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<DataSet> searchDataSets(String sessionToken, DataSetSearchCriterion searchCriterion, DataSetFetchOptions fetchOptions)
+    public SearchResult<DataSet> searchDataSets(String sessionToken, DataSetSearchCriterion searchCriterion, DataSetFetchOptions fetchOptions)
     {
         return searchDataSetExecutor.search(sessionToken, searchCriterion, fetchOptions);
     }
@@ -461,7 +462,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Override
     @Transactional(readOnly = true)
     @RolesAllowed({ RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<Material> searchMaterials(String sessionToken, MaterialSearchCriterion searchCriterion, MaterialFetchOptions fetchOptions)
+    public SearchResult<Material> searchMaterials(String sessionToken, MaterialSearchCriterion searchCriterion, MaterialFetchOptions fetchOptions)
     {
         return searchMaterialExecutor.search(sessionToken, searchCriterion, fetchOptions);
     }
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApiLogger.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApiLogger.java
index 8e048d6c1fa3dcd6a0d90288f713208d2e7d702a..d5e66546597e9643375809791f48d5a8e20deef9 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApiLogger.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApiLogger.java
@@ -68,6 +68,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ExperimentSearchCrit
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.MaterialSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ProjectSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SampleSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SearchResult;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SpaceSearchCriterion;
 import ch.systemsx.cisd.authentication.ISessionManager;
 import ch.systemsx.cisd.openbis.common.spring.IInvocationLoggerContext;
@@ -226,42 +227,43 @@ public class ApplicationServerApiLogger extends AbstractServerLogger implements
     }
 
     @Override
-    public List<Space> searchSpaces(String sessionToken, SpaceSearchCriterion searchCriterion, SpaceFetchOptions fetchOptions)
+    public SearchResult<Space> searchSpaces(String sessionToken, SpaceSearchCriterion searchCriterion, SpaceFetchOptions fetchOptions)
     {
         logAccess(sessionToken, "search-for-spaces", "SEARCH_CRITERION:\n%s\nFETCH_OPTIONS:\n%s\n", searchCriterion, fetchOptions);
         return null;
     }
 
     @Override
-    public List<Project> searchProjects(String sessionToken, ProjectSearchCriterion searchCriterion, ProjectFetchOptions fetchOptions)
+    public SearchResult<Project> searchProjects(String sessionToken, ProjectSearchCriterion searchCriterion, ProjectFetchOptions fetchOptions)
     {
         logAccess(sessionToken, "search-for-projects", "SEARCH_CRITERION:\n%s\nFETCH_OPTIONS:\n%s\n", searchCriterion, fetchOptions);
         return null;
     }
 
     @Override
-    public List<Experiment> searchExperiments(String sessionToken, ExperimentSearchCriterion searchCriterion, ExperimentFetchOptions fetchOptions)
+    public SearchResult<Experiment> searchExperiments(String sessionToken, ExperimentSearchCriterion searchCriterion,
+            ExperimentFetchOptions fetchOptions)
     {
         logAccess(sessionToken, "search-for-experiments", "SEARCH_CRITERION:\n%s\nFETCH_OPTIONS:\n%s\n", searchCriterion, fetchOptions);
         return null;
     }
 
     @Override
-    public List<Sample> searchSamples(String sessionToken, SampleSearchCriterion searchCriterion, SampleFetchOptions fetchOptions)
+    public SearchResult<Sample> searchSamples(String sessionToken, SampleSearchCriterion searchCriterion, SampleFetchOptions fetchOptions)
     {
         logAccess(sessionToken, "search-for-samples", "SEARCH_CRITERION:\n%s\nFETCH_OPTIONS:\n%s\n", searchCriterion, fetchOptions);
         return null;
     }
 
     @Override
-    public List<DataSet> searchDataSets(String sessionToken, DataSetSearchCriterion searchCriterion, DataSetFetchOptions fetchOptions)
+    public SearchResult<DataSet> searchDataSets(String sessionToken, DataSetSearchCriterion searchCriterion, DataSetFetchOptions fetchOptions)
     {
         logAccess(sessionToken, "search-for-data-sets", "SEARCH_CRITERION:\n%s\nFETCH_OPTIONS:\n%s\n", searchCriterion, fetchOptions);
         return null;
     }
 
     @Override
-    public List<Material> searchMaterials(String sessionToken, MaterialSearchCriterion searchCriterion, MaterialFetchOptions fetchOptions)
+    public SearchResult<Material> searchMaterials(String sessionToken, MaterialSearchCriterion searchCriterion, MaterialFetchOptions fetchOptions)
     {
         logAccess(sessionToken, "search-for-materials", "SEARCH_CRITERION:\n%s\nFETCH_OPTIONS:\n%s\n", searchCriterion, fetchOptions);
         return null;
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/method/AbstractSearchMethodExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/method/AbstractSearchMethodExecutor.java
index cf677f9e0f8e70991fb6e35126a7f303fb5e0b52..b1d73886dbf7f9e4b09be6d1c473e420e052025f 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/method/AbstractSearchMethodExecutor.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/method/AbstractSearchMethodExecutor.java
@@ -17,51 +17,152 @@
 package ch.ethz.sis.openbis.generic.server.api.v3.executor.method;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.Cache;
+import org.springframework.cache.Cache.ValueWrapper;
+import org.springframework.cache.CacheManager;
+
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.IOperationContext;
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.common.ISearchObjectExecutor;
 import ch.ethz.sis.openbis.generic.server.api.v3.translator.ITranslator;
 import ch.ethz.sis.openbis.generic.server.api.v3.translator.TranslationContext;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.CacheMode;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.FetchOptions;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.FetchOptionsMatcher;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.sort.SortAndPage;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.AbstractObjectSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SearchResult;
 
 /**
  * @author pkupczyk
  */
-public abstract class AbstractSearchMethodExecutor<OBJECT, OBJECT_PE, CRITERION extends AbstractObjectSearchCriterion<?>, FETCH_OPTIONS> extends
-        AbstractMethodExecutor implements ISearchMethodExecutor<OBJECT, CRITERION, FETCH_OPTIONS>
+public abstract class AbstractSearchMethodExecutor<OBJECT, OBJECT_PE, CRITERION extends AbstractObjectSearchCriterion<?>, FETCH_OPTIONS extends FetchOptions<OBJECT>>
+        extends AbstractMethodExecutor implements ISearchMethodExecutor<OBJECT, CRITERION, FETCH_OPTIONS>
 {
 
+    @Autowired
+    private CacheManager cacheManager;
+
+    protected abstract ISearchObjectExecutor<CRITERION, OBJECT_PE> getSearchExecutor();
+
+    protected abstract ITranslator<OBJECT_PE, OBJECT, FETCH_OPTIONS> getTranslator();
+
     @Override
-    public List<OBJECT> search(final String sessionToken, final CRITERION criterion, final FETCH_OPTIONS fetchOptions)
+    public SearchResult<OBJECT> search(final String sessionToken, final CRITERION criterion, final FETCH_OPTIONS fetchOptions)
     {
-        return executeInContext(sessionToken, new IMethodAction<List<OBJECT>>()
+        return executeInContext(sessionToken, new IMethodAction<SearchResult<OBJECT>>()
             {
                 @Override
-                public List<OBJECT> execute(IOperationContext context)
+                public SearchResult<OBJECT> execute(IOperationContext context)
                 {
-                    List<OBJECT_PE> results = getSearchExecutor().search(context, criterion);
-                    return translate(context, results, fetchOptions);
+                    Collection<OBJECT> allResults = searchAndTranslate(context, criterion, fetchOptions);
+                    List<OBJECT> sortedAndPaged = sortAndPage(context, allResults, fetchOptions);
+                    return new SearchResult<OBJECT>(sortedAndPaged, allResults.size());
                 }
             });
     }
 
-    private List<OBJECT> translate(IOperationContext context, List<OBJECT_PE> peList, FETCH_OPTIONS fetchOptions)
+    @SuppressWarnings("unchecked")
+    private Collection<OBJECT> searchAndTranslate(IOperationContext context, CRITERION criterion, FETCH_OPTIONS fetchOptions)
     {
-        if (peList == null || peList.isEmpty())
+        if (CacheMode.NO_CACHE.equals(fetchOptions.getCacheMode()))
         {
-            return Collections.emptyList();
+            return doSearchAndTranslate(context, criterion, fetchOptions);
+        } else if (CacheMode.CACHE.equals(fetchOptions.getCacheMode()) || CacheMode.RELOAD_AND_CACHE.equals(fetchOptions.getCacheMode()))
+        {
+            Cache cache = cacheManager.getCache("searchCache");
+            CacheKey key = new CacheKey(context.getSession().getSessionToken(), fetchOptions);
+            Collection<OBJECT> results = null;
+
+            if (CacheMode.RELOAD_AND_CACHE.equals(fetchOptions.getCacheMode()))
+            {
+                cache.evict(key);
+            } else
+            {
+                ValueWrapper wrapper = cache.get(key);
+                if (wrapper != null)
+                {
+                    results = (Collection<OBJECT>) wrapper.get();
+                }
+            }
+
+            if (results == null)
+            {
+                results = doSearchAndTranslate(context, criterion, fetchOptions);
+                cache.put(key, results);
+            }
+
+            return results;
+        } else
+        {
+            throw new IllegalArgumentException("Unsupported cache mode: " + fetchOptions.getCacheMode());
         }
+    }
 
+    private Collection<OBJECT> doSearchAndTranslate(IOperationContext context, CRITERION criterion, FETCH_OPTIONS fetchOptions)
+    {
+        List<OBJECT_PE> ids = getSearchExecutor().search(context, criterion);
         TranslationContext translationContext = new TranslationContext(context.getSession());
-        Map<OBJECT_PE, OBJECT> peToObjectMap = getTranslator().translate(translationContext, peList, fetchOptions);
-        return new ArrayList<OBJECT>(peToObjectMap.values());
+        Map<OBJECT_PE, OBJECT> idToObjectMap = getTranslator().translate(translationContext, ids, fetchOptions);
+        return idToObjectMap.values();
     }
 
-    protected abstract ISearchObjectExecutor<CRITERION, OBJECT_PE> getSearchExecutor();
+    private List<OBJECT> sortAndPage(IOperationContext context, Collection<OBJECT> results, FETCH_OPTIONS fetchOptions)
+    {
+        if (results == null || results.isEmpty())
+        {
+            return Collections.emptyList();
+        }
 
-    protected abstract ITranslator<OBJECT_PE, OBJECT, FETCH_OPTIONS> getTranslator();
+        SortAndPage sap = new SortAndPage();
+        Collection<OBJECT> objects = sap.sortAndPage(results, fetchOptions);
+
+        return new ArrayList<OBJECT>(objects);
+    }
+
+    private static class CacheKey
+    {
+
+        private String sessionToken;
+
+        private FetchOptions<?> fetchOptions;
+
+        public CacheKey(String sessionToken, FetchOptions<?> fetchOptions)
+        {
+            this.sessionToken = sessionToken;
+            this.fetchOptions = fetchOptions;
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return sessionToken.hashCode() + fetchOptions.getClass().hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj)
+        {
+            if (this == obj)
+            {
+                return true;
+            }
+            if (obj == null)
+            {
+                return false;
+            }
+            if (getClass() != obj.getClass())
+            {
+                return false;
+            }
+
+            CacheKey other = (CacheKey) obj;
+            return sessionToken.equals(other.sessionToken) && FetchOptionsMatcher.arePartsEqual(fetchOptions, other.fetchOptions);
+        }
+    }
 
 }
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/method/ISearchMethodExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/method/ISearchMethodExecutor.java
index 0367622f1cac947c4b7bdeeba439286d1afe91e4..c3b6167524d78f37257bd0285da8edbf6c14d586 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/method/ISearchMethodExecutor.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/method/ISearchMethodExecutor.java
@@ -16,7 +16,7 @@
 
 package ch.ethz.sis.openbis.generic.server.api.v3.executor.method;
 
-import java.util.List;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SearchResult;
 
 /**
  * @author pkupczyk
@@ -24,6 +24,6 @@ import java.util.List;
 public interface ISearchMethodExecutor<OBJECT, CRITERION, FETCH_OPTIONS>
 {
 
-    public List<OBJECT> search(String sessionToken, CRITERION searchCriterion, FETCH_OPTIONS fetchOptions);
+    public SearchResult<OBJECT> search(String sessionToken, CRITERION searchCriterion, FETCH_OPTIONS fetchOptions);
 
 }
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/method/SearchMaterialSqlMethodExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/method/SearchMaterialSqlMethodExecutor.java
index 0c4b09eba6b492cb5ecccb0e09055bc23f11aa01..b6c670c673f39ea9fdcc71666ad71731ea407492 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/method/SearchMaterialSqlMethodExecutor.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/method/SearchMaterialSqlMethodExecutor.java
@@ -16,35 +16,23 @@
 
 package ch.ethz.sis.openbis.generic.server.api.v3.executor.method;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.cache.Cache;
-import org.springframework.cache.Cache.ValueWrapper;
-import org.springframework.cache.CacheManager;
 import org.springframework.stereotype.Component;
 
-import ch.ethz.sis.openbis.generic.server.api.v3.executor.IOperationContext;
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.common.ISearchObjectExecutor;
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.material.ISearchMaterialIdExecutor;
-import ch.ethz.sis.openbis.generic.server.api.v3.translator.TranslationContext;
+import ch.ethz.sis.openbis.generic.server.api.v3.translator.ITranslator;
 import ch.ethz.sis.openbis.generic.server.api.v3.translator.entity.material.sql.IMaterialSqlTranslator;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.material.Material;
-import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.CacheMode;
-import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.FetchOptions;
-import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.FetchOptionsMatcher;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.material.MaterialFetchOptions;
-import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.sort.SortAndPage;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.MaterialSearchCriterion;
 
 /**
  * @author pkupczyk
  */
 @Component
-public class SearchMaterialSqlMethodExecutor extends AbstractMethodExecutor implements ISearchMaterialMethodExecutor
+public class SearchMaterialSqlMethodExecutor extends AbstractSearchMethodExecutor<Material, Long, MaterialSearchCriterion, MaterialFetchOptions>
+        implements ISearchMaterialMethodExecutor
 {
 
     @Autowired
@@ -53,118 +41,16 @@ public class SearchMaterialSqlMethodExecutor extends AbstractMethodExecutor impl
     @Autowired
     private IMaterialSqlTranslator translator;
 
-    @Autowired
-    private CacheManager cacheManager;
-
     @Override
-    public List<Material> search(final String sessionToken, final MaterialSearchCriterion criterion, final MaterialFetchOptions fetchOptions)
-    {
-        return executeInContext(sessionToken, new IMethodAction<List<Material>>()
-            {
-                @Override
-                public List<Material> execute(IOperationContext context)
-                {
-                    Collection<Material> results = searchAndTranslate(context, criterion, fetchOptions);
-                    return sortAndPage(context, results, fetchOptions);
-                }
-            });
-    }
-
-    @SuppressWarnings("unchecked")
-    private Collection<Material> searchAndTranslate(IOperationContext context, MaterialSearchCriterion criterion, MaterialFetchOptions fetchOptions)
-    {
-        if (CacheMode.NO_CACHE.equals(fetchOptions.getCacheMode()))
-        {
-            return doSearchAndTranslate(context, criterion, fetchOptions);
-        } else if (CacheMode.CACHE.equals(fetchOptions.getCacheMode()) || CacheMode.RELOAD_AND_CACHE.equals(fetchOptions.getCacheMode()))
-        {
-            Cache cache = cacheManager.getCache("searchCache");
-            CacheKey key = new CacheKey(context.getSession().getSessionToken(), fetchOptions);
-            Collection<Material> results = null;
-
-            if (CacheMode.RELOAD_AND_CACHE.equals(fetchOptions.getCacheMode()))
-            {
-                cache.evict(key);
-            } else
-            {
-                ValueWrapper wrapper = cache.get(key);
-                if (wrapper != null)
-                {
-                    results = (Collection<Material>) wrapper.get();
-                }
-            }
-
-            if (results == null)
-            {
-                results = doSearchAndTranslate(context, criterion, fetchOptions);
-                cache.put(key, results);
-            }
-
-            return results;
-        } else
-        {
-            throw new IllegalArgumentException("Unsupported cache mode: " + fetchOptions.getCacheMode());
-        }
-    }
-
-    private Collection<Material> doSearchAndTranslate(IOperationContext context, MaterialSearchCriterion criterion, MaterialFetchOptions fetchOptions)
+    protected ISearchObjectExecutor<MaterialSearchCriterion, Long> getSearchExecutor()
     {
-        List<Long> ids = searchExecutor.search(context, criterion);
-        TranslationContext translationContext = new TranslationContext(context.getSession());
-        Map<Long, Material> idToObjectMap = translator.translate(translationContext, ids, fetchOptions);
-        return idToObjectMap.values();
+        return searchExecutor;
     }
 
-    private List<Material> sortAndPage(IOperationContext context, Collection<Material> results, MaterialFetchOptions fetchOptions)
+    @Override
+    protected ITranslator<Long, Material, MaterialFetchOptions> getTranslator()
     {
-        if (results == null || results.isEmpty())
-        {
-            return Collections.emptyList();
-        }
-
-        SortAndPage sap = new SortAndPage();
-        Collection<Material> objects = sap.sortAndPage(results, fetchOptions);
-
-        return new ArrayList<Material>(objects);
+        return translator;
     }
 
-    private static class CacheKey
-    {
-
-        private String sessionToken;
-
-        private FetchOptions<?> fetchOptions;
-
-        public CacheKey(String sessionToken, FetchOptions<?> fetchOptions)
-        {
-            this.sessionToken = sessionToken;
-            this.fetchOptions = fetchOptions;
-        }
-
-        @Override
-        public int hashCode()
-        {
-            return sessionToken.hashCode() + fetchOptions.getClass().hashCode();
-        }
-
-        @Override
-        public boolean equals(Object obj)
-        {
-            if (this == obj)
-            {
-                return true;
-            }
-            if (obj == null)
-            {
-                return false;
-            }
-            if (getClass() != obj.getClass())
-            {
-                return false;
-            }
-
-            CacheKey other = (CacheKey) obj;
-            return sessionToken.equals(other.sessionToken) && FetchOptionsMatcher.arePartsEqual(fetchOptions, other.fetchOptions);
-        }
-    }
 }
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchDataSetTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchDataSetTest.java
index 29fb16af6860c288f3a77ea27b95a9f6b250d238..c5440b1cf6792902495a12dfb2461053add387f9 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchDataSetTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchDataSetTest.java
@@ -43,6 +43,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.entitytype.EntityTypePer
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.tag.TagPermId;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.DataSetSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SampleSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SearchResult;
 
 /**
  * @author pkupczyk
@@ -408,7 +409,8 @@ public class SearchDataSetTest extends AbstractDataSetTest
     private List<DataSet> searchDataSets(String user, DataSetSearchCriterion criterion, DataSetFetchOptions fetchOptions)
     {
         String sessionToken = v3api.login(user, PASSWORD);
-        List<DataSet> dataSets = v3api.searchDataSets(sessionToken, criterion, fetchOptions);
+        SearchResult<DataSet> searchResult = v3api.searchDataSets(sessionToken, criterion, fetchOptions);
+        List<DataSet> dataSets = searchResult.getObjects();
         v3api.logout(sessionToken);
         return dataSets;
     }
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchExperimentTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchExperimentTest.java
index 0b10d60f7c430f4c13741aef823b22d3de4f6d5d..0c5bd8ca943e2409bec14e45e5d2bc5c97c07f2f 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchExperimentTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchExperimentTest.java
@@ -35,6 +35,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.space.SpacePermId;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.tag.TagCode;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.tag.TagPermId;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ExperimentSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SearchResult;
 
 /**
  * @author pkupczyk
@@ -236,7 +237,7 @@ public class SearchExperimentTest extends AbstractExperimentTest
         criterion.withProperty("COMMENT");
         testSearch(TEST_USER, criterion, "/CISD/NEMO/EXP-TEST-1");
     }
-    
+
     @Test
     public void testSearchWithPropertyThatContains()
     {
@@ -555,8 +556,9 @@ public class SearchExperimentTest extends AbstractExperimentTest
     {
         String sessionToken = v3api.login(user, PASSWORD);
 
-        List<Experiment> experiments =
+        SearchResult<Experiment> searchResult =
                 v3api.searchExperiments(sessionToken, criterion, new ExperimentFetchOptions());
+        List<Experiment> experiments = searchResult.getObjects();
 
         assertExperimentIdentifiers(experiments, expectedIdentifiers);
         v3api.logout(sessionToken);
@@ -566,8 +568,9 @@ public class SearchExperimentTest extends AbstractExperimentTest
     {
         String sessionToken = v3api.login(user, PASSWORD);
 
-        List<Experiment> experiments =
+        SearchResult<Experiment> searchResult =
                 v3api.searchExperiments(sessionToken, criterion, new ExperimentFetchOptions());
+        List<Experiment> experiments = searchResult.getObjects();
 
         assertEquals(experiments.size(), expectedCount);
         v3api.logout(sessionToken);
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchMaterialTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchMaterialTest.java
index 5b0a5a74027d759cb824acd00414fad30354f8e1..e896ab6fb65c8d10391f5764b930b2dc36c2afe3 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchMaterialTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchMaterialTest.java
@@ -28,6 +28,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.entitytype.EntityTypePer
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.material.MaterialPermId;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.tag.TagPermId;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.MaterialSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SearchResult;
 import ch.systemsx.cisd.common.action.IDelegatedAction;
 
 /**
@@ -259,8 +260,9 @@ public class SearchMaterialTest extends AbstractTest
     {
         String sessionToken = v3api.login(user, PASSWORD);
 
-        List<Material> materials =
+        SearchResult<Material> searchResult =
                 v3api.searchMaterials(sessionToken, criterion, new MaterialFetchOptions());
+        List<Material> materials = searchResult.getObjects();
 
         assertMaterialPermIds(materials, expectedPermIds);
         v3api.logout(sessionToken);
@@ -270,8 +272,9 @@ public class SearchMaterialTest extends AbstractTest
     {
         String sessionToken = v3api.login(user, PASSWORD);
 
-        List<Material> materials =
+        SearchResult<Material> searchResult =
                 v3api.searchMaterials(sessionToken, criterion, new MaterialFetchOptions());
+        List<Material> materials = searchResult.getObjects();
 
         assertEquals(materials.size(), expectedCount);
         v3api.logout(sessionToken);
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchProjectTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchProjectTest.java
index b6d2cf528ba49f26632cd5d93c04bd977c70dd08..5a2b63f05211320ea7fc4522225397fa286d2af2 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchProjectTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchProjectTest.java
@@ -26,6 +26,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.ProjectIdentifie
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.ProjectPermId;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.space.SpacePermId;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ProjectSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SearchResult;
 
 /**
  * @author pkupczyk
@@ -154,7 +155,8 @@ public class SearchProjectTest extends AbstractTest
     {
         String sessionToken = v3api.login(user, PASSWORD);
 
-        List<Project> projects = v3api.searchProjects(sessionToken, criterion, new ProjectFetchOptions());
+        SearchResult<Project> searchResult = v3api.searchProjects(sessionToken, criterion, new ProjectFetchOptions());
+        List<Project> projects = searchResult.getObjects();
 
         assertProjectIdentifiers(projects, expectedIdentifiers);
         v3api.logout(sessionToken);
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchSampleTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchSampleTest.java
index be5b4d60d7336f3744037c97e09138f9dc5c8e69..d3c1691c7a100c6cad9d38404b0af2214035478a 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchSampleTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchSampleTest.java
@@ -35,6 +35,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.space.SpacePermId;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.tag.TagCode;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.tag.TagPermId;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SampleSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SearchResult;
 
 /**
  * @author pkupczyk
@@ -476,8 +477,9 @@ public class SearchSampleTest extends AbstractSampleTest
     {
         String sessionToken = v3api.login(user, PASSWORD);
 
-        List<Sample> samples =
+        SearchResult<Sample> searchResult =
                 v3api.searchSamples(sessionToken, criterion, new SampleFetchOptions());
+        List<Sample> samples = searchResult.getObjects();
 
         assertSampleIdentifiers(samples, expectedIdentifiers);
         v3api.logout(sessionToken);
@@ -487,8 +489,9 @@ public class SearchSampleTest extends AbstractSampleTest
     {
         String sessionToken = v3api.login(user, PASSWORD);
 
-        List<Sample> samples =
+        SearchResult<Sample> searchResult =
                 v3api.searchSamples(sessionToken, criterion, new SampleFetchOptions());
+        List<Sample> samples = searchResult.getObjects();
 
         assertEquals(samples.size(), expectedCount);
         v3api.logout(sessionToken);
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchSpaceTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchSpaceTest.java
index d15cdb8d8d0cbfdd8e9af6c6b2ba90cab81b45cc..bc3547081e6ad9def5960d4ed1eef1347441d17d 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchSpaceTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchSpaceTest.java
@@ -23,6 +23,7 @@ import org.testng.annotations.Test;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.space.Space;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.space.SpaceFetchOptions;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.space.SpacePermId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SearchResult;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SpaceSearchCriterion;
 
 /**
@@ -144,8 +145,9 @@ public class SearchSpaceTest extends AbstractTest
     {
         String sessionToken = v3api.login(user, PASSWORD);
 
-        List<Space> spaces =
+        SearchResult<Space> searchResult =
                 v3api.searchSpaces(sessionToken, criterion, new SpaceFetchOptions());
+        List<Space> spaces = searchResult.getObjects();
 
         assertSpaceCodes(spaces, expectedCodes);
         v3api.logout(sessionToken);
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/IApplicationServerApi.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/IApplicationServerApi.java
index ded9320b353bb196d55e0f1b894796c32bc70703..e08d53925ca8cb7c7598c77c63e38ebfc262671f 100644
--- a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/IApplicationServerApi.java
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/IApplicationServerApi.java
@@ -67,6 +67,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ExperimentSearchCrit
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.MaterialSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ProjectSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SampleSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SearchResult;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SpaceSearchCriterion;
 import ch.systemsx.cisd.common.api.IRpcService;
 
@@ -135,17 +136,18 @@ public interface IApplicationServerApi extends IRpcService
 
     public Map<IMaterialId, Material> mapMaterials(String sessionToken, List<? extends IMaterialId> materialIds, MaterialFetchOptions fetchOptions);
 
-    public List<Space> searchSpaces(String sessionToken, SpaceSearchCriterion searchCriterion, SpaceFetchOptions fetchOptions);
+    public SearchResult<Space> searchSpaces(String sessionToken, SpaceSearchCriterion searchCriterion, SpaceFetchOptions fetchOptions);
 
-    public List<Project> searchProjects(String sessionToken, ProjectSearchCriterion searchCriterion, ProjectFetchOptions fetchOptions);
+    public SearchResult<Project> searchProjects(String sessionToken, ProjectSearchCriterion searchCriterion, ProjectFetchOptions fetchOptions);
 
-    public List<Experiment> searchExperiments(String sessionToken, ExperimentSearchCriterion searchCriterion, ExperimentFetchOptions fetchOptions);
+    public SearchResult<Experiment> searchExperiments(String sessionToken, ExperimentSearchCriterion searchCriterion,
+            ExperimentFetchOptions fetchOptions);
 
-    public List<Sample> searchSamples(String sessionToken, SampleSearchCriterion searchCriterion, SampleFetchOptions fetchOptions);
+    public SearchResult<Sample> searchSamples(String sessionToken, SampleSearchCriterion searchCriterion, SampleFetchOptions fetchOptions);
 
-    public List<DataSet> searchDataSets(String sessionToken, DataSetSearchCriterion searchCriterion, DataSetFetchOptions fetchOptions);
+    public SearchResult<DataSet> searchDataSets(String sessionToken, DataSetSearchCriterion searchCriterion, DataSetFetchOptions fetchOptions);
 
-    public List<Material> searchMaterials(String sessionToken, MaterialSearchCriterion searchCriterion, MaterialFetchOptions fetchOptions);
+    public SearchResult<Material> searchMaterials(String sessionToken, MaterialSearchCriterion searchCriterion, MaterialFetchOptions fetchOptions);
 
     public void deleteSpaces(String sessionToken, List<? extends ISpaceId> spaceIds, SpaceDeletionOptions deletionOptions);
 
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/search/SearchResult.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/search/SearchResult.java
index d1601f66cebc1379a6bfffd10a26f47e737aeac0..2f5a66919a60ab9320b586d660057e2e0bd2a622 100644
--- a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/search/SearchResult.java
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/search/SearchResult.java
@@ -24,8 +24,24 @@ import java.util.List;
 public class SearchResult<OBJECT>
 {
 
-    private List<Object> objects;
+    private List<OBJECT> objects;
 
     private int totalCount;
 
+    public SearchResult(List<OBJECT> objects, int totalCount)
+    {
+        this.objects = objects;
+        this.totalCount = totalCount;
+    }
+
+    public List<OBJECT> getObjects()
+    {
+        return objects;
+    }
+
+    public int getTotalCount()
+    {
+        return totalCount;
+    }
+
 }