You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by ba...@apache.org on 2024/03/01 13:24:07 UTC

(jackrabbit-oak) branch issue/oak-10680 created (now 322004c9b0)

This is an automated email from the ASF dual-hosted git repository.

baedke pushed a change to branch issue/oak-10680
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git


      at 322004c9b0 OAK-10680: tests failures with com.arakelian/docker-junit-rule

This branch includes the following new commits:

     new 1a715b61dc OAK-10464: Use Testcontainers instead of com.arakelian:docker-junit-rule (#1143)
     new 322004c9b0 OAK-10680: tests failures with com.arakelian/docker-junit-rule

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(jackrabbit-oak) 01/02: OAK-10464: Use Testcontainers instead of com.arakelian:docker-junit-rule (#1143)

Posted by ba...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

baedke pushed a commit to branch issue/oak-10680
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git

commit 1a715b61dce20ec4d03a60c973d0451e8a12693c
Author: t-rana <14...@users.noreply.github.com>
AuthorDate: Tue Oct 31 19:44:26 2023 +0530

    OAK-10464: Use Testcontainers instead of com.arakelian:docker-junit-rule (#1143)
    
    * OAK-10464 testcontainers dependency
    
    * OAK-10464 testcontainers dependency
    
    * modified blob endpoint, logback, add testcontainer dependency
    
    * migrate mongo docker rule to testcontainer
    
    * minor dependency changes
    
    * minor changes
    
    * refactor dependency, removed unused ones
    
    * revert logback changes
    
    * revert logback changes
    
    * removed test container dependency from child modules
    
    * remove unused dependency
    
    ---------
    
    Co-authored-by: smiroslav <mi...@apache.org>
---
 oak-blob-cloud-azure/pom.xml                       |   4 +-
 .../cloud/azure/blobstorage/AzuriteDockerRule.java | 122 ++++++++++++++++++++
 oak-it/pom.xml                                     |   4 +-
 oak-jcr/pom.xml                                    |   4 +-
 oak-lucene/pom.xml                                 |   4 +-
 oak-parent/pom.xml                                 |   8 +-
 oak-pojosr/pom.xml                                 |   4 +-
 oak-run-commons/pom.xml                            |   4 +-
 oak-run/pom.xml                                    |   4 +-
 oak-segment-azure/pom.xml                          |   4 +-
 oak-store-document/pom.xml                         |   5 -
 .../plugins/document/MongoConnectionFactory.java   |   2 +-
 .../plugins/document/mongo/MongoDockerRule.java    | 127 +++++++++++++++------
 oak-upgrade/pom.xml                                |   8 +-
 14 files changed, 239 insertions(+), 65 deletions(-)

diff --git a/oak-blob-cloud-azure/pom.xml b/oak-blob-cloud-azure/pom.xml
index 7540889f68..0c9121ef7e 100644
--- a/oak-blob-cloud-azure/pom.xml
+++ b/oak-blob-cloud-azure/pom.xml
@@ -176,8 +176,8 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>testcontainers</artifactId>
             <scope>test</scope>
         </dependency>
     </dependencies>
diff --git a/oak-blob-cloud-azure/src/test/java/org/apache/jackrabbit/oak/blob/cloud/azure/blobstorage/AzuriteDockerRule.java b/oak-blob-cloud-azure/src/test/java/org/apache/jackrabbit/oak/blob/cloud/azure/blobstorage/AzuriteDockerRule.java
new file mode 100644
index 0000000000..3eaf27980a
--- /dev/null
+++ b/oak-blob-cloud-azure/src/test/java/org/apache/jackrabbit/oak/blob/cloud/azure/blobstorage/AzuriteDockerRule.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage;
+
+import com.microsoft.azure.storage.CloudStorageAccount;
+import com.microsoft.azure.storage.StorageException;
+import com.microsoft.azure.storage.blob.CloudBlobClient;
+import com.microsoft.azure.storage.blob.CloudBlobContainer;
+import org.junit.Assume;
+import org.junit.rules.ExternalResource;
+import org.junit.runner.Description;
+import org.junit.runners.model.MultipleFailureException;
+import org.junit.runners.model.Statement;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import java.net.URISyntaxException;
+import java.security.InvalidKeyException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class AzuriteDockerRule extends ExternalResource {
+
+    private static final DockerImageName DOCKER_IMAGE_NAME = DockerImageName.parse("mcr.microsoft.com/azure-storage/azurite:3.19.0");
+    public static final String ACCOUNT_KEY = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
+    public static final String ACCOUNT_NAME = "devstoreaccount1";
+    private static final AtomicReference<Exception> STARTUP_EXCEPTION = new AtomicReference<>();
+
+    private GenericContainer<?> azuriteContainer;
+
+    @Override
+    protected void before() throws Throwable {
+        azuriteContainer = new GenericContainer<>(DOCKER_IMAGE_NAME)
+                .withExposedPorts(10000)
+                .withEnv(Map.of("executable", "blob"))
+                .withStartupTimeout(Duration.ofSeconds(30));
+
+        try {
+            azuriteContainer.start();
+        } catch (IllegalStateException e) {
+            STARTUP_EXCEPTION.set(e);
+            throw e;
+        }
+    }
+
+    @Override
+    protected void after() {
+        if (azuriteContainer != null) {
+            azuriteContainer.stop();
+        }
+    }
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                try {
+                    before();
+                } catch (IllegalStateException e) {
+                    Assume.assumeNoException(STARTUP_EXCEPTION.get());
+                    throw e;
+                }
+
+                List<Throwable> errors = new ArrayList<Throwable>();
+                try {
+                    base.evaluate();
+                } catch (Throwable t) {
+                    errors.add(t);
+                } finally {
+                    try {
+                        after();
+                    } catch (Throwable t) {
+                        errors.add(t);
+                    }
+                }
+                MultipleFailureException.assertEmpty(errors);
+            }
+        };
+    }
+
+    public String getBlobEndpoint() {
+        return "http://127.0.0.1:" + getMappedPort() + "/devstoreaccount1";
+    }
+
+    public CloudBlobContainer getContainer(String name) throws URISyntaxException, StorageException, InvalidKeyException {
+        CloudStorageAccount cloud = getCloudStorageAccount();
+        CloudBlobClient cloudBlobClient = cloud.createCloudBlobClient();
+        CloudBlobContainer container = cloudBlobClient.getContainerReference(name);
+        container.deleteIfExists();
+        container.create();
+        return container;
+    }
+
+    public CloudStorageAccount getCloudStorageAccount() throws URISyntaxException, InvalidKeyException {
+        String blobEndpoint = "BlobEndpoint=" + getBlobEndpoint();
+        String accountName = "AccountName=" + ACCOUNT_NAME;
+        String accountKey = "AccountKey=" + ACCOUNT_KEY;
+        return CloudStorageAccount.parse("DefaultEndpointsProtocol=http;" + ";" + accountName + ";" + accountKey + ";" + blobEndpoint);
+    }
+
+    public int getMappedPort() {
+        return azuriteContainer.getMappedPort(10000);
+    }
+}
diff --git a/oak-it/pom.xml b/oak-it/pom.xml
index 7a0010ac17..9ec2cf2196 100644
--- a/oak-it/pom.xml
+++ b/oak-it/pom.xml
@@ -220,8 +220,8 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>com.arakelian</groupId>
-            <artifactId>docker-junit-rule</artifactId>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>testcontainers</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
diff --git a/oak-jcr/pom.xml b/oak-jcr/pom.xml
index d4dc1bfabb..f02d67453c 100644
--- a/oak-jcr/pom.xml
+++ b/oak-jcr/pom.xml
@@ -498,8 +498,8 @@
       <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>com.arakelian</groupId>
-      <artifactId>docker-junit-rule</artifactId>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>testcontainers</artifactId>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/oak-lucene/pom.xml b/oak-lucene/pom.xml
index cbde5da851..920c340505 100644
--- a/oak-lucene/pom.xml
+++ b/oak-lucene/pom.xml
@@ -430,8 +430,8 @@
       <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>com.arakelian</groupId>
-      <artifactId>docker-junit-rule</artifactId>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>testcontainers</artifactId>
       <scope>test</scope>
     </dependency>
     <dependency>
diff --git a/oak-parent/pom.xml b/oak-parent/pom.xml
index 7ab36157cc..8dee1e76c5 100644
--- a/oak-parent/pom.xml
+++ b/oak-parent/pom.xml
@@ -681,10 +681,12 @@
         <artifactId>httpmime</artifactId>
         <version>4.5.14</version>
       </dependency>
+      <!-- Testcontainers dependency -->
       <dependency>
-        <groupId>com.arakelian</groupId>
-        <artifactId>docker-junit-rule</artifactId>
-        <version>2.3.0</version>
+        <groupId>org.testcontainers</groupId>
+        <artifactId>testcontainers</artifactId>
+        <version>${testcontainers.version}</version>
+        <scope>test</scope>
       </dependency>
       <dependency>
         <groupId>com.microsoft.azure</groupId>
diff --git a/oak-pojosr/pom.xml b/oak-pojosr/pom.xml
index cd6d1e5372..03fa7ac322 100644
--- a/oak-pojosr/pom.xml
+++ b/oak-pojosr/pom.xml
@@ -369,8 +369,8 @@
       <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>com.arakelian</groupId>
-      <artifactId>docker-junit-rule</artifactId>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>testcontainers</artifactId>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/oak-run-commons/pom.xml b/oak-run-commons/pom.xml
index 3af6591a4e..6b190a6650 100644
--- a/oak-run-commons/pom.xml
+++ b/oak-run-commons/pom.xml
@@ -149,8 +149,8 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>com.arakelian</groupId>
-            <artifactId>docker-junit-rule</artifactId>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>testcontainers</artifactId>
             <scope>test</scope>
         </dependency>
     </dependencies>
diff --git a/oak-run/pom.xml b/oak-run/pom.xml
index 7cb5fc32ca..ab82bc0086 100644
--- a/oak-run/pom.xml
+++ b/oak-run/pom.xml
@@ -447,8 +447,8 @@
       <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>com.arakelian</groupId>
-      <artifactId>docker-junit-rule</artifactId>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>testcontainers</artifactId>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/oak-segment-azure/pom.xml b/oak-segment-azure/pom.xml
index 44215631d4..22d2bf0753 100644
--- a/oak-segment-azure/pom.xml
+++ b/oak-segment-azure/pom.xml
@@ -176,8 +176,8 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>com.arakelian</groupId>
-            <artifactId>docker-junit-rule</artifactId>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>testcontainers</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
diff --git a/oak-store-document/pom.xml b/oak-store-document/pom.xml
index e9b93ce162..b10bf91a57 100644
--- a/oak-store-document/pom.xml
+++ b/oak-store-document/pom.xml
@@ -318,11 +318,6 @@
       <artifactId>metrics-core</artifactId>
       <scope>test</scope>
     </dependency>
-    <dependency>
-      <groupId>com.arakelian</groupId>
-      <artifactId>docker-junit-rule</artifactId>
-      <scope>test</scope>
-    </dependency>
     <dependency>
       <groupId>org.testcontainers</groupId>
       <artifactId>testcontainers</artifactId>
diff --git a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/MongoConnectionFactory.java b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/MongoConnectionFactory.java
index 3ffefb8cf6..dd571a9105 100644
--- a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/MongoConnectionFactory.java
+++ b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/MongoConnectionFactory.java
@@ -55,7 +55,7 @@ public class MongoConnectionFactory extends ExternalResource {
         MongoConnection c = MongoUtils.getConnection(dbName);
         if (c == null && MongoDockerRule.isDockerAvailable()) {
             // fall back to docker if available
-            c = new MongoConnection("localhost", mongo.getPort(), dbName);
+            c = new MongoConnection(mongo.getHost(), mongo.getPort(), dbName);
         }
         assumeNotNull(c);
         if (c != null) {
diff --git a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDockerRule.java b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDockerRule.java
index 3339b6f6c5..c7d204b69d 100644
--- a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDockerRule.java
+++ b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDockerRule.java
@@ -16,60 +16,117 @@
  */
 package org.apache.jackrabbit.oak.plugins.document.mongo;
 
-import java.util.UUID;
-
-import com.arakelian.docker.junit.DockerRule;
-import com.arakelian.docker.junit.model.ImmutableDockerConfig;
-import com.spotify.docker.client.DefaultDockerClient;
-import com.spotify.docker.client.auth.FixedRegistryAuthSupplier;
-
-import org.apache.jackrabbit.oak.plugins.document.MongoUtils;
+import org.junit.Assume;
+import org.junit.rules.ExternalResource;
+import org.junit.runner.Description;
+import org.junit.runners.model.MultipleFailureException;
+import org.junit.runners.model.Statement;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.testcontainers.DockerClientFactory;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.images.RemoteDockerImage;
+import org.testcontainers.utility.DockerImageName;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
- * A MongoDB {@link DockerRule}.
+ * A MongoDB {@link GenericContainer}.
  */
-public class MongoDockerRule extends DockerRule {
+public class MongoDockerRule extends ExternalResource {
 
     private static final Logger LOG = LoggerFactory.getLogger(MongoDockerRule.class);
 
-    private static final String CONFIG_NAME = "MongoDB-" + UUID.randomUUID().toString().substring(0, 8);
-
     private static final String VERSION = System.getProperty("mongo.version", "3.6");
-
-    private static final String IMAGE = "mongo:" + VERSION;
-
+    private static final String MONGO_IMAGE = "mongo:" + VERSION;
+    private static final AtomicReference<Exception> STARTUP_EXCEPTION = new AtomicReference<>();
+    private static final int DEFAULT_MONGO_PORT = 27017;
+    private static final DockerImageName DOCKER_IMAGE_NAME = DockerImageName.parse(MONGO_IMAGE);
     private static final boolean DOCKER_AVAILABLE;
+    private static GenericContainer<?> mongoContainer;
 
     static {
-        boolean available = false;
-        try (DefaultDockerClient client = DefaultDockerClient.fromEnv()
-                .connectTimeoutMillis(5000L).readTimeoutMillis(20000L)
-                .registryAuthSupplier(new FixedRegistryAuthSupplier())
-                .build()) {
-            client.ping();
-            client.pull(IMAGE);
-            available = true;
+
+        boolean dockerAvailable = false;
+        boolean imageAvailable = false;
+        try {
+            dockerAvailable = checkDockerAvailability();
+            if (dockerAvailable) {
+                imageAvailable = checkImageAvailability();
+            } else {
+                LOG.info("docker not available");
+            }
         } catch (Throwable t) {
-            LOG.info("Cannot connect to docker or pull image", t);
+            LOG.error("not able to pull specified mongo image: {}, error: ", MONGO_IMAGE, t);
         }
-        DOCKER_AVAILABLE = available;
+        DOCKER_AVAILABLE = dockerAvailable && imageAvailable;
     }
 
-    public MongoDockerRule() {
-        super(ImmutableDockerConfig.builder()
-                .name(CONFIG_NAME)
-                .image(IMAGE)
-                .ports("27017")
-                .allowRunningBetweenUnitTests(true)
-                .alwaysRemoveContainer(true)
-                .addStartedListener(container -> container.waitForPort("27017/tcp"))
-                .build());
+    @Override
+    protected void before() throws Throwable {
+        if (mongoContainer != null && mongoContainer.isRunning()) {
+            return;
+        }
+        mongoContainer = new GenericContainer<>(DOCKER_IMAGE_NAME)
+                .withExposedPorts(DEFAULT_MONGO_PORT)
+                .withStartupTimeout(Duration.ofMinutes(1));
+
+        try {
+            long startTime = Instant.now().toEpochMilli();
+            mongoContainer.start();
+            LOG.info("mongo container started in: " + (Instant.now().toEpochMilli() - startTime) + " ms");
+        } catch (Exception e) {
+            LOG.error("error while starting mongoDb container, error: ", e);
+            STARTUP_EXCEPTION.set(e);
+            throw e;
+        }
+    }
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                try {
+                    before();
+                } catch (Throwable e) {
+                    Assume.assumeNoException(STARTUP_EXCEPTION.get());
+                    throw e;
+                }
+
+                List<Throwable> errors = new ArrayList<>();
+                try {
+                    base.evaluate();
+                } catch (Throwable t) {
+                    errors.add(t);
+                }
+                MultipleFailureException.assertEmpty(errors);
+            }
+        };
+    }
+
+    private static boolean checkImageAvailability() throws TimeoutException {
+        RemoteDockerImage remoteDockerImage = new RemoteDockerImage(DOCKER_IMAGE_NAME);
+        remoteDockerImage.get(1, TimeUnit.MINUTES);
+        return true;
+    }
+
+    private static boolean checkDockerAvailability() {
+        return DockerClientFactory.instance().isDockerAvailable();
     }
 
     public int getPort() {
-        return getContainer().getPortBinding("27017/tcp").getPort();
+        return mongoContainer.getMappedPort(DEFAULT_MONGO_PORT);
+    }
+
+    public String getHost() {
+        return mongoContainer.getHost();
     }
 
     public static boolean isDockerAvailable() {
diff --git a/oak-upgrade/pom.xml b/oak-upgrade/pom.xml
index 011c382819..3f94cb31e4 100644
--- a/oak-upgrade/pom.xml
+++ b/oak-upgrade/pom.xml
@@ -220,11 +220,9 @@
       <type>test-jar</type>
       <scope>test</scope>
     </dependency>
-
-	<dependency>
-      <groupId>com.arakelian</groupId>
-      <artifactId>docker-junit-rule</artifactId>
-      <version>2.1.0</version>
+    <dependency>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>testcontainers</artifactId>
       <scope>test</scope>
     </dependency>
     <dependency>


(jackrabbit-oak) 02/02: OAK-10680: tests failures with com.arakelian/docker-junit-rule

Posted by ba...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

baedke pushed a commit to branch issue/oak-10680
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git

commit 322004c9b05f08ce43f412818cdeeb8207110668
Author: Manfred Baedke <ma...@gmail.com>
AuthorDate: Fri Mar 1 14:21:07 2024 +0100

    OAK-10680: tests failures with com.arakelian/docker-junit-rule
    
    Now using Testcontainers instead of com.arakelian:docker-junit-rule.
---
 .../cloud/azure/blobstorage/AzuriteDockerRule.java |   6 +-
 .../oak/composite/CompositeNodeStoreQueryTest.java |   2 +-
 oak-parent/pom.xml                                 |   1 +
 oak-search-elastic/pom.xml                         | 183 ++++------------
 ...DefaultElasticsearchIndexCoordinateFactory.java |  42 ----
 .../elasticsearch/ElasticsearchConnection.java     | 123 +++++++++++
 .../ElasticsearchConnectionFactory.java            |  76 -------
 .../elasticsearch/ElasticsearchCoordinate.java     |  33 ---
 .../elasticsearch/ElasticsearchCoordinateImpl.java | 140 ------------
 .../ElasticsearchIndexCoordinate.java              |  24 ---
 .../ElasticsearchIndexCoordinateFactory.java       |  23 --
 ...Impl.java => ElasticsearchIndexDescriptor.java} | 237 ++++++++++-----------
 .../ElasticsearchIndexProviderService.java         | 162 ++++++++------
 .../index/ElasticsearchIndexEditorContext.java     |  10 +-
 .../index/ElasticsearchIndexEditorProvider.java    |  18 +-
 .../index/ElasticsearchIndexWriter.java            | 119 +++++------
 .../index/ElasticsearchIndexWriterFactory.java     |  10 +-
 .../elasticsearch/query/ElasticsearchIndex.java    |  16 +-
 .../query/ElasticsearchIndexNode.java              |  28 ++-
 .../query/ElasticsearchIndexProvider.java          |  12 +-
 .../query/ElasticsearchIndexStatistics.java        |  21 +-
 .../query/ElasticsearchResultRowIterator.java      |  90 ++++----
 .../elasticsearch/query/ElasticsearchSearcher.java |  22 +-
 .../elasticsearch/ElasticsearchConnectionTest.java |  31 +++
 .../elasticsearch/ElasticsearchDockerRule.java     |  78 -------
 .../ElasticsearchIndexProviderServiceTest.java     |  62 ++++++
 .../elasticsearch/ElasticsearchManagementRule.java | 101 ---------
 .../ElasticsearchPropertyIndexTest.java            |  47 ++--
 .../elasticsearch/ElasticsearchTestUtils.java      |  47 ----
 oak-segment-azure/pom.xml                          |   7 +
 .../segment/azure/tool/SegmentCopyTestBase.java    |   2 +-
 .../oak/segment/azure/AzureArchiveManagerTest.java |   1 +
 .../oak/segment/azure/AzureGCJournalTest.java      |   1 +
 .../oak/segment/azure/AzureJournalFileTest.java    |   1 +
 .../oak/segment/azure/AzureManifestFileTest.java   |   1 +
 .../oak/segment/azure/AzureReadSegmentTest.java    |   1 +
 .../oak/segment/azure/AzureRepositoryLockTest.java |   1 +
 .../oak/segment/azure/AzureTarFileTest.java        |   1 +
 .../oak/segment/azure/AzureTarFilesTest.java       |   1 +
 .../oak/segment/azure/AzureTarWriterTest.java      |   1 +
 .../oak/segment/azure/AzuriteDockerRule.java       |  86 --------
 .../azure/journal/AzureJournalReaderTest.java      |   2 +-
 .../azure/journal/AzureTarRevisionsTest.java       |   2 +-
 .../azure/journal/ReverseFileReaderTest.java       |   2 +-
 .../split/SplitPersistenceBlobTest.java            |   2 +-
 .../persistence/split/SplitPersistenceTest.java    |   2 +-
 oak-shaded-guava/dependency-reduced-pom.xml        |  94 ++++++++
 oak-store-document/pom.xml                         |   8 +-
 .../oak/plugins/document/mongo/MongodProcess.java  |  43 ++--
 .../document/mongo/MongodProcessFactory.java       |  35 ++-
 oak-upgrade/pom.xml                                |   2 +-
 .../upgrade/cli/SegmentAzureToSegmentTarTest.java  |   2 +-
 .../upgrade/cli/SegmentTarToSegmentAzureTest.java  |   2 +-
 .../container/SegmentAzureNodeStoreContainer.java  |   2 +-
 54 files changed, 830 insertions(+), 1236 deletions(-)

diff --git a/oak-blob-cloud-azure/src/test/java/org/apache/jackrabbit/oak/blob/cloud/azure/blobstorage/AzuriteDockerRule.java b/oak-blob-cloud-azure/src/test/java/org/apache/jackrabbit/oak/blob/cloud/azure/blobstorage/AzuriteDockerRule.java
index 3eaf27980a..daa59ffa4c 100644
--- a/oak-blob-cloud-azure/src/test/java/org/apache/jackrabbit/oak/blob/cloud/azure/blobstorage/AzuriteDockerRule.java
+++ b/oak-blob-cloud-azure/src/test/java/org/apache/jackrabbit/oak/blob/cloud/azure/blobstorage/AzuriteDockerRule.java
@@ -20,6 +20,9 @@ import com.microsoft.azure.storage.CloudStorageAccount;
 import com.microsoft.azure.storage.StorageException;
 import com.microsoft.azure.storage.blob.CloudBlobClient;
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
+
+import com.google.common.collect.ImmutableMap;
+
 import org.junit.Assume;
 import org.junit.rules.ExternalResource;
 import org.junit.runner.Description;
@@ -33,7 +36,6 @@ import java.security.InvalidKeyException;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import java.util.concurrent.atomic.AtomicReference;
 
 public class AzuriteDockerRule extends ExternalResource {
@@ -49,7 +51,7 @@ public class AzuriteDockerRule extends ExternalResource {
     protected void before() throws Throwable {
         azuriteContainer = new GenericContainer<>(DOCKER_IMAGE_NAME)
                 .withExposedPorts(10000)
-                .withEnv(Map.of("executable", "blob"))
+                .withEnv(ImmutableMap.of("executable", "blob"))
                 .withStartupTimeout(Duration.ofSeconds(30));
 
         try {
diff --git a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreQueryTest.java b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreQueryTest.java
index 150ae25495..e029e18b7c 100644
--- a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreQueryTest.java
+++ b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreQueryTest.java
@@ -53,7 +53,7 @@ import org.junit.runners.Parameterized;
 
 import com.google.common.collect.ImmutableSet;
 
-import jersey.repackaged.com.google.common.collect.Lists;
+import com.google.common.collect.Lists;
 
 /**
  * Tests indexing and queries when using the composite node store.
diff --git a/oak-parent/pom.xml b/oak-parent/pom.xml
index 8dee1e76c5..5dc96ee2c6 100644
--- a/oak-parent/pom.xml
+++ b/oak-parent/pom.xml
@@ -66,6 +66,7 @@
     <guava.osgi.import>com.google.common.*;version="[15.0,20)"</guava.osgi.import>
     <derby.version>10.14.2.0</derby.version>
     <jackson.version>2.15.2</jackson.version>
+    <testcontainers.version>1.19.0</testcontainers.version>
     <groovy.version>2.5.20</groovy.version>
     <java.version>1.8</java.version>
     <java.version.signature>java18</java.version.signature>
diff --git a/oak-search-elastic/pom.xml b/oak-search-elastic/pom.xml
index 23644beb71..ffc4ff6338 100644
--- a/oak-search-elastic/pom.xml
+++ b/oak-search-elastic/pom.xml
@@ -33,8 +33,8 @@
   <description>Oak Elasticsearch integration subproject</description>
 
   <properties>
-    <elasticsearch.version>7.1.1</elasticsearch.version>
-    <lucene.version>8.0.0</lucene.version>
+    <elasticsearch.version>7.6.0</elasticsearch.version>
+    <lucene.version>8.4.0</lucene.version>
   </properties>
 
   <build>
@@ -52,10 +52,11 @@
               *
             </Import-Package>
             <Embed-Dependency>
-              lucene-core;inline=true
-              elasticsearch-rest-high-level-client;inline=true
+              oak-search;inline=true,
+              elasticsearch;groupId=org.elasticsearch,
+              *;scope=compile|runtime
             </Embed-Dependency>
-            <Export-Package>!*</Export-Package>
+            <Embed-Transitive>true</Embed-Transitive>
           </instructions>
         </configuration>
       </plugin>
@@ -96,40 +97,68 @@
       <scope>provided</scope>
     </dependency>
 
+    <!-- Elastic/Lucene -->
+    <dependency>
+      <groupId>org.elasticsearch.client</groupId>
+      <artifactId>elasticsearch-rest-high-level-client</artifactId>
+      <version>${elasticsearch.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.elasticsearch.client</groupId>
+      <artifactId>elasticsearch-rest-client</artifactId>
+      <version>${elasticsearch.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.elasticsearch</groupId>
+      <artifactId>elasticsearch</artifactId>
+      <version>${elasticsearch.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.elasticsearch</groupId>
+      <artifactId>elasticsearch-x-content</artifactId>
+      <version>${elasticsearch.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.lucene</groupId>
+      <artifactId>lucene-core</artifactId>
+      <version>${lucene.version}</version>
+    </dependency>
+
     <dependency>
       <groupId>org.apache.jackrabbit</groupId>
       <artifactId>oak-api</artifactId>
       <version>${project.version}</version>
+      <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.jackrabbit</groupId>
       <artifactId>oak-core-spi</artifactId>
       <version>${project.version}</version>
+      <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.jackrabbit</groupId>
       <artifactId>oak-store-spi</artifactId>
       <version>${project.version}</version>
+      <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.jackrabbit</groupId>
       <artifactId>oak-query-spi</artifactId>
       <version>${project.version}</version>
+      <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.jackrabbit</groupId>
       <artifactId>oak-core</artifactId>
       <version>${project.version}</version>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.jackrabbit</groupId>
-      <artifactId>oak-store-document</artifactId>
-      <version>${project.version}</version>
+      <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.jackrabbit</groupId>
       <artifactId>oak-search</artifactId>
       <version>${project.version}</version>
+      <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>com.google.guava</groupId>
@@ -141,35 +170,22 @@
     <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
+      <scope>provided</scope>
     </dependency>
 
     <!-- Nullability annotations -->
     <dependency>
       <groupId>org.jetbrains</groupId>
       <artifactId>annotations</artifactId>
+      <scope>provided</scope>
     </dependency>
 
     <!-- Test Dependencies -->
-    <dependency>
-      <groupId>ch.qos.logback</groupId>
-      <artifactId>logback-classic</artifactId>
-      <scope>test</scope>
-    </dependency>
     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
       <scope>test</scope>
     </dependency>
-    <dependency>
-      <groupId>org.elasticsearch.client</groupId>
-      <artifactId>elasticsearch-rest-high-level-client</artifactId>
-      <version>${elasticsearch.version}</version>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.lucene</groupId>
-      <artifactId>lucene-core</artifactId>
-      <version>${lucene.version}</version>
-    </dependency>
     <dependency>
       <groupId>org.apache.jackrabbit</groupId>
       <artifactId>oak-core</artifactId>
@@ -179,132 +195,27 @@
     </dependency>
     <dependency>
       <groupId>org.apache.jackrabbit</groupId>
-      <artifactId>oak-blob-plugins</artifactId>
-      <version>${project.version}</version>
-      <classifier>tests</classifier>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.jackrabbit</groupId>
-      <artifactId>oak-store-spi</artifactId>
+      <artifactId>oak-security-spi</artifactId>
       <version>${project.version}</version>
-      <type>test-jar</type>
       <scope>test</scope>
     </dependency>
-    <dependency>
-      <groupId>org.apache.jackrabbit</groupId>
-      <artifactId>oak-segment-tar</artifactId>
-      <version>${project.version}</version>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.jackrabbit</groupId>
-      <artifactId>oak-segment-tar</artifactId>
-      <version>${project.version}</version>
-      <type>test-jar</type>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.jackrabbit</groupId>
-      <artifactId>oak-store-document</artifactId>
-      <version>${project.version}</version>
-      <type>test-jar</type>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.jackrabbit</groupId>
-      <artifactId>oak-jcr</artifactId>
-      <version>${project.version}</version>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.jackrabbit</groupId>
-      <artifactId>oak-jcr</artifactId>
-      <version>${project.version}</version>
-      <type>test-jar</type>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.jackrabbit</groupId>
-      <artifactId>oak-commons</artifactId>
-      <version>${project.version}</version>
-      <type>test-jar</type>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.jackrabbit</groupId>
-      <artifactId>jackrabbit-jcr-tests</artifactId>
-      <version>${jackrabbit.version}</version>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.jackrabbit</groupId>
-      <artifactId>jackrabbit-core</artifactId>
-      <version>${jackrabbit.version}</version>
-      <classifier>tests</classifier>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.tika</groupId>
-      <artifactId>tika-parsers</artifactId>
-      <version>${tika.version}</version>
-      <scope>test</scope>
-      <exclusions>
-        <exclusion>
-          <groupId>commons-logging</groupId>
-          <artifactId>commons-logging</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>org.slf4j</groupId>
-          <artifactId>slf4j-log4j12</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
     <dependency>
       <groupId>org.apache.sling</groupId>
       <artifactId>org.apache.sling.testing.osgi-mock</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.hamcrest</groupId>
-      <artifactId>hamcrest-all</artifactId>
-      <version>1.3</version>
       <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>org.mockito</groupId>
-      <artifactId>mockito-core</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>io.dropwizard.metrics</groupId>
-      <artifactId>metrics-core</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.commons</groupId>
-      <artifactId>commons-exec</artifactId>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-core</artifactId>
       <version>1.3</version>
       <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>com.google.code.gson</groupId>
-      <artifactId>gson</artifactId>
-      <version>2.8.0</version>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.arakelian</groupId>
-      <artifactId>docker-junit-rule</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.jackrabbit</groupId>
-      <artifactId>oak-search</artifactId>
-      <classifier>tests</classifier>
-      <version>${project.version}</version>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>elasticsearch</artifactId>
+      <version>1.12.5</version>
       <scope>test</scope>
     </dependency>
 
-
   </dependencies>
 </project>
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/DefaultElasticsearchIndexCoordinateFactory.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/DefaultElasticsearchIndexCoordinateFactory.java
deleted file mode 100644
index 5328b53028..0000000000
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/DefaultElasticsearchIndexCoordinateFactory.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.plugins.index.elasticsearch;
-
-import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
-import org.apache.jackrabbit.oak.spi.state.NodeState;
-
-import java.util.Map;
-
-public class DefaultElasticsearchIndexCoordinateFactory implements ElasticsearchIndexCoordinateFactory {
-    private final Map<String, String> config;
-    private final ElasticsearchConnectionFactory factory;
-
-    DefaultElasticsearchIndexCoordinateFactory(ElasticsearchConnectionFactory factory, Map<String, String> config) {
-        this.factory = factory;
-        this.config = config;
-    }
-
-    @Override
-    public ElasticsearchIndexCoordinate getElasticsearchIndexCoordinate(IndexDefinition indexDefinition) {
-        ElasticsearchCoordinate esCoord = getElasticsearchCoordinate(indexDefinition.getDefinitionNodeState());
-        return new ElasticsearchIndexCoordinateImpl(esCoord, indexDefinition);
-    }
-
-    private ElasticsearchCoordinate getElasticsearchCoordinate(NodeState indexDefinition) {
-        return ElasticsearchCoordinateImpl.construct(factory, indexDefinition, config);
-    }
-}
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchConnection.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchConnection.java
new file mode 100644
index 0000000000..ab6b7b52d1
--- /dev/null
+++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchConnection.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.plugins.index.elasticsearch;
+
+import org.apache.http.HttpHost;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.client.RestHighLevelClient;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+
+/**
+ * This class represents an Elasticsearch Connection with the related <code>RestHighLevelClient</code>.
+ * As per Elasticsearch documentation: the client is thread-safe, there should be one instance per application and it
+ * must be closed when it is not needed anymore.
+ * https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/_changing_the_client_8217_s_initialization_code.html
+ *
+ * The getClient() initializes the rest client on the first call.
+ * Once close() is invoked this instance cannot be used anymore.
+ */
+public class ElasticsearchConnection implements Closeable {
+
+    protected static final String SCHEME_PROP = "elasticsearch.scheme";
+    protected static final String DEFAULT_SCHEME = "http";
+    protected static final String HOST_PROP = "elasticsearch.host";
+    protected static final String DEFAULT_HOST = "127.0.0.1";
+    protected static final String PORT_PROP = "elasticsearch.port";
+    protected static final int DEFAULT_PORT = 9200;
+
+    protected static final Supplier<ElasticsearchConnection> defaultConnection = () ->
+            new ElasticsearchConnection(DEFAULT_SCHEME, DEFAULT_HOST, DEFAULT_PORT);
+
+    private String scheme;
+    private String host;
+    private int port;
+
+    private volatile RestHighLevelClient client;
+
+    private AtomicBoolean isClosed = new AtomicBoolean(false);
+
+    protected ElasticsearchConnection(String scheme, String host, Integer port) {
+        if (scheme == null || host == null || port == null) {
+            throw new IllegalArgumentException();
+        }
+        this.scheme = scheme;
+        this.host = host;
+        this.port = port;
+    }
+
+    public RestHighLevelClient getClient() {
+        if (isClosed.get()) {
+            throw new IllegalStateException("Already closed");
+        }
+
+        // double checked locking to get good performance and avoid double initialization
+        if (client == null) {
+            synchronized (this) {
+                if (client == null) {
+                    client = new RestHighLevelClient(RestClient.builder(new HttpHost(host, port, scheme)));
+                }
+            }
+        }
+        return client;
+    }
+
+    public String getScheme() {
+        return scheme;
+    }
+
+    public String getHost() {
+        return host;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    @Override
+    public synchronized void close() throws IOException {
+        if (client != null) {
+            client.close();
+        }
+        isClosed.set(true);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        ElasticsearchConnection that = (ElasticsearchConnection) o;
+        return port == that.port &&
+                Objects.equals(scheme, that.scheme) &&
+                Objects.equals(host, that.host);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getScheme(), getHost(), getPort());
+    }
+
+    @Override
+    public String toString() {
+        return getScheme() + "://" + getHost() + ":" + getPort();
+    }
+
+}
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchConnectionFactory.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchConnectionFactory.java
deleted file mode 100644
index 1894777e5b..0000000000
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchConnectionFactory.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.plugins.index.elasticsearch;
-
-import com.google.common.collect.Maps;
-import org.apache.http.HttpHost;
-import org.elasticsearch.client.RestClient;
-import org.elasticsearch.client.RestHighLevelClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-
-public class ElasticsearchConnectionFactory implements Closeable {
-    private static final Logger LOG = LoggerFactory.getLogger(ElasticsearchConnectionFactory.class);
-    private final ConcurrentMap<ElasticsearchCoordinate, RestHighLevelClient> clientMap = Maps.newConcurrentMap();
-
-    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
-    private final AtomicBoolean isClosed = new AtomicBoolean();
-
-    public RestHighLevelClient getConnection(ElasticsearchCoordinate esCoord) {
-        lock.readLock().lock();
-        try {
-            if (isClosed.get()) {
-                throw new IllegalStateException("Already closed");
-            }
-
-            return clientMap.computeIfAbsent(esCoord, elasticsearchCoordinate -> {
-                LOG.info("Creating client {}", elasticsearchCoordinate);
-                return new RestHighLevelClient(
-                    RestClient.builder(
-                            new HttpHost(elasticsearchCoordinate.getHost(),
-                                    elasticsearchCoordinate.getPort(),
-                                    elasticsearchCoordinate.getScheme())
-                    ));
-            });
-        } finally {
-            lock.readLock().unlock();
-        }
-    }
-
-    @Override
-    public void close() {
-        lock.writeLock().lock();
-        try {
-            isClosed.set(true);
-            clientMap.values().forEach(restHighLevelClient -> {
-                try {
-                    restHighLevelClient.close();
-                } catch (IOException e) {
-                    LOG.error("Error occurred while closing a connection", e);
-                }
-            });
-        } finally {
-            lock.writeLock().unlock();
-        }
-    }
-}
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchCoordinate.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchCoordinate.java
deleted file mode 100644
index 97f87b5ef8..0000000000
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchCoordinate.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.plugins.index.elasticsearch;
-
-import org.elasticsearch.client.RestHighLevelClient;
-
-public interface ElasticsearchCoordinate {
-    String SCHEME_PROP = "elasticsearch.scheme";
-    String DEFAULT_SCHEME = "http";
-    String HOST_PROP = "elasticsearch.host";
-    String DEFAULT_HOST = "127.0.0.1";
-    String PORT_PROP = "elasticsearch.port";
-    int DEFAULT_PORT = 9200;
-
-    RestHighLevelClient getClient();
-    String getScheme();
-    String getHost();
-    int getPort();
-}
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchCoordinateImpl.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchCoordinateImpl.java
deleted file mode 100644
index 7d99e9b55e..0000000000
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchCoordinateImpl.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.plugins.index.elasticsearch;
-
-import com.google.common.base.Objects;
-import org.apache.jackrabbit.oak.plugins.index.search.util.ConfigUtil;
-import org.apache.jackrabbit.oak.spi.state.NodeState;
-import org.elasticsearch.client.RestHighLevelClient;
-
-import java.util.Map;
-
-public class ElasticsearchCoordinateImpl implements ElasticsearchCoordinate {
-    private final ElasticsearchConnectionFactory connectionFactory;
-    private final String scheme;
-    private final String host;
-    private final int port;
-
-    ElasticsearchCoordinateImpl(ElasticsearchConnectionFactory connectionFactory,
-                                String scheme, String host, int port) {
-        this.connectionFactory = connectionFactory;
-        this.scheme = scheme;
-        this.host = host;
-        this.port = port;
-    }
-
-    static ElasticsearchCoordinate construct(ElasticsearchConnectionFactory connectionFactory,
-                                             NodeState indexDefn, Map<String, String> configMap) {
-        ElasticsearchCoordinate esCoord;
-
-        // index defn is at highest prio
-        esCoord = readFrom(connectionFactory, indexDefn);
-        if (esCoord != null) {
-            return esCoord;
-        }
-
-        // command line comes next
-        esCoord = construct(connectionFactory,
-                System.getProperty(SCHEME_PROP), System.getProperty(HOST_PROP), Integer.getInteger(PORT_PROP));
-        if (esCoord != null) {
-            return esCoord;
-        }
-
-        // config map
-        if (configMap != null) {
-            Integer port = null;
-            try {
-                port = Integer.parseInt(configMap.get(PORT_PROP));
-            } catch (NumberFormatException nfe) {
-                // ignore
-            }
-            esCoord = construct(connectionFactory, configMap.get(SCHEME_PROP), configMap.get(HOST_PROP), port);
-            if (esCoord != null) {
-                return esCoord;
-            }
-        }
-
-        return new ElasticsearchCoordinateImpl(connectionFactory, DEFAULT_SCHEME, DEFAULT_HOST, DEFAULT_PORT);
-    }
-
-    @Override
-    public RestHighLevelClient getClient() {
-        return connectionFactory.getConnection(this);
-    }
-
-    @Override
-    public String getScheme() {
-        return scheme;
-    }
-
-    @Override
-    public String getHost() {
-        return host;
-    }
-
-    @Override
-    public int getPort() {
-        return port;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (!(o instanceof ElasticsearchCoordinate)) {
-            return false;
-        }
-
-        ElasticsearchCoordinate other = (ElasticsearchCoordinate)o;
-        return hashCode() == other.hashCode() // just to have a quicker comparison
-                && getScheme().equals(other.getScheme())
-                && getHost().equals(other.getHost())
-                && getPort() == other.getPort();
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hashCode(getScheme(), getHost(), getPort());
-    }
-
-    @Override
-    public String toString() {
-        return getScheme() + "://" + getHost() + ":" + getPort();
-    }
-
-    private static ElasticsearchCoordinate readFrom(ElasticsearchConnectionFactory factory, NodeState definition) {
-        if (definition == null
-                || !definition.hasProperty(SCHEME_PROP)
-                || !definition.hasProperty(HOST_PROP)
-                || !definition.hasProperty(PORT_PROP)) {
-            return null;
-        }
-
-        String scheme = ConfigUtil.getOptionalValue(definition, SCHEME_PROP, DEFAULT_SCHEME);
-        String host = ConfigUtil.getOptionalValue(definition, HOST_PROP, DEFAULT_HOST);
-        int port = ConfigUtil.getOptionalValue(definition, PORT_PROP, DEFAULT_PORT);
-
-        return new ElasticsearchCoordinateImpl(factory, scheme, host, port);
-    }
-
-    private static ElasticsearchCoordinate construct(ElasticsearchConnectionFactory factory,
-                                                     String scheme, String host, Integer port) {
-        if (scheme == null || host == null || port == null) {
-            return null;
-        }
-
-        return new ElasticsearchCoordinateImpl(factory, scheme, host, port);
-    }
-}
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexCoordinate.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexCoordinate.java
deleted file mode 100644
index 7e63aa0d8b..0000000000
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexCoordinate.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.plugins.index.elasticsearch;
-
-import org.elasticsearch.client.RestHighLevelClient;
-
-public interface ElasticsearchIndexCoordinate {
-    RestHighLevelClient getClient();
-    String getEsIndexName();
-}
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexCoordinateFactory.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexCoordinateFactory.java
deleted file mode 100644
index 5b9626e21a..0000000000
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexCoordinateFactory.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.plugins.index.elasticsearch;
-
-import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
-
-public interface ElasticsearchIndexCoordinateFactory {
-    ElasticsearchIndexCoordinate getElasticsearchIndexCoordinate(IndexDefinition indexDefinition);
-}
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexCoordinateImpl.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexDescriptor.java
similarity index 52%
rename from oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexCoordinateImpl.java
rename to oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexDescriptor.java
index 0889365c38..72bf997a22 100644
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexCoordinateImpl.java
+++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexDescriptor.java
@@ -1,126 +1,111 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.plugins.index.elasticsearch;
-
-import com.google.common.base.Joiner;
-import com.google.common.base.Objects;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import org.apache.jackrabbit.oak.commons.PathUtils;
-import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
-import org.elasticsearch.client.RestHighLevelClient;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
-import static org.elasticsearch.common.Strings.INVALID_FILENAME_CHARS;
-
-public class ElasticsearchIndexCoordinateImpl implements ElasticsearchIndexCoordinate {
-
-    private static final int MAX_NAME_LENGTH = 255;
-
-    private final ElasticsearchCoordinate esCoord;
-    private final String esIndexName;
-
-    ElasticsearchIndexCoordinateImpl(@NotNull ElasticsearchCoordinate esCoord, IndexDefinition indexDefinition) {
-        this.esCoord = esCoord;
-        esIndexName = getRemoteIndexName(indexDefinition, indexDefinition.getIndexPath());
-    }
-
-    @Override
-    public RestHighLevelClient getClient() {
-        return esCoord.getClient();
-    }
-
-    @Override
-    public String getEsIndexName() {
-        return esIndexName;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hashCode(esCoord, esIndexName);
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (! (o instanceof ElasticsearchIndexCoordinateImpl)) {
-            return false;
-        }
-        ElasticsearchIndexCoordinateImpl other = (ElasticsearchIndexCoordinateImpl)o;
-        return hashCode() == other.hashCode()
-                && esCoord.equals(other.esCoord)
-                && esIndexName.equals(other.esIndexName);
-    }
-
-    private String getRemoteIndexName(IndexDefinition definition, String indexPath) {
-        String suffix = definition.getUniqueId();
-
-        if (suffix == null) {
-            suffix = String.valueOf(definition.getReindexCount());
-        }
-
-        return getESSafeIndexName(indexPath + "-" + suffix);
-    }
-
-    /**
-     * <ul>
-     *     <li>abc -> abc</li>
-     *     <li>xy:abc -> xyabc</li>
-     *     <li>/oak:index/abc -> abc</li>
-     * </ul>
-     *
-     * The resulting file name would be truncated to MAX_NAME_LENGTH
-     */
-    private static String getESSafeIndexName(String indexPath) {
-        List<String> elements = Lists.newArrayList(PathUtils.elements(indexPath));
-        Collections.reverse(elements);
-        List<String> result = Lists.newArrayListWithCapacity(2);
-
-        //Max 3 nodeNames including oak:index which is the immediate parent for any indexPath
-        for (String e : Iterables.limit(elements, 3)) {
-            if ("oak:index".equals(e)) {
-                continue;
-            }
-
-            result.add(getESSafeName(e));
-        }
-
-        Collections.reverse(result);
-        String name = Joiner.on('_').join(result);
-        if (name.length() > MAX_NAME_LENGTH){
-            name = name.substring(0, MAX_NAME_LENGTH);
-        }
-        return name;
-    }
-
-    /**
-     * Convert {@code e} to Elasticsearch safe index name.
-     * Ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html
-     */
-    private static String getESSafeName(String suggestedIndexName) {
-        String invalidCharsRegex = Pattern.quote(INVALID_FILENAME_CHARS
-                .stream()
-                .map(Object::toString)
-                .collect(Collectors.joining("")));
-        return suggestedIndexName.replaceAll(invalidCharsRegex, "").toLowerCase();
-    }
-
-}
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.plugins.index.elasticsearch;
+
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import static org.elasticsearch.common.Strings.INVALID_FILENAME_CHARS;
+
+public class ElasticsearchIndexDescriptor {
+
+    private static final int MAX_NAME_LENGTH = 255;
+
+    private static final String INVALID_CHARS_REGEX = Pattern.quote(INVALID_FILENAME_CHARS
+            .stream()
+            .map(Object::toString)
+            .collect(Collectors.joining("")));
+
+    private final ElasticsearchConnection connection;
+    private final String indexName;
+
+    public ElasticsearchIndexDescriptor(@NotNull ElasticsearchConnection connection, IndexDefinition indexDefinition) {
+        this.connection = connection;
+        this.indexName = getRemoteIndexName(indexDefinition);
+    }
+
+    public RestHighLevelClient getClient() {
+        return connection.getClient();
+    }
+
+    public String getIndexName() {
+        return indexName;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(connection, indexName);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        ElasticsearchIndexDescriptor that = (ElasticsearchIndexDescriptor) o;
+        return connection.equals(that.connection) &&
+                indexName.equals(that.indexName);
+    }
+
+    private String getRemoteIndexName(IndexDefinition definition) {
+        String suffix = definition.getUniqueId();
+
+        if (suffix == null) {
+            suffix = String.valueOf(definition.getReindexCount());
+        }
+
+        return getESSafeIndexName(definition.getIndexPath() + "-" + suffix);
+    }
+
+    /**
+     * <ul>
+     *     <li>abc -> abc</li>
+     *     <li>xy:abc -> xyabc</li>
+     *     <li>/oak:index/abc -> abc</li>
+     * </ul>
+     * <p>
+     * The resulting file name would be truncated to MAX_NAME_LENGTH
+     */
+    private static String getESSafeIndexName(String indexPath) {
+        String name = StreamSupport
+                .stream(PathUtils.elements(indexPath).spliterator(), false)
+                .limit(3) //Max 3 nodeNames including oak:index which is the immediate parent for any indexPath
+                .filter(p -> !"oak:index".equals(p))
+                .map(ElasticsearchIndexDescriptor::getESSafeName)
+                .collect(Collectors.joining("_"));
+
+        if (name.length() > MAX_NAME_LENGTH) {
+            name = name.substring(0, MAX_NAME_LENGTH);
+        }
+        return name;
+    }
+
+    /**
+     * Convert {@code e} to Elasticsearch safe index name.
+     * Ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html
+     */
+    private static String getESSafeName(String suggestedIndexName) {
+        return suggestedIndexName.replaceAll(INVALID_CHARS_REGEX, "").toLowerCase();
+    }
+
+}
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexProviderService.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexProviderService.java
index fd6a2a969f..7d3789fca7 100644
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexProviderService.java
+++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexProviderService.java
@@ -16,11 +16,15 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elasticsearch;
 
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import org.apache.commons.io.FilenameUtils;
-import org.apache.felix.scr.annotations.*;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.ReferencePolicy;
+import org.apache.felix.scr.annotations.ReferencePolicyOption;
 import org.apache.jackrabbit.oak.api.jmx.CacheStatsMBean;
 import org.apache.jackrabbit.oak.cache.CacheStats;
 import org.apache.jackrabbit.oak.commons.IOUtils;
@@ -31,22 +35,24 @@ import org.apache.jackrabbit.oak.plugins.index.elasticsearch.index.Elasticsearch
 import org.apache.jackrabbit.oak.plugins.index.elasticsearch.query.ElasticsearchIndexProvider;
 import org.apache.jackrabbit.oak.plugins.index.fulltext.PreExtractedTextProvider;
 import org.apache.jackrabbit.oak.plugins.index.search.ExtractedTextCache;
-import org.apache.jackrabbit.oak.plugins.index.search.TextExtractionStatsMBean;
+import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider;
 import org.apache.jackrabbit.oak.spi.whiteboard.Registration;
 import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
 import org.apache.jackrabbit.oak.stats.StatisticsProvider;
+import org.jetbrains.annotations.NotNull;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceRegistration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
+import java.util.ArrayList;
 import java.util.Dictionary;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
-import static com.google.common.base.Preconditions.checkNotNull;
 import static org.apache.commons.io.FileUtils.ONE_MB;
 import static org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils.registerMBean;
 
@@ -83,26 +89,23 @@ public class ElasticsearchIndexProviderService {
     )
     private static final String PROP_PRE_EXTRACTED_TEXT_ALWAYS_USE = "alwaysUsePreExtractedCache";
 
-    private static final String PROP_ELASTICSEARCH_SCHEME_DEFAULT = "http";
-//    @Property(
-//            value = PROP_ELASTICSEARCH_SCHEME_DEFAULT,
-//            label = "Elasticsearch connection scheme"
-//    )
-    private static final String PROP_ELASTICSEARCH_SCHEME = "elasticsearch.scheme";
-
-    private static final String PROP_ELASTICSEARCH_HOST_DEFAULT = "localhost";
-//    @Property(
-//            value = PROP_ELASTICSEARCH_HOST_DEFAULT,
-//            label = "Elasticsearch connection host"
-//    )
-    private static final String PROP_ELASTICSEARCH_HOST = "elasticsearch.host";
-
-    private static final int PROP_ELASTICSEARCH_PORT_DEFAULT = 9200;
-//    @Property(
-//            intValue = PROP_ELASTICSEARCH_PORT_DEFAULT,
-//            label = "Elasticsearch connection port"
-//    )
-    private static final String PROP_ELASTICSEARCH_PORT = "elasticsearch.port";
+    @Property(
+            value = ElasticsearchConnection.DEFAULT_SCHEME,
+            label = "Elasticsearch connection scheme"
+    )
+    private static final String PROP_ELASTICSEARCH_SCHEME = ElasticsearchConnection.SCHEME_PROP;
+
+    @Property(
+            value = ElasticsearchConnection.DEFAULT_HOST,
+            label = "Elasticsearch connection host"
+    )
+    private static final String PROP_ELASTICSEARCH_HOST = ElasticsearchConnection.HOST_PROP;
+
+    @Property(
+            intValue = ElasticsearchConnection.DEFAULT_PORT,
+            label = "Elasticsearch connection port"
+    )
+    private static final String PROP_ELASTICSEARCH_PORT = ElasticsearchConnection.PORT_PROP;
 
     @Property(
             label = "Local text extraction cache path",
@@ -121,26 +124,25 @@ public class ElasticsearchIndexProviderService {
 
     private ExtractedTextCache extractedTextCache;
 
-    private ElasticsearchConnectionFactory connectionFactory = null;
-
-    private final List<ServiceRegistration> regs = Lists.newArrayList();
-    private final List<Registration> oakRegs = Lists.newArrayList();
+    private final List<ServiceRegistration> regs = new ArrayList<>();
+    private final List<Registration> oakRegs = new ArrayList<>();
 
     private Whiteboard whiteboard;
     private File textExtractionDir;
 
+    private ElasticsearchConnection elasticsearchConnection;
+
     @Activate
     private void activate(BundleContext bundleContext, Map<String, ?> config) {
         whiteboard = new OsgiWhiteboard(bundleContext);
 
-        initializeTextExtractionDir(bundleContext, config);
-        initializeExtractedTextCache(config, statisticsProvider);
+        //initializeTextExtractionDir(bundleContext, config);
+        //initializeExtractedTextCache(config, statisticsProvider);
 
-        connectionFactory = new ElasticsearchConnectionFactory();
-        ElasticsearchIndexCoordinateFactory esIndexCoordFactory = getElasticsearchIndexCoordinateFactory(config);
+        elasticsearchConnection = getElasticsearchCoordinate(config);
 
-        registerIndexProvider(bundleContext, esIndexCoordFactory);
-        registerIndexEditor(bundleContext, esIndexCoordFactory);
+        registerIndexProvider(bundleContext);
+        registerIndexEditor(bundleContext);
     }
 
     @Deactivate
@@ -153,33 +155,32 @@ public class ElasticsearchIndexProviderService {
             reg.unregister();
         }
 
-        IOUtils.closeQuietly(connectionFactory);
-        connectionFactory = null;
+        IOUtils.closeQuietly(elasticsearchConnection);
 
         if (extractedTextCache != null) {
             extractedTextCache.close();
         }
     }
 
-    private void registerIndexProvider(BundleContext bundleContext, ElasticsearchIndexCoordinateFactory esIndexCoordFactory) {
-        ElasticsearchIndexProvider indexProvider = new ElasticsearchIndexProvider(esIndexCoordFactory);
+    private void registerIndexProvider(BundleContext bundleContext) {
+        ElasticsearchIndexProvider indexProvider = new ElasticsearchIndexProvider(elasticsearchConnection);
 
         Dictionary<String, Object> props = new Hashtable<>();
         props.put("type", ElasticsearchIndexConstants.TYPE_ELASTICSEARCH);
-        regs.add(bundleContext.registerService(IndexEditorProvider.class.getName(), indexProvider, props));
+        regs.add(bundleContext.registerService(QueryIndexProvider.class.getName(), indexProvider, props));
     }
 
-    private void registerIndexEditor(BundleContext bundleContext, ElasticsearchIndexCoordinateFactory esIndexCoordFactory) {
-        ElasticsearchIndexEditorProvider editorProvider = new ElasticsearchIndexEditorProvider(esIndexCoordFactory, extractedTextCache);
+    private void registerIndexEditor(BundleContext bundleContext) {
+        ElasticsearchIndexEditorProvider editorProvider = new ElasticsearchIndexEditorProvider(elasticsearchConnection, extractedTextCache);
 
         Dictionary<String, Object> props = new Hashtable<>();
         props.put("type", ElasticsearchIndexConstants.TYPE_ELASTICSEARCH);
         regs.add(bundleContext.registerService(IndexEditorProvider.class.getName(), editorProvider, props));
-        oakRegs.add(registerMBean(whiteboard,
-                TextExtractionStatsMBean.class,
-                editorProvider.getExtractedTextCache().getStatsMBean(),
-                TextExtractionStatsMBean.TYPE,
-                "TextExtraction statistics"));
+//        oakRegs.add(registerMBean(whiteboard,
+//                TextExtractionStatsMBean.class,
+//                editorProvider.getExtractedTextCache().getStatsMBean(),
+//                TextExtractionStatsMBean.TYPE,
+//                "TextExtraction statistics"));
     }
 
     private void initializeExtractedTextCache(Map<String, ?> config, StatisticsProvider statisticsProvider) {
@@ -196,11 +197,11 @@ public class ElasticsearchIndexProviderService {
                 alwaysUsePreExtractedCache,
                 textExtractionDir,
                 statisticsProvider);
-        if (extractedTextProvider != null){
+        if (extractedTextProvider != null) {
             registerExtractedTextProvider(extractedTextProvider);
         }
         CacheStats stats = extractedTextCache.getCacheStats();
-        if (stats != null){
+        if (stats != null) {
             oakRegs.add(registerMBean(whiteboard,
                     CacheStatsMBean.class, stats,
                     CacheStatsMBean.TYPE, stats.getName()));
@@ -209,28 +210,30 @@ public class ElasticsearchIndexProviderService {
         }
     }
 
-    void initializeTextExtractionDir(BundleContext bundleContext, Map<String, ?> config) {
+    private void initializeTextExtractionDir(BundleContext bundleContext, Map<String, ?> config) {
         String textExtractionDir = PropertiesUtil.toString(config.get(PROP_LOCAL_TEXT_EXTRACTION_DIR), null);
-        if (Strings.isNullOrEmpty(textExtractionDir)) {
+        if (textExtractionDir == null || textExtractionDir.trim().isEmpty()) {
             String repoHome = bundleContext.getProperty(REPOSITORY_HOME);
-            if (repoHome != null){
+            if (repoHome != null) {
                 textExtractionDir = FilenameUtils.concat(repoHome, "index");
             }
         }
 
-        checkNotNull(textExtractionDir, "Text extraction directory cannot be determined as neither " +
-                "directory path [%s] nor repository home [%s] defined", PROP_LOCAL_TEXT_EXTRACTION_DIR, REPOSITORY_HOME);
+        if (textExtractionDir == null) {
+            throw new IllegalStateException(String.format("Text extraction directory cannot be determined as neither " +
+                    "directory path [%s] nor repository home [%s] defined", PROP_LOCAL_TEXT_EXTRACTION_DIR, REPOSITORY_HOME));
+        }
 
         this.textExtractionDir = new File(textExtractionDir);
     }
 
-    private void registerExtractedTextProvider(PreExtractedTextProvider provider){
-        if (extractedTextCache != null){
-            if (provider != null){
+    private void registerExtractedTextProvider(PreExtractedTextProvider provider) {
+        if (extractedTextCache != null) {
+            if (provider != null) {
                 String usage = extractedTextCache.isAlwaysUsePreExtractedCache() ?
                         "always" : "only during reindexing phase";
                 LOG.info("Registering PreExtractedTextProvider {} with extracted text cache. " +
-                        "It would be used {}",  provider, usage);
+                        "It would be used {}", provider, usage);
             } else {
                 LOG.info("Unregistering PreExtractedTextProvider with extracted text cache");
             }
@@ -238,16 +241,35 @@ public class ElasticsearchIndexProviderService {
         }
     }
 
-    private ElasticsearchIndexCoordinateFactory getElasticsearchIndexCoordinateFactory(Map<String, ?> config) {
-        ElasticsearchIndexCoordinateFactory esIndexCoordFactory;
-        Map<String, String> esCfg = Maps.newHashMap();
-        esCfg.put(ElasticsearchCoordinate.SCHEME_PROP,
-                PropertiesUtil.toString(config.get(PROP_ELASTICSEARCH_SCHEME), PROP_ELASTICSEARCH_SCHEME_DEFAULT));
-        esCfg.put(ElasticsearchCoordinate.HOST_PROP,
-                PropertiesUtil.toString(config.get(PROP_ELASTICSEARCH_HOST), PROP_ELASTICSEARCH_HOST_DEFAULT));
-        esCfg.put(ElasticsearchCoordinate.PORT_PROP, String.valueOf(
-                PropertiesUtil.toInteger(config.get(PROP_ELASTICSEARCH_PORT), PROP_ELASTICSEARCH_PORT_DEFAULT)));
-        esIndexCoordFactory = new DefaultElasticsearchIndexCoordinateFactory(connectionFactory, esCfg);
-        return esIndexCoordFactory;
+    private ElasticsearchConnection getElasticsearchCoordinate(Map<String, ?> contextConfig) {
+        // system properties have priority
+        ElasticsearchConnection connection = build(System.getProperties().entrySet()
+                .stream()
+                .collect(Collectors.toMap(
+                        e -> String.valueOf(e.getKey()),
+                        e -> String.valueOf(e.getValue()))
+                )
+        );
+
+        if (connection == null) {
+            connection = build(contextConfig);
+        }
+
+        return connection != null ? connection : ElasticsearchConnection.defaultConnection.get();
+    }
+
+    private ElasticsearchConnection build(@NotNull Map<String, ?> config) {
+        ElasticsearchConnection coordinate = null;
+        Object p = config.get(PROP_ELASTICSEARCH_PORT);
+        if (p != null) {
+            try {
+                Integer port = Integer.parseInt(p.toString());
+                coordinate = new ElasticsearchConnection((String) config.get(PROP_ELASTICSEARCH_SCHEME),
+                        (String) config.get(PROP_ELASTICSEARCH_HOST), port);
+            } catch (NumberFormatException nfe) {
+                LOG.warn("{} value ({}) cannot be parsed to a valid number", PROP_ELASTICSEARCH_PORT, p);
+            }
+        }
+        return coordinate;
     }
 }
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexEditorContext.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexEditorContext.java
index ea04cab133..b3a8a2bb4a 100644
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexEditorContext.java
+++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexEditorContext.java
@@ -26,6 +26,8 @@ import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.jetbrains.annotations.Nullable;
 
+import java.io.IOException;
+
 public class ElasticsearchIndexEditorContext extends FulltextIndexEditorContext<ElasticsearchDocument> {
     ElasticsearchIndexEditorContext(NodeState root,
                                     NodeBuilder definition, @Nullable IndexDefinition indexDefinition,
@@ -53,11 +55,15 @@ public class ElasticsearchIndexEditorContext extends FulltextIndexEditorContext<
 
         // Now, that index definition _might_ have been migrated by super call, it would be ok to
         // get writer and provision index settings and mappings
-        getWriter().setProvisioningRequired();
+        try {
+            getWriter().provisionIndex();
+        } catch (IOException e) {
+            throw new IllegalStateException("Unable to provision index", e);
+        }
     }
 
     @Override
     public ElasticsearchIndexWriter getWriter() {
-        return (ElasticsearchIndexWriter)super.getWriter();
+        return (ElasticsearchIndexWriter) super.getWriter();
     }
 }
\ No newline at end of file
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexEditorProvider.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexEditorProvider.java
index b73bd05455..4a55b83662 100644
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexEditorProvider.java
+++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexEditorProvider.java
@@ -21,7 +21,7 @@ import org.apache.jackrabbit.oak.plugins.index.ContextAwareCallback;
 import org.apache.jackrabbit.oak.plugins.index.IndexEditorProvider;
 import org.apache.jackrabbit.oak.plugins.index.IndexUpdateCallback;
 import org.apache.jackrabbit.oak.plugins.index.IndexingContext;
-import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexCoordinateFactory;
+import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchConnection;
 import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexDefinition;
 import org.apache.jackrabbit.oak.plugins.index.search.ExtractedTextCache;
 import org.apache.jackrabbit.oak.spi.commit.Editor;
@@ -30,17 +30,16 @@ import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexConstants.TYPE_ELASTICSEARCH;
 
 public class ElasticsearchIndexEditorProvider implements IndexEditorProvider {
 
-    private final ElasticsearchIndexCoordinateFactory esIndexCoordFactory;
+    private final ElasticsearchConnection elasticsearchConnection;
     private final ExtractedTextCache extractedTextCache;
 
-    public ElasticsearchIndexEditorProvider(@NotNull ElasticsearchIndexCoordinateFactory esIndexCoordFactory,
+    public ElasticsearchIndexEditorProvider(@NotNull ElasticsearchConnection elasticsearchConnection,
                                             ExtractedTextCache extractedTextCache) {
-        this.esIndexCoordFactory = esIndexCoordFactory;
+        this.elasticsearchConnection = elasticsearchConnection;
         this.extractedTextCache = extractedTextCache != null ? extractedTextCache : new ExtractedTextCache(0, 0);
     }
 
@@ -49,15 +48,16 @@ public class ElasticsearchIndexEditorProvider implements IndexEditorProvider {
                                            @NotNull NodeBuilder definition, @NotNull NodeState root,
                                            @NotNull IndexUpdateCallback callback) throws CommitFailedException {
         if (TYPE_ELASTICSEARCH.equals(type)) {
-            checkArgument(callback instanceof ContextAwareCallback, "callback instance not of type " +
-                    "ContextAwareCallback [%s]", callback);
-            IndexingContext indexingContext = ((ContextAwareCallback)callback).getIndexingContext();
+            if (!(callback instanceof ContextAwareCallback)) {
+                throw new IllegalStateException("callback instance not of type ContextAwareCallback [" + callback + "]");
+            }
+            IndexingContext indexingContext = ((ContextAwareCallback) callback).getIndexingContext();
 
             String indexPath = indexingContext.getIndexPath();
             ElasticsearchIndexDefinition indexDefinition =
                     new ElasticsearchIndexDefinition(root, definition.getNodeState(), indexPath);
 
-            ElasticsearchIndexWriterFactory writerFactory = new ElasticsearchIndexWriterFactory(esIndexCoordFactory);
+            ElasticsearchIndexWriterFactory writerFactory = new ElasticsearchIndexWriterFactory(elasticsearchConnection);
 
             ElasticsearchIndexEditorContext context = new ElasticsearchIndexEditorContext(root,
                     definition, indexDefinition,
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexWriter.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexWriter.java
index 348c2a516a..78b739264a 100644
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexWriter.java
+++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexWriter.java
@@ -16,8 +16,8 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elasticsearch.index;
 
-import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexCoordinateFactory;
-import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexCoordinate;
+import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchConnection;
+import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexDescriptor;
 import org.apache.jackrabbit.oak.plugins.index.search.FieldNames;
 import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
 import org.apache.jackrabbit.oak.plugins.index.search.spi.editor.FulltextIndexWriter;
@@ -26,7 +26,6 @@ import org.elasticsearch.action.delete.DeleteResponse;
 import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.index.IndexResponse;
 import org.elasticsearch.client.RequestOptions;
-import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.client.indices.CreateIndexRequest;
 import org.elasticsearch.client.indices.CreateIndexResponse;
 import org.elasticsearch.common.Strings;
@@ -49,119 +48,97 @@ import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
 public class ElasticsearchIndexWriter implements FulltextIndexWriter<ElasticsearchDocument> {
     private static final Logger LOG = LoggerFactory.getLogger(ElasticsearchIndexWriter.class);
 
-    private final ElasticsearchIndexCoordinate esIndexCoord;
-    private final RestHighLevelClient client;
-    private boolean shouldProvisionIndex;
+    private final ElasticsearchIndexDescriptor indexDescriptor;
 
     private final boolean isAsync;
 
     // TODO: use bulk API - https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/java-docs-bulk-processor.html
     ElasticsearchIndexWriter(@NotNull IndexDefinition indexDefinition,
-                             ElasticsearchIndexCoordinateFactory esIndexCoordFactory) {
-        esIndexCoord = esIndexCoordFactory.getElasticsearchIndexCoordinate(indexDefinition);
-        client = esIndexCoord.getClient();
+                             @NotNull ElasticsearchConnection elasticsearchConnection) {
+        indexDescriptor = new ElasticsearchIndexDescriptor(elasticsearchConnection, indexDefinition);
 
         // TODO: ES indexing put another bit delay before docs appear in search.
         // For test without "async" indexing, we can use following hack BUT those where we
         // would setup async, we'd need to find another way.
         isAsync = indexDefinition.getDefinitionNodeState().getProperty("async") != null;
-
-        shouldProvisionIndex = false;
     }
 
     @Override
     public void updateDocument(String path, ElasticsearchDocument doc) throws IOException {
-        provisionIndex();
-        IndexRequest request = new IndexRequest(esIndexCoord.getEsIndexName())
+        IndexRequest request = new IndexRequest(indexDescriptor.getIndexName())
                 .id(pathToId(path))
                 // immediate refresh would slow indexing response such that next
                 // search would see the effect of this indexed doc. Must only get
                 // enabled in tests (hopefully there are no non-async indexes in real life)
                 .setRefreshPolicy(isAsync ? NONE : IMMEDIATE)
                 .source(doc.build(), XContentType.JSON);
-        IndexResponse response = client.index(request, RequestOptions.DEFAULT);
+        IndexResponse response = indexDescriptor.getClient().index(request, RequestOptions.DEFAULT);
         LOG.trace("update {} - {}. Response: {}", path, doc, response);
     }
 
     @Override
     public void deleteDocuments(String path) throws IOException {
-        provisionIndex();
-        DeleteRequest request = new DeleteRequest(esIndexCoord.getEsIndexName())
+        DeleteRequest request = new DeleteRequest(indexDescriptor.getIndexName())
                 .id(pathToId(path))
                 // immediate refresh would slow indexing response such that next
                 // search would see the effect of this indexed doc. Must only get
                 // enabled in tests (hopefully there are no non-async indexes in real life)
                 .setRefreshPolicy(isAsync ? NONE : IMMEDIATE);
-        DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
+        DeleteResponse response = indexDescriptor.getClient().delete(request, RequestOptions.DEFAULT);
         LOG.trace("delete {}. Response: {}", path, response);
 
     }
 
     @Override
     public boolean close(long timestamp) throws IOException {
-        provisionIndex();
         // TODO : track index updates and return accordingly
         // TODO : if/when we do async push, this is where to wait for those ops to complete
         return false;
     }
 
-    /**
-     * This method <b>won't</b> immediately provision index. But, provision would be done <b>before</b>
-     * any updates are sent to the index
-     */
-    void setProvisioningRequired() {
-        shouldProvisionIndex = true;
-    }
-
-    private void provisionIndex() throws IOException {
-        if (!shouldProvisionIndex) {
-            return;
-        }
-
-        try {
-            CreateIndexRequest request = new CreateIndexRequest(esIndexCoord.getEsIndexName());
-
-            // provision settings
-            request.settings(Settings.builder()
-                            .put("analysis.analyzer.ancestor_analyzer.type", "custom")
-                            .put("analysis.analyzer.ancestor_analyzer.tokenizer", "path_hierarchy"));
-
-            // provision mappings
-            XContentBuilder mappingBuilder = XContentFactory.jsonBuilder();
-            mappingBuilder.startObject();
+    // TODO: we need to check if the index already exists and in that case we have to figure out if there are
+    // "breaking changes" in the index definition
+    protected void provisionIndex() throws IOException {
+        CreateIndexRequest request = new CreateIndexRequest(indexDescriptor.getIndexName());
+
+        // provision settings
+        request.settings(Settings.builder()
+                .put("analysis.analyzer.ancestor_analyzer.type", "custom")
+                .put("analysis.analyzer.ancestor_analyzer.tokenizer", "path_hierarchy"));
+
+        // provision mappings
+        XContentBuilder mappingBuilder = XContentFactory.jsonBuilder();
+        mappingBuilder.startObject();
+        {
+            mappingBuilder.startObject("properties");
             {
-                mappingBuilder.startObject("properties");
-                {
-                    mappingBuilder.startObject(FieldNames.ANCESTORS)
-                            .field("type", "text")
-                            .field("analyzer", "ancestor_analyzer")
-                            .field("search_analyzer", "keyword")
-                            .field("search_quote_analyzer", "keyword")
-                            .endObject();
-                    mappingBuilder.startObject(FieldNames.PATH_DEPTH)
-                            .field("type", "integer")
-                            .endObject();
-                    mappingBuilder.startObject(FieldNames.SUGGEST)
-                            .field("type", "completion")
-                            .endObject();
-                    mappingBuilder.startObject(FieldNames.NOT_NULL_PROPS)
-                            .field("type", "keyword")
-                            .endObject();
-                    mappingBuilder.startObject(FieldNames.NULL_PROPS)
-                            .field("type", "keyword")
-                            .endObject();
-                }
-                mappingBuilder.endObject();
+                mappingBuilder.startObject(FieldNames.ANCESTORS)
+                        .field("type", "text")
+                        .field("analyzer", "ancestor_analyzer")
+                        .field("search_analyzer", "keyword")
+                        .field("search_quote_analyzer", "keyword")
+                        .endObject();
+                mappingBuilder.startObject(FieldNames.PATH_DEPTH)
+                        .field("type", "integer")
+                        .endObject();
+                mappingBuilder.startObject(FieldNames.SUGGEST)
+                        .field("type", "completion")
+                        .endObject();
+                mappingBuilder.startObject(FieldNames.NOT_NULL_PROPS)
+                        .field("type", "keyword")
+                        .endObject();
+                mappingBuilder.startObject(FieldNames.NULL_PROPS)
+                        .field("type", "keyword")
+                        .endObject();
             }
             mappingBuilder.endObject();
-            request.mapping(mappingBuilder);
+        }
+        mappingBuilder.endObject();
+        request.mapping(mappingBuilder);
 
-            String requestMsg = Strings.toString(request.toXContent(jsonBuilder(), EMPTY_PARAMS));
-            CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
+        String requestMsg = Strings.toString(request.toXContent(jsonBuilder(), EMPTY_PARAMS));
+        CreateIndexResponse response = indexDescriptor.getClient().indices().create(request, RequestOptions.DEFAULT);
 
-            LOG.info("Updated settings {}. Response acknowledged: {}", requestMsg, response.isAcknowledged());
-        } finally {
-            shouldProvisionIndex = false;
-        }
+        LOG.info("Updated settings {}. Response acknowledged: {}", requestMsg, response.isAcknowledged());
     }
 }
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexWriterFactory.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexWriterFactory.java
index c9e3ac0c97..bac172c64d 100644
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexWriterFactory.java
+++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/index/ElasticsearchIndexWriterFactory.java
@@ -16,21 +16,21 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elasticsearch.index;
 
-import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexCoordinateFactory;
+import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchConnection;
 import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
 import org.apache.jackrabbit.oak.plugins.index.search.spi.editor.FulltextIndexWriterFactory;
 import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.jetbrains.annotations.NotNull;
 
 public class ElasticsearchIndexWriterFactory implements FulltextIndexWriterFactory<ElasticsearchDocument> {
-    private final ElasticsearchIndexCoordinateFactory esIndexCoordFactory;
+    private final ElasticsearchConnection elasticsearchConnection;
 
-    ElasticsearchIndexWriterFactory(@NotNull ElasticsearchIndexCoordinateFactory esIndexCoordFactory) {
-        this.esIndexCoordFactory = esIndexCoordFactory;
+    ElasticsearchIndexWriterFactory(@NotNull ElasticsearchConnection elasticsearchConnection) {
+        this.elasticsearchConnection = elasticsearchConnection;
     }
 
     @Override
     public ElasticsearchIndexWriter newInstance(IndexDefinition definition, NodeBuilder definitionBuilder, boolean reindex) {
-        return new ElasticsearchIndexWriter(definition, esIndexCoordFactory);
+        return new ElasticsearchIndexWriter(definition, elasticsearchConnection);
     }
 }
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndex.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndex.java
index ed6c8f87f3..4e103a5252 100644
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndex.java
+++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndex.java
@@ -16,12 +16,12 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elasticsearch.query;
 
-import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexCoordinateFactory;
+import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchConnection;
 import org.apache.jackrabbit.oak.plugins.index.search.IndexNode;
 import org.apache.jackrabbit.oak.plugins.index.search.SizeEstimator;
-import org.apache.jackrabbit.oak.plugins.index.search.util.LMSEstimator;
 import org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndex;
 import org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndexPlanner;
+import org.apache.jackrabbit.oak.plugins.index.search.util.LMSEstimator;
 import org.apache.jackrabbit.oak.spi.query.Cursor;
 import org.apache.jackrabbit.oak.spi.query.Filter;
 import org.apache.jackrabbit.oak.spi.query.QueryLimits;
@@ -45,11 +45,11 @@ public class ElasticsearchIndex extends FulltextIndex {
     // higher than some threshold below which the query should rather be answered by something else if possible
     private static final double MIN_COST = 100.1;
 
-    private final ElasticsearchIndexCoordinateFactory esIndexCoordFactory;
+    private final ElasticsearchConnection elasticsearchConnection;
     private final NodeState root;
 
-    ElasticsearchIndex(@NotNull ElasticsearchIndexCoordinateFactory esIndexCoordFactory, @NotNull NodeState root) {
-        this.esIndexCoordFactory = esIndexCoordFactory;
+    ElasticsearchIndex(@NotNull ElasticsearchConnection elasticsearchConnection, @NotNull NodeState root) {
+        this.elasticsearchConnection = elasticsearchConnection;
         this.root = root;
     }
 
@@ -85,9 +85,7 @@ public class ElasticsearchIndex extends FulltextIndex {
 
     @Override
     protected IndexNode acquireIndexNode(String indexPath) {
-        ElasticsearchIndexNode elasticsearchIndexNode = ElasticsearchIndexNode.fromIndexPath(root, indexPath);
-        elasticsearchIndexNode.setFactory(esIndexCoordFactory);
-        return elasticsearchIndexNode;
+        return new ElasticsearchIndexNode(root, indexPath, elasticsearchConnection);
     }
 
     @Override
@@ -104,7 +102,7 @@ public class ElasticsearchIndex extends FulltextIndex {
         final FulltextIndexPlanner.PlanResult pr = getPlanResult(plan);
         QueryLimits settings = filter.getQueryLimits();
 
-        Iterator<FulltextResultRow> itr = new ElasticsearchResultRowIterator(esIndexCoordFactory, filter, pr, plan,
+        Iterator<FulltextResultRow> itr = new ElasticsearchResultRowIterator(filter, pr, plan,
                 acquireIndexNode(plan), FulltextIndex::shouldInclude, getEstimator(plan.getPlanName()));
         SizeEstimator sizeEstimator = getSizeEstimator(plan);
 
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndexNode.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndexNode.java
index 1a90adee0c..11d589ccf2 100644
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndexNode.java
+++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndexNode.java
@@ -16,8 +16,9 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elasticsearch.query;
 
-import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexCoordinateFactory;
+import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchConnection;
 import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexDefinition;
+import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexDescriptor;
 import org.apache.jackrabbit.oak.plugins.index.search.IndexNode;
 import org.apache.jackrabbit.oak.plugins.index.search.IndexStatistics;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
@@ -28,16 +29,13 @@ import org.jetbrains.annotations.Nullable;
 public class ElasticsearchIndexNode implements IndexNode {
 
     private final ElasticsearchIndexDefinition indexDefinition;
-    private ElasticsearchIndexCoordinateFactory factory;
+    private final ElasticsearchIndexDescriptor indexDescriptor;
 
-    static ElasticsearchIndexNode fromIndexPath(@NotNull NodeState root, @NotNull String indexPath) {
-        NodeState indexNS = NodeStateUtils.getNode(root, indexPath);
-        ElasticsearchIndexDefinition indexDefinition = new ElasticsearchIndexDefinition(root, indexNS, indexPath);
-        return new ElasticsearchIndexNode(indexDefinition);
-    }
-
-    private ElasticsearchIndexNode(ElasticsearchIndexDefinition indexDefinition) {
-        this.indexDefinition = indexDefinition;
+    protected ElasticsearchIndexNode(@NotNull NodeState root, @NotNull String indexPath,
+                                     @NotNull ElasticsearchConnection elasticsearchConnection) {
+        final NodeState indexNS = NodeStateUtils.getNode(root, indexPath);
+        this.indexDefinition = new ElasticsearchIndexDefinition(root, indexNS, indexPath);
+        this.indexDescriptor = new ElasticsearchIndexDescriptor(elasticsearchConnection, indexDefinition);
     }
 
     @Override
@@ -50,6 +48,10 @@ public class ElasticsearchIndexNode implements IndexNode {
         return indexDefinition;
     }
 
+    public ElasticsearchIndexDescriptor getIndexDescriptor() {
+        return indexDescriptor;
+    }
+
     @Override
     public int getIndexNodeId() {
         // TODO: does it matter that we simply return 0 as there's no observation based _refresh_ going on here
@@ -59,10 +61,6 @@ public class ElasticsearchIndexNode implements IndexNode {
 
     @Override
     public @Nullable IndexStatistics getIndexStatistics() {
-        return new ElasticsearchIndexStatistics(factory.getElasticsearchIndexCoordinate(indexDefinition));
-    }
-
-    public void setFactory(ElasticsearchIndexCoordinateFactory factory) {
-        this.factory = factory;
+        return new ElasticsearchIndexStatistics(indexDescriptor);
     }
 }
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndexProvider.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndexProvider.java
index a09924291a..840c67320a 100644
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndexProvider.java
+++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndexProvider.java
@@ -16,24 +16,24 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elasticsearch.query;
 
-import com.google.common.collect.ImmutableList;
-import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexCoordinateFactory;
+import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchConnection;
 import org.apache.jackrabbit.oak.spi.query.QueryIndex;
 import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.jetbrains.annotations.NotNull;
 
+import java.util.Collections;
 import java.util.List;
 
 public class ElasticsearchIndexProvider implements QueryIndexProvider {
-    private final ElasticsearchIndexCoordinateFactory esIndexCoordFactory;
+    private final ElasticsearchConnection elasticsearchConnection;
 
-    public ElasticsearchIndexProvider(@NotNull ElasticsearchIndexCoordinateFactory esIndexCoordFactory) {
-        this.esIndexCoordFactory = esIndexCoordFactory;
+    public ElasticsearchIndexProvider(ElasticsearchConnection elasticsearchConnection) {
+        this.elasticsearchConnection = elasticsearchConnection;
     }
 
     @Override
     public @NotNull List<? extends QueryIndex> getQueryIndexes(NodeState nodeState) {
-        return ImmutableList.of(new ElasticsearchIndex(esIndexCoordFactory, nodeState));
+        return Collections.singletonList(new ElasticsearchIndex(elasticsearchConnection, nodeState));
     }
 }
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndexStatistics.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndexStatistics.java
index c404174660..caf1eef994 100644
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndexStatistics.java
+++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchIndexStatistics.java
@@ -16,31 +16,28 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elasticsearch.query;
 
-import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexCoordinate;
+import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexDescriptor;
 import org.apache.jackrabbit.oak.plugins.index.search.IndexStatistics;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.core.CountRequest;
 import org.elasticsearch.client.core.CountResponse;
 import org.elasticsearch.index.query.QueryBuilders;
-import org.elasticsearch.search.builder.SearchSourceBuilder;
 
 import java.io.IOException;
 
 public class ElasticsearchIndexStatistics implements IndexStatistics {
-    private final ElasticsearchIndexCoordinate elasticsearchIndexCoordinate;
+    private final ElasticsearchIndexDescriptor elasticsearchIndexDescriptor;
 
-    ElasticsearchIndexStatistics(ElasticsearchIndexCoordinate elasticsearchIndexCoordinate) {
-        this.elasticsearchIndexCoordinate = elasticsearchIndexCoordinate;
+    ElasticsearchIndexStatistics(ElasticsearchIndexDescriptor elasticsearchIndexDescriptor) {
+        this.elasticsearchIndexDescriptor = elasticsearchIndexDescriptor;
     }
 
     @Override
     public int numDocs() {
         CountRequest countRequest = new CountRequest();
-        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
-        searchSourceBuilder.query(QueryBuilders.matchAllQuery());
-        countRequest.source(searchSourceBuilder);
+        countRequest.query(QueryBuilders.matchAllQuery());
         try {
-            CountResponse count = elasticsearchIndexCoordinate.getClient().count(countRequest, RequestOptions.DEFAULT);
+            CountResponse count = elasticsearchIndexDescriptor.getClient().count(countRequest, RequestOptions.DEFAULT);
             return (int) count.getCount();
         } catch (IOException e) {
             // ignore failure
@@ -51,11 +48,9 @@ public class ElasticsearchIndexStatistics implements IndexStatistics {
     @Override
     public int getDocCountFor(String key) {
         CountRequest countRequest = new CountRequest();
-        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
-        searchSourceBuilder.query(QueryBuilders.existsQuery(key));
-        countRequest.source(searchSourceBuilder);
+        countRequest.query(QueryBuilders.existsQuery(key));
         try {
-            CountResponse count = elasticsearchIndexCoordinate.getClient().count(countRequest, RequestOptions.DEFAULT);
+            CountResponse count = elasticsearchIndexDescriptor.getClient().count(countRequest, RequestOptions.DEFAULT);
             return (int) count.getCount();
         } catch (IOException e) {
             // ignore failure
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchResultRowIterator.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchResultRowIterator.java
index 27088a93a1..ddc5a53e98 100644
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchResultRowIterator.java
+++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchResultRowIterator.java
@@ -16,24 +16,25 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elasticsearch.query;
 
-import com.google.common.collect.AbstractIterator;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Queues;
 import org.apache.jackrabbit.oak.api.Type;
 import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.commons.PerfLogger;
-import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexCoordinateFactory;
 import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexDefinition;
 import org.apache.jackrabbit.oak.plugins.index.search.FieldNames;
 import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
 import org.apache.jackrabbit.oak.plugins.index.search.PropertyDefinition;
-import org.apache.jackrabbit.oak.plugins.index.search.util.LMSEstimator;
 import org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndex;
 import org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndexPlanner.PlanResult;
+import org.apache.jackrabbit.oak.plugins.index.search.util.LMSEstimator;
 import org.apache.jackrabbit.oak.spi.query.Filter;
 import org.apache.jackrabbit.oak.spi.query.QueryConstants;
 import org.apache.jackrabbit.oak.spi.query.QueryIndex.IndexPlan;
-import org.apache.jackrabbit.oak.spi.query.fulltext.*;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextAnd;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextContains;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextExpression;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextOr;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextTerm;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextVisitor;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.index.query.BoolQueryBuilder;
 import org.elasticsearch.index.query.QueryBuilder;
@@ -48,25 +49,41 @@ import javax.jcr.PropertyType;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Deque;
+import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiPredicate;
+import java.util.stream.StreamSupport;
 
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
 import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES;
 import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
 import static org.apache.jackrabbit.oak.api.Type.STRING;
 import static org.apache.jackrabbit.oak.commons.PathUtils.denotesRoot;
 import static org.apache.jackrabbit.oak.commons.PathUtils.getParentPath;
-import static org.apache.jackrabbit.oak.plugins.index.elasticsearch.util.TermQueryBuilderFactory.*;
+import static org.apache.jackrabbit.oak.plugins.index.elasticsearch.util.TermQueryBuilderFactory.newAncestorQuery;
+import static org.apache.jackrabbit.oak.plugins.index.elasticsearch.util.TermQueryBuilderFactory.newDepthQuery;
+import static org.apache.jackrabbit.oak.plugins.index.elasticsearch.util.TermQueryBuilderFactory.newMixinTypeQuery;
+import static org.apache.jackrabbit.oak.plugins.index.elasticsearch.util.TermQueryBuilderFactory.newNodeTypeQuery;
+import static org.apache.jackrabbit.oak.plugins.index.elasticsearch.util.TermQueryBuilderFactory.newNotNullPropQuery;
+import static org.apache.jackrabbit.oak.plugins.index.elasticsearch.util.TermQueryBuilderFactory.newNullPropQuery;
+import static org.apache.jackrabbit.oak.plugins.index.elasticsearch.util.TermQueryBuilderFactory.newPathQuery;
+import static org.apache.jackrabbit.oak.plugins.index.elasticsearch.util.TermQueryBuilderFactory.newPrefixPathQuery;
+import static org.apache.jackrabbit.oak.plugins.index.elasticsearch.util.TermQueryBuilderFactory.newPrefixQuery;
+import static org.apache.jackrabbit.oak.plugins.index.elasticsearch.util.TermQueryBuilderFactory.newPropertyRestrictionQuery;
+import static org.apache.jackrabbit.oak.plugins.index.elasticsearch.util.TermQueryBuilderFactory.newWildcardPathQuery;
+import static org.apache.jackrabbit.oak.plugins.index.elasticsearch.util.TermQueryBuilderFactory.newWildcardQuery;
 import static org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndex.isNodePath;
 import static org.apache.jackrabbit.oak.spi.query.QueryConstants.JCR_PATH;
 import static org.apache.jackrabbit.util.ISO8601.parse;
-import static org.elasticsearch.index.query.QueryBuilders.*;
+import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
+import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
+import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
+import static org.elasticsearch.index.query.QueryBuilders.termQuery;
 
-public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextIndex.FulltextResultRow> {
+public class ElasticsearchResultRowIterator implements Iterator<FulltextIndex.FulltextResultRow> {
     private static final Logger LOG = LoggerFactory
             .getLogger(ElasticsearchResultRowIterator.class);
     private static final PerfLogger PERF_LOGGER =
@@ -83,15 +100,13 @@ public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextInd
 
     private static final int ELASTICSEARCH_QUERY_MAX_BATCH_SIZE = 10000;
 
-
-    private final Deque<FulltextIndex.FulltextResultRow> queue = Queues.newArrayDeque();
+    private final Deque<FulltextIndex.FulltextResultRow> queue = new ArrayDeque<>();
     // TODO : find if ES can return dup docs - if so how to avoid
 //    private final Set<String> seenPaths = Sets.newHashSet();
     private SearchHit lastDoc;
     private int nextBatchSize = ELASTICSEARCH_QUERY_BATCH_SIZE;
     private boolean noDocs = false;
 
-    private final ElasticsearchIndexCoordinateFactory esIndexCoordFactory;
     private final Filter filter;
     private final PlanResult pr;
     private final IndexPlan plan;
@@ -99,14 +114,12 @@ public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextInd
     private final RowInclusionPredicate rowInclusionPredicate;
     private final LMSEstimator estimator;
 
-    ElasticsearchResultRowIterator(@NotNull ElasticsearchIndexCoordinateFactory esIndexCoordFactory,
-                                   @NotNull Filter filter,
+    ElasticsearchResultRowIterator(@NotNull Filter filter,
                                    @NotNull PlanResult pr,
                                    @NotNull IndexPlan plan,
                                    ElasticsearchIndexNode indexNode,
                                    RowInclusionPredicate rowInclusionPredicate,
                                    LMSEstimator estimator) {
-        this.esIndexCoordFactory = esIndexCoordFactory;
         this.filter = filter;
         this.pr = pr;
         this.plan = plan;
@@ -116,16 +129,18 @@ public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextInd
     }
 
     @Override
-    protected FulltextIndex.FulltextResultRow computeNext() {
-        if (!queue.isEmpty() || loadDocs()) {
-            return queue.remove();
-        } else {
-            return endOfData();
-        }
+    public boolean hasNext() {
+        return !queue.isEmpty() || loadDocs();
+    }
+
+    @Override
+    public FulltextIndex.FulltextResultRow next() {
+        return queue.remove();
     }
 
     /**
      * Loads the lucene documents in batches
+     *
      * @return true if any document is loaded
      */
     private boolean loadDocs() {
@@ -134,9 +149,11 @@ public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextInd
             return false;
         }
 
-        SearchHit lastDocToRecord = null;
+        if (indexNode == null) {
+            throw new IllegalStateException("indexNode cannot be null");
+        }
 
-        checkState(indexNode != null);
+        SearchHit lastDocToRecord = null;
         try {
             ElasticsearchSearcher searcher = getCurrentSearcher(indexNode);
             QueryBuilder query = getESRequest(plan, pr);
@@ -202,7 +219,7 @@ public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextInd
     }
 
     private ElasticsearchSearcher getCurrentSearcher(ElasticsearchIndexNode indexNode) {
-        return new ElasticsearchSearcher(esIndexCoordFactory, indexNode);
+        return new ElasticsearchSearcher(indexNode);
     }
 
     private FulltextIndex.FulltextResultRow convertToRow(SearchHit hit) throws IOException {
@@ -240,7 +257,7 @@ public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextInd
     /**
      * Get the Elasticsearch query for the given filter.
      *
-     * @param plan index plan containing filter details
+     * @param plan       index plan containing filter details
      * @param planResult
      * @return the Lucene query
      */
@@ -329,7 +346,7 @@ public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextInd
                     if (x instanceof BoolQueryBuilder) {
                         BoolQueryBuilder bq = (BoolQueryBuilder) x;
                         if (bq.mustNot().size() == 1
-                            // no other clauses
+                                // no other clauses
                                 && bq.should().isEmpty() && bq.must().isEmpty() && bq.filter().isEmpty()) {
                             hasMustNot = true;
                             q.mustNot(bq.mustNot().get(0));
@@ -352,9 +369,6 @@ public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextInd
             private boolean visitTerm(String propertyName, String text, String boost, boolean not) {
                 String p = getLuceneFieldName(propertyName, pr);
                 QueryBuilder q = tokenToQuery(text, p, pr);
-                if (q == null) {
-                    return false;
-                }
                 if (boost != null) {
                     q.boost(Float.parseFloat(boost));
                 }
@@ -430,7 +444,6 @@ public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextInd
      */
     @NotNull
     private static QueryBuilder performAdditionalWraps(@NotNull List<QueryBuilder> qs) {
-        checkNotNull(qs);
         if (qs.size() == 1) {
             // we don't need to worry about all-negatives in a bool query as
             // BoolQueryBuilder.adjustPureNegative is on by default anyway
@@ -455,13 +468,11 @@ public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextInd
     /**
      * unwraps any NOT clauses from the provided boolean query into another boolean query.
      *
-     * @param input the query to be analysed for the existence of NOT clauses. Cannot be null.
+     * @param input  the query to be analysed for the existence of NOT clauses. Cannot be null.
      * @param output the query where the unwrapped NOTs will be saved into. Cannot be null.
      * @return true if there where at least one unwrapped NOT. false otherwise.
      */
     private static boolean unwrapMustNot(@NotNull BoolQueryBuilder input, @NotNull BoolQueryBuilder output) {
-        checkNotNull(input);
-        checkNotNull(output);
         boolean unwrapped = false;
         for (QueryBuilder mustNot : input.mustNot()) {
             output.mustNot(mustNot);
@@ -480,6 +491,9 @@ public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextInd
 
     private static void addNonFullTextConstraints(List<QueryBuilder> qs,
                                                   IndexPlan plan, PlanResult planResult) {
+        final BiPredicate<Iterable<String>, String> any = (iterable, value) ->
+                StreamSupport.stream(iterable.spliterator(), false).anyMatch(value::equals);
+
         Filter filter = plan.getFilter();
         IndexDefinition defn = planResult.indexDefinition;
         if (!filter.matchesAllTypes()) {
@@ -509,7 +523,7 @@ public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextInd
                 // deduced
                 if (planResult.isPathTransformed()) {
                     String parentPathSegment = planResult.getParentPathSegment();
-                    if ( ! Iterables.any(PathUtils.elements(parentPathSegment), "*"::equals)) {
+                    if (!any.test(PathUtils.elements(parentPathSegment), "*")) {
                         qs.add(newPathQuery(path + parentPathSegment));
                     }
                 } else {
@@ -527,7 +541,7 @@ public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextInd
                     // deduced
                     if (planResult.isPathTransformed()) {
                         String parentPathSegment = planResult.getParentPathSegment();
-                        if ( ! Iterables.any(PathUtils.elements(parentPathSegment), "*"::equals)) {
+                        if (!any.test(PathUtils.elements(parentPathSegment), "*")) {
                             qs.add(newPathQuery(getParentPath(path) + parentPathSegment));
                         }
                     } else {
@@ -657,7 +671,7 @@ public class ElasticsearchResultRowIterator extends AbstractIterator<FulltextInd
 
     @Nullable
     private static QueryBuilder createQuery(String propertyName, Filter.PropertyRestriction pr,
-                                     PropertyDefinition defn) {
+                                            PropertyDefinition defn) {
         int propType = FulltextIndex.determinePropertyType(defn, pr);
 
         if (pr.isNullRestriction()) {
diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchSearcher.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchSearcher.java
index 24a7cf6cb3..c69e954b4e 100644
--- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchSearcher.java
+++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/query/ElasticsearchSearcher.java
@@ -16,14 +16,11 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elasticsearch.query;
 
-import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexCoordinateFactory;
-import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexCoordinate;
-import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexDefinition;
+import org.apache.jackrabbit.oak.plugins.index.elasticsearch.ElasticsearchIndexDescriptor;
 import org.apache.jackrabbit.oak.plugins.index.search.FieldNames;
 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.builder.SearchSourceBuilder;
 import org.jetbrains.annotations.NotNull;
@@ -31,14 +28,10 @@ import org.jetbrains.annotations.NotNull;
 import java.io.IOException;
 
 public class ElasticsearchSearcher {
-    private final ElasticsearchIndexCoordinate esIndexCoord;
-    private final RestHighLevelClient client;
+    private final ElasticsearchIndexDescriptor indexDescriptor;
 
-    ElasticsearchSearcher(@NotNull ElasticsearchIndexCoordinateFactory esIndexCoordFactory,
-                          @NotNull ElasticsearchIndexNode indexNode) {
-        ElasticsearchIndexDefinition defn = indexNode.getDefinition();
-        esIndexCoord = esIndexCoordFactory.getElasticsearchIndexCoordinate(defn);
-        client = esIndexCoord.getClient();
+    ElasticsearchSearcher(@NotNull ElasticsearchIndexNode indexNode) {
+        indexDescriptor = indexNode.getIndexDescriptor();
     }
 
     public SearchResponse search(QueryBuilder query, int batchSize) throws IOException {
@@ -46,12 +39,11 @@ public class ElasticsearchSearcher {
                 .query(query)
                 .fetchSource(false)
                 .storedField(FieldNames.PATH)
-                .size(batchSize)
-                ;
+                .size(batchSize);
 
-        SearchRequest request = new SearchRequest(esIndexCoord.getEsIndexName())
+        SearchRequest request = new SearchRequest(indexDescriptor.getIndexName())
                 .source(searchSourceBuilder);
 
-        return client.search(request, RequestOptions.DEFAULT);
+        return indexDescriptor.getClient().search(request, RequestOptions.DEFAULT);
     }
 }
diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchConnectionTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchConnectionTest.java
new file mode 100644
index 0000000000..0da1a63a0c
--- /dev/null
+++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchConnectionTest.java
@@ -0,0 +1,31 @@
+package org.apache.jackrabbit.oak.plugins.index.elasticsearch;
+
+import org.elasticsearch.client.RestHighLevelClient;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+public class ElasticsearchConnectionTest {
+
+    @Test
+    public void uniqueClient() throws IOException {
+        ElasticsearchConnection connection = ElasticsearchConnection.defaultConnection.get();
+
+        RestHighLevelClient client1 = connection.getClient();
+        RestHighLevelClient client2 = connection.getClient();
+
+        assertEquals(client1, client2);
+
+        connection.close();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void alreadyClosedConnection() throws IOException {
+        ElasticsearchConnection connection = ElasticsearchConnection.defaultConnection.get();
+        connection.close();
+
+        connection.getClient();
+    }
+}
diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchDockerRule.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchDockerRule.java
deleted file mode 100644
index 204ebe1653..0000000000
--- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchDockerRule.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.plugins.index.elasticsearch;
-
-import com.arakelian.docker.junit.DockerRule;
-import com.arakelian.docker.junit.model.ImmutableDockerConfig;
-import com.spotify.docker.client.DefaultDockerClient;
-import com.spotify.docker.client.auth.FixedRegistryAuthSupplier;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * An Elasticsearch {@link DockerRule}.
- */
-class ElasticsearchDockerRule extends DockerRule {
-
-    //Mimic following:
-    // docker run -p 9200:9200 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.1.1
-
-    private static final Logger LOG = LoggerFactory.getLogger(ElasticsearchDockerRule.class);
-
-    private static final String CONFIG_NAME = "Elasticsearch";
-
-    private static final String VERSION = System.getProperty("elasticsearch.version", "7.1.1");
-
-    private static final String IMAGE = "elasticsearch:" + VERSION;
-
-    private static final boolean DOCKER_AVAILABLE;
-
-    static {
-        boolean available = false;
-        try (DefaultDockerClient client = DefaultDockerClient.fromEnv()
-                .connectTimeoutMillis(5000L).readTimeoutMillis(20000L)
-                .registryAuthSupplier(new FixedRegistryAuthSupplier())
-                .build()) {
-            client.ping();
-            client.pull(IMAGE);
-            available = true;
-        } catch (Throwable t) {
-            LOG.info("Cannot connect to docker or pull image", t);
-        }
-        DOCKER_AVAILABLE = available;
-    }
-
-    ElasticsearchDockerRule() {
-        super(ImmutableDockerConfig.builder()
-                .name(CONFIG_NAME)
-                .image(IMAGE)
-                .ports("9200")
-                .allowRunningBetweenUnitTests(true)
-                .alwaysRemoveContainer(true)
-                .addStartedListener(container -> container.waitForLog("LicenseService"))
-                .addContainerConfigurer(builder -> builder.env("discovery.type=single-node"))
-                .build());
-    }
-
-    int getPort() {
-        return getContainer().getPortBinding("9200/tcp").getPort();
-    }
-
-    boolean isDockerAvailable() {
-        return DOCKER_AVAILABLE;
-    }
-}
diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexProviderServiceTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexProviderServiceTest.java
new file mode 100644
index 0000000000..9216f96f6b
--- /dev/null
+++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchIndexProviderServiceTest.java
@@ -0,0 +1,62 @@
+package org.apache.jackrabbit.oak.plugins.index.elasticsearch;
+
+import org.apache.jackrabbit.oak.osgi.OsgiWhiteboard;
+import org.apache.jackrabbit.oak.plugins.index.IndexEditorProvider;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
+import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
+import org.apache.jackrabbit.oak.spi.mount.Mounts;
+import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils;
+import org.apache.jackrabbit.oak.stats.StatisticsProvider;
+import org.apache.sling.testing.mock.osgi.MockOsgi;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.util.Collections;
+
+import static org.junit.Assert.assertNotNull;
+
+public class ElasticsearchIndexProviderServiceTest {
+
+    @Rule
+    public final TemporaryFolder folder = new TemporaryFolder(new File("target"));
+
+    @Rule
+    public final OsgiContext context = new OsgiContext();
+
+    private final ElasticsearchIndexProviderService service = new ElasticsearchIndexProviderService();
+
+    private Whiteboard wb;
+
+    @Before
+    public void setUp() {
+        MountInfoProvider mip = Mounts.newBuilder().build();
+        context.registerService(MountInfoProvider.class, mip);
+        context.registerService(NodeStore.class, new MemoryNodeStore());
+        context.registerService(StatisticsProvider.class, StatisticsProvider.NOOP);
+
+        wb = new OsgiWhiteboard(context.bundleContext());
+        MockOsgi.injectServices(service, context.bundleContext());
+    }
+
+    @Test
+    public void defaultSetup() throws Exception {
+        MockOsgi.activate(service, context.bundleContext(),
+                Collections.singletonMap("localTextExtractionDir", folder.newFolder("localTextExtractionDir").getAbsolutePath())
+        );
+
+        assertNotNull(context.getService(QueryIndexProvider.class));
+        assertNotNull(context.getService(IndexEditorProvider.class));
+
+        assertNotNull(WhiteboardUtils.getServices(wb, Runnable.class));
+
+        MockOsgi.deactivate(service, context.bundleContext());
+    }
+
+}
diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchManagementRule.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchManagementRule.java
deleted file mode 100644
index ce8aa7e8a6..0000000000
--- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchManagementRule.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.plugins.index.elasticsearch;
-
-import com.google.common.collect.Sets;
-import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
-import org.apache.jackrabbit.oak.spi.state.NodeState;
-import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
-import org.elasticsearch.client.RequestOptions;
-import org.junit.Assume;
-import org.junit.rules.ExternalResource;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.Set;
-
-public class ElasticsearchManagementRule extends ExternalResource
-        implements ElasticsearchIndexCoordinateFactory {
-    private static final Logger LOG = LoggerFactory.getLogger(ElasticsearchManagementRule.class);
-
-    private final ElasticsearchDockerRule elasticsearch = new ElasticsearchDockerRule();
-
-    private final ElasticsearchConnectionFactory connectionFactory = new ElasticsearchConnectionFactory();
-
-    private final Set<ElasticsearchIndexCoordinate> indices = Sets.newHashSet();
-
-    private boolean usingDocker;
-
-    @Override
-    public Statement apply(Statement base, Description description) {
-        Statement s = super.apply(base, description);
-        // see if local instance is available... initialize docker rule only if that's not the case
-        ElasticsearchCoordinate esCoord = ElasticsearchCoordinateImpl.construct(connectionFactory,
-                null, null);
-        if (!ElasticsearchTestUtils.isAvailable(esCoord) && elasticsearch.isDockerAvailable()) {
-            s = elasticsearch.apply(s, description);
-            usingDocker = true;
-        }
-
-        return s;
-    }
-
-    @Override
-    public ElasticsearchIndexCoordinate getElasticsearchIndexCoordinate(IndexDefinition indexDefinition) {
-        ElasticsearchCoordinate esCoord = getElasticsearchCoordinate(indexDefinition.getDefinitionNodeState());
-        ElasticsearchIndexCoordinate esIdxCoord = new ElasticsearchIndexCoordinateImpl(esCoord, indexDefinition);
-        indices.add(esIdxCoord);
-        return esIdxCoord;
-    }
-
-    @Override
-    protected void after() {
-        deletedIndices();
-        connectionFactory.close();
-    }
-
-    private ElasticsearchCoordinate getElasticsearchCoordinate(NodeState indexDefinition) {
-        ElasticsearchCoordinate esCoord = ElasticsearchCoordinateImpl.construct(connectionFactory,
-                indexDefinition, null);
-
-        if (!ElasticsearchTestUtils.isAvailable(esCoord) && usingDocker) {
-            int port = elasticsearch.getPort();
-            esCoord = new ElasticsearchCoordinateImpl(connectionFactory, "http", "localhost", port);
-        }
-
-        Assume.assumeTrue(ElasticsearchTestUtils.isAvailable(esCoord));
-
-        return esCoord;
-    }
-
-    private void deletedIndices() {
-        indices.forEach(idxCoord -> {
-            DeleteIndexRequest request = new DeleteIndexRequest(idxCoord.getEsIndexName());
-            try {
-                idxCoord.getClient().indices().delete(request, RequestOptions.DEFAULT);
-
-                LOG.info("Cleaned up index {}", idxCoord.getEsIndexName());
-
-            } catch (IOException e) {
-                LOG.warn("Failed to cleanup index {}", idxCoord.getEsIndexName());
-            }
-        });
-    }
-}
\ No newline at end of file
diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchPropertyIndexTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchPropertyIndexTest.java
index 4f826d2c4e..210552cc3d 100644
--- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchPropertyIndexTest.java
+++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchPropertyIndexTest.java
@@ -33,29 +33,35 @@ import org.apache.jackrabbit.oak.query.AbstractQueryTest;
 import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
 import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.elasticsearch.Version;
 import org.junit.Assert;
-import org.junit.Rule;
+import org.junit.ClassRule;
 import org.junit.Test;
+import org.testcontainers.elasticsearch.ElasticsearchContainer;
 
 import java.util.Arrays;
-import java.util.Collections;
-import java.util.Set;
 
-import static com.google.common.collect.ImmutableSet.of;
 import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME;
 import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.PROPDEF_PROP_NODE_NAME;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 public class ElasticsearchPropertyIndexTest extends AbstractQueryTest {
-    @Rule
-    public ElasticsearchManagementRule esMgmt = new ElasticsearchManagementRule();
+
+    @ClassRule
+    public static final ElasticsearchContainer ELASTIC =
+            new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:" + Version.CURRENT);
 
     @Override
     protected ContentRepository createRepository() {
-        ElasticsearchIndexEditorProvider editorProvider = new ElasticsearchIndexEditorProvider(esMgmt,
-                new ExtractedTextCache(10* FileUtils.ONE_MB, 100));
-        ElasticsearchIndexProvider indexProvider = new ElasticsearchIndexProvider(esMgmt);
+        ElasticsearchConnection coordinate = new ElasticsearchConnection(
+                ElasticsearchConnection.DEFAULT_SCHEME,
+                ELASTIC.getContainerIpAddress(),
+                ELASTIC.getMappedPort(ElasticsearchConnection.DEFAULT_PORT)
+        );
+        ElasticsearchIndexEditorProvider editorProvider = new ElasticsearchIndexEditorProvider(coordinate,
+                new ExtractedTextCache(10 * FileUtils.ONE_MB, 100));
+        ElasticsearchIndexProvider indexProvider = new ElasticsearchIndexProvider(coordinate);
 
         // remove all indexes to avoid cost competition (essentially a TODO for fixing cost ES cost estimation)
         NodeBuilder builder = InitialContentHelper.INITIAL_CONTENT.builder();
@@ -80,8 +86,8 @@ public class ElasticsearchPropertyIndexTest extends AbstractQueryTest {
 
     @Test
     public void indexSelection() throws Exception {
-        setIndex("test1", createIndex(of("propa", "propb")));
-        setIndex("test2", createIndex(of("propc")));
+        setIndex("test1", createIndex("propa", "propb"));
+        setIndex("test2", createIndex("propc"));
 
         Tree test = root.getTree("/").addChild("test");
         test.addChild("a").setProperty("propa", "foo");
@@ -102,9 +108,9 @@ public class ElasticsearchPropertyIndexTest extends AbstractQueryTest {
 
     //OAK-3825
     @Test
-    public void nodeNameViaPropDefinition() throws Exception{
+    public void nodeNameViaPropDefinition() throws Exception {
         //make index
-        IndexDefinitionBuilder builder = createIndex(Collections.EMPTY_SET);
+        IndexDefinitionBuilder builder = createIndex();
         builder.includedPaths("/test")
                 .evaluatePathRestrictions()
                 .indexRule("nt:base")
@@ -136,8 +142,8 @@ public class ElasticsearchPropertyIndexTest extends AbstractQueryTest {
     }
 
     @Test
-    public void emptyIndex() throws Exception{
-        setIndex("test1", createIndex(of("propa", "propb")));
+    public void emptyIndex() throws Exception {
+        setIndex("test1", createIndex("propa", "propb"));
         root.commit();
 
         Tree test = root.getTree("/").addChild("test");
@@ -150,7 +156,7 @@ public class ElasticsearchPropertyIndexTest extends AbstractQueryTest {
 
     @Test
     public void propertyExistenceQuery() throws Exception {
-        setIndex("test1", createIndex(of("propa", "propb")));
+        setIndex("test1", createIndex("propa", "propb"));
 
         Tree test = root.getTree("/").addChild("test");
         test.addChild("a").setProperty("propa", "a");
@@ -161,11 +167,12 @@ public class ElasticsearchPropertyIndexTest extends AbstractQueryTest {
         assertQuery("select [jcr:path] from [nt:base] where propa is not null", Arrays.asList("/test/a", "/test/b"));
     }
 
-    private static IndexDefinitionBuilder createIndex(Set<String> propNames) {
+    private static IndexDefinitionBuilder createIndex(String... propNames) {
         IndexDefinitionBuilder builder = new ElasticsearchIndexDefinitionBuilder().noAsync();
         IndexDefinitionBuilder.IndexRule indexRule = builder.indexRule("nt:base");
-        propNames.forEach(propName -> indexRule.property(propName).propertyIndex());
-
+        for (String propName : propNames) {
+            indexRule.property(propName).propertyIndex();
+        }
         return builder;
     }
 
@@ -173,7 +180,7 @@ public class ElasticsearchPropertyIndexTest extends AbstractQueryTest {
         builder.build(root.getTree("/").addChild(INDEX_DEFINITIONS_NAME).addChild(idxName));
     }
 
-    private String explain(String query){
+    private String explain(String query) {
         String explain = "explain " + query;
         return executeQuery(explain, "JCR-SQL2").get(0);
     }
diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchTestUtils.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchTestUtils.java
deleted file mode 100644
index 967c90ba95..0000000000
--- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elasticsearch/ElasticsearchTestUtils.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.plugins.index.elasticsearch;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.net.HttpURLConnection;
-import java.net.URL;
-
-class ElasticsearchTestUtils {
-    private static String createHealthURL(@NotNull final ElasticsearchCoordinate esCoord) {
-        return esCoord.getScheme() + "://" + esCoord.getHost() + ":" + esCoord.getPort() + "/_cat/health";
-    }
-
-    static boolean isAvailable(final ElasticsearchCoordinate esCoord) {
-        if (esCoord == null) {
-            return false;
-        }
-
-        try {
-            URL url = new URL(createHealthURL(esCoord));
-
-            HttpURLConnection con = (HttpURLConnection) url.openConnection();
-            con.setRequestMethod("GET");
-
-            int responseCode = con.getResponseCode();
-
-            return responseCode == 200;
-        } catch (Throwable t) {
-            return false;
-        }
-    }
-}
diff --git a/oak-segment-azure/pom.xml b/oak-segment-azure/pom.xml
index 22d2bf0753..26a899ebd7 100644
--- a/oak-segment-azure/pom.xml
+++ b/oak-segment-azure/pom.xml
@@ -164,6 +164,13 @@
             <artifactId>oak-blob</artifactId>
             <version>${project.version}</version>
             <scope>test</scope>
+        </dependency>
+         <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>oak-blob-cloud-azure</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.mockito</groupId>
diff --git a/oak-segment-azure/src/test/java/oak/apache/jackrabbit/oak/segment/azure/tool/SegmentCopyTestBase.java b/oak-segment-azure/src/test/java/oak/apache/jackrabbit/oak/segment/azure/tool/SegmentCopyTestBase.java
index dc78c5c8b3..cb1e37cd01 100644
--- a/oak-segment-azure/src/test/java/oak/apache/jackrabbit/oak/segment/azure/tool/SegmentCopyTestBase.java
+++ b/oak-segment-azure/src/test/java/oak/apache/jackrabbit/oak/segment/azure/tool/SegmentCopyTestBase.java
@@ -29,12 +29,12 @@ import java.io.PrintWriter;
 import java.util.Collections;
 import java.util.List;
 
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.commons.Buffer;
 import org.apache.jackrabbit.oak.segment.SegmentCache;
 import org.apache.jackrabbit.oak.segment.SegmentNodeStore;
 import org.apache.jackrabbit.oak.segment.SegmentNodeStoreBuilders;
 import org.apache.jackrabbit.oak.segment.azure.AzurePersistence;
-import org.apache.jackrabbit.oak.segment.azure.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.azure.tool.SegmentCopy;
 import org.apache.jackrabbit.oak.segment.azure.tool.ToolUtils.SegmentStoreType;
 import org.apache.jackrabbit.oak.segment.file.FileStore;
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureArchiveManagerTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureArchiveManagerTest.java
index 332138cde5..225b309fde 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureArchiveManagerTest.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureArchiveManagerTest.java
@@ -21,6 +21,7 @@ import com.microsoft.azure.storage.blob.CloudBlob;
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
 import com.microsoft.azure.storage.blob.ListBlobItem;
 import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.SegmentNodeStore;
 import org.apache.jackrabbit.oak.segment.SegmentNodeStoreBuilders;
 import org.apache.jackrabbit.oak.segment.file.FileStore;
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureGCJournalTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureGCJournalTest.java
index 5cb8a37f46..046413c3b0 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureGCJournalTest.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureGCJournalTest.java
@@ -20,6 +20,7 @@ package org.apache.jackrabbit.oak.segment.azure;
 
 import com.microsoft.azure.storage.StorageException;
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.spi.persistence.SegmentNodeStorePersistence;
 import org.apache.jackrabbit.oak.segment.file.GcJournalTest;
 import org.junit.Before;
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureJournalFileTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureJournalFileTest.java
index 9a8408497a..15fb924367 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureJournalFileTest.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureJournalFileTest.java
@@ -20,6 +20,7 @@ import com.microsoft.azure.storage.StorageException;
 import com.microsoft.azure.storage.blob.CloudAppendBlob;
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
 import com.microsoft.azure.storage.blob.ListBlobItem;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.spi.persistence.JournalFileReader;
 import org.apache.jackrabbit.oak.segment.spi.persistence.JournalFileWriter;
 import org.junit.Before;
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureManifestFileTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureManifestFileTest.java
index d505bd7712..d041a9e6ca 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureManifestFileTest.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureManifestFileTest.java
@@ -20,6 +20,7 @@ package org.apache.jackrabbit.oak.segment.azure;
 
 import com.microsoft.azure.storage.StorageException;
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.spi.persistence.ManifestFile;
 import org.junit.Before;
 import org.junit.ClassRule;
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureReadSegmentTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureReadSegmentTest.java
index 40b154fb7c..f98e2dab33 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureReadSegmentTest.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureReadSegmentTest.java
@@ -22,6 +22,7 @@ import com.microsoft.azure.storage.StorageException;
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
 import com.microsoft.azure.storage.blob.CloudBlobDirectory;
 
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.commons.Buffer;
 import org.apache.jackrabbit.oak.segment.SegmentId;
 import org.apache.jackrabbit.oak.segment.SegmentNotFoundException;
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureRepositoryLockTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureRepositoryLockTest.java
index 5a0f7a5bb4..5d602bdabd 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureRepositoryLockTest.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureRepositoryLockTest.java
@@ -21,6 +21,7 @@ package org.apache.jackrabbit.oak.segment.azure;
 import com.microsoft.azure.storage.StorageException;
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
 import com.microsoft.azure.storage.blob.CloudBlockBlob;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.spi.persistence.RepositoryLock;
 import org.junit.Before;
 import org.junit.ClassRule;
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureTarFileTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureTarFileTest.java
index ecb75617ee..bdaae84aad 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureTarFileTest.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureTarFileTest.java
@@ -18,6 +18,7 @@ package org.apache.jackrabbit.oak.segment.azure;
 
 import com.microsoft.azure.storage.StorageException;
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.spi.monitor.FileStoreMonitorAdapter;
 import org.apache.jackrabbit.oak.segment.spi.monitor.IOMonitorAdapter;
 import org.apache.jackrabbit.oak.segment.file.tar.TarFileTest;
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureTarFilesTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureTarFilesTest.java
index 23f0c30fc5..a2e73f3e63 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureTarFilesTest.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureTarFilesTest.java
@@ -17,6 +17,7 @@
 package org.apache.jackrabbit.oak.segment.azure;
 
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.spi.monitor.FileStoreMonitorAdapter;
 import org.apache.jackrabbit.oak.segment.spi.monitor.IOMonitorAdapter;
 import org.apache.jackrabbit.oak.segment.file.tar.TarFiles;
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureTarWriterTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureTarWriterTest.java
index 22a4dbef00..9f826cf59c 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureTarWriterTest.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureTarWriterTest.java
@@ -18,6 +18,7 @@ package org.apache.jackrabbit.oak.segment.azure;
 
 import com.microsoft.azure.storage.StorageException;
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.spi.monitor.IOMonitorAdapter;
 import org.apache.jackrabbit.oak.segment.file.tar.TarWriterTest;
 import org.apache.jackrabbit.oak.segment.spi.monitor.RemoteStoreMonitorAdapter;
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzuriteDockerRule.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzuriteDockerRule.java
deleted file mode 100644
index 283d1cc2ca..0000000000
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzuriteDockerRule.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.segment.azure;
-
-import com.arakelian.docker.junit.DockerRule;
-import com.arakelian.docker.junit.model.ImmutableDockerConfig;
-import com.microsoft.azure.storage.CloudStorageAccount;
-import com.microsoft.azure.storage.StorageException;
-import com.microsoft.azure.storage.blob.CloudBlobContainer;
-import com.spotify.docker.client.DefaultDockerClient;
-import com.spotify.docker.client.auth.FixedRegistryAuthSupplier;
-
-import org.junit.Assume;
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.net.URISyntaxException;
-import java.security.InvalidKeyException;
-
-public class AzuriteDockerRule implements TestRule {
-
-    private static final String IMAGE = "trekawek/azurite";
-
-    private final DockerRule wrappedRule;
-
-    public AzuriteDockerRule() {
-        wrappedRule = new DockerRule(ImmutableDockerConfig.builder()
-                .image(IMAGE)
-                .name("oak-test-azurite")
-                .ports("10000")
-                .addStartedListener(container -> {
-                    container.waitForPort("10000/tcp");
-                    container.waitForLog("Azure Blob Storage Emulator listening on port 10000");
-                })
-                .addContainerConfigurer(builder -> builder.env("executable=blob"))
-                .alwaysRemoveContainer(true)
-                .build());
-
-    }
-
-    public CloudBlobContainer getContainer(String name) throws URISyntaxException, StorageException, InvalidKeyException {
-        int mappedPort = getMappedPort();
-        CloudStorageAccount cloud = CloudStorageAccount.parse("DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:" + mappedPort + "/devstoreaccount1;");
-        CloudBlobContainer container = cloud.createCloudBlobClient().getContainerReference(name);
-        container.deleteIfExists();
-        container.create();
-        return container;
-    }
-
-    @Override
-    public Statement apply(Statement statement, Description description) {
-        try {
-            DefaultDockerClient client = DefaultDockerClient.fromEnv()
-                    .connectTimeoutMillis(5000L)
-                    .readTimeoutMillis(20000L)
-                    .registryAuthSupplier(new FixedRegistryAuthSupplier())
-                    .build();
-            client.ping();
-            client.pull(IMAGE);
-            client.close();
-        } catch (Throwable t) {
-            Assume.assumeNoException(t);
-        }
-
-        return wrappedRule.apply(statement, description);
-    }
-
-    public int getMappedPort() {
-        return wrappedRule.getContainer().getPortBinding("10000/tcp").getPort();
-    }
-}
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/journal/AzureJournalReaderTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/journal/AzureJournalReaderTest.java
index fcf766d1a3..6d9843eff3 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/journal/AzureJournalReaderTest.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/journal/AzureJournalReaderTest.java
@@ -19,7 +19,7 @@ package org.apache.jackrabbit.oak.segment.azure.journal;
 import com.microsoft.azure.storage.StorageException;
 import com.microsoft.azure.storage.blob.CloudAppendBlob;
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
-import org.apache.jackrabbit.oak.segment.azure.AzuriteDockerRule;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.file.JournalReader;
 import org.apache.jackrabbit.oak.segment.file.JournalReaderTest;
 import org.apache.jackrabbit.oak.segment.azure.AzureJournalFile;
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/journal/AzureTarRevisionsTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/journal/AzureTarRevisionsTest.java
index 910afe2dbc..847f1b65c3 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/journal/AzureTarRevisionsTest.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/journal/AzureTarRevisionsTest.java
@@ -18,7 +18,7 @@ package org.apache.jackrabbit.oak.segment.azure.journal;
 
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
 import org.apache.jackrabbit.oak.segment.spi.persistence.SegmentNodeStorePersistence;
-import org.apache.jackrabbit.oak.segment.azure.AzuriteDockerRule;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.file.TarRevisionsTest;
 import org.apache.jackrabbit.oak.segment.azure.AzurePersistence;
 import org.junit.Before;
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/journal/ReverseFileReaderTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/journal/ReverseFileReaderTest.java
index f3d1425a60..e7303b7510 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/journal/ReverseFileReaderTest.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/journal/ReverseFileReaderTest.java
@@ -19,7 +19,7 @@ package org.apache.jackrabbit.oak.segment.azure.journal;
 import com.microsoft.azure.storage.StorageException;
 import com.microsoft.azure.storage.blob.CloudAppendBlob;
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
-import org.apache.jackrabbit.oak.segment.azure.AzuriteDockerRule;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.azure.ReverseFileReader;
 import org.junit.Assert;
 import org.junit.Before;
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/spi/persistence/split/SplitPersistenceBlobTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/spi/persistence/split/SplitPersistenceBlobTest.java
index eee7cb43bf..192b2bd68b 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/spi/persistence/split/SplitPersistenceBlobTest.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/spi/persistence/split/SplitPersistenceBlobTest.java
@@ -33,7 +33,7 @@ import org.apache.jackrabbit.oak.plugins.blob.datastore.OakFileDataStore;
 import org.apache.jackrabbit.oak.segment.SegmentNodeStore;
 import org.apache.jackrabbit.oak.segment.SegmentNodeStoreBuilders;
 import org.apache.jackrabbit.oak.segment.azure.AzurePersistence;
-import org.apache.jackrabbit.oak.segment.azure.AzuriteDockerRule;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.file.FileStore;
 import org.apache.jackrabbit.oak.segment.file.FileStoreBuilder;
 import org.apache.jackrabbit.oak.segment.file.InvalidFileStoreVersionException;
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/spi/persistence/split/SplitPersistenceTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/spi/persistence/split/SplitPersistenceTest.java
index b20f9b92b6..ca5118d246 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/spi/persistence/split/SplitPersistenceTest.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/spi/persistence/split/SplitPersistenceTest.java
@@ -21,7 +21,7 @@ import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.segment.SegmentNodeStore;
 import org.apache.jackrabbit.oak.segment.SegmentNodeStoreBuilders;
 import org.apache.jackrabbit.oak.segment.azure.AzurePersistence;
-import org.apache.jackrabbit.oak.segment.azure.AzuriteDockerRule;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.file.FileStore;
 import org.apache.jackrabbit.oak.segment.file.FileStoreBuilder;
 import org.apache.jackrabbit.oak.segment.file.InvalidFileStoreVersionException;
diff --git a/oak-shaded-guava/dependency-reduced-pom.xml b/oak-shaded-guava/dependency-reduced-pom.xml
new file mode 100755
index 0000000000..3d52236e44
--- /dev/null
+++ b/oak-shaded-guava/dependency-reduced-pom.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <parent>
+    <artifactId>oak-parent</artifactId>
+    <groupId>org.apache.jackrabbit</groupId>
+    <version>1.61-SNAPSHOT</version>
+    <relativePath>../oak-parent/pom.xml</relativePath>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+  <artifactId>oak-shaded-guava</artifactId>
+  <packaging>bundle</packaging>
+  <name>Jackrabbit Guava Wrapper</name>
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-shade-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <artifactSet>
+                <includes>
+                  <include>com.google.guava:guava</include>
+                  <include>com.google.guava:failureaccess</include>
+                </includes>
+              </artifactSet>
+              <relocations>
+                <relocation>
+                  <pattern>com.google</pattern>
+                  <shadedPattern>${pref}</shadedPattern>
+                </relocation>
+              </relocations>
+              <createDependencyReducedPom>true</createDependencyReducedPom>
+              <createSourcesJar>true</createSourcesJar>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.rat</groupId>
+        <artifactId>apache-rat-plugin</artifactId>
+        <configuration>
+          <excludes>
+            <exclude>dependency-reduced-pom.xml</exclude>
+          </excludes>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <extensions>true</extensions>
+        <configuration>
+          <instructions>
+            <Bundle-SymbolicName>org.apache.jackrabbit.guava</Bundle-SymbolicName>
+            <Export-Package>${pref}.common.annotations;version="${exported.guava.version}",
+              ${pref}.common.base;version="${exported.guava.version}",
+              ${pref}.common.cache;version="${exported.guava.version}";uses:="${pref}.common.base,${pref}.common.collect,${pref}.common.util.concurrent",
+              ${pref}.common.collect;version="${exported.guava.version}";uses:="${pref}.common.base",
+              ${pref}.common.escape;version="${exported.guava.version}";uses:="${pref}.common.base",
+              ${pref}.common.eventbus;version="${exported.guava.version}",
+              ${pref}.common.graph;version="${exported.guava.version}";uses:="${pref}.common.collect",
+              ${pref}.common.hash;version="${exported.guava.version}";uses:="${pref}.common.base",
+              ${pref}.common.html;version="${exported.guava.version}";uses:="${pref}.common.escape",
+              ${pref}.common.io;version="${exported.guava.version}";uses:="${pref}.common.base,${pref}.common.collect,${pref}.common.graph,${pref}.common.hash",
+              ${pref}.common.math;version="${exported.guava.version}",${pref}.common.net;version="${exported.guava.version}";uses:="${pref}.common.base,${pref}.common.collect,${pref}.common.escape",
+              ${pref}.common.primitives;version="${exported.guava.version}";uses:="${pref}.common.base",
+              ${pref}.common.reflect;version="${exported.guava.version}";uses:="${pref}.common.collect,${pref}.common.io",
+              ${pref}.common.util.concurrent;version="${exported.guava.version}";uses:="${pref}.common.base,${pref}.common.collect,${pref}.common.util.concurrent.internal",
+              ${pref}.common.xml;version="${exported.guava.version}";uses:="${pref}.common.escape"</Export-Package>
+            <Import-Package>javax.annotation;resolution:=optional;version="[3.0,4)",
+              javax.crypto;resolution:=optional,
+              javax.crypto.spec;resolution:=optional,
+              sun.misc;resolution:=optional</Import-Package>
+          </instructions>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+    <dependency>
+      <groupId>org.jetbrains</groupId>
+      <artifactId>annotations</artifactId>
+      <version>18.0.0</version>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
+  <properties>
+    <pref>org.apache.jackrabbit.guava</pref>
+    <shaded.guava.version>33.0.0-jre</shaded.guava.version>
+    <exported.guava.version>33.0.0</exported.guava.version>
+  </properties>
+</project>
diff --git a/oak-store-document/pom.xml b/oak-store-document/pom.xml
index b10bf91a57..4fa3fe97f0 100644
--- a/oak-store-document/pom.xml
+++ b/oak-store-document/pom.xml
@@ -310,7 +310,7 @@
     <dependency>
       <groupId>de.flapdoodle.embed</groupId>
       <artifactId>de.flapdoodle.embed.mongo</artifactId>
-      <version>2.0.0</version>
+      <version>3.2.6</version>
       <scope>test</scope>
     </dependency>
     <dependency>
@@ -321,19 +321,19 @@
     <dependency>
       <groupId>org.testcontainers</groupId>
       <artifactId>testcontainers</artifactId>
-      <version>1.15.2</version>
+      <version>${testcontainers.version}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.testcontainers</groupId>
       <artifactId>toxiproxy</artifactId>
-      <version>1.15.2</version>
+      <version>${testcontainers.version}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.testcontainers</groupId>
       <artifactId>mongodb</artifactId>
-      <version>1.15.2</version>
+      <version>${testcontainers.version}</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongodProcess.java b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongodProcess.java
index 4fbb0229a8..d9cfea6568 100644
--- a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongodProcess.java
+++ b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongodProcess.java
@@ -27,18 +27,18 @@ import com.mongodb.ServerAddress;
 import org.apache.commons.io.FileUtils;
 
 import de.flapdoodle.embed.mongo.MongodStarter;
-import de.flapdoodle.embed.mongo.config.IMongodConfig;
-import de.flapdoodle.embed.mongo.config.MongoCmdOptionsBuilder;
-import de.flapdoodle.embed.mongo.config.MongodConfigBuilder;
+import de.flapdoodle.embed.mongo.config.ImmutableMongoCmdOptions;
+import de.flapdoodle.embed.mongo.config.ImmutableMongodConfig;
+import de.flapdoodle.embed.mongo.config.MongodConfig;
 import de.flapdoodle.embed.mongo.config.Net;
 import de.flapdoodle.embed.mongo.config.Storage;
+import de.flapdoodle.embed.mongo.distribution.Feature;
 import de.flapdoodle.embed.mongo.distribution.Versions;
+import de.flapdoodle.embed.process.io.directories.Directory;
 import de.flapdoodle.embed.process.io.directories.FixedPath;
-import de.flapdoodle.embed.process.io.directories.IDirectory;
+import de.flapdoodle.embed.process.io.file.Files;
 import de.flapdoodle.embed.process.runtime.IStopable;
 
-import static de.flapdoodle.embed.process.io.directories.Directories.join;
-
 /**
  * Helper class for starting/stopping a mongod process.
  */
@@ -46,13 +46,13 @@ public class MongodProcess {
 
     private static final String VERSION = "3.4.10";
 
-    private static final IDirectory TMP_DIR = join(new FixedPath("target"), new FixedPath("tmp"));
+    private static final Directory TMP_DIR = join(new FixedPath("target"), new FixedPath("tmp"));
 
     private IStopable process;
 
     private final MongodStarter starter;
 
-    private final IMongodConfig config;
+    private final MongodConfig config;
 
     MongodProcess(MongodStarter starter, String rsName, int port)
             throws IOException {
@@ -83,14 +83,33 @@ public class MongodProcess {
         return new ServerAddress(config.net().getBindIp(), config.net().getPort());
     }
 
-    private IMongodConfig createConfiguration(String rsName, int p)
+    static Directory join(final Directory left, final Directory right) {
+        return new Directory() {
+
+            @Override
+            public boolean isGenerated() {
+                return left.isGenerated() || right.isGenerated();
+            }
+
+            @Override
+            public File asFile() {
+                return Files.fileOf(left.asFile(), right.asFile());
+            }
+        };
+    }
+
+    private MongodConfig createConfiguration(String rsName, int p)
             throws IOException {
-        return new MongodConfigBuilder()
-                .version(Versions.withFeatures(() -> VERSION))
+        return ImmutableMongodConfig.builder()
+                .version(Versions.withFeatures(() -> VERSION, Feature.NO_HTTP_INTERFACE_ARG))
                 .net(new Net(InetAddress.getLoopbackAddress().getHostAddress(), p, false))
                 .replication(newStorage(p, rsName))
                 // enable journal
-                .cmdOptions(new MongoCmdOptionsBuilder().useNoJournal(false).build())
+                .cmdOptions(ImmutableMongoCmdOptions.builder()
+                        .useNoPrealloc(false)
+                        .useNoJournal(false)
+                        .useSmallFiles(false)
+                        .build())
                 .build();
     }
 
diff --git a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongodProcessFactory.java b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongodProcessFactory.java
index 357e2e58c1..49493336c9 100644
--- a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongodProcessFactory.java
+++ b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongodProcessFactory.java
@@ -36,16 +36,14 @@ import org.slf4j.LoggerFactory;
 
 import de.flapdoodle.embed.mongo.Command;
 import de.flapdoodle.embed.mongo.MongodStarter;
-import de.flapdoodle.embed.mongo.config.DownloadConfigBuilder;
-import de.flapdoodle.embed.mongo.config.ExtractedArtifactStoreBuilder;
-import de.flapdoodle.embed.mongo.config.RuntimeConfigBuilder;
-import de.flapdoodle.embed.process.config.IRuntimeConfig;
+import de.flapdoodle.embed.mongo.config.Defaults;
+import de.flapdoodle.embed.process.config.RuntimeConfig;
+import de.flapdoodle.embed.process.io.directories.Directory;
 import de.flapdoodle.embed.process.io.directories.FixedPath;
-import de.flapdoodle.embed.process.io.directories.IDirectory;
 import de.flapdoodle.embed.process.io.progress.Slf4jProgressListener;
 import de.flapdoodle.embed.process.runtime.Network;
 
-import static de.flapdoodle.embed.process.io.directories.Directories.join;
+import static org.apache.jackrabbit.oak.plugins.document.mongo.MongodProcess.join;
 import static org.junit.Assert.assertTrue;
 
 /**
@@ -55,26 +53,21 @@ public class MongodProcessFactory extends ExternalResource {
 
     private static final Logger LOG = LoggerFactory.getLogger(MongodProcessFactory.class);
 
-    private static final IDirectory EXTRACT_DIR = join(new FixedPath("target"), new FixedPath("mongo-extracted"));
+    private static final Directory DOWNLOAD_DIR = join(new FixedPath("target"), new FixedPath("mongo-download"));
 
-    private static final IDirectory DOWNLOAD_DIR = join(new FixedPath("target"), new FixedPath("mongo-download"));
-
-    private static final IDirectory TMP_DIR = join(new FixedPath("target"), new FixedPath("tmp"));
+    private static final Directory TMP_DIR = join(new FixedPath("target"), new FixedPath("tmp"));
 
     static {
         System.setProperty("de.flapdoodle.embed.io.tmpdir", TMP_DIR.asFile().getAbsolutePath());
     }
 
-    private static final IRuntimeConfig CONFIG = new RuntimeConfigBuilder()
-            .defaultsWithLogger(Command.MongoD, LoggerFactory.getLogger(MongodProcessFactory.class))
-            .artifactStore(new ExtractedArtifactStoreBuilder()
-                    .defaults(Command.MongoD)
-                    .download(new DownloadConfigBuilder()
-                            .defaultsForCommand(Command.MongoD)
+    private static final RuntimeConfig CONFIG = Defaults.runtimeConfigFor(Command.MongoD, LOG)
+            .artifactStore(Defaults.extractedArtifactStoreFor(Command.MongoD)
+                    .withDownloadConfig(Defaults.downloadConfigFor(Command.MongoD)
                             .progressListener(new Slf4jProgressListener(LOG))
                             .artifactStorePath(DOWNLOAD_DIR).build())
-                    .extractDir(EXTRACT_DIR).build())
-            .daemonProcess(false)
+            )
+            .isDaemonProcess(false)
             .build();
 
     private static final MongodStarter STARTER = MongodStarter.getInstance(CONFIG);
@@ -83,7 +76,7 @@ public class MongodProcessFactory extends ExternalResource {
 
     public Map<Integer, MongodProcess> startReplicaSet(String replicaSetName, int size)
             throws IOException {
-        int[] ports = Network.getFreeServerPorts(InetAddress.getLoopbackAddress(), size);
+        int[] ports = Network.freeServerPorts(InetAddress.getLoopbackAddress(), size);
         return startReplicaSet(replicaSetName, ports);
     }
 
@@ -103,9 +96,6 @@ public class MongodProcessFactory extends ExternalResource {
 
     @Override
     protected void before() {
-        if (!EXTRACT_DIR.asFile().exists()) {
-            assertTrue(EXTRACT_DIR.asFile().mkdirs());
-        }
         if (!TMP_DIR.asFile().exists()) {
             assertTrue(TMP_DIR.asFile().mkdirs());
         }
@@ -160,4 +150,5 @@ public class MongodProcessFactory extends ExternalResource {
                     new Document("replSetInitiate", config));
         }
     }
+
 }
diff --git a/oak-upgrade/pom.xml b/oak-upgrade/pom.xml
index 3f94cb31e4..3478c77e13 100644
--- a/oak-upgrade/pom.xml
+++ b/oak-upgrade/pom.xml
@@ -215,7 +215,7 @@
     </dependency>
     <dependency>
       <groupId>org.apache.jackrabbit</groupId>
-      <artifactId>oak-segment-azure</artifactId>
+      <artifactId>oak-blob-cloud-azure</artifactId>
       <version>${project.version}</version>
       <type>test-jar</type>
       <scope>test</scope>
diff --git a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/cli/SegmentAzureToSegmentTarTest.java b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/cli/SegmentAzureToSegmentTarTest.java
index ba8cae7c50..3aa2907711 100644
--- a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/cli/SegmentAzureToSegmentTarTest.java
+++ b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/cli/SegmentAzureToSegmentTarTest.java
@@ -19,7 +19,7 @@ package org.apache.jackrabbit.oak.upgrade.cli;
 
 import java.io.IOException;
 
-import org.apache.jackrabbit.oak.segment.azure.AzuriteDockerRule;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.upgrade.cli.container.NodeStoreContainer;
 import org.apache.jackrabbit.oak.upgrade.cli.container.SegmentAzureNodeStoreContainer;
 import org.apache.jackrabbit.oak.upgrade.cli.container.SegmentTarNodeStoreContainer;
diff --git a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/cli/SegmentTarToSegmentAzureTest.java b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/cli/SegmentTarToSegmentAzureTest.java
index 44b0e8ab1b..f801ca1e7c 100644
--- a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/cli/SegmentTarToSegmentAzureTest.java
+++ b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/cli/SegmentTarToSegmentAzureTest.java
@@ -19,7 +19,7 @@ package org.apache.jackrabbit.oak.upgrade.cli;
 
 import java.io.IOException;
 
-import org.apache.jackrabbit.oak.segment.azure.AzuriteDockerRule;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.upgrade.cli.container.NodeStoreContainer;
 import org.apache.jackrabbit.oak.upgrade.cli.container.SegmentAzureNodeStoreContainer;
 import org.apache.jackrabbit.oak.upgrade.cli.container.SegmentTarNodeStoreContainer;
diff --git a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/cli/container/SegmentAzureNodeStoreContainer.java b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/cli/container/SegmentAzureNodeStoreContainer.java
index 7857df6488..2e5d851b58 100644
--- a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/cli/container/SegmentAzureNodeStoreContainer.java
+++ b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/cli/container/SegmentAzureNodeStoreContainer.java
@@ -24,7 +24,7 @@ import java.security.InvalidKeyException;
 import org.apache.jackrabbit.oak.segment.SegmentNodeStoreBuilders;
 import org.apache.jackrabbit.oak.segment.azure.AzurePersistence;
 import org.apache.jackrabbit.oak.segment.azure.AzureUtilities;
-import org.apache.jackrabbit.oak.segment.azure.AzuriteDockerRule;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.file.FileStore;
 import org.apache.jackrabbit.oak.segment.file.FileStoreBuilder;
 import org.apache.jackrabbit.oak.segment.file.InvalidFileStoreVersionException;