Merge branch 'stable-3.5' into stable-3.6

* stable-3.5: (23 commits)
  Prune _source to only contain stored fields
  Introduce 'elasticsearch.codec' config
  Disable tracking total number of hits
  Use bool 'filter' queries instead of 'match'
  Replace camelCase format name dateOptionalTime
  Return cardinality from predicate when available
  Update testcontainers to 1.17.5
  Update testcontainers to 1.16.3
  Elasticsearch tests: Recreate container for each test suite
  Paginate no-limit queries
  Introduce a SEARCH_AFTER index pagination type
  Remove support for vulnerable ES versions
  Add 7.16.* to supported versions
  tests: Print container logs of startup failure
  Use official elasticsearch docker image for tests
  Add 7.10.* to supported versions
  Use errors output field to determine _bulk API failure
  elasticsearch-rest-client: Update to latest 8.3.2
  docs: Split README content into more files
  Do not wait for refresh when AutoFlush.DISABLED
  ...

Release-Notes: skip
Change-Id: I71cc9b49874c408e84e448586f1a49168dc60f72
diff --git a/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index d4f2c5d..fce86b9 100644
--- a/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -436,7 +436,7 @@
             };
           }
         } else {
-          logger.atSevere().log(statusLine.getReasonPhrase());
+          logger.atSevere().log("%s", statusLine.getReasonPhrase());
         }
         return new ListResultSet<>(ImmutableList.of());
       } catch (IOException e) {
diff --git a/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java b/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java
index 100022a..4217f45 100644
--- a/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java
+++ b/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java
@@ -49,7 +49,7 @@
           String.format(
               "Failed to discover index versions for %s: %d: %s",
               name, statusLine.getStatusCode(), statusLine.getReasonPhrase());
-      logger.atSevere().log(message);
+      logger.atSevere().log("%s", message);
       throw new IOException(message);
     }
 
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/test/java/com/google/gerrit/elasticsearch/ElasticContainer.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticContainer.java
index 4fddc72..86b1bac 100644
--- a/src/test/java/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -33,7 +33,7 @@
       container.start();
     } catch (ContainerLaunchException e) {
       logger.atSevere().log(
-          "Failed to launch elastic container. Logs from container :\n" + container.getLogs());
+          "Failed to launch elastic container. Logs from container :\n%s", container.getLogs());
       throw e;
     }
     return container;
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
index 9160b9e..6d0602b 100644
--- a/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
@@ -15,14 +15,18 @@
 package com.google.gerrit.elasticsearch;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.GerritTestName;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
+import com.google.gerrit.testing.InMemoryRepositoryManager.Repo;
 import com.google.inject.Injector;
 import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
 import org.apache.http.impl.nio.client.HttpAsyncClients;
@@ -79,6 +83,52 @@
     ElasticTestUtils.createAllIndexes(injector);
   }
 
+  @Test
+  @Override
+  // TODO(davido): overrides byTopic() method to adjust to ES behaviour for
+  // "prefixtopic" predicate. This should be fixed in a follow-up change.
+  public void byTopic() throws Exception {
+
+    TestRepository<Repo> repo = createProject("repo");
+    ChangeInserter ins1 = newChangeWithTopic(repo, "feature1");
+    Change change1 = insert(repo, ins1);
+
+    ChangeInserter ins2 = newChangeWithTopic(repo, "feature2");
+    Change change2 = insert(repo, ins2);
+
+    ChangeInserter ins3 = newChangeWithTopic(repo, "Cherrypick-feature2");
+    Change change3 = insert(repo, ins3);
+
+    ChangeInserter ins4 = newChangeWithTopic(repo, "feature2-fixup");
+    Change change4 = insert(repo, ins4);
+
+    ChangeInserter ins5 = newChangeWithTopic(repo, "https://gerrit.local");
+    Change change5 = insert(repo, ins5);
+
+    ChangeInserter ins6 = newChangeWithTopic(repo, "git_gerrit_training");
+    Change change6 = insert(repo, ins6);
+
+    Change change_no_topic = insert(repo, newChange(repo));
+
+    assertQuery("intopic:foo");
+    assertQuery("intopic:feature1", change1);
+    assertQuery("intopic:feature2", change4, change3, change2);
+    assertQuery("topic:feature2", change2);
+    assertQuery("intopic:feature2", change4, change3, change2);
+    assertQuery("intopic:fixup", change4);
+    assertQuery("intopic:gerrit", change6, change5);
+    assertQuery("topic:\"\"", change_no_topic);
+    assertQuery("intopic:\"\"", change_no_topic);
+
+    assume().that(getSchema().hasField(ChangeField.PREFIX_TOPIC)).isTrue();
+    // change3 is considered by ES in prefixtopic:feature query, see
+    // https://www.elastic.co/guide/en/elasticsearch/reference/8.2/query-dsl-match-query-phrase-prefix.html
+    // assertQuery("prefixtopic:feature", change4, change2, change1);
+    assertQuery("prefixtopic:feature", change4, change3, change2, change1);
+    assertQuery("prefixtopic:Cher", change3);
+    assertQuery("prefixtopic:feature22");
+  }
+
   @Override
   protected Injector createInjector() {
     return ElasticTestUtils.createInjector(config, testName, container);