From e997ae066a705d96d6d10706b7b6d02dd4fe0ccf Mon Sep 17 00:00:00 2001
From: gcornut <guillaume.cornut@inra.fr>
Date: Mon, 13 May 2019 19:55:52 +0200
Subject: [PATCH 1/3] feat: Remove max result window limitation on BrAPI calls.

- Implement cached, deep paginated search using ES search_after
- Keep max result window on data discovery calls
- Simplify ESRequestFactory
---
 .../api/brapi/v1/BrapiExceptionHandler.java   |   8 +
 .../faidare/v1/DataDiscoveryController.java   |   3 +-
 .../faidare/config/ElasticSearchConfig.java   |  12 +
 .../criteria/base/PaginationCriteriaImpl.java |   1 -
 .../base/PaginationMaxResultValidator.java    |  26 +-
 .../domain/criteria/base/ValidPagination.java |  28 ---
 .../criteria/DataDiscoveryCriteriaImpl.java   |   2 +
 .../elasticsearch/ESRequestFactory.java       | 118 +++++----
 .../elasticsearch/ESResponseParser.java       |  17 ++
 .../elasticsearch/ESScrollIterator.java       |  14 +-
 .../document/DocumentMetadata.java            |   6 +
 .../repository/impl/BaseESRepository.java     |   2 +-
 .../impl/ESGenericFindRepository.java         | 225 +++++++++++++-----
 .../impl/ESGenericGetByIdRepository.java      |  36 +--
 .../impl/ESGenericSuggestRepository.java      |  10 +-
 .../es/DataDiscoveryRepositoryImpl.java       |  13 +-
 .../es/GermplasmAttributeRepositoryImpl.java  |   3 +-
 .../es/GermplasmRepositoryImpl.java           |  10 +-
 .../base/MaxResultWindowPagination.java       |  38 +++
 .../brapi/v1/BrapiExceptionHandlerTest.java   |   3 -
 .../base/PaginationValidatorTest.java         |  24 +-
 .../elasticsearch/ESRequestFactoryTest.java   |   2 +-
 .../gnpis/v1/DataDiscoveryControllerTest.java |  53 +++++
 23 files changed, 451 insertions(+), 203 deletions(-)
 create mode 100644 backend/src/main/java/fr/inra/urgi/gpds/domain/criteria/base/MaxResultWindowPagination.java
 create mode 100644 backend/src/test/java/fr/inra/urgi/gpds/api/gnpis/v1/DataDiscoveryControllerTest.java

diff --git a/backend/src/main/java/fr/inra/urgi/faidare/api/brapi/v1/BrapiExceptionHandler.java b/backend/src/main/java/fr/inra/urgi/faidare/api/brapi/v1/BrapiExceptionHandler.java
index 3d4f78b1..d30b90c7 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/api/brapi/v1/BrapiExceptionHandler.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/api/brapi/v1/BrapiExceptionHandler.java
@@ -6,6 +6,13 @@ import fr.inra.urgi.faidare.api.brapi.v1.exception.BrapiPaginationException;
 import fr.inra.urgi.faidare.domain.brapi.v1.response.BrapiPagination;
 import fr.inra.urgi.faidare.domain.brapi.v1.response.BrapiStatus;
 import fr.inra.urgi.faidare.domain.response.ApiResponseFactory;
+import fr.inra.urgi.gpds.api.BadRequestException;
+import fr.inra.urgi.gpds.api.NotFoundException;
+import fr.inra.urgi.gpds.api.brapi.v1.exception.BrapiPaginationException;
+import fr.inra.urgi.gpds.domain.brapi.v1.response.BrapiPagination;
+import fr.inra.urgi.gpds.domain.brapi.v1.response.BrapiStatus;
+import fr.inra.urgi.gpds.domain.response.ApiResponseFactory;
+import fr.inra.urgi.gpds.elasticsearch.repository.impl.ESGenericFindRepository;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.validation.BindException;
@@ -103,6 +110,7 @@ public class BrapiExceptionHandler {
      */
     @ExceptionHandler({
         BadRequestException.class,
+        ESGenericFindRepository.PageOutOfBoundException.class,
         BindException.class,
         MethodArgumentNotValidException.class,
         HttpMediaTypeNotSupportedException.class,
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/api/faidare/v1/DataDiscoveryController.java b/backend/src/main/java/fr/inra/urgi/faidare/api/faidare/v1/DataDiscoveryController.java
index b4bef53d..c1424ed2 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/api/faidare/v1/DataDiscoveryController.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/api/faidare/v1/DataDiscoveryController.java
@@ -15,7 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 import javax.validation.Valid;
-import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
@@ -46,6 +46,7 @@ public class DataDiscoveryController {
         if (fetchSize == null) {
             fetchSize = Integer.MAX_VALUE;
         }
+    ) {
         return dataDiscoveryRepository.suggest(field, StringFunctions.asUTF8(text), fetchSize, criteria);
     }
 
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/config/ElasticSearchConfig.java b/backend/src/main/java/fr/inra/urgi/faidare/config/ElasticSearchConfig.java
index 8d4901f9..33f42fab 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/config/ElasticSearchConfig.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/config/ElasticSearchConfig.java
@@ -2,6 +2,7 @@ package fr.inra.urgi.faidare.config;
 
 import org.apache.http.HttpHost;
 import org.apache.http.impl.nio.reactor.IOReactorConfig;
+import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.client.NodeSelector;
 import org.elasticsearch.client.RestClient;
 import org.elasticsearch.client.RestClientBuilder;
@@ -37,4 +38,15 @@ public class ElasticSearchConfig {
             );
     }
 
+    @Bean
+    public IndicesOptions indicesOptions() {
+        boolean ignoreUnavailable = true;
+        boolean allowNoIndices = true;
+        boolean expandToOpenIndices = true;
+        boolean expandToClosedIndices = false;
+        return IndicesOptions.fromOptions(
+            ignoreUnavailable, allowNoIndices, expandToOpenIndices, expandToClosedIndices
+        );
+    }
+
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationCriteriaImpl.java b/backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationCriteriaImpl.java
index 939ddbd4..148e0252 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationCriteriaImpl.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationCriteriaImpl.java
@@ -9,7 +9,6 @@ import javax.validation.constraints.NotNull;
 /**
  * @author gcornut
  */
-@ValidPagination // global validation rule
 public class PaginationCriteriaImpl implements PaginationCriteria {
     public static final long MIN_PAGE = 0;
     public static final long MIN_PAGE_SIZE = 1;
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationMaxResultValidator.java b/backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationMaxResultValidator.java
index 1f7f0096..4b2e8b75 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationMaxResultValidator.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationMaxResultValidator.java
@@ -8,21 +8,13 @@ import javax.validation.ConstraintValidatorContext;
  *
  * @author gcornut
  */
-public class PaginationMaxResultValidator implements ConstraintValidator<ValidPagination, PaginationCriteriaImpl> {
+public class PaginationMaxResultValidator implements ConstraintValidator<MaxResultWindowPagination, PaginationCriteriaImpl> {
 
-    /**
-     * Default constrain on elasticsearch search result window
-     *
-     * @link https://www.elastic.co/guide/en/elasticsearch/reference/2.4/index-modules.html
-     */
-    public static final long MAX_RESULT_WINDOW = 10000;
-
-    public static final String ERROR_MAX_RESULT_WINDOW =
-        "The result window (page x pageSize) cannot be over " + MAX_RESULT_WINDOW + ". " +
-            "Please use an export API to download all the requested data.";
+    private long maxResultWindow;
 
     @Override
-    public void initialize(ValidPagination annotation) {
+    public void initialize(MaxResultWindowPagination annotation) {
+        maxResultWindow = annotation.value();
     }
 
     @Override
@@ -31,12 +23,18 @@ public class PaginationMaxResultValidator implements ConstraintValidator<ValidPa
         Long page = paginationCriteria.getPage();
 
         if (page != null && pageSize != null) {
-            if (page * pageSize >= (MAX_RESULT_WINDOW - pageSize)) {
+            if (page * pageSize >= (maxResultWindow)) {
                 context.disableDefaultConstraintViolation();
-                context.buildConstraintViolationWithTemplate(ERROR_MAX_RESULT_WINDOW).addConstraintViolation();
+                context.buildConstraintViolationWithTemplate(getErrorMessage())
+                    .addConstraintViolation();
                 return false;
             }
         }
         return true;
     }
+
+    private String getErrorMessage() {
+        return "The result window (page x pageSize) cannot be over " + maxResultWindow + ". " +
+            "Please use an export API to download all the requested data.";
+    }
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/ValidPagination.java b/backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/ValidPagination.java
index 23097613..e69de29b 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/ValidPagination.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/ValidPagination.java
@@ -1,28 +0,0 @@
-package fr.inra.urgi.faidare.domain.criteria.base;
-
-import javax.validation.Constraint;
-import javax.validation.Payload;
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
-import static java.lang.annotation.ElementType.TYPE;
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-/**
- * @author gcornut
- */
-
-@Target({TYPE, ANNOTATION_TYPE})
-@Retention(RUNTIME)
-@Constraint(validatedBy = PaginationMaxResultValidator.class)
-@Documented
-public @interface ValidPagination {
-
-    String message() default "";
-
-    Class<?>[] groups() default {};
-
-    Class<? extends Payload>[] payload() default {};
-}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/domain/datadiscovery/criteria/DataDiscoveryCriteriaImpl.java b/backend/src/main/java/fr/inra/urgi/faidare/domain/datadiscovery/criteria/DataDiscoveryCriteriaImpl.java
index 892f95d0..37a729da 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/domain/datadiscovery/criteria/DataDiscoveryCriteriaImpl.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/domain/datadiscovery/criteria/DataDiscoveryCriteriaImpl.java
@@ -1,5 +1,6 @@
 package fr.inra.urgi.faidare.domain.datadiscovery.criteria;
 
+import fr.inra.urgi.faidare.domain.criteria.base.MaxResultWindowPagination;
 import fr.inra.urgi.faidare.domain.criteria.base.PaginationCriteriaImpl;
 import fr.inra.urgi.faidare.domain.datadiscovery.data.DataDiscoveryDocument;
 import fr.inra.urgi.faidare.elasticsearch.criteria.annotation.CriteriaForDocument;
@@ -13,6 +14,7 @@ import java.util.List;
 /**
  * @author gcornut
  */
+@MaxResultWindowPagination // Restrict this criteria to the ES max result window
 @CriteriaForDocument(DataDiscoveryDocument.class)
 public class DataDiscoveryCriteriaImpl extends PaginationCriteriaImpl implements DataDiscoveryCriteria {
 
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/ESRequestFactory.java b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/ESRequestFactory.java
index 21102c83..e6b159e2 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/ESRequestFactory.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/ESRequestFactory.java
@@ -2,15 +2,17 @@ package fr.inra.urgi.faidare.elasticsearch;
 
 import com.google.common.base.Joiner;
 import fr.inra.urgi.faidare.config.FaidareProperties;
+import fr.inra.urgi.faidare.domain.criteria.base.SortCriteria;
+import fr.inra.urgi.faidare.elasticsearch.criteria.AnnotatedCriteriaMapper;
+import fr.inra.urgi.faidare.elasticsearch.document.DocumentMetadata;
 import fr.inra.urgi.faidare.repository.http.UserGroupsResourceClient;
 import org.elasticsearch.action.search.SearchRequest;
-import org.elasticsearch.action.search.SearchRequestBuilder;
-import org.elasticsearch.action.search.SearchScrollRequest;
 import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.index.query.QueryBuilder;
 import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.search.sort.FieldSortBuilder;
+import org.elasticsearch.search.sort.SortOrder;
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
 
 import java.util.ArrayList;
@@ -19,42 +21,32 @@ import java.util.List;
 @Component
 public class ESRequestFactory {
 
-    private static final Logger logger = LoggerFactory.getLogger(ESRequestFactory.class);
-
     public static final int MAX_TERM_AGG_SIZE = Integer.MAX_VALUE;
 
-    private UserGroupsResourceClient userGroupsResourceClient;
-    private FaidareProperties properties;
-
+    private final UserGroupsResourceClient userGroupsResourceClient;
+    private final FaidareProperties properties;
     private final IndicesOptions indicesOptions;
 
     public ESRequestFactory(
         UserGroupsResourceClient userGroupsResourceClient,
-        FaidareProperties properties
+        FaidareProperties properties,
+        IndicesOptions indicesOptions
     ) {
         this.userGroupsResourceClient = userGroupsResourceClient;
         this.properties = properties;
-        boolean ignoreUnavailable = true;
-        boolean allowNoIndices = true;
-        boolean expandToOpenIndices = true;
-        boolean expandToClosedIndices = false;
-        indicesOptions = IndicesOptions.fromOptions(
-            ignoreUnavailable, allowNoIndices, expandToOpenIndices, expandToClosedIndices
-        );
+        this.indicesOptions = indicesOptions;
     }
 
     /**
-     * Utility method to retrieve a new {@link SearchRequestBuilder} already
-     * configured with the index. This method allows to not forget the index and
+     * Utility method to retrieve a new {@link SearchRequest} already
+     * configured with the groupId filtered aliases. This method allows to not forget the index and
      * so to not query the whole cluster!
      *
-     * @return the {@link SearchRequestBuilder} configured with index
+     * @return the {@link SearchRequest} configured with index
      */
-    public SearchRequest prepareSearch(String documentType, QueryBuilder query) {
-        String[] aliases = getAliases(documentType, null);
-
+    public SearchRequest prepareSearch(String documentType) {
         SearchRequest request = new SearchRequest();
-        request.indices(aliases);
+        request.indices(getAliases(documentType, null));
         request.source(new SearchSourceBuilder());
         request.indicesOptions(indicesOptions);
 
@@ -62,37 +54,77 @@ public class ESRequestFactory {
             request.types(documentType);
         }
 
-        if (query != null) {
-            request.source().query(query);
+        return request;
+    }
+
+    /**
+     * Prepare a search request with query and criteria.
+     *
+     * Same as {@link #prepareSearch(String)} but with added query, sort and field exclusion
+     */
+    public <C> SearchRequest prepareSearch(
+        QueryBuilder query, C criteria
+    ) {
+        // Get document metadata from criteria annotations
+        DocumentMetadata<?> documentMetadata = AnnotatedCriteriaMapper.getMapping(criteria.getClass()).getDocumentMetadata();
+
+        // Build request
+        String documentType = documentMetadata.getDocumentType();
+        SearchRequest request = prepareSearch(documentType);
+        request.source().query(query);
+
+        // Add sort
+        FieldSortBuilder idSort = new FieldSortBuilder("_id");
+        if (criteria instanceof SortCriteria) {
+            SortCriteria sortCriteria = (SortCriteria) criteria;
+            String field = sortCriteria.getSortBy();
+            if (field == null) {
+                // Default ES sort
+                field = idSort.getFieldName();
+            }
+
+            String strOrder = sortCriteria.getSortOrder();
+            SortOrder order = SortOrder.ASC;
+            if (strOrder != null) {
+                order = SortOrder.valueOf(strOrder.toUpperCase());
+            }
+            request.source().sort(field, order);
+        }
+        request.source().sort(idSort);
+
+        // Add excluded fields if requested
+        String[] excludedFields = documentMetadata.getExcludedFields();
+        if (excludedFields != null && excludedFields.length >= 1) {
+            request.source().fetchSource(null, excludedFields);
         }
 
+        return request;
+    }
+
+
+    /**
+     * Utility function to log a SearchRequest in DEBUG
+     */
+    public static void logRequest(Logger logger, SearchRequest request) {
         // Debug
         if (logger.isDebugEnabled()) {
-            String aliasesString = Joiner.on(",").join(aliases);
+
             StringBuilder message = new StringBuilder();
             message.append("Search ES:\n");
-            message.append("POST ").append(aliasesString);
-            if (documentType != null) {
-                message.append("/").append(documentType);
+
+            message.append("POST ").append(Joiner.on(",").join(request.indices()));
+            if (request.types() != null && request.types().length > 0) {
+                message.append("/").append(Joiner.on(",").join(request.types()));
             }
             message.append("/_search");
-            if (query != null) {
+            if (request.source() != null) {
                 message.append("\n");
                 message.append(request.source().toString());
             }
             logger.debug(message.toString());
         }
-
-        return request;
     }
 
-    /**
-     * List ElasticSearch index group id filtered aliases to a document type
-     */
-    public String[] getAliases(String documentType) {
-        List<String> aliasList = getAliasList(documentType, null);
-        return aliasList.toArray(new String[0]);
-    }
 
     /**
      * List ElasticSearch index group id filtered aliases to a document type
@@ -118,12 +150,4 @@ public class ESRequestFactory {
         return aliases;
     }
 
-    public SearchRequest prepareSearch(String documentType) {
-        return prepareSearch(documentType, null);
-    }
-
-    public SearchScrollRequest getSearchScroll(String scrollId) {
-        return new SearchScrollRequest(scrollId);
-    }
-
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/ESResponseParser.java b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/ESResponseParser.java
index 09289239..f8a4649a 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/ESResponseParser.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/ESResponseParser.java
@@ -14,6 +14,7 @@ import org.elasticsearch.search.aggregations.Aggregation;
 import org.elasticsearch.search.aggregations.Aggregations;
 import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregation;
 import org.elasticsearch.search.aggregations.bucket.terms.Terms;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import java.io.IOException;
@@ -31,6 +32,7 @@ public class ESResponseParser {
 
     private final ObjectMapper objectMapper;
 
+    @Autowired
     public ESResponseParser(ObjectMapper objectMapper) {
         this.objectMapper = objectMapper;
     }
@@ -171,4 +173,19 @@ public class ESResponseParser {
     public List<FacetTermImpl> parseFacetTerms(SearchResponse response, String... aggregationPath) {
         return parseFacetTerms(response, Arrays.asList(aggregationPath));
     }
+
+    /**
+     * Parse the last hit sort values
+     * (used for next page "searchAfter")
+     */
+    public Object[] parseLastSortValues(SearchResponse response) {
+        SearchHits hits = response.getHits();
+        if (hits == null) return null;
+
+        SearchHit[] searchHits = hits.getHits();
+        if (searchHits == null || searchHits.length == 0) return null;
+
+        SearchHit lastHit = searchHits[searchHits.length - 1];
+        return lastHit.getSortValues();
+    }
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/ESScrollIterator.java b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/ESScrollIterator.java
index f106e9b7..bd8d656e 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/ESScrollIterator.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/ESScrollIterator.java
@@ -38,7 +38,6 @@ public class ESScrollIterator<T> implements Iterator<T> {
 
     private final Class<T> documentClass;
     private final RestHighLevelClient client;
-    private final ESRequestFactory requestFactory;
     private final ESResponseParser parser;
     private final long totalHits;
 
@@ -54,21 +53,21 @@ public class ESScrollIterator<T> implements Iterator<T> {
         int fetchSize
     ) {
         this.client = client;
-        this.requestFactory = requestFactory;
         this.parser = parser;
         this.documentClass = documentClass;
 
         DocumentMetadata<T> documentMetadata = DocumentAnnotationUtil.getDocumentObjectMetadata(documentClass);
 
         SearchRequest request = requestFactory
-            .prepareSearch(documentMetadata.getDocumentType(), query)
+            .prepareSearch(documentMetadata.getDocumentType())
             .scroll(DEFAULT_KEEP_ALIVE_TIME);
 
         request.source()
             .size(fetchSize)
-            .sort(FieldSortBuilder.DOC_FIELD_NAME, SortOrder.ASC);
+            .sort(FieldSortBuilder.DOC_FIELD_NAME, SortOrder.ASC)
+            .query(query);
 
-        SearchResponse response = null;
+        SearchResponse response;
         try {
             response = client.search(request, RequestOptions.DEFAULT);
         } catch (IOException e) {
@@ -99,13 +98,12 @@ public class ESScrollIterator<T> implements Iterator<T> {
             return true;
         } else if (totalHits > hitIndex) {
 
-            SearchScrollRequest request = requestFactory
-                .getSearchScroll(scrollId)
+            SearchScrollRequest request = new SearchScrollRequest(scrollId)
                 .scroll(DEFAULT_KEEP_ALIVE_TIME);
 
             LOGGER.debug("Scroll new page: " + scrollId);
 
-            SearchResponse response = null;
+            SearchResponse response;
             try {
                 response = client.scroll(request, RequestOptions.DEFAULT);
             } catch (IOException e) {
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/document/DocumentMetadata.java b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/document/DocumentMetadata.java
index 6bf7389b..994d5b94 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/document/DocumentMetadata.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/document/DocumentMetadata.java
@@ -57,6 +57,12 @@ public class DocumentMetadata<VO> {
         return idField;
     }
 
+    public void checkIdField() {
+        if (idField == null) {
+            throw new DocumentAnnotationUtil.ValueObjectMetadataException("Could not identify an identifier field on document " + documentClass.getSimpleName());
+        }
+    }
+
     public String[] getExcludedFields() {
         return excludedFields;
     }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/BaseESRepository.java b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/BaseESRepository.java
index bce6728b..81d7d317 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/BaseESRepository.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/BaseESRepository.java
@@ -26,7 +26,7 @@ public class BaseESRepository<C extends PaginationCriteria, VO>
         ESResponseParser parser
     ) {
         getByIdRepository = new ESGenericGetByIdRepository<>(client, requestFactory, voClass, parser);
-        findRepository = new ESGenericFindRepository<>(client, requestFactory, voClass, parser);
+        findRepository = new ESGenericFindRepository<>(client, requestFactory, voClass, new ESGenericQueryFactory<>(), parser);
     }
 
     @Override
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericFindRepository.java b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericFindRepository.java
index 54e7b287..a3ee3408 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericFindRepository.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericFindRepository.java
@@ -1,27 +1,36 @@
 package fr.inra.urgi.faidare.elasticsearch.repository.impl;
 
-import fr.inra.urgi.faidare.domain.criteria.base.PaginationCriteria;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import fr.inra.urgi.gpds.domain.criteria.base.PaginationCriteria;
+import fr.inra.urgi.gpds.domain.response.PaginatedList;
+import fr.inra.urgi.gpds.domain.response.Pagination;
+import fr.inra.urgi.gpds.domain.response.PaginationImpl;
+import fr.inra.urgi.gpds.elasticsearch.ESRequestFactory;
+import fr.inra.urgi.gpds.elasticsearch.ESResponseParser;
+import fr.inra.urgi.gpds.elasticsearch.query.ESQueryFactory;
+import fr.inra.urgi.gpds.elasticsearch.repository.ESFindRepository;
 import fr.inra.urgi.faidare.domain.criteria.base.SortCriteria;
-import fr.inra.urgi.faidare.domain.response.PaginatedList;
-import fr.inra.urgi.faidare.domain.response.Pagination;
-import fr.inra.urgi.faidare.domain.response.PaginationImpl;
-import fr.inra.urgi.faidare.elasticsearch.ESRequestFactory;
-import fr.inra.urgi.faidare.elasticsearch.ESResponseParser;
 import fr.inra.urgi.faidare.elasticsearch.document.DocumentAnnotationUtil;
 import fr.inra.urgi.faidare.elasticsearch.document.DocumentMetadata;
-import fr.inra.urgi.faidare.elasticsearch.query.ESQueryFactory;
 import fr.inra.urgi.faidare.elasticsearch.query.impl.ESGenericQueryFactory;
-import fr.inra.urgi.faidare.elasticsearch.repository.ESFindRepository;
 import org.elasticsearch.action.search.SearchRequest;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.index.query.QueryBuilder;
-import org.elasticsearch.search.sort.SortOrder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import static fr.inra.urgi.gpds.domain.criteria.base.MaxResultWindowPagination.MAX_RESULT_WINDOW;
 
 /**
  * Generic implementation of an ElasticSearch document repository which can find documents by criteria.
@@ -30,10 +39,15 @@ import java.util.List;
  */
 public class ESGenericFindRepository<C extends PaginationCriteria, VO> implements ESFindRepository<C, VO> {
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(ESGenericFindRepository.class);
+    private static final Cache<String, Map<PageCoordinate, Object[]>> SORT_VALUES_CACHE = CacheBuilder.newBuilder()
+        .expireAfterWrite(1, TimeUnit.HOURS)
+        .build();
+
     private final RestHighLevelClient client;
     private final ESRequestFactory requestFactory;
+    private final Class<VO> voClass;
     private final ESQueryFactory<C> queryFactory;
-    private final DocumentMetadata<VO> documentMetadata;
     private final ESResponseParser parser;
 
     public ESGenericFindRepository(
@@ -45,62 +59,63 @@ public class ESGenericFindRepository<C extends PaginationCriteria, VO> implement
     ) {
         this.client = client;
         this.requestFactory = requestFactory;
+        this.voClass = voClass;
         this.queryFactory = queryFactory;
-        this.documentMetadata = DocumentAnnotationUtil.getDocumentObjectMetadata(voClass);
         this.parser = parser;
     }
 
-    public ESGenericFindRepository(
-        RestHighLevelClient client,
-        ESRequestFactory requestFactory,
-        Class<VO> voClass,
-        ESResponseParser parser
-    ) {
-        this(client, requestFactory, voClass, new ESGenericQueryFactory<>(), parser);
-    }
-
-    public static <C extends PaginationCriteria, D> SearchRequest prepareSearchRequest(
-        QueryBuilder query, C criteria, DocumentMetadata<D> documentMetadata, ESRequestFactory requestFactory
-    ) {
-        // Build query
-        int size = criteria.getPageSize().intValue();
-        int from = criteria.getPage().intValue() * size;
-
-        // Build request
-        String documentType = documentMetadata.getDocumentType();
-        SearchRequest request = requestFactory.prepareSearch(documentType, query);
-        request.source()
-            .from(from)
-            .size(size);
-
-        // Add sort if requested
-        if (criteria instanceof SortCriteria) {
-            SortCriteria sortCriteria = (SortCriteria) criteria;
-            String field = sortCriteria.getSortBy();
-            if (field == null) {
-                // Default ES sort
-                field = "_doc";
-            }
-
-            String strOrder = sortCriteria.getSortOrder();
-            SortOrder order = SortOrder.ASC;
-            if (strOrder != null) {
-                order = SortOrder.valueOf(strOrder.toUpperCase());
+    /**
+     * Get search after value for query
+     */
+    private Object[] getSearchAfter(
+        SearchRequest request,
+        PageCoordinate page
+    ) throws IOException, ExecutionException {
+        String searchSource = request.source().toString();
+        Map<PageCoordinate, Object[]> pageToLastSorted = SORT_VALUES_CACHE.get(searchSource, HashMap::new);
+
+        Object[] lastSortValues = pageToLastSorted.get(page.previousPage());
+
+        if (lastSortValues == null) {
+            // Find last sort value for previous page
+
+            // Start at first page under the max result window
+            PageCoordinate currentPage = page;
+            while (currentPage.from >= MAX_RESULT_WINDOW) {
+                currentPage = currentPage.previousPage();
             }
-            request.source().sort(field, order);
-        }
-
-        // Add excluded fields if requested
-        String[] excludedFields = documentMetadata.getExcludedFields();
-        if (excludedFields != null && excludedFields.length >= 1) {
-            request.source().fetchSource(null, excludedFields);
+            request.source().size(currentPage.size);
+
+            Object[] previousSortValues = null;
+            do {
+                Object[] currentSortValues = pageToLastSorted.get(currentPage);
+                if (currentSortValues == null) {
+                    // Page sort value not yet fetched
+
+                    if (previousSortValues == null) {
+                        request.source().from(currentPage.from);
+                    } else {
+                        request.source().from(0);
+                        request.source().searchAfter(previousSortValues);
+                    }
+
+                    ESRequestFactory.logRequest(LOGGER, request);
+                    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
+                    checkPaginationBound(page, response);
+
+                    currentSortValues = parser.parseLastSortValues(response);
+
+                    // Cache page sort value
+                    pageToLastSorted.put(currentPage, currentSortValues);
+                }
+
+                // Next page
+                previousSortValues = currentSortValues;
+                currentPage = currentPage.nextPage();
+            } while (!currentPage.equals(page));
+            return previousSortValues;
         }
-
-        Logger logger = LoggerFactory.getLogger(ESGenericFindRepository.class);
-        if (logger.isDebugEnabled()) {
-            logger.debug(request.toString());
-        }
-        return request;
+        return lastSortValues;
     }
 
     @Override
@@ -108,18 +123,46 @@ public class ESGenericFindRepository<C extends PaginationCriteria, VO> implement
         try {
             QueryBuilder query = queryFactory.createQuery(criteria);
 
-            SearchRequest request = prepareSearchRequest(
-                query, criteria, documentMetadata, requestFactory
+            SearchRequest request = requestFactory.prepareSearch(
+                query, criteria
             );
+            String sourceQueryWithoutPagination = request.source().toString();
+
+            // Set pagination
+            int size = criteria.getPageSize().intValue();
+            int from = criteria.getPage().intValue() * size;
+            PageCoordinate page = new PageCoordinate(size, from);
+
+            if (from < MAX_RESULT_WINDOW) {
+                // Classic search from / size
+                request.source().from(from);
+                request.source().size(size);
+            } else {
+                // More complex search using search after
+                Object[] searchAfterSortValues = getSearchAfter(
+                    request,
+                    page
+                );
+                request.source().size(size);
+                request.source().from(0);
+                request.source().searchAfter(searchAfterSortValues);
+            }
 
             // Execute request
-            SearchResponse result = client.search(request, RequestOptions.DEFAULT);
+            ESRequestFactory.logRequest(LOGGER, request);
+            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
+            checkPaginationBound(page, response);
+
+            // Cache last sort value for later
+            SORT_VALUES_CACHE
+                .get(sourceQueryWithoutPagination, HashMap::new)
+                .put(page, parser.parseLastSortValues(response));
 
             // Prepare pagination info
-            Pagination pagination = PaginationImpl.create(criteria, parser.parseTotalHits(result));
+            Pagination pagination = PaginationImpl.create(criteria, parser.parseTotalHits(response));
 
             // Parse result list
-            List<VO> resultList = parser.parseHits(result, documentMetadata.getDocumentClass());
+            List<VO> resultList = parser.parseHits(response, voClass);
 
             // Return paginated list
             return new PaginatedList<>(pagination, resultList);
@@ -128,4 +171,56 @@ public class ESGenericFindRepository<C extends PaginationCriteria, VO> implement
         }
     }
 
+
+    private void checkPaginationBound(PageCoordinate page, SearchResponse response) {
+        Long totalHits = parser.parseTotalHits(response);
+        if (page.from > totalHits) {
+            throw new PageOutOfBoundException();
+        }
+    }
+
+    /**
+     * RuntimeException occurring when requesting a page above the maximum number of page of a result set.
+     */
+    static public class PageOutOfBoundException extends RuntimeException {
+        PageOutOfBoundException() {
+            super("The current page number should be strictly less than the total number of pages.");
+        }
+    }
+
+    /**
+     * Data class used to represent from/size Elasticsearch page coordinates
+     */
+    static private class PageCoordinate {
+        int size;
+        int from;
+
+        PageCoordinate(int size, int from) {
+            this.size = size;
+            this.from = from;
+        }
+
+        PageCoordinate previousPage() {
+            int nextFrom = this.from - size;
+            if (nextFrom < 0) {
+                nextFrom = 0;
+            }
+            return new PageCoordinate(size, nextFrom);
+        }
+
+        PageCoordinate nextPage() {
+            return new PageCoordinate(size, from + size);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(size, from);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return o instanceof PageCoordinate && hashCode() == o.hashCode();
+        }
+    }
+
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericGetByIdRepository.java b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericGetByIdRepository.java
index 967fc51f..7ee3086d 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericGetByIdRepository.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericGetByIdRepository.java
@@ -9,7 +9,6 @@ import org.elasticsearch.action.search.SearchRequest;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.RestHighLevelClient;
-import org.elasticsearch.index.query.QueryBuilder;
 import org.elasticsearch.index.query.QueryBuilders;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -17,20 +16,22 @@ import org.slf4j.LoggerFactory;
 import java.util.List;
 
 /**
- * Generic implementation of an ElasticSearch query for document by its identifier
+ * Generic implementation of an ElasticSearch query for document by its identifier.
+ *
+ * Requires the document VO to have the @{@link fr.inra.urgi.gpds.elasticsearch.document.annotation.Document}
+ * and @{@link fr.inra.urgi.gpds.elasticsearch.document.annotation.Id} annotations
  *
  * @author gcornut
  */
 public class ESGenericGetByIdRepository<VO> implements ESGetByIdRepository<VO> {
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(ESGenericGetByIdRepository.class);
+
     private final RestHighLevelClient client;
     private final ESRequestFactory requestFactory;
     private final Class<VO> voClass;
     private final ESResponseParser parser;
-
-    private DocumentMetadata<VO> documentMetadata;
-
-    private Logger logger = LoggerFactory.getLogger(getClass());
+    private final DocumentMetadata<VO> documentMetadata;
 
     public ESGenericGetByIdRepository(
         RestHighLevelClient client,
@@ -42,24 +43,31 @@ public class ESGenericGetByIdRepository<VO> implements ESGetByIdRepository<VO> {
         this.requestFactory = requestFactory;
         this.voClass = voClass;
         this.parser = parser;
-        documentMetadata = DocumentAnnotationUtil.getDocumentObjectMetadata(voClass);
-        if (documentMetadata.getIdField() == null) {
-            throw new RuntimeException("Could not identify an identifier field on document " + voClass.getSimpleName());
-        }
+        this.documentMetadata = DocumentAnnotationUtil.getDocumentObjectMetadata(voClass);
+        this.documentMetadata.checkIdField();
     }
 
+    /**
+     * Get document by ID.
+     *
+     * The implementation uses a search request rather than a get document
+     * request because we want to get this document for in a list of index
+     * aliases with cannot be done with the get document request.
+     */
     @Override
     public VO getById(String id) {
         try {
             String documentType = documentMetadata.getDocumentType();
+            String idField = documentMetadata.getIdField();
 
-            // Build term id query
-            QueryBuilder query = QueryBuilders.termQuery(documentMetadata.getIdField(), id);
+            // Prepare request
+            SearchRequest request = requestFactory.prepareSearch(documentType);
 
-            // Build request
-            SearchRequest request = requestFactory.prepareSearch(documentType, query);
+            // Build term id query
+            request.source().query(QueryBuilders.termQuery(idField, id));
 
             // Execute request
+            ESRequestFactory.logRequest(LOGGER, request);
             SearchResponse result = client.search(request, RequestOptions.DEFAULT);
 
             // Parse result list
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericSuggestRepository.java b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericSuggestRepository.java
index 9be7e0ce..ed414bbd 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericSuggestRepository.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericSuggestRepository.java
@@ -60,14 +60,6 @@ public class ESGenericSuggestRepository<C extends PaginationCriteria, VO> implem
         this.parser = parser;
     }
 
-    public ESGenericSuggestRepository(
-        RestHighLevelClient client,
-        ESRequestFactory requestFactory,
-        Class<VO> voClass,
-        ESResponseParser parser) {
-        this(client, requestFactory, voClass, new ESGenericQueryFactory<C>(), parser);
-    }
-
     @Override
     public LinkedHashSet<String> suggest(String field, String searchText, Integer fetchSize, C criteria) {
         int size = fetchSize != null ? fetchSize : 10;
@@ -104,10 +96,10 @@ public class ESGenericSuggestRepository<C extends PaginationCriteria, VO> implem
         request.source().size(0);
         request.source().aggregation(agg);
 
-        LOGGER.debug(request.toString());
 
         SearchResponse searchResponse = null;
         try {
+            ESRequestFactory.logRequest(LOGGER, request);
             searchResponse = client.search(request, RequestOptions.DEFAULT);
         } catch (IOException e) {
             throw new RuntimeException(e);
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/repository/es/DataDiscoveryRepositoryImpl.java b/backend/src/main/java/fr/inra/urgi/faidare/repository/es/DataDiscoveryRepositoryImpl.java
index 73662c5d..e13faece 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/repository/es/DataDiscoveryRepositoryImpl.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/repository/es/DataDiscoveryRepositoryImpl.java
@@ -91,7 +91,7 @@ public class DataDiscoveryRepositoryImpl implements DataDiscoveryRepository {
             SearchRequest request = prepareSearchRequest(criteria);
 
             // Execute request
-            LOGGER.debug(request.toString());
+            ESRequestFactory.logRequest(LOGGER, request);
             SearchResponse response = client.search(request, RequestOptions.DEFAULT);
 
             // Parse request result
@@ -112,8 +112,15 @@ public class DataDiscoveryRepositoryImpl implements DataDiscoveryRepository {
         QueryBuilder query = queryFactory.createQueryExcludingFields(criteria, documentFieldsInFacets);
 
         // Prepare search request with query
-        SearchRequest request = ESGenericFindRepository.prepareSearchRequest(
-            query, criteria, documentMetadata, requestFactory);
+        SearchRequest request = requestFactory.prepareSearch(query, criteria);
+
+        // Set pagination
+        int size = criteria.getPageSize().intValue();
+        int from = criteria.getPage().intValue() * size;
+
+        request.source()
+            .from(from)
+            .size(size);
 
         // Build facet aggregations
         if (facetFields != null) {
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/repository/es/GermplasmAttributeRepositoryImpl.java b/backend/src/main/java/fr/inra/urgi/faidare/repository/es/GermplasmAttributeRepositoryImpl.java
index 2fee54cd..5417cd9a 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/repository/es/GermplasmAttributeRepositoryImpl.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/repository/es/GermplasmAttributeRepositoryImpl.java
@@ -7,6 +7,7 @@ import fr.inra.urgi.faidare.elasticsearch.ESRequestFactory;
 import fr.inra.urgi.faidare.elasticsearch.ESResponseParser;
 import fr.inra.urgi.faidare.elasticsearch.repository.ESFindRepository;
 import fr.inra.urgi.faidare.elasticsearch.repository.impl.ESGenericFindRepository;
+import fr.inra.urgi.faidare.elasticsearch.query.impl.ESGenericQueryFactory;
 import org.elasticsearch.client.RestHighLevelClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Repository;
@@ -27,7 +28,7 @@ public class GermplasmAttributeRepositoryImpl
         ESRequestFactory requestFactory
     ) {
         Class<GermplasmAttributeValueListVO> voClass = GermplasmAttributeValueListVO.class;
-        findAttributeRepository = new ESGenericFindRepository<>(client, requestFactory, voClass, parser);
+        findAttributeRepository = new ESGenericFindRepository<>(client, requestFactory, voClass, new ESGenericQueryFactory<>(), parser);
     }
 
     @Override
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/repository/es/GermplasmRepositoryImpl.java b/backend/src/main/java/fr/inra/urgi/faidare/repository/es/GermplasmRepositoryImpl.java
index e89383e1..c1476788 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/repository/es/GermplasmRepositoryImpl.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/repository/es/GermplasmRepositoryImpl.java
@@ -57,7 +57,7 @@ public class GermplasmRepositoryImpl implements GermplasmRepository {
         this.parser = parser;
         Class<GermplasmVO> voClass = GermplasmVO.class;
         this.queryFactory = new ESGenericQueryFactory<>();
-        this.findRepository = new ESGenericFindRepository<>(client, requestFactory, voClass, this.parser);
+        this.findRepository = new ESGenericFindRepository<>(client, requestFactory, voClass, queryFactory, this.parser);
         this.getByIdRepository = new ESGenericGetByIdRepository<>(client, requestFactory, voClass, this.parser);
     }
 
@@ -78,10 +78,12 @@ public class GermplasmRepositoryImpl implements GermplasmRepository {
         return findRepository.find(criteria);
     }
 
+    // TODO: Replace this using a ESGenericGetByIdRepository<PedigreeVO>
     @Override
     public PedigreeVO findPedigree(String germplasmDbId) {
         QueryBuilder termQuery = QueryBuilders.termQuery("germplasmDbId", germplasmDbId);
-        SearchRequest request = requestFactory.prepareSearch("germplasmPedigree", termQuery);
+        SearchRequest request = requestFactory.prepareSearch("germplasmPedigree");
+        request.source().query(termQuery);
         SearchResponse response = null;
         try {
             response = client.search(request, RequestOptions.DEFAULT);
@@ -108,10 +110,12 @@ public class GermplasmRepositoryImpl implements GermplasmRepository {
         return pedigreeVO;
     }
 
+    // TODO: Replace this using a ESGenericGetByIdRepository<ProgenyVO>
     @Override
     public ProgenyVO findProgeny(String germplasmDbId) {
         QueryBuilder termQuery = QueryBuilders.termQuery("germplasmDbId", germplasmDbId);
-        SearchRequest request = requestFactory.prepareSearch("germplasmProgeny", termQuery);
+        SearchRequest request = requestFactory.prepareSearch("germplasmProgeny");
+        request.source().query(termQuery);
         SearchResponse response = null;
         try {
             response = client.search(request, RequestOptions.DEFAULT);
diff --git a/backend/src/main/java/fr/inra/urgi/gpds/domain/criteria/base/MaxResultWindowPagination.java b/backend/src/main/java/fr/inra/urgi/gpds/domain/criteria/base/MaxResultWindowPagination.java
new file mode 100644
index 00000000..c55fb0f7
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/gpds/domain/criteria/base/MaxResultWindowPagination.java
@@ -0,0 +1,38 @@
+package fr.inra.urgi.gpds.domain.criteria.base;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Constraint on pagination criteria to not get over the max result window
+ *
+ * @author gcornut
+ */
+@Target({TYPE, ANNOTATION_TYPE})
+@Retention(RUNTIME)
+@Constraint(validatedBy = PaginationMaxResultValidator.class)
+@Documented
+public @interface MaxResultWindowPagination {
+
+    /**
+     * Default constrain on elasticsearch search result window
+     *
+     * @link https://www.elastic.co/guide/en/elasticsearch/reference/2.4/index-modules.html
+     */
+    long MAX_RESULT_WINDOW = 10000L;
+
+    long value() default MAX_RESULT_WINDOW;
+
+    String message() default "";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+}
diff --git a/backend/src/test/java/fr/inra/urgi/faidare/api/brapi/v1/BrapiExceptionHandlerTest.java b/backend/src/test/java/fr/inra/urgi/faidare/api/brapi/v1/BrapiExceptionHandlerTest.java
index d395c9d0..4f4895a3 100644
--- a/backend/src/test/java/fr/inra/urgi/faidare/api/brapi/v1/BrapiExceptionHandlerTest.java
+++ b/backend/src/test/java/fr/inra/urgi/faidare/api/brapi/v1/BrapiExceptionHandlerTest.java
@@ -48,9 +48,6 @@ class BrapiExceptionHandlerTest {
                 "       {" +
                 "           \"name\":\"Bad Request: Page size cannot be above 1000\"," +
                 "           \"code\":\"400\"" +
-                "       },{" +
-                "           \"name\":\"Bad Request: The result window (page x pageSize) cannot be over 10000. Please use an export API to download all the requested data.\"," +
-                "           \"code\":\"400\"" +
                 "       }" +
                 "   ]," +
                 "   \"datafiles\": []\n" +
diff --git a/backend/src/test/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationValidatorTest.java b/backend/src/test/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationValidatorTest.java
index 368e64ce..2fed235a 100644
--- a/backend/src/test/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationValidatorTest.java
+++ b/backend/src/test/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationValidatorTest.java
@@ -1,5 +1,6 @@
 package fr.inra.urgi.faidare.domain.criteria.base;
 
+import fr.inra.urgi.gpds.domain.datadiscovery.criteria.DataDiscoveryCriteriaImpl;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
@@ -67,18 +68,33 @@ public class PaginationValidatorTest {
     }
 
     @Test
-    public void should_Control_Max_Result_Window() {
+    public void should_Control_No_Max_Result_Window() {
+        // The default pagination criteria does not have a max result window
         PaginationCriteriaImpl criteria = new PaginationCriteriaImpl();
 
         criteria.setPageSize(101L);
         criteria.setPage(100L);
 
+        Set<ConstraintViolation<PaginationCriteria>> violations;
+        violations = validator.validate(((PaginationCriteria) criteria));
+        Assertions.assertThat(violations).isNotNull().hasSize(0);
+    }
+
+    @Test
+    public void should_Control_Data_Discovery_Criteria_Max_Result_Window() {
+        // The data discovery criteria does have a max result window
+        DataDiscoveryCriteriaImpl criteria = new DataDiscoveryCriteriaImpl();
+
+        criteria.setPageSize(101L);
+        criteria.setPage(100L);
+
         Set<ConstraintViolation<PaginationCriteria>> violations;
         violations = validator.validate(((PaginationCriteria) criteria));
         Assertions.assertThat(violations).isNotNull().hasSize(1);
-        Assertions.assertThat(violations)
-            .extracting("message")
-            .containsOnly(PaginationMaxResultValidator.ERROR_MAX_RESULT_WINDOW);
+        String message = violations.iterator().next().getMessage();
+        Assertions.assertThat(message)
+            .startsWith("The result window (page x pageSize) cannot be over "
+                + MaxResultWindowPagination.MAX_RESULT_WINDOW);
     }
 
     @Test
diff --git a/backend/src/test/java/fr/inra/urgi/faidare/elasticsearch/ESRequestFactoryTest.java b/backend/src/test/java/fr/inra/urgi/faidare/elasticsearch/ESRequestFactoryTest.java
index 5ff7d1ca..d43f1bd5 100644
--- a/backend/src/test/java/fr/inra/urgi/faidare/elasticsearch/ESRequestFactoryTest.java
+++ b/backend/src/test/java/fr/inra/urgi/faidare/elasticsearch/ESRequestFactoryTest.java
@@ -41,7 +41,7 @@ class ESRequestFactoryTest {
 
         when(userGroupsResourceClient.getUserGroups()).thenReturn(groups);
 
-        String[] aliases = requestFactory.getAliases(documentType);
+        String[] aliases = requestFactory.getAliases(documentType, null);
         assertThat(aliases).containsExactly(index0);
     }
 
diff --git a/backend/src/test/java/fr/inra/urgi/gpds/api/gnpis/v1/DataDiscoveryControllerTest.java b/backend/src/test/java/fr/inra/urgi/gpds/api/gnpis/v1/DataDiscoveryControllerTest.java
new file mode 100644
index 00000000..f6a29afd
--- /dev/null
+++ b/backend/src/test/java/fr/inra/urgi/gpds/api/gnpis/v1/DataDiscoveryControllerTest.java
@@ -0,0 +1,53 @@
+package fr.inra.urgi.gpds.api.gnpis.v1;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import fr.inra.urgi.gpds.domain.datadiscovery.criteria.DataDiscoveryCriteriaImpl;
+import fr.inra.urgi.gpds.repository.es.DataDiscoveryRepository;
+import fr.inra.urgi.gpds.repository.file.DataSourceRepository;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author gcornut
+ */
+@ExtendWith(SpringExtension.class)
+@WebMvcTest(controllers = DataDiscoveryController.class)
+public class DataDiscoveryControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @MockBean
+    private DataDiscoveryRepository repository;
+
+    @MockBean
+    private DataSourceRepository dataSourceRepository;
+
+    @Test
+    void should_Throw_Pagination_Max_Size_Exception() throws Exception {
+        DataDiscoveryCriteriaImpl criteria = new DataDiscoveryCriteriaImpl();
+        criteria.setPage(10L);
+        criteria.setPageSize(1000L);
+
+        MvcResult res = mockMvc.perform(
+            post("/gnpis/v1/datadiscovery/search")
+                .contentType(MediaType.APPLICATION_JSON_UTF8)
+                .content(new ObjectMapper().writeValueAsString(criteria))
+        ).andExpect(status().isBadRequest()).andReturn();
+
+        assertThat(res.getResponse().getContentAsString()).contains(
+            "The result window (page x pageSize) cannot be over 10000"
+        );
+    }
+}
-- 
GitLab


From f112c2b37f3d9c73a38e51b1d2a1f89205dbc9a2 Mon Sep 17 00:00:00 2001
From: jdestin <jeremy.destin@inra.fr>
Date: Fri, 21 Feb 2020 11:44:32 +0100
Subject: [PATCH 2/3] fix: Rebase on master.GNP-5623

---
 .../api/brapi/v1/BrapiExceptionHandler.java   |  8 +------
 .../faidare/v1/DataDiscoveryController.java   | 12 ++++------
 .../base/MaxResultWindowPagination.java       |  2 +-
 .../repository/impl/BaseESRepository.java     |  1 +
 .../impl/ESGenericFindRepository.java         | 22 ++++++++-----------
 backend/src/main/resources/application.yml    |  5 -----
 6 files changed, 16 insertions(+), 34 deletions(-)
 rename backend/src/main/java/fr/inra/urgi/{gpds => faidare}/domain/criteria/base/MaxResultWindowPagination.java (95%)

diff --git a/backend/src/main/java/fr/inra/urgi/faidare/api/brapi/v1/BrapiExceptionHandler.java b/backend/src/main/java/fr/inra/urgi/faidare/api/brapi/v1/BrapiExceptionHandler.java
index d30b90c7..580cf146 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/api/brapi/v1/BrapiExceptionHandler.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/api/brapi/v1/BrapiExceptionHandler.java
@@ -6,13 +6,7 @@ import fr.inra.urgi.faidare.api.brapi.v1.exception.BrapiPaginationException;
 import fr.inra.urgi.faidare.domain.brapi.v1.response.BrapiPagination;
 import fr.inra.urgi.faidare.domain.brapi.v1.response.BrapiStatus;
 import fr.inra.urgi.faidare.domain.response.ApiResponseFactory;
-import fr.inra.urgi.gpds.api.BadRequestException;
-import fr.inra.urgi.gpds.api.NotFoundException;
-import fr.inra.urgi.gpds.api.brapi.v1.exception.BrapiPaginationException;
-import fr.inra.urgi.gpds.domain.brapi.v1.response.BrapiPagination;
-import fr.inra.urgi.gpds.domain.brapi.v1.response.BrapiStatus;
-import fr.inra.urgi.gpds.domain.response.ApiResponseFactory;
-import fr.inra.urgi.gpds.elasticsearch.repository.impl.ESGenericFindRepository;
+import fr.inra.urgi.faidare.elasticsearch.repository.impl.ESGenericFindRepository;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.validation.BindException;
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/api/faidare/v1/DataDiscoveryController.java b/backend/src/main/java/fr/inra/urgi/faidare/api/faidare/v1/DataDiscoveryController.java
index c1424ed2..0463bddd 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/api/faidare/v1/DataDiscoveryController.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/api/faidare/v1/DataDiscoveryController.java
@@ -15,7 +15,6 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 import javax.validation.Valid;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
@@ -42,15 +41,12 @@ public class DataDiscoveryController {
         @RequestParam(required = false) String text,
         @RequestParam(required = false) Integer fetchSize,
         @RequestBody(required = false) @Valid DataDiscoveryCriteriaImpl criteria
-    ) throws UnsupportedEncodingException {
-        if (fetchSize == null) {
-            fetchSize = Integer.MAX_VALUE;
-        }
     ) {
-        return dataDiscoveryRepository.suggest(field, StringFunctions.asUTF8(text), fetchSize, criteria);
-    }
+            return dataDiscoveryRepository.suggest(field, StringFunctions.asUTF8(text), fetchSize, criteria);
+        }
+
 
-    @ApiOperation("Search for data discovery documents")
+        @ApiOperation("Search for data discovery documents")
     @PostMapping(value = "/search", consumes = APPLICATION_JSON_VALUE)
     public DataDiscoveryResponse search(
         @RequestBody @Valid DataDiscoveryCriteriaImpl criteria
diff --git a/backend/src/main/java/fr/inra/urgi/gpds/domain/criteria/base/MaxResultWindowPagination.java b/backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/MaxResultWindowPagination.java
similarity index 95%
rename from backend/src/main/java/fr/inra/urgi/gpds/domain/criteria/base/MaxResultWindowPagination.java
rename to backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/MaxResultWindowPagination.java
index c55fb0f7..2fc880f6 100644
--- a/backend/src/main/java/fr/inra/urgi/gpds/domain/criteria/base/MaxResultWindowPagination.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/domain/criteria/base/MaxResultWindowPagination.java
@@ -1,4 +1,4 @@
-package fr.inra.urgi.gpds.domain.criteria.base;
+package fr.inra.urgi.faidare.domain.criteria.base;
 
 import javax.validation.Constraint;
 import javax.validation.Payload;
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/BaseESRepository.java b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/BaseESRepository.java
index 81d7d317..aa3b6796 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/BaseESRepository.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/BaseESRepository.java
@@ -4,6 +4,7 @@ import fr.inra.urgi.faidare.domain.criteria.base.PaginationCriteria;
 import fr.inra.urgi.faidare.domain.response.PaginatedList;
 import fr.inra.urgi.faidare.elasticsearch.ESRequestFactory;
 import fr.inra.urgi.faidare.elasticsearch.ESResponseParser;
+import fr.inra.urgi.faidare.elasticsearch.query.impl.ESGenericQueryFactory;
 import fr.inra.urgi.faidare.elasticsearch.repository.ESFindRepository;
 import fr.inra.urgi.faidare.elasticsearch.repository.ESGetByIdRepository;
 import org.elasticsearch.client.RestHighLevelClient;
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericFindRepository.java b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericFindRepository.java
index a3ee3408..f55cdf1d 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericFindRepository.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/elasticsearch/repository/impl/ESGenericFindRepository.java
@@ -2,18 +2,14 @@ package fr.inra.urgi.faidare.elasticsearch.repository.impl;
 
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
-import fr.inra.urgi.gpds.domain.criteria.base.PaginationCriteria;
-import fr.inra.urgi.gpds.domain.response.PaginatedList;
-import fr.inra.urgi.gpds.domain.response.Pagination;
-import fr.inra.urgi.gpds.domain.response.PaginationImpl;
-import fr.inra.urgi.gpds.elasticsearch.ESRequestFactory;
-import fr.inra.urgi.gpds.elasticsearch.ESResponseParser;
-import fr.inra.urgi.gpds.elasticsearch.query.ESQueryFactory;
-import fr.inra.urgi.gpds.elasticsearch.repository.ESFindRepository;
-import fr.inra.urgi.faidare.domain.criteria.base.SortCriteria;
-import fr.inra.urgi.faidare.elasticsearch.document.DocumentAnnotationUtil;
-import fr.inra.urgi.faidare.elasticsearch.document.DocumentMetadata;
-import fr.inra.urgi.faidare.elasticsearch.query.impl.ESGenericQueryFactory;
+import fr.inra.urgi.faidare.domain.criteria.base.PaginationCriteria;
+import fr.inra.urgi.faidare.domain.response.PaginatedList;
+import fr.inra.urgi.faidare.domain.response.Pagination;
+import fr.inra.urgi.faidare.domain.response.PaginationImpl;
+import fr.inra.urgi.faidare.elasticsearch.ESRequestFactory;
+import fr.inra.urgi.faidare.elasticsearch.ESResponseParser;
+import fr.inra.urgi.faidare.elasticsearch.query.ESQueryFactory;
+import fr.inra.urgi.faidare.elasticsearch.repository.ESFindRepository;
 import org.elasticsearch.action.search.SearchRequest;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.client.RequestOptions;
@@ -30,7 +26,7 @@ import java.util.Objects;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 
-import static fr.inra.urgi.gpds.domain.criteria.base.MaxResultWindowPagination.MAX_RESULT_WINDOW;
+import static fr.inra.urgi.faidare.domain.criteria.base.MaxResultWindowPagination.MAX_RESULT_WINDOW;
 
 /**
  * Generic implementation of an ElasticSearch document repository which can find documents by criteria.
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
index 8f989fcc..e0f33da1 100644
--- a/backend/src/main/resources/application.yml
+++ b/backend/src/main/resources/application.yml
@@ -66,11 +66,6 @@ faidare:
     url: http://tropgenedb.cirad.fr
     name: CIRAD TropGENE
     image: https://urgi.versailles.inra.fr/files/faidare/logo/CIRAD.jpg
-  # EVA
-  - uri: https://www.ebi.ac.uk/eva
-    url: https://www.ebi.ac.uk/eva
-    name: EVA
-    image: https://urgi.versailles.inra.fr/files/faidare/logo/EVA.png
   # ENA
   - uri: https://www.ebi.ac.uk/eva
     url: https://www.ebi.ac.uk/eva
-- 
GitLab


From 44be382e211ee95ebbeb1346b019cf43b2b32aad Mon Sep 17 00:00:00 2001
From: jdestin <jeremy.destin@inra.fr>
Date: Fri, 21 Feb 2020 16:50:15 +0100
Subject: [PATCH 3/3] fix: Fix backend tests.GNP-5623

---
 .../api/faidare}/v1/DataDiscoveryControllerTest.java | 12 ++++--------
 .../criteria/base/PaginationValidatorTest.java       |  2 +-
 2 files changed, 5 insertions(+), 9 deletions(-)
 rename backend/src/test/java/fr/inra/urgi/{gpds/api/gnpis => faidare/api/faidare}/v1/DataDiscoveryControllerTest.java (82%)

diff --git a/backend/src/test/java/fr/inra/urgi/gpds/api/gnpis/v1/DataDiscoveryControllerTest.java b/backend/src/test/java/fr/inra/urgi/faidare/api/faidare/v1/DataDiscoveryControllerTest.java
similarity index 82%
rename from backend/src/test/java/fr/inra/urgi/gpds/api/gnpis/v1/DataDiscoveryControllerTest.java
rename to backend/src/test/java/fr/inra/urgi/faidare/api/faidare/v1/DataDiscoveryControllerTest.java
index f6a29afd..6ec3b6e1 100644
--- a/backend/src/test/java/fr/inra/urgi/gpds/api/gnpis/v1/DataDiscoveryControllerTest.java
+++ b/backend/src/test/java/fr/inra/urgi/faidare/api/faidare/v1/DataDiscoveryControllerTest.java
@@ -1,9 +1,8 @@
-package fr.inra.urgi.gpds.api.gnpis.v1;
+package fr.inra.urgi.faidare.api.faidare.v1;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import fr.inra.urgi.gpds.domain.datadiscovery.criteria.DataDiscoveryCriteriaImpl;
-import fr.inra.urgi.gpds.repository.es.DataDiscoveryRepository;
-import fr.inra.urgi.gpds.repository.file.DataSourceRepository;
+import fr.inra.urgi.faidare.domain.datadiscovery.criteria.DataDiscoveryCriteriaImpl;
+import fr.inra.urgi.faidare.repository.es.DataDiscoveryRepository;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -31,9 +30,6 @@ public class DataDiscoveryControllerTest {
     @MockBean
     private DataDiscoveryRepository repository;
 
-    @MockBean
-    private DataSourceRepository dataSourceRepository;
-
     @Test
     void should_Throw_Pagination_Max_Size_Exception() throws Exception {
         DataDiscoveryCriteriaImpl criteria = new DataDiscoveryCriteriaImpl();
@@ -41,7 +37,7 @@ public class DataDiscoveryControllerTest {
         criteria.setPageSize(1000L);
 
         MvcResult res = mockMvc.perform(
-            post("/gnpis/v1/datadiscovery/search")
+            post("/faidare/v1/datadiscovery/search")
                 .contentType(MediaType.APPLICATION_JSON_UTF8)
                 .content(new ObjectMapper().writeValueAsString(criteria))
         ).andExpect(status().isBadRequest()).andReturn();
diff --git a/backend/src/test/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationValidatorTest.java b/backend/src/test/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationValidatorTest.java
index 2fed235a..4cc9bd4b 100644
--- a/backend/src/test/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationValidatorTest.java
+++ b/backend/src/test/java/fr/inra/urgi/faidare/domain/criteria/base/PaginationValidatorTest.java
@@ -1,6 +1,6 @@
 package fr.inra.urgi.faidare.domain.criteria.base;
 
-import fr.inra.urgi.gpds.domain.datadiscovery.criteria.DataDiscoveryCriteriaImpl;
+import fr.inra.urgi.faidare.domain.datadiscovery.criteria.DataDiscoveryCriteriaImpl;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
-- 
GitLab