Merge branch 'stable-3.5' into stable-3.6

* stable-3.5:
  Remove dependency on //java/com/google/gerrit/proto
  Restore dockerized integration tests
  Adapt Bazel build and deps to latest stable-3.5
  Adapt to the latest Index interface in stable-3.5

Since Ia64a599440 TimestampRangePredicate is migrated to use Instant
instead of obsolete Date class. Adapt the ElasticQueryBuilder for this
migration.

ElasticV7QueryChangesTest overrides byTopic() method to adjust to ES
behaviour for "prefixtopic" predicate. "prefixtopic" predicate was added
in release 3.6 and that's why this is done in the merge commit.

Change-Id: Ie7c4ba0d3ddb4c50a79f51bb328fbaefc15df7ed
diff --git a/BUILD b/BUILD
index 06352ab..4750354 100644
--- a/BUILD
+++ b/BUILD
@@ -1,6 +1,5 @@
 load("@rules_java//java:defs.bzl", "java_library")
 load("//tools/bzl:junit.bzl", "junit_tests")
-load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 load(
     "//tools/bzl:plugin.bzl",
     "PLUGIN_DEPS",
@@ -8,72 +7,54 @@
     "gerrit_plugin",
 )
 
-gerrit_plugin   (
+gerrit_plugin(
     name = "index-elasticsearch",
     srcs = glob(["src/main/java/**/*.java"]),
     deps = [
-        "//java/com/google/gerrit/common:annotations",
-        "//java/com/google/gerrit/entities",
-        "//java/com/google/gerrit/exceptions",
-        "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gerrit/index",
-        "//java/com/google/gerrit/index:query_exception",
-        "//java/com/google/gerrit/index/project",
-        "//java/com/google/gerrit/lifecycle",
-        "//java/com/google/gerrit/proto",
-        "//java/com/google/gerrit/server",
-        "//lib:gson",
-        "//lib:guava",
-        "//lib:jgit",
-        "//lib:protobuf",
-        "//lib/commons:lang",
         "@elasticsearch-rest-client//jar",
-        "//lib/flogger:api",
-        "//lib/guice",
-        "//lib/guice:guice-assistedinject",
         "@httpasyncclient//jar",
-        "//lib/httpcomponents:httpclient",
-        "//lib/httpcomponents:httpcore",
         "@httpcore-nio//jar",
         "@jackson-core//jar",
     ],
 )
 
+ELASTICSEARCH_DEPS = [
+    "@docker-java-api//jar",
+    "@docker-java-transport//jar",
+    "@duct-tape//jar",
+    "@httpasyncclient//jar",
+    "@jackson-annotations//jar",
+    "@jackson-core//jar",
+    "@jna//jar",
+    "@testcontainers-elasticsearch//jar",
+    "@testcontainers//jar",
+]
+
 java_library(
-    name = "elasticsearch_test_utils",
+    name = "index-elasticsearch__plugin_test_deps",
     testonly = True,
     srcs = [],
     visibility = ["//visibility:public"],
-    deps = [
-        "//java/com/google/gerrit/index",
-        "//lib:guava",
-        "//lib:jgit",
-        "//lib:junit",
-        "//lib/guice",
-        "//lib/httpcomponents:httpcore",
-        "//lib/jackson:jackson-annotations",
-        "//lib/testcontainers",
-        "//lib/testcontainers:docker-java-api",
-        "//lib/testcontainers:docker-java-transport",
-        "@testcontainers-elasticsearch//jar",
+    exports = ELASTICSEARCH_DEPS,
+)
+
+java_library(
+    name = "elasticsearch_test_utils",
+    testonly = True,
+    srcs = glob(
+        ["src/test/java/**/*.java"],
+        exclude = ["src/test/java/**/*Test.java"],
+    ),
+    visibility = ["//visibility:public"],
+    deps = ELASTICSEARCH_DEPS + PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
         ":index-elasticsearch__plugin",
     ],
 )
 
-ELASTICSEARCH_DEPS = [
-    ":elasticsearch_test_utils",
-    "//java/com/google/gerrit/testing:gerrit-test-util",
-    "//lib/guice",
-    "//lib:jgit",
-]
-
-HTTP_TEST_DEPS = [
-    "@httpasyncclient//jar",
-    "//lib/httpcomponents:httpclient",
-]
-
 QUERY_TESTS_DEP = "//javatests/com/google/gerrit/server/query/%s:abstract_query_tests"
 
+ACCOUNT_QUERY_TESTS_DEP = "//javatests/com/google/gerrit/server/query/account:abstract_query_tests"
+
 TYPES = [
     "account",
     "change",
@@ -85,26 +66,30 @@
 
 ELASTICSEARCH_TESTS_V7 = {i: "ElasticV7Query" + i.capitalize() + SUFFIX for i in TYPES}
 
-ELASTICSEARCH_TAGS = [
-    "docker",
-    "elastic",
-]
+[junit_tests(
+    name = "elasticsearch_query_%ss_test_V7" % name,
+    size = "enormous",
+    srcs = ["src/test/java/com/google/gerrit/elasticsearch/" + src],
+    tags = [
+        "docker",
+        "elastic",
+    ],
+    deps = ELASTICSEARCH_DEPS + PLUGIN_TEST_DEPS + [
+        QUERY_TESTS_DEP % name,
+        ":elasticsearch_test_utils",
+        ":index-elasticsearch__plugin",
+    ],
+) for name, src in ELASTICSEARCH_TESTS_V7.items()]
 
 junit_tests(
     name = "index-elasticsearch_tests",
     size = "small",
     srcs = glob(
         ["src/test/java/**/*Test.java"],
-        exclude = ["Elastic*Query*" + SUFFIX],
+        exclude = ["src/test/java/**/Elastic*Query*" + SUFFIX],
     ),
     tags = ["elastic"],
-    deps = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
-        "//java/com/google/gerrit/testing:gerrit-test-util",
-        "//lib:guava",
-        "//lib:jgit",
-        "//lib/guice",
-        "//lib/httpcomponents:httpcore",
-        "//lib/truth",
-        ":elasticsearch_test_utils",
+    deps = PLUGIN_TEST_DEPS + [
+        ":index-elasticsearch__plugin",
     ],
 )
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index b863175..fc1420c 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -5,6 +5,12 @@
 # Ensure artifacts compatibility by selecting them from the Bill Of Materials
 # https://search.maven.org/artifact/net.openhft/chronicle-bom/2.20.191/pom
 def external_plugin_deps():
+    maven_jar(
+        name = "testcontainers",
+        artifact = "org.testcontainers:testcontainers:" + TESTCONTAINERS_VERSION,
+        sha1 = "95c6cfde71c2209f0c29cb14e432471e0b111880",
+    )
+
     # When upgrading elasticsearch-rest-client, also upgrade httpcore-nio
     # and httpasyncclient as necessary in tools/nongoogle.bzl. Consider
     # also the other org.apache.httpcomponents dependencies in
@@ -21,6 +27,38 @@
         sha1 = "595e3a50f59cd3c1d281ca6c1bc4037e277a1353",
     )
 
+    maven_jar(
+        name = "duct-tape",
+        artifact = "org.rnorth.duct-tape:duct-tape:1.0.8",
+        sha1 = "92edc22a9ab2f3e17c9bf700aaee377d50e8b530",
+    )
+
+    maven_jar(
+        name = "visible-assertions",
+        artifact = "org.rnorth.visible-assertions:visible-assertions:2.1.2",
+        sha1 = "20d31a578030ec8e941888537267d3123c2ad1c1",
+    )
+
+    maven_jar(
+        name = "jna",
+        artifact = "net.java.dev.jna:jna:5.5.0",
+        sha1 = "0e0845217c4907822403912ad6828d8e0b256208",
+    )
+
+    DOCKER_JAVA_VERS = "3.2.8"
+
+    maven_jar(
+        name = "docker-java-api",
+        artifact = "com.github.docker-java:docker-java-api:" + DOCKER_JAVA_VERS,
+        sha1 = "4ac22a72d546a9f3523cd4b5fabffa77c4a6ec7c",
+    )
+
+    maven_jar(
+        name = "docker-java-transport",
+        artifact = "com.github.docker-java:docker-java-transport:" + DOCKER_JAVA_VERS,
+        sha1 = "c3b5598c67d0a5e2e780bf48f520da26b9915eab",
+    )
+
     # elasticsearch-rest-client explicitly depends on this version
     maven_jar(
         name = "httpasyncclient",
@@ -42,8 +80,13 @@
     )
 
     maven_jar(
+        name = "jackson-annotations",
+        artifact = "com.fasterxml.jackson.core:jackson-annotations:2.10.3",
+        sha1 = "0f63b3b1da563767d04d2e4d3fc1ae0cdeffebe7",
+    )
+
+    maven_jar(
         name = "httpasyncclient",
         artifact = "org.apache.httpcomponents:httpasyncclient:4.1.4",
         sha1 = "f3a3240681faae3fa46b573a4c7e50cec9db0d86",
     )
-
diff --git a/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index 562464d..90ce18b 100644
--- a/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -152,6 +152,11 @@
   }
 
   @Override
+  public void insert(V obj) {
+    replace(obj);
+  }
+
+  @Override
   public Schema<V> getSchema() {
     return schema;
   }
diff --git a/src/main/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java b/src/main/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
index 40ac603..9fc070a 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/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
index b6f6e51..1720f52 100644
--- a/src/main/resources/Documentation/build.md
+++ b/src/main/resources/Documentation/build.md
@@ -34,4 +34,20 @@
 
 ```sh
 bazelisk test plugins/index-elasticsearch/...
-```
\ No newline at end of file
+```
+
+This project can be imported into the Eclipse IDE.
+Add the plugin name to the `CUSTOM_PLUGINS` and to the
+`CUSTOM_PLUGINS_TEST_DEPS` set in Gerrit core in
+`tools/bzl/plugins.bzl`, and execute:
+
+```
+  ./tools/eclipse/project.py
+```
+
+More information about Bazel can be found in the [Gerrit
+documentation](../../../Documentation/dev-bazel.html).
+
+[Back to @PLUGIN@ documentation index][index]
+
+[index]: index.html
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticContainer.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticContainer.java
new file mode 100644
index 0000000..503852b
--- /dev/null
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import org.apache.http.HttpHost;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.elasticsearch.ElasticsearchContainer;
+import org.testcontainers.utility.DockerImageName;
+
+/* Helper class for running ES integration tests in docker container */
+public class ElasticContainer extends ElasticsearchContainer {
+  private static final int ELASTICSEARCH_DEFAULT_PORT = 9200;
+
+  public static ElasticContainer createAndStart(ElasticVersion version) {
+    ElasticContainer container = new ElasticContainer(version);
+    container.start();
+    return container;
+  }
+
+  private static String getImageName(ElasticVersion version) {
+    switch (version) {
+      case V7_6:
+        return "blacktop/elasticsearch:7.6.2";
+      case V7_7:
+        return "blacktop/elasticsearch:7.7.1";
+      case V7_8:
+        return "blacktop/elasticsearch:7.8.1";
+    }
+    throw new IllegalStateException("No tests for version: " + version.name());
+  }
+
+  private ElasticContainer(ElasticVersion version) {
+    super(
+        DockerImageName.parse(getImageName(version))
+            .asCompatibleSubstituteFor("docker.elastic.co/elasticsearch/elasticsearch"));
+  }
+
+  @Override
+  protected Logger logger() {
+    return LoggerFactory.getLogger("org.testcontainers");
+  }
+
+  public HttpHost getHttpHost() {
+    return new HttpHost(getContainerIpAddress(), getMappedPort(ELASTICSEARCH_DEFAULT_PORT));
+  }
+}
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java
new file mode 100644
index 0000000..c3ca595
--- /dev/null
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.server.LibModuleType;
+import com.google.gerrit.testing.GerritTestName;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+import java.util.Collection;
+import java.util.UUID;
+import org.eclipse.jgit.lib.Config;
+
+public final class ElasticTestUtils {
+  public static void configure(Config config, ElasticContainer container, String prefix) {
+    String hostname = container.getHttpHost().getHostName();
+    int port = container.getHttpHost().getPort();
+    config.setString("index", null, "type", "elasticsearch");
+    config.setString("elasticsearch", null, "server", "http://" + hostname + ":" + port);
+    config.setString("elasticsearch", null, "prefix", prefix);
+    config.setInt("index", null, "maxLimit", 10000);
+  }
+
+  public static void createAllIndexes(Injector injector) {
+    Collection<IndexDefinition<?, ?, ?>> indexDefs =
+        injector.getInstance(Key.get(new TypeLiteral<Collection<IndexDefinition<?, ?, ?>>>() {}));
+    for (IndexDefinition<?, ?, ?> indexDef : indexDefs) {
+      indexDef.getIndexCollection().getSearchIndex().deleteAll();
+    }
+  }
+
+  public static Config getConfig(ElasticVersion version) {
+    ElasticContainer container = ElasticContainer.createAndStart(version);
+    String indicesPrefix = UUID.randomUUID().toString();
+    Config cfg = new Config();
+    configure(cfg, container, indicesPrefix);
+    return cfg;
+  }
+
+  public static Config createConfig() {
+    Config cfg = IndexConfig.create();
+
+    // For some reason enabling the staleness checker increases the flakiness of the Elasticsearch
+    // tests. Hence disable the staleness checker.
+    cfg.setBoolean("index", null, "autoReindexIfStale", false);
+
+    return cfg;
+  }
+
+  public static void configureElasticModule(Config elasticsearchConfig) {
+    elasticsearchConfig.setString(
+        "index",
+        null,
+        "install" + LibModuleType.INDEX_MODULE_TYPE.getConfigKey(),
+        "com.google.gerrit.elasticsearch.ElasticIndexModule");
+  }
+
+  public static Injector createInjector(
+      Config config, GerritTestName testName, ElasticContainer container) {
+    Config elasticsearchConfig = new Config(config);
+    ElasticTestUtils.configureElasticModule(elasticsearchConfig);
+    InMemoryModule.setDefaults(elasticsearchConfig);
+    String indicesPrefix = testName.getSanitizedMethodName();
+    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
+  }
+
+  private ElasticTestUtils() {
+    // hide default constructor
+  }
+}
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
new file mode 100644
index 0000000..4ee5a16
--- /dev/null
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import com.google.gerrit.server.query.account.AbstractQueryAccountsTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Injector;
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class ElasticV7QueryAccountsTest extends AbstractQueryAccountsTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return ElasticTestUtils.createConfig();
+  }
+
+  private static ElasticContainer container;
+
+  @BeforeClass
+  public static void startIndexService() {
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V7_8);
+    }
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    return ElasticTestUtils.createInjector(config, testName, container);
+  }
+}
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
new file mode 100644
index 0000000..d704e40
--- /dev/null
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
@@ -0,0 +1,141 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import static com.google.common.truth.TruthJUnit.assume;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import com.google.gerrit.entities.Change;
+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.Repo;
+import com.google.inject.Injector;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
+import org.apache.http.impl.nio.client.HttpAsyncClients;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class ElasticV7QueryChangesTest extends AbstractQueryChangesTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return ElasticTestUtils.createConfig();
+  }
+
+  private static ElasticContainer container;
+  private static CloseableHttpAsyncClient client;
+
+  @BeforeClass
+  public static void startIndexService() {
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V7_8);
+      client = HttpAsyncClients.createDefault();
+      client.start();
+    }
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  @Rule public final GerritTestName testName = new GerritTestName();
+
+  @After
+  public void closeIndex() throws Exception {
+    // Close the index after each test to prevent exceeding Elasticsearch's
+    // shard limit (see Issue 10120).
+    client
+        .execute(
+            new HttpPost(
+                String.format(
+                    "http://%s:%d/%s*/_close",
+                    container.getHttpHost().getHostName(),
+                    container.getHttpHost().getPort(),
+                    testName.getSanitizedMethodName())),
+            HttpClientContext.create(),
+            null)
+        .get(5, MINUTES);
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    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);
+  }
+}
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
new file mode 100644
index 0000000..649c0bc
--- /dev/null
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import com.google.gerrit.server.query.group.AbstractQueryGroupsTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Injector;
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class ElasticV7QueryGroupsTest extends AbstractQueryGroupsTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return ElasticTestUtils.createConfig();
+  }
+
+  private static ElasticContainer container;
+
+  @BeforeClass
+  public static void startIndexService() {
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V7_8);
+    }
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    return ElasticTestUtils.createInjector(config, testName, container);
+  }
+}
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
new file mode 100644
index 0000000..d3b3d44
--- /dev/null
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import com.google.gerrit.server.query.project.AbstractQueryProjectsTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Injector;
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class ElasticV7QueryProjectsTest extends AbstractQueryProjectsTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return ElasticTestUtils.createConfig();
+  }
+
+  private static ElasticContainer container;
+
+  @BeforeClass
+  public static void startIndexService() {
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V7_8);
+    }
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    return ElasticTestUtils.createInjector(config, testName, container);
+  }
+}