Merge branch 'stable-3.5' into stable-3.6

* stable-3.5:
  Index.replace: Log responses containing errors
  Remove warning stating that Elasticsearch is not production ready
  Add trace timer around Elasticsearch's performRequest
  Fix Flogger issues flagged by error prone
  Fix incorrect symlink in build docs
  Fix Flogger compile time errors

Release-Notes: skip
Change-Id: I42642e61af9166bf1155f333fe57a01a46c5b80c
diff --git a/BUILD b/BUILD
index 4070a77..94ce436 100644
--- a/BUILD
+++ b/BUILD
@@ -90,7 +90,8 @@
         exclude = ["src/test/java/**/Elastic*Query*" + SUFFIX],
     ),
     tags = ["elastic"],
-    deps = PLUGIN_TEST_DEPS + [
+    deps = ELASTICSEARCH_DEPS + PLUGIN_TEST_DEPS + [
+        ":elasticsearch_test_utils",
         ":index-elasticsearch__plugin",
     ],
 )
diff --git a/src/main/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java b/src/main/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
index 2a5fcde..0379ed9 100644
--- a/src/main/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
+++ b/src/main/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
@@ -18,6 +18,8 @@
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.PaginationType;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.ProvisionException;
@@ -70,7 +72,12 @@
   final String prefix;
 
   @Inject
-  ElasticConfiguration(@GerritServerConfig Config cfg) {
+  ElasticConfiguration(@GerritServerConfig Config cfg, IndexConfig indexConfig) {
+    if (PaginationType.NONE == indexConfig.paginationType()) {
+      throw new ProvisionException(
+          "The 'index.paginationType = NONE' configuration is not supported by Elasticsearch");
+    }
+
     this.cfg = cfg;
     this.password = cfg.getString(SECTION_ELASTICSEARCH, null, KEY_PASSWORD);
     this.username =
diff --git a/src/main/java/com/google/gerrit/elasticsearch/ElasticMapping.java b/src/main/java/com/google/gerrit/elasticsearch/ElasticMapping.java
index 1668450..9ea9fcb 100644
--- a/src/main/java/com/google/gerrit/elasticsearch/ElasticMapping.java
+++ b/src/main/java/com/google/gerrit/elasticsearch/ElasticMapping.java
@@ -40,8 +40,10 @@
           || fieldType == FieldType.LONG) {
         mapping.addNumber(name);
       } else if (fieldType == FieldType.FULL_TEXT) {
-        mapping.addStringWithAnalyzer(name);
-      } else if (fieldType == FieldType.PREFIX || fieldType == FieldType.STORED_ONLY) {
+        mapping.addStringWithAnalyzer(name, "custom_with_char_filter");
+      } else if (fieldType == FieldType.PREFIX) {
+        mapping.addStringWithAnalyzer(name, "keyword_tokenizer");
+      } else if (fieldType == FieldType.STORED_ONLY) {
         mapping.addString(name);
       } else {
         throw new IllegalStateException("Unsupported field type: " + fieldType.getName());
@@ -101,9 +103,9 @@
       return this;
     }
 
-    Builder addStringWithAnalyzer(String name) {
+    Builder addStringWithAnalyzer(String name, String analyzer) {
       FieldProperties key = new FieldProperties(adapter.stringFieldType());
-      key.analyzer = "custom_with_char_filter";
+      key.analyzer = analyzer;
       fields.put(name, key);
       return this;
     }
diff --git a/src/main/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java b/src/main/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
index cde5560..3580089 100644
--- a/src/main/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
+++ b/src/main/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.RegexPredicate;
 import com.google.gerrit.index.query.TimestampRangePredicate;
-import java.time.Instant;
 
 public class ElasticQueryBuilder {
 
@@ -119,9 +118,8 @@
   }
 
   private <T> QueryBuilder notTimestamp(TimestampRangePredicate<T> r) throws QueryParseException {
-    if (r.getMinTimestamp().getTime() == 0) {
-      return QueryBuilders.rangeQuery(r.getField().getName())
-          .gt(Instant.ofEpochMilli(r.getMaxTimestamp().getTime()));
+    if (r.getMinTimestamp().toEpochMilli() == 0) {
+      return QueryBuilders.rangeQuery(r.getField().getName()).gt(r.getMaxTimestamp());
     }
     throw new QueryParseException("cannot negate: " + r);
   }
@@ -129,15 +127,14 @@
   private <T> QueryBuilder timestampQuery(IndexPredicate<T> p) throws QueryParseException {
     if (p instanceof TimestampRangePredicate) {
       TimestampRangePredicate<T> r = (TimestampRangePredicate<T>) p;
-      if (r.getMaxTimestamp().getTime() == Long.MAX_VALUE) {
+      if (r.getMaxTimestamp().toEpochMilli() == Long.MAX_VALUE) {
         // The time range only has the start value, search from the start to the max supported value
         // Long.MAX_VALUE
-        return QueryBuilders.rangeQuery(r.getField().getName())
-            .gte(Instant.ofEpochMilli(r.getMinTimestamp().getTime()));
+        return QueryBuilders.rangeQuery(r.getField().getName()).gte(r.getMinTimestamp());
       }
       return QueryBuilders.rangeQuery(r.getField().getName())
-          .gte(Instant.ofEpochMilli(r.getMinTimestamp().getTime()))
-          .lte(Instant.ofEpochMilli(r.getMaxTimestamp().getTime()));
+          .gte(r.getMinTimestamp())
+          .lte(r.getMaxTimestamp());
     }
     throw new QueryParseException("not a timestamp: " + p);
   }
diff --git a/src/main/java/com/google/gerrit/elasticsearch/ElasticSetting.java b/src/main/java/com/google/gerrit/elasticsearch/ElasticSetting.java
index 0059574..2fcfc90 100644
--- a/src/main/java/com/google/gerrit/elasticsearch/ElasticSetting.java
+++ b/src/main/java/com/google/gerrit/elasticsearch/ElasticSetting.java
@@ -59,6 +59,7 @@
 
       FieldProperties analyzer = new FieldProperties();
       analyzer.customWithCharFilter = customAnalyzer;
+      analyzer.keywordTokenizer = ImmutableMap.of("tokenizer", "keyword");
       fields.put("analyzer", analyzer);
       return this;
     }
@@ -92,6 +93,7 @@
     String[] mappings;
     FieldProperties customMapping;
     FieldProperties customWithCharFilter;
+    Map<String, String> keywordTokenizer;
 
     FieldProperties() {}
 
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 74fe50c..5d0dead 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -10,6 +10,16 @@
 value is not configured during site initialization, defaults to 10000, which is the default value
 of `index.max_result_window` in Elasticsearch.
 
+### index.paginationType
+
+The pagination type to use when index queries are repeated to obtain the next set of results.
+Supported values are: `OFFSET` and `SEARCH_AFTER`. For more information, refer to
+[`index.paginationType`](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#index.paginationType).
+
+Defaults to `OFFSET`.
+Note: paginationType `NONE` is not supported and Gerrit will not start if it is configured (results
+in `ProvisionException`).
+
 ## Section elasticsearch
 
 For compatibility information, please refer to the [project homepage](https://www.gerritcodereview.com/elasticsearch.html).
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
index 7e044c3..9d20edc 100644
--- a/src/test/java/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
@@ -25,6 +25,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.index.IndexConfig;
 import com.google.inject.ProvisionException;
 import java.util.Arrays;
 import org.apache.http.HttpHost;
@@ -35,7 +36,7 @@
   @Test
   public void singleServerNoOtherConfig() throws Exception {
     Config cfg = newConfig();
-    ElasticConfiguration esCfg = new ElasticConfiguration(cfg);
+    ElasticConfiguration esCfg = newElasticConfig(cfg);
     assertHosts(esCfg, "http://elastic:1234");
     assertThat(esCfg.username).isNull();
     assertThat(esCfg.password).isNull();
@@ -46,7 +47,7 @@
   public void serverWithoutPortSpecified() throws Exception {
     Config cfg = new Config();
     cfg.setString(SECTION_ELASTICSEARCH, null, KEY_SERVER, "http://elastic");
-    ElasticConfiguration esCfg = new ElasticConfiguration(cfg);
+    ElasticConfiguration esCfg = newElasticConfig(cfg);
     assertHosts(esCfg, "http://elastic:9200");
   }
 
@@ -54,7 +55,7 @@
   public void prefix() throws Exception {
     Config cfg = newConfig();
     cfg.setString(SECTION_ELASTICSEARCH, null, KEY_PREFIX, "myprefix");
-    ElasticConfiguration esCfg = new ElasticConfiguration(cfg);
+    ElasticConfiguration esCfg = newElasticConfig(cfg);
     assertThat(esCfg.prefix).isEqualTo("myprefix");
   }
 
@@ -63,7 +64,7 @@
     Config cfg = newConfig();
     cfg.setString(SECTION_ELASTICSEARCH, null, KEY_USERNAME, "myself");
     cfg.setString(SECTION_ELASTICSEARCH, null, KEY_PASSWORD, "s3kr3t");
-    ElasticConfiguration esCfg = new ElasticConfiguration(cfg);
+    ElasticConfiguration esCfg = newElasticConfig(cfg);
     assertThat(esCfg.username).isEqualTo("myself");
     assertThat(esCfg.password).isEqualTo("s3kr3t");
   }
@@ -72,7 +73,7 @@
   public void withAuthenticationPasswordOnlyUsesDefaultUsername() throws Exception {
     Config cfg = newConfig();
     cfg.setString(SECTION_ELASTICSEARCH, null, KEY_PASSWORD, "s3kr3t");
-    ElasticConfiguration esCfg = new ElasticConfiguration(cfg);
+    ElasticConfiguration esCfg = newElasticConfig(cfg);
     assertThat(esCfg.username).isEqualTo(DEFAULT_USERNAME);
     assertThat(esCfg.password).isEqualTo("s3kr3t");
   }
@@ -85,20 +86,20 @@
         null,
         KEY_SERVER,
         ImmutableList.of("http://elastic1:1234", "http://elastic2:1234"));
-    ElasticConfiguration esCfg = new ElasticConfiguration(cfg);
+    ElasticConfiguration esCfg = newElasticConfig(cfg);
     assertHosts(esCfg, "http://elastic1:1234", "http://elastic2:1234");
   }
 
   @Test
   public void noServers() throws Exception {
-    assertProvisionException(new Config());
+    assertProvisionException(new Config(), "No valid Elasticsearch servers configured");
   }
 
   @Test
   public void singleServerInvalid() throws Exception {
     Config cfg = new Config();
     cfg.setString(SECTION_ELASTICSEARCH, null, KEY_SERVER, "foo");
-    assertProvisionException(cfg);
+    assertProvisionException(cfg, "No valid Elasticsearch servers configured");
   }
 
   @Test
@@ -106,24 +107,35 @@
     Config cfg = new Config();
     cfg.setStringList(
         SECTION_ELASTICSEARCH, null, KEY_SERVER, ImmutableList.of("http://elastic1:1234", "foo"));
-    ElasticConfiguration esCfg = new ElasticConfiguration(cfg);
+    ElasticConfiguration esCfg = newElasticConfig(cfg);
     assertHosts(esCfg, "http://elastic1:1234");
   }
 
+  @Test
+  public void unsupportedPaginationTypeNone() {
+    Config cfg = new Config();
+    cfg.setString("index", null, "paginationType", "NONE");
+    assertProvisionException(
+        cfg, "The 'index.paginationType = NONE' configuration is not supported by Elasticsearch");
+  }
+
   private static Config newConfig() {
     Config config = new Config();
     config.setString(SECTION_ELASTICSEARCH, null, KEY_SERVER, "http://elastic:1234");
     return config;
   }
 
+  private static ElasticConfiguration newElasticConfig(Config cfg) {
+    return new ElasticConfiguration(cfg, IndexConfig.fromConfig(cfg).build());
+  }
+
   private void assertHosts(ElasticConfiguration cfg, Object... hostURIs) throws Exception {
     assertThat(Arrays.asList(cfg.getHosts()).stream().map(HttpHost::toURI).collect(toList()))
         .containsExactly(hostURIs);
   }
 
-  private void assertProvisionException(Config cfg) {
-    ProvisionException thrown =
-        assertThrows(ProvisionException.class, () -> new ElasticConfiguration(cfg));
-    assertThat(thrown).hasMessageThat().contains("No valid Elasticsearch servers configured");
+  private void assertProvisionException(Config cfg, String msg) {
+    ProvisionException thrown = assertThrows(ProvisionException.class, () -> newElasticConfig(cfg));
+    assertThat(thrown).hasMessageThat().contains(msg);
   }
 }