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

rnewson pushed a commit to branch import-nouveau-reorg
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 92a20a7238310d098e1f48da4ab71c2c7f7baf59
Author: Robert Newson <[email protected]>
AuthorDate: Thu Dec 29 23:20:27 2022 +0000

    isolate Lucene behind interface
---
 .tool-versions                                     |   4 +
 Makefile                                           |   4 +-
 .../apache/couchdb/nouveau/NouveauApplication.java |  28 ++-
 .../couchdb/nouveau/core/DocumentFactory.java      |  50 -----
 .../org/apache/couchdb/nouveau/core/Index.java     | 176 ++++++++++++++++++
 ...allelSearcherFactory.java => IndexFactory.java} |  23 +--
 .../apache/couchdb/nouveau/core/IndexManager.java  | 206 ++-------------------
 ...zerFactory.java => Lucene9AnalyzerFactory.java} |   5 +-
 .../SearchResource.java => core/Lucene9Index.java} | 194 ++++++++++++++-----
 .../couchdb/nouveau/core/Lucene9IndexFactory.java  |  43 +++++
 ...ry.java => Lucene9ParallelSearcherFactory.java} |   2 +-
 .../nouveau/health/IndexManagerHealthCheck.java    |  27 +--
 .../couchdb/nouveau/resources/AnalyzeResource.java |   6 +-
 .../couchdb/nouveau/resources/IndexResource.java   |  28 +--
 .../couchdb/nouveau/resources/SearchResource.java  | 196 +-------------------
 .../apache/couchdb/nouveau/IntegrationTest.java    |  10 +-
 .../couchdb/nouveau/api/SearchRequestTest.java     |   4 +-
 .../couchdb/nouveau/core/IndexManagerTest.java     |   2 +-
 ...ryTest.java => Lucene9AnalyzerFactoryTest.java} |   4 +-
 19 files changed, 443 insertions(+), 569 deletions(-)

diff --git a/.tool-versions b/.tool-versions
new file mode 100644
index 000000000..ad8836716
--- /dev/null
+++ b/.tool-versions
@@ -0,0 +1,4 @@
+java zulu-11.60.19
+maven 3.8.6
+elixir 1.13.4-otp-23
+erlang 23.3.4.14
diff --git a/Makefile b/Makefile
index 19c1af066..5c77b2868 100644
--- a/Makefile
+++ b/Makefile
@@ -516,7 +516,7 @@ derived:
 
 .PHONY: nouveau
 nouveau:
-       @cd java/nouveau && mvn
+       @cd java/nouveau && mvn test
 
 .PHONY: nouveau-clean
 nouveau-clean:
@@ -524,4 +524,4 @@ nouveau-clean:
 
 .PHONY: nouveau-start
 nouveau-start: nouveau
-       @cd java/nouveau && mvn exec:exec -Dexec.executable="java"
+       @cd java/nouveau/server && mvn exec:exec -Dexec.executable="java"
diff --git 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java
 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java
index a97f9e1a3..215483d84 100644
--- 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java
+++ 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java
@@ -13,14 +13,12 @@
 
 package org.apache.couchdb.nouveau;
 
-import java.util.concurrent.ExecutorService;
-
-import org.apache.couchdb.nouveau.core.AnalyzerFactory;
-import org.apache.couchdb.nouveau.core.DocumentFactory;
 import org.apache.couchdb.nouveau.core.FileAlreadyExistsExceptionMapper;
 import org.apache.couchdb.nouveau.core.FileNotFoundExceptionMapper;
 import org.apache.couchdb.nouveau.core.IndexManager;
-import org.apache.couchdb.nouveau.core.ParallelSearcherFactory;
+import org.apache.couchdb.nouveau.core.Lucene9AnalyzerFactory;
+import org.apache.couchdb.nouveau.core.Lucene9IndexFactory;
+import org.apache.couchdb.nouveau.core.Lucene9ParallelSearcherFactory;
 import org.apache.couchdb.nouveau.core.UpdatesOutOfOrderExceptionMapper;
 import org.apache.couchdb.nouveau.core.ser.LuceneModule;
 import org.apache.couchdb.nouveau.health.AnalyzeHealthCheck;
@@ -28,10 +26,10 @@ import 
org.apache.couchdb.nouveau.health.IndexManagerHealthCheck;
 import org.apache.couchdb.nouveau.resources.AnalyzeResource;
 import org.apache.couchdb.nouveau.resources.IndexResource;
 import org.apache.couchdb.nouveau.resources.SearchResource;
-import com.fasterxml.jackson.databind.ObjectMapper;
 
 import com.codahale.metrics.MetricRegistry;
 import 
com.codahale.metrics.jersey2.InstrumentedResourceMethodApplicationListener;
+import com.fasterxml.jackson.databind.ObjectMapper;
 
 import io.dropwizard.Application;
 import io.dropwizard.setup.Environment;
@@ -52,14 +50,14 @@ public class NouveauApplication extends 
Application<NouveauApplicationConfigurat
         final MetricRegistry metricsRegistry = new MetricRegistry();
         environment.jersey().register(new 
InstrumentedResourceMethodApplicationListener(metricsRegistry));
 
-        final DocumentFactory documentFactory = new DocumentFactory();
-        final AnalyzerFactory analyzerFactory = new AnalyzerFactory();
+        final Lucene9AnalyzerFactory analyzerFactory = new 
Lucene9AnalyzerFactory();
 
-        final ExecutorService searchExecutor =
-            
environment.lifecycle().executorService("nouveau-search-%d").build();
+        final Lucene9ParallelSearcherFactory searcherFactory = new 
Lucene9ParallelSearcherFactory();
+        
searcherFactory.setExecutor(environment.lifecycle().executorService("nouveau-search-thread-%d").build());
 
-        final ParallelSearcherFactory searcherFactory = new 
ParallelSearcherFactory();
-        searcherFactory.setExecutor(searchExecutor);
+        final Lucene9IndexFactory indexFactory = new Lucene9IndexFactory();
+        indexFactory.setAnalyzerFactory(analyzerFactory);
+        indexFactory.setSearcherFactory(searcherFactory);
 
         final ObjectMapper objectMapper = environment.getObjectMapper();
         objectMapper.registerModule(new LuceneModule());
@@ -70,9 +68,9 @@ public class NouveauApplication extends 
Application<NouveauApplicationConfigurat
         indexManager.setMaxIndexesOpen(configuration.getMaxIndexesOpen());
         
indexManager.setCommitIntervalSeconds(configuration.getCommitIntervalSeconds());
         indexManager.setIdleSeconds(configuration.getIdleSeconds());
-        indexManager.setAnalyzerFactory(analyzerFactory);
         indexManager.setObjectMapper(objectMapper);
-        indexManager.setSearcherFactory(searcherFactory);
+        indexManager.setAnalyzerFactory(analyzerFactory);
+        indexManager.setIndexFactory(indexFactory);
         environment.lifecycle().manage(indexManager);
 
         environment.jersey().register(new FileNotFoundExceptionMapper());
@@ -81,7 +79,7 @@ public class NouveauApplication extends 
Application<NouveauApplicationConfigurat
 
         final AnalyzeResource analyzeResource = new 
AnalyzeResource(analyzerFactory);
         environment.jersey().register(analyzeResource);
-        environment.jersey().register(new IndexResource(indexManager, 
documentFactory));
+        environment.jersey().register(new IndexResource(indexManager));
         environment.jersey().register(new SearchResource(indexManager));
 
         // health checks
diff --git 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/DocumentFactory.java
 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/DocumentFactory.java
deleted file mode 100644
index 904a215c0..000000000
--- 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/DocumentFactory.java
+++ /dev/null
@@ -1,50 +0,0 @@
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package org.apache.couchdb.nouveau.core;
-
-import java.io.IOException;
-
-import org.apache.couchdb.nouveau.api.DocumentUpdateRequest;
-
-import org.apache.lucene.document.Document;
-import org.apache.lucene.document.Field.Store;
-import org.apache.lucene.index.IndexableField;
-import org.apache.lucene.util.BytesRef;
-
-public class DocumentFactory {
-
-    public Document build(final String docId, final DocumentUpdateRequest 
request) throws IOException {
-        final Document result = new Document();
-
-        // id
-        result.add(new org.apache.lucene.document.StringField("_id", docId, 
Store.YES));
-        result.add(new org.apache.lucene.document.SortedDocValuesField("_id", 
new BytesRef(docId)));
-
-        // partition (optional)
-        if (request.hasPartition()) {
-            result.add(new 
org.apache.lucene.document.StringField("_partition", request.getPartition(), 
Store.NO));
-        }
-
-        for (IndexableField field : request.getFields()) {
-            // Underscore-prefix is reserved.
-            if (field.name().startsWith("_")) {
-                continue;
-            }
-            result.add(field);
-        }
-
-        return result;
-    }
-
-}
diff --git 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Index.java 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Index.java
new file mode 100644
index 000000000..739768913
--- /dev/null
+++ 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Index.java
@@ -0,0 +1,176 @@
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.couchdb.nouveau.core;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.apache.couchdb.nouveau.api.DocumentDeleteRequest;
+import org.apache.couchdb.nouveau.api.DocumentUpdateRequest;
+import org.apache.couchdb.nouveau.api.IndexInfo;
+import org.apache.couchdb.nouveau.api.SearchRequest;
+import org.apache.couchdb.nouveau.api.SearchResults;
+
+public abstract class Index implements Closeable {
+
+    /*
+     * The close lock is to ensure there are no readers/searchers when
+     * we want to close the index.
+     */
+    private ReentrantReadWriteLock closeLock = new ReentrantReadWriteLock();
+
+    /*
+     * The update lock ensures serial updates to the index.
+     */
+    private ReentrantReadWriteLock updateLock = new ReentrantReadWriteLock();
+
+    private long updateSeq;
+
+    private boolean deleteOnClose = false;
+
+    private boolean closed = false;
+
+    protected Index(final long updateSeq) {
+        this.updateSeq = updateSeq;
+    }
+
+    public final IndexInfo info() throws IOException {
+        final long updateSeq = getUpdateSeq();
+        closeLock.readLock().lock();
+        try {
+            final int numDocs = doNumDocs();
+            return new IndexInfo(updateSeq, numDocs);
+        } finally {
+            closeLock.readLock().unlock();
+        }
+    }
+
+    protected abstract int doNumDocs() throws IOException;
+
+    public final void update(final String docId, final DocumentUpdateRequest 
request) throws IOException {
+        updateLock.writeLock().lock();
+        try {
+            assertUpdateSeqIsLower(request.getSeq());
+            closeLock.readLock().lock();
+            try {
+                doUpdate(docId, request);
+            } finally {
+                closeLock.readLock().unlock();
+            }
+            incrementUpdateSeq(request.getSeq());
+        } finally {
+            updateLock.writeLock().unlock();
+        }
+    }
+
+    protected abstract void doUpdate(final String docId, final 
DocumentUpdateRequest request) throws IOException;
+
+    public final void delete(final String docId, final DocumentDeleteRequest 
request) throws IOException {
+        updateLock.writeLock().lock();
+        try {
+            assertUpdateSeqIsLower(request.getSeq());
+            closeLock.readLock().lock();
+            try {
+                doDelete(docId, request);
+            } finally {
+                closeLock.readLock().unlock();
+            }
+            incrementUpdateSeq(request.getSeq());
+        } finally {
+            updateLock.writeLock().unlock();
+        }
+    }
+
+    protected abstract void doDelete(final String docId, final 
DocumentDeleteRequest request) throws IOException;
+
+    public final SearchResults search(final SearchRequest request) throws 
IOException, QueryParserException {
+        closeLock.readLock().lock();
+        try {
+            return doSearch(request);
+        } finally {
+            closeLock.readLock().unlock();
+        }
+    }
+
+    protected abstract SearchResults doSearch(final SearchRequest request) 
throws IOException, QueryParserException;
+
+    public final boolean commit() throws IOException {
+        final long updateSeq = getUpdateSeq();
+        closeLock.readLock().lock();
+        try {
+            return doCommit(updateSeq);
+        } finally {
+            closeLock.readLock().unlock();
+        }
+    }
+
+    protected abstract boolean doCommit(final long updateSeq) throws 
IOException;
+
+    public final void close() throws IOException {
+        closeLock.writeLock().lock();
+        try {
+            doClose(deleteOnClose);
+            closed = true;
+        } finally {
+            closeLock.writeLock().unlock();
+        }
+    }
+
+    protected abstract void doClose(final boolean deleteOnClose) throws 
IOException;
+
+    public final void lock() {
+        closeLock.readLock().lock();
+    }
+
+    public final void unlock() {
+        closeLock.readLock().unlock();
+    }
+
+    public final boolean isClosed() {
+        return closed;
+    }
+
+    public final void setDeleteOnClose(final boolean deleteOnClose) {
+        closeLock.writeLock().lock();
+        try {
+            this.deleteOnClose = true;
+        } finally {
+            closeLock.writeLock().unlock();
+        }
+    }
+
+    protected final void assertUpdateSeqIsLower(final long updateSeq) throws 
UpdatesOutOfOrderException {
+        assert updateLock.isWriteLockedByCurrentThread();
+        if (!(updateSeq > this.updateSeq)) {
+            throw new UpdatesOutOfOrderException();
+        }
+    }
+
+    protected final void incrementUpdateSeq(final long updateSeq) throws 
IOException {
+        assert updateLock.isWriteLockedByCurrentThread();
+        assertUpdateSeqIsLower(updateSeq);
+        this.updateSeq = updateSeq;
+    }
+
+    private long getUpdateSeq() {
+        updateLock.readLock().lock();
+        try {
+            return this.updateSeq;
+        } finally {
+            updateLock.readLock().unlock();
+        }
+    }
+
+}
\ No newline at end of file
diff --git 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/ParallelSearcherFactory.java
 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/IndexFactory.java
similarity index 50%
copy from 
java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/ParallelSearcherFactory.java
copy to 
java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/IndexFactory.java
index bd31801fd..8d728c080 100644
--- 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/ParallelSearcherFactory.java
+++ 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/IndexFactory.java
@@ -14,27 +14,12 @@
 package org.apache.couchdb.nouveau.core;
 
 import java.io.IOException;
-import java.util.concurrent.Executor;
+import java.nio.file.Path;
 
-import org.apache.lucene.index.IndexReader;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.SearcherFactory;
+import org.apache.couchdb.nouveau.api.IndexDefinition;
 
-public class ParallelSearcherFactory extends SearcherFactory {
+public interface IndexFactory {
 
-    private Executor executor;
-
-    public Executor getExecutor() {
-        return executor;
-    }
-
-    public void setExecutor(Executor executor) {
-        this.executor = executor;
-    }
-
-    @Override
-    public IndexSearcher newSearcher(final IndexReader reader, final 
IndexReader previousReader) throws IOException {
-        return new IndexSearcher(reader, executor);
-    }
+    Index open(final Path path, final IndexDefinition indexDefinition) throws 
IOException;
 
 }
diff --git 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java
 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java
index dd0f1f2e0..a5bb2970b 100644
--- 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java
+++ 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java
@@ -18,13 +18,7 @@ import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.time.Duration;
-import java.util.Collections;
-import java.util.Map;
 import java.util.concurrent.CompletionException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.stream.Stream;
 
 import javax.validation.constraints.Min;
@@ -34,21 +28,13 @@ import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Response.Status;
 
 import org.apache.couchdb.nouveau.api.IndexDefinition;
-import org.apache.lucene.analysis.Analyzer;
-import org.apache.lucene.index.IndexWriter;
-import org.apache.lucene.index.IndexWriterConfig;
-import org.apache.lucene.misc.store.DirectIODirectory;
-import org.apache.lucene.search.SearcherFactory;
-import org.apache.lucene.search.SearcherManager;
-import org.apache.lucene.store.Directory;
-import org.apache.lucene.store.FSDirectory;
-import org.apache.lucene.store.LockObtainFailedException;
-import org.apache.lucene.util.IOUtils;
 import org.checkerframework.checker.nullness.qual.NonNull;
 import org.checkerframework.checker.nullness.qual.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.caffeine.MetricsStatsCounter;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.github.benmanes.caffeine.cache.CacheLoader;
@@ -58,9 +44,6 @@ import com.github.benmanes.caffeine.cache.RemovalCause;
 import com.github.benmanes.caffeine.cache.RemovalListener;
 import com.github.benmanes.caffeine.cache.Scheduler;
 
-import com.codahale.metrics.MetricRegistry;
-import com.codahale.metrics.caffeine.MetricsStatsCounter;
-
 import io.dropwizard.lifecycle.Managed;
 
 public class IndexManager implements Managed {
@@ -69,124 +52,6 @@ public class IndexManager implements Managed {
     private static final int RETRY_SLEEP_MS = 5;
     private static final Logger LOGGER = 
LoggerFactory.getLogger(IndexManager.class);
 
-    public class Index {
-        private static final String DEFAULT_FIELD = "default";
-        private final String name;
-        private IndexWriter writer;
-        private SearcherManager searcherManager;
-        private Analyzer analyzer;
-        private final AtomicBoolean deleteOnClose = new AtomicBoolean();
-        private final AtomicLong updateSeq = new AtomicLong();
-
-        // The write lock is to ensure there are no readers/searchers when
-        // we want to close the index.
-        private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
-        private Lock rl = rwl.readLock();
-        private Lock wl = rwl.writeLock();
-
-        private Index(
-                      String name,
-                      IndexWriter writer,
-                      SearcherManager searcherManager,
-                      Analyzer analyzer,
-                      long updateSeq) {
-            this.name = name;
-            this.writer = writer;
-            this.searcherManager = searcherManager;
-            this.analyzer = analyzer;
-            this.updateSeq.set(updateSeq);
-        }
-
-        public String getName() {
-            return name;
-        }
-
-        public IndexWriter getWriter() {
-            return writer;
-        }
-
-        public SearcherManager getSearcherManager() {
-            return searcherManager;
-        }
-
-        public QueryParser getQueryParser() {
-            return new NouveauQueryParser(DEFAULT_FIELD, analyzer);
-        }
-
-        public boolean commit() throws IOException {
-            rl.lock();
-            try {
-                writer.setLiveCommitData(generateCommitData().entrySet());
-                return writer.commit() != -1;
-            } finally {
-                rl.unlock();
-            }
-        }
-
-        public long getUpdateSeq() throws IOException {
-            return updateSeq.get();
-        }
-
-        public void incrementUpdateSeq(final long updateSeq) throws 
IOException {
-            final long newSeq = this.updateSeq.accumulateAndGet(updateSeq, (a, 
b) -> Math.max(a, b));
-            if (newSeq != updateSeq) {
-                throw new UpdatesOutOfOrderException();
-            }
-        }
-
-        public void close() throws IOException {
-            wl.lock();
-            try {
-                if (writer == null) {
-                    // Already closed.
-                    return;
-                }
-
-                // Close searcher manager
-                if (searcherManager != null) {
-                    try {
-                        searcherManager.close();
-                    } catch (IOException e) {
-                        LOGGER.info(this + " threw exception when closing 
searcherManager.", e);
-                    } finally {
-                        searcherManager = null;
-                    }
-                }
-
-                if (deleteOnClose.get()) {
-                    try {
-                        // No need to commit in this case.
-                        writer.rollback();
-                    } catch (IOException e) {
-                        LOGGER.info(this + " threw exception when rolling back 
writer.", e);
-                    } finally {
-                        writer = null;
-                    }
-                    IOUtils.rm(indexRootPath(name));
-                } else {
-                    try {
-                        
writer.setLiveCommitData(generateCommitData().entrySet());
-                        writer.close();
-                        LOGGER.info("{} closed.", this);
-                    } finally {
-                        writer = null;
-                    }
-                }
-            } finally {
-                wl.unlock();
-            }
-        }
-
-        private Map<String, String> generateCommitData() {
-            return Collections.singletonMap("update_seq", 
Long.toString(updateSeq.get()));
-        }
-
-        @Override
-        public String toString() {
-            return "Index [name=" + name + "]";
-        }
-    }
-
     private class IndexLoader implements CacheLoader<String, Index> {
 
         @Override
@@ -237,12 +102,12 @@ public class IndexManager implements Managed {
     private Path rootDir;
 
     @NotNull
-    private AnalyzerFactory analyzerFactory;
+    private Lucene9AnalyzerFactory analyzerFactory;
 
     @NotNull
     private ObjectMapper objectMapper;
 
-    private SearcherFactory searcherFactory;
+    private IndexFactory indexFactory;
 
     private MetricRegistry metricRegistry;
 
@@ -253,11 +118,11 @@ public class IndexManager implements Managed {
             final Index result = getFromCache(name);
 
             // Check if we're in the middle of closing.
-            result.rl.lock();
-            if (result.writer != null) {
+            result.lock();
+            if (!result.isClosed()) {
                 return result;
             }
-            result.rl.unlock();
+            result.unlock();
 
             // Retry after a short sleep.
             try {
@@ -271,7 +136,7 @@ public class IndexManager implements Managed {
     }
 
     public void release(final Index index) throws IOException {
-        index.rl.unlock();
+        index.unlock();
     }
 
     public void create(final String name, IndexDefinition indexDefinition) 
throws IOException {
@@ -301,7 +166,7 @@ public class IndexManager implements Managed {
     private void deleteIndex(final String name) throws IOException {
         final Index index = acquire(name);
         try {
-            index.deleteOnClose.set(true);
+            index.setDeleteOnClose(true);
             cache.invalidate(name);
         } finally {
             release(index);
@@ -344,7 +209,7 @@ public class IndexManager implements Managed {
         this.rootDir = rootDir;
     }
 
-    public void setAnalyzerFactory(final AnalyzerFactory analyzerFactory) {
+    public void setAnalyzerFactory(final Lucene9AnalyzerFactory 
analyzerFactory) {
         this.analyzerFactory = analyzerFactory;
     }
 
@@ -352,8 +217,8 @@ public class IndexManager implements Managed {
         this.objectMapper = objectMapper;
     }
 
-    public void setSearcherFactory(final SearcherFactory searcherFactory) {
-        this.searcherFactory = searcherFactory;
+    public void setIndexFactory(final IndexFactory indexFactory) {
+        this.indexFactory = indexFactory;
     }
 
     public void setMetricRegistry(final MetricRegistry metricRegistry) {
@@ -405,48 +270,9 @@ public class IndexManager implements Managed {
     }
 
     private Index openExistingIndex(final String name) throws IOException {
-        final IndexDefinition indexDefinition = 
objectMapper.readValue(indexDefinitionPath(name).toFile(), 
IndexDefinition.class);
-        final Analyzer analyzer =  
analyzerFactory.fromDefinition(indexDefinition);
         final Path path = indexPath(name);
-        final Directory dir = directory(path);
-        final IndexWriter writer = newWriter(dir, analyzer);
-        final SearcherManager searcherManager = new SearcherManager(writer, 
searcherFactory);
-        final long updateSeq = getUpdateSeq(writer);
-        return new Index(name, writer, searcherManager, analyzer, updateSeq);
-    }
-
-    private long getUpdateSeq(final IndexWriter writer) throws IOException {
-        final Iterable<Map.Entry<String, String>> commitData = 
writer.getLiveCommitData();
-        if (commitData == null) {
-            return 0L;
-        }
-        for (Map.Entry<String, String> entry : commitData) {
-            if (entry.getKey().equals("update_seq")) {
-                return Long.parseLong(entry.getValue());
-            }
-        }
-        return 0L;
-    }
-
-    private IndexWriter newWriter(final Directory dir, final Analyzer 
analyzer) throws IOException {
-        LockObtainFailedException exceptionThrown = null;
-        for (int i = 0; i < RETRY_LIMIT; i++) {
-            try {
-                final IndexWriterConfig config = new 
IndexWriterConfig(analyzer);
-                config.setCommitOnClose(true);
-                config.setUseCompoundFile(false);
-                return new IndexWriter(dir, config);
-            } catch (LockObtainFailedException e) {
-                exceptionThrown = e;
-                try {
-                    Thread.sleep(RETRY_SLEEP_MS);
-                } catch (InterruptedException e1) {
-                    Thread.interrupted();
-                    break;
-                }
-            }
-        }
-        throw exceptionThrown;
+        final IndexDefinition indexDefinition = 
objectMapper.readValue(indexDefinitionPath(name).toFile(), 
IndexDefinition.class);
+        return indexFactory.open(path, indexDefinition);
     }
 
     private boolean isIndex(final Path path) {
@@ -470,8 +296,4 @@ public class IndexManager implements Managed {
                 Status.BAD_REQUEST);
     }
 
-    private Directory directory(final Path path) throws IOException {
-        return new DirectIODirectory(FSDirectory.open(path));
-    }
-
 }
diff --git 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/AnalyzerFactory.java
 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Lucene9AnalyzerFactory.java
similarity index 98%
rename from 
java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/AnalyzerFactory.java
rename to 
java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Lucene9AnalyzerFactory.java
index 0ad6c0311..5544af267 100644
--- 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/AnalyzerFactory.java
+++ 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Lucene9AnalyzerFactory.java
@@ -62,7 +62,10 @@ import org.apache.lucene.analysis.sv.SwedishAnalyzer;
 import org.apache.lucene.analysis.th.ThaiAnalyzer;
 import org.apache.lucene.analysis.tr.TurkishAnalyzer;
 
-public class AnalyzerFactory {
+public final class Lucene9AnalyzerFactory {
+
+    public Lucene9AnalyzerFactory() {
+    }
 
     public Analyzer fromDefinition(final IndexDefinition indexDefinition) {
         final Analyzer defaultAnalyzer = 
newAnalyzer(indexDefinition.getDefaultAnalyzer());
diff --git 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/SearchResource.java
 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Lucene9Index.java
similarity index 57%
copy from 
java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/SearchResource.java
copy to 
java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Lucene9Index.java
index dd78e861c..d62beff9b 100644
--- 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/SearchResource.java
+++ 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Lucene9Index.java
@@ -11,10 +11,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package org.apache.couchdb.nouveau.resources;
+package org.apache.couchdb.nouveau.core;
 
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -22,26 +24,17 @@ import java.util.Map.Entry;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import javax.validation.Valid;
-import javax.validation.constraints.NotNull;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
 import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response.Status;
 
+import org.apache.couchdb.nouveau.api.DocumentDeleteRequest;
+import org.apache.couchdb.nouveau.api.DocumentUpdateRequest;
 import org.apache.couchdb.nouveau.api.SearchHit;
 import org.apache.couchdb.nouveau.api.SearchRequest;
 import org.apache.couchdb.nouveau.api.SearchResults;
-import org.apache.couchdb.nouveau.core.IndexManager;
-import org.apache.couchdb.nouveau.core.IndexManager.Index;
-import org.apache.couchdb.nouveau.core.QueryParserException;
-import com.codahale.metrics.annotation.Timed;
-
+import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field.Store;
 import org.apache.lucene.facet.FacetResult;
 import org.apache.lucene.facet.Facets;
 import org.apache.lucene.facet.FacetsCollector;
@@ -51,66 +44,100 @@ import org.apache.lucene.facet.StringDocValuesReaderState;
 import org.apache.lucene.facet.StringValueFacetCounts;
 import org.apache.lucene.facet.range.DoubleRange;
 import org.apache.lucene.facet.range.DoubleRangeFacetCounts;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
 import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.misc.store.DirectIODirectory;
 import org.apache.lucene.search.CollectorManager;
 import org.apache.lucene.search.FieldDoc;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.MultiCollectorManager;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.SearcherFactory;
 import org.apache.lucene.search.SearcherManager;
 import org.apache.lucene.search.Sort;
 import org.apache.lucene.search.SortField;
+import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.search.TopDocs;
 import org.apache.lucene.search.TopFieldCollector;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.apache.lucene.util.BytesRef;
 
-@Path("/index/{name}")
-@Consumes(MediaType.APPLICATION_JSON)
-@Produces(MediaType.APPLICATION_JSON)
-public class SearchResource {
+class Lucene9Index extends Index {
 
     private static final DoubleRange[] EMPTY_DOUBLE_RANGE_ARRAY = new 
DoubleRange[0];
     private static final Sort DEFAULT_SORT = new Sort(SortField.FIELD_SCORE,
             new SortField("_id", SortField.Type.STRING));
     private static final Pattern SORT_FIELD_RE = 
Pattern.compile("^([-+])?([\\.\\w]+)(?:<(\\w+)>)?$");
-    private final IndexManager indexManager;
 
-    public SearchResource(final IndexManager indexManager) {
-        this.indexManager = indexManager;
+    private final Analyzer analyzer;
+    private final IndexWriter writer;
+    private final SearcherManager searcherManager;
+
+    static Index open(final Path path, final Analyzer analyzer, final 
SearcherFactory searcherFactory)
+            throws IOException {
+        final Directory dir = new DirectIODirectory(FSDirectory.open(path));
+        final IndexWriterConfig config = new IndexWriterConfig(analyzer);
+        config.setCommitOnClose(true);
+        config.setUseCompoundFile(false);
+        final IndexWriter writer = new IndexWriter(dir, config);
+        final long updateSeq = getUpdateSeq(writer);
+        final SearcherManager searcherManager = new SearcherManager(writer, 
searcherFactory);
+        return new Lucene9Index(analyzer, writer, updateSeq, searcherManager);
     }
 
-    @POST
-    @Timed
-    @Path("/search")
-    public SearchResults searchIndex(@PathParam("name") String name, @NotNull 
@Valid SearchRequest searchRequest)
-            throws IOException, QueryParserException {
-        final Index index = indexManager.acquire(name);
-        try {
-            final Query query = index.getQueryParser().parse(searchRequest);
+    private Lucene9Index(final Analyzer analyzer, final IndexWriter writer, 
final long updateSeq,
+            final SearcherManager searcherManager) {
+        super(updateSeq);
+        this.analyzer = analyzer;
+        this.writer = writer;
+        this.searcherManager = searcherManager;
+    }
 
-            // Construct CollectorManagers.
-            final MultiCollectorManager cm;
-            final CollectorManager<?, ? extends TopDocs> hits = 
hitCollector(searchRequest);
+    @Override
+    public int doNumDocs() throws IOException {
+        return writer.getDocStats().numDocs;
+    }
 
-            final SearcherManager searcherManager = index.getSearcherManager();
-            searcherManager.maybeRefreshBlocking();
+    @Override
+    public void doUpdate(final String docId, final DocumentUpdateRequest 
request) throws IOException {
+        final Term docIdTerm = docIdTerm(docId);
+        final Document doc = toDocument(docId, request);
+        writer.updateDocument(docIdTerm, doc);
+    }
 
-            final IndexSearcher searcher = searcherManager.acquire();
-            try {
-                if (searchRequest.hasCounts() || searchRequest.hasRanges()) {
-                    cm = new MultiCollectorManager(hits, new 
FacetsCollectorManager());
-                } else {
-                    cm = new MultiCollectorManager(hits);
-                }
-                final Object[] reduces = searcher.search(query, cm);
-                return toSearchResults(searchRequest, searcher, reduces);
-            } catch (IllegalStateException e) {
-                throw new WebApplicationException(e.getMessage(), e, 
Status.BAD_REQUEST);
-            } finally {
-                searcherManager.release(searcher);
+    @Override
+    public void doDelete(final String docId, final DocumentDeleteRequest 
request) throws IOException {
+        final Query query = docIdQuery(docId);
+        writer.deleteDocuments(query);
+    }
+
+    @Override
+    public SearchResults doSearch(final SearchRequest request) throws 
IOException, QueryParserException {
+        final Query query = newQueryParser().parse(request);
+
+        // Construct CollectorManagers.
+        final MultiCollectorManager cm;
+        final CollectorManager<?, ? extends TopDocs> hits = 
hitCollector(request);
+
+        searcherManager.maybeRefreshBlocking();
+
+        final IndexSearcher searcher = searcherManager.acquire();
+        try {
+            if (request.hasCounts() || request.hasRanges()) {
+                cm = new MultiCollectorManager(hits, new 
FacetsCollectorManager());
+            } else {
+                cm = new MultiCollectorManager(hits);
             }
+            final Object[] reduces = searcher.search(query, cm);
+            return toSearchResults(request, searcher, reduces);
+        } catch (IllegalStateException e) {
+            throw new WebApplicationException(e.getMessage(), e, 
Status.BAD_REQUEST);
         } finally {
-            indexManager.release(index);
+            searcherManager.release(searcher);
         }
     }
 
@@ -214,12 +241,12 @@ public class SearchResource {
         final List<String> sort = new 
ArrayList<String>(searchRequest.getSort());
         final String last = sort.get(sort.size() - 1);
         // Append _id field if not already present.
-        switch(last) {
+        switch (last) {
             case "-_id<string>":
             case "_id<string>":
                 break;
             default:
-            sort.add("_id<string>");
+                sort.add("_id<string>");
         }
         return convertSort(sort);
     }
@@ -246,4 +273,71 @@ public class SearchResource {
         return new SortField(m.group(2), type, reverse);
     }
 
+    @Override
+    public boolean doCommit(final long updateSeq) throws IOException {
+        writer.setLiveCommitData(Collections.singletonMap("update_seq", 
Long.toString(updateSeq)).entrySet());
+        return writer.commit() != -1;
+    }
+
+    @Override
+    public void doClose(final boolean deleteOnClose) throws IOException {
+        if (deleteOnClose) {
+            // No need to commit in this case.
+            writer.rollback();
+            final Directory dir = writer.getDirectory();
+            for (final String name : dir.listAll()) {
+                dir.deleteFile(name);
+            }
+        }
+        writer.close();
+    }
+
+    private static Document toDocument(final String docId, final 
DocumentUpdateRequest request) throws IOException {
+        final Document result = new Document();
+
+        // id
+        result.add(new org.apache.lucene.document.StringField("_id", docId, 
Store.YES));
+        result.add(new org.apache.lucene.document.SortedDocValuesField("_id", 
new BytesRef(docId)));
+
+        // partition (optional)
+        if (request.hasPartition()) {
+            result.add(new 
org.apache.lucene.document.StringField("_partition", request.getPartition(), 
Store.NO));
+        }
+
+        for (IndexableField field : request.getFields()) {
+            // Underscore-prefix is reserved.
+            if (field.name().startsWith("_")) {
+                continue;
+            }
+            result.add(field);
+        }
+
+        return result;
+    }
+
+    private static Query docIdQuery(final String docId) {
+        return new TermQuery(docIdTerm(docId));
+    }
+
+    private static Term docIdTerm(final String docId) {
+        return new Term("_id", docId);
+    }
+
+    private static long getUpdateSeq(final IndexWriter writer) throws 
IOException {
+        final Iterable<Map.Entry<String, String>> commitData = 
writer.getLiveCommitData();
+        if (commitData == null) {
+            return 0L;
+        }
+        for (Map.Entry<String, String> entry : commitData) {
+            if (entry.getKey().equals("update_seq")) {
+                return Long.parseLong(entry.getValue());
+            }
+        }
+        return 0L;
+    }
+
+    public QueryParser newQueryParser() {
+        return new NouveauQueryParser("default", analyzer);
+    }
+
 }
diff --git 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Lucene9IndexFactory.java
 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Lucene9IndexFactory.java
new file mode 100644
index 000000000..016f427c1
--- /dev/null
+++ 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Lucene9IndexFactory.java
@@ -0,0 +1,43 @@
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.couchdb.nouveau.core;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+import org.apache.couchdb.nouveau.api.IndexDefinition;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.search.SearcherFactory;
+
+public class Lucene9IndexFactory implements IndexFactory {
+
+    private Lucene9AnalyzerFactory analyzerFactory;
+
+    private SearcherFactory searcherFactory;
+
+    public void setAnalyzerFactory(final Lucene9AnalyzerFactory 
analyzerFactory) {
+        this.analyzerFactory = analyzerFactory;
+    }
+
+    public void setSearcherFactory(final SearcherFactory searcherFactory) {
+        this.searcherFactory = searcherFactory;
+    }
+
+    @Override
+    public Index open(Path path, final IndexDefinition indexDefinition) throws 
IOException {
+        final Analyzer analyzer = 
analyzerFactory.fromDefinition(indexDefinition);
+        return Lucene9Index.open(path, analyzer, searcherFactory);
+    }
+
+}
diff --git 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/ParallelSearcherFactory.java
 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Lucene9ParallelSearcherFactory.java
similarity index 94%
rename from 
java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/ParallelSearcherFactory.java
rename to 
java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Lucene9ParallelSearcherFactory.java
index bd31801fd..7021f2997 100644
--- 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/ParallelSearcherFactory.java
+++ 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/Lucene9ParallelSearcherFactory.java
@@ -20,7 +20,7 @@ import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.SearcherFactory;
 
-public class ParallelSearcherFactory extends SearcherFactory {
+public class Lucene9ParallelSearcherFactory extends SearcherFactory {
 
     private Executor executor;
 
diff --git 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/health/IndexManagerHealthCheck.java
 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/health/IndexManagerHealthCheck.java
index b2653db38..e76d8da9c 100644
--- 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/health/IndexManagerHealthCheck.java
+++ 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/health/IndexManagerHealthCheck.java
@@ -13,16 +13,17 @@
 
 package org.apache.couchdb.nouveau.health;
 
+import static org.apache.couchdb.nouveau.api.LuceneVersion.LUCENE_9;
+
 import java.io.IOException;
+import java.util.Collections;
 
+import org.apache.couchdb.nouveau.api.DocumentUpdateRequest;
 import org.apache.couchdb.nouveau.api.IndexDefinition;
-import static org.apache.couchdb.nouveau.api.LuceneVersion.*;
+import org.apache.couchdb.nouveau.core.Index;
 import org.apache.couchdb.nouveau.core.IndexManager;
-import org.apache.couchdb.nouveau.core.IndexManager.Index;
-import com.codahale.metrics.health.HealthCheck;
 
-import org.apache.lucene.document.Document;
-import org.apache.lucene.index.IndexWriter;
+import com.codahale.metrics.health.HealthCheck;
 
 public class IndexManagerHealthCheck extends HealthCheck {
 
@@ -44,14 +45,14 @@ public class IndexManagerHealthCheck extends HealthCheck {
         indexManager.create(name, new IndexDefinition(LUCENE_9, "standard", 
null));
         final Index index = indexManager.acquire(name);
         try {
-            final IndexWriter writer = index.getWriter();
-            try {
-                writer.addDocument(new Document());
-                writer.commit();
-                return Result.healthy();
-            } finally {
-                indexManager.deleteAll(name);
-            }
+            final DocumentUpdateRequest request = new DocumentUpdateRequest(1, 
null, Collections.emptyList());
+            index.update("foo", request);
+            index.commit();
+            index.setDeleteOnClose(true);
+            index.close();
+            return Result.healthy();
+        } catch (final IOException e) {
+            return Result.unhealthy(e);
         } finally {
             indexManager.release(index);
         }
diff --git 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/AnalyzeResource.java
 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/AnalyzeResource.java
index 60e8c8ca3..c495cf5fa 100644
--- 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/AnalyzeResource.java
+++ 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/AnalyzeResource.java
@@ -29,7 +29,7 @@ import javax.ws.rs.core.Response.Status;
 
 import org.apache.couchdb.nouveau.api.AnalyzeRequest;
 import org.apache.couchdb.nouveau.api.AnalyzeResponse;
-import org.apache.couchdb.nouveau.core.AnalyzerFactory;
+import org.apache.couchdb.nouveau.core.Lucene9AnalyzerFactory;
 import com.codahale.metrics.annotation.Timed;
 
 import org.apache.lucene.analysis.Analyzer;
@@ -41,9 +41,9 @@ import 
org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
 @Produces(MediaType.APPLICATION_JSON)
 public class AnalyzeResource {
 
-    private final AnalyzerFactory analyzerFactory;
+    private final Lucene9AnalyzerFactory analyzerFactory;
 
-    public AnalyzeResource(AnalyzerFactory analyzerFactory) {
+    public AnalyzeResource(Lucene9AnalyzerFactory analyzerFactory) {
         this.analyzerFactory = analyzerFactory;
     }
 
diff --git 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java
 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java
index cd10226db..ca7ee15c0 100644
--- 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java
+++ 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java
@@ -30,15 +30,10 @@ import org.apache.couchdb.nouveau.api.DocumentDeleteRequest;
 import org.apache.couchdb.nouveau.api.DocumentUpdateRequest;
 import org.apache.couchdb.nouveau.api.IndexDefinition;
 import org.apache.couchdb.nouveau.api.IndexInfo;
-import org.apache.couchdb.nouveau.core.DocumentFactory;
+import org.apache.couchdb.nouveau.core.Index;
 import org.apache.couchdb.nouveau.core.IndexManager;
-import org.apache.couchdb.nouveau.core.IndexManager.Index;
-import com.codahale.metrics.annotation.Timed;
 
-import org.apache.lucene.document.Document;
-import org.apache.lucene.index.IndexWriter;
-import org.apache.lucene.index.Term;
-import org.apache.lucene.search.TermQuery;
+import com.codahale.metrics.annotation.Timed;
 
 @Path("/index/{name}")
 @Consumes(MediaType.APPLICATION_JSON)
@@ -46,26 +41,20 @@ import org.apache.lucene.search.TermQuery;
 public class IndexResource {
 
     private final IndexManager indexManager;
-    private final DocumentFactory documentFactory;
 
-    public IndexResource(final IndexManager indexManager, final 
DocumentFactory documentFactory) {
+    public IndexResource(final IndexManager indexManager) {
         this.indexManager = indexManager;
-        this.documentFactory = documentFactory;
     }
 
     @GET
     @SuppressWarnings("resource")
     public IndexInfo indexInfo(@PathParam("name") String name) throws 
IOException {
-        final long updateSeq;
-        final int numDocs;
         final Index index = indexManager.acquire(name);
         try {
-            updateSeq = index.getUpdateSeq();
-            numDocs = index.getWriter().getDocStats().numDocs;
+            return index.info();
         } finally {
             indexManager.release(index);
         }
-        return new IndexInfo(updateSeq, numDocs);
     }
 
     @DELETE
@@ -84,9 +73,7 @@ public class IndexResource {
     public void deleteDoc(@PathParam("name") String name, @PathParam("docId") 
String docId, @NotNull @Valid final DocumentDeleteRequest request) throws 
IOException {
         final Index index = indexManager.acquire(name);
         try {
-            final IndexWriter writer = index.getWriter();
-            writer.deleteDocuments(new TermQuery(new Term("_id", docId)));
-            index.incrementUpdateSeq(request.getSeq());
+            index.delete(docId, request);
         } finally {
             indexManager.release(index);
         }
@@ -98,10 +85,7 @@ public class IndexResource {
     public void updateDoc(@PathParam("name") String name, @PathParam("docId") 
String docId, @NotNull @Valid final DocumentUpdateRequest request) throws 
IOException {
         final Index index = indexManager.acquire(name);
         try {
-            final IndexWriter writer = index.getWriter();
-            final Document doc = documentFactory.build(docId, request);
-            writer.updateDocument(new Term("_id", docId), doc);
-            index.incrementUpdateSeq(request.getSeq());
+            index.update(docId, request);
         } finally {
             indexManager.release(index);
         }
diff --git 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/SearchResource.java
 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/SearchResource.java
index dd78e861c..03f6c0c20 100644
--- 
a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/SearchResource.java
+++ 
b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/SearchResource.java
@@ -14,13 +14,6 @@
 package org.apache.couchdb.nouveau.resources;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 import javax.validation.Valid;
 import javax.validation.constraints.NotNull;
@@ -29,50 +22,21 @@ import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
-import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response.Status;
 
-import org.apache.couchdb.nouveau.api.SearchHit;
 import org.apache.couchdb.nouveau.api.SearchRequest;
 import org.apache.couchdb.nouveau.api.SearchResults;
+import org.apache.couchdb.nouveau.core.Index;
 import org.apache.couchdb.nouveau.core.IndexManager;
-import org.apache.couchdb.nouveau.core.IndexManager.Index;
 import org.apache.couchdb.nouveau.core.QueryParserException;
-import com.codahale.metrics.annotation.Timed;
 
-import org.apache.lucene.document.Document;
-import org.apache.lucene.facet.FacetResult;
-import org.apache.lucene.facet.Facets;
-import org.apache.lucene.facet.FacetsCollector;
-import org.apache.lucene.facet.FacetsCollectorManager;
-import org.apache.lucene.facet.LabelAndValue;
-import org.apache.lucene.facet.StringDocValuesReaderState;
-import org.apache.lucene.facet.StringValueFacetCounts;
-import org.apache.lucene.facet.range.DoubleRange;
-import org.apache.lucene.facet.range.DoubleRangeFacetCounts;
-import org.apache.lucene.index.IndexableField;
-import org.apache.lucene.search.CollectorManager;
-import org.apache.lucene.search.FieldDoc;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.MultiCollectorManager;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.ScoreDoc;
-import org.apache.lucene.search.SearcherManager;
-import org.apache.lucene.search.Sort;
-import org.apache.lucene.search.SortField;
-import org.apache.lucene.search.TopDocs;
-import org.apache.lucene.search.TopFieldCollector;
+import com.codahale.metrics.annotation.Timed;
 
 @Path("/index/{name}")
 @Consumes(MediaType.APPLICATION_JSON)
 @Produces(MediaType.APPLICATION_JSON)
 public class SearchResource {
 
-    private static final DoubleRange[] EMPTY_DOUBLE_RANGE_ARRAY = new 
DoubleRange[0];
-    private static final Sort DEFAULT_SORT = new Sort(SortField.FIELD_SCORE,
-            new SortField("_id", SortField.Type.STRING));
-    private static final Pattern SORT_FIELD_RE = 
Pattern.compile("^([-+])?([\\.\\w]+)(?:<(\\w+)>)?$");
     private final IndexManager indexManager;
 
     public SearchResource(final IndexManager indexManager) {
@@ -86,164 +50,10 @@ public class SearchResource {
             throws IOException, QueryParserException {
         final Index index = indexManager.acquire(name);
         try {
-            final Query query = index.getQueryParser().parse(searchRequest);
-
-            // Construct CollectorManagers.
-            final MultiCollectorManager cm;
-            final CollectorManager<?, ? extends TopDocs> hits = 
hitCollector(searchRequest);
-
-            final SearcherManager searcherManager = index.getSearcherManager();
-            searcherManager.maybeRefreshBlocking();
-
-            final IndexSearcher searcher = searcherManager.acquire();
-            try {
-                if (searchRequest.hasCounts() || searchRequest.hasRanges()) {
-                    cm = new MultiCollectorManager(hits, new 
FacetsCollectorManager());
-                } else {
-                    cm = new MultiCollectorManager(hits);
-                }
-                final Object[] reduces = searcher.search(query, cm);
-                return toSearchResults(searchRequest, searcher, reduces);
-            } catch (IllegalStateException e) {
-                throw new WebApplicationException(e.getMessage(), e, 
Status.BAD_REQUEST);
-            } finally {
-                searcherManager.release(searcher);
-            }
+            return index.search(searchRequest);
         } finally {
             indexManager.release(index);
         }
     }
 
-    private CollectorManager<?, ? extends TopDocs> hitCollector(final 
SearchRequest searchRequest) {
-        final Sort sort = toSort(searchRequest);
-
-        final FieldDoc after = searchRequest.getAfter();
-        if (after != null) {
-            if (getLastSortField(sort).getReverse()) {
-                after.doc = 0;
-            } else {
-                after.doc = Integer.MAX_VALUE;
-            }
-        }
-
-        return TopFieldCollector.createSharedManager(
-                sort,
-                searchRequest.getLimit(),
-                after,
-                1000);
-    }
-
-    private SortField getLastSortField(final Sort sort) {
-        final SortField[] sortFields = sort.getSort();
-        return sortFields[sortFields.length - 1];
-    }
-
-    private SearchResults toSearchResults(final SearchRequest searchRequest, 
final IndexSearcher searcher,
-            final Object[] reduces) throws IOException {
-        final SearchResults result = new SearchResults();
-        collectHits(searcher, (TopDocs) reduces[0], result);
-        if (reduces.length == 2) {
-            collectFacets(searchRequest, searcher, (FacetsCollector) 
reduces[1], result);
-        }
-        return result;
-    }
-
-    private void collectHits(final IndexSearcher searcher, final TopDocs 
topDocs, final SearchResults searchResults)
-            throws IOException {
-        final List<SearchHit> hits = new 
ArrayList<SearchHit>(topDocs.scoreDocs.length);
-
-        for (final ScoreDoc scoreDoc : topDocs.scoreDocs) {
-            final Document doc = searcher.doc(scoreDoc.doc);
-
-            final List<IndexableField> fields = new 
ArrayList<IndexableField>(doc.getFields());
-            for (IndexableField field : doc.getFields()) {
-                if (field.name().equals("_id")) {
-                    fields.remove(field);
-                }
-            }
-
-            hits.add(new SearchHit(doc.get("_id"), (FieldDoc) scoreDoc, 
fields));
-        }
-
-        searchResults.setTotalHits(topDocs.totalHits);
-        searchResults.setHits(hits);
-    }
-
-    private void collectFacets(final SearchRequest searchRequest, final 
IndexSearcher searcher,
-            final FacetsCollector fc, final SearchResults searchResults) 
throws IOException {
-        if (searchRequest.hasCounts()) {
-            final Map<String, Map<String, Number>> countsMap = new 
HashMap<String, Map<String, Number>>(
-                    searchRequest.getCounts().size());
-            for (final String field : searchRequest.getCounts()) {
-                final StringDocValuesReaderState state = new 
StringDocValuesReaderState(searcher.getIndexReader(),
-                        field);
-                final StringValueFacetCounts counts = new 
StringValueFacetCounts(state, fc);
-                countsMap.put(field, collectFacets(counts, 
searchRequest.getTopN(), field));
-            }
-            searchResults.setCounts(countsMap);
-        }
-
-        if (searchRequest.hasRanges()) {
-            final Map<String, Map<String, Number>> rangesMap = new 
HashMap<String, Map<String, Number>>(
-                    searchRequest.getRanges().size());
-            for (final Entry<String, List<DoubleRange>> entry : 
searchRequest.getRanges().entrySet()) {
-                final DoubleRangeFacetCounts counts = new 
DoubleRangeFacetCounts(entry.getKey(), fc,
-                        entry.getValue().toArray(EMPTY_DOUBLE_RANGE_ARRAY));
-                rangesMap.put(entry.getKey(), collectFacets(counts, 
searchRequest.getTopN(), entry.getKey()));
-            }
-            searchResults.setRanges(rangesMap);
-        }
-    }
-
-    private Map<String, Number> collectFacets(final Facets facets, final int 
topN, final String dim)
-            throws IOException {
-        final FacetResult topChildren = facets.getTopChildren(topN, dim);
-        final Map<String, Number> result = new HashMap<String, 
Number>(topChildren.childCount);
-        for (final LabelAndValue lv : topChildren.labelValues) {
-            result.put(lv.label, lv.value);
-        }
-        return result;
-    }
-
-    // Ensure _id is final sort field so we can paginate.
-    private Sort toSort(final SearchRequest searchRequest) {
-        if (!searchRequest.hasSort()) {
-            return DEFAULT_SORT;
-        }
-
-        final List<String> sort = new 
ArrayList<String>(searchRequest.getSort());
-        final String last = sort.get(sort.size() - 1);
-        // Append _id field if not already present.
-        switch(last) {
-            case "-_id<string>":
-            case "_id<string>":
-                break;
-            default:
-            sort.add("_id<string>");
-        }
-        return convertSort(sort);
-    }
-
-    private Sort convertSort(final List<String> sort) {
-        final SortField[] fields = new SortField[sort.size()];
-        for (int i = 0; i < sort.size(); i++) {
-            fields[i] = convertSortField(sort.get(i));
-        }
-        return new Sort(fields);
-    }
-
-    private SortField convertSortField(final String sortString) {
-        final Matcher m = SORT_FIELD_RE.matcher(sortString);
-        if (!m.matches()) {
-            throw new WebApplicationException(
-                    sortString + " is not a valid sort parameter", 
Status.BAD_REQUEST);
-        }
-        final boolean reverse = "-".equals(m.group(1));
-        SortField.Type type = SortField.Type.DOUBLE;
-        if ("string".equals(m.group(3))) {
-            type = SortField.Type.STRING;
-        }
-        return new SortField(m.group(2), type, reverse);
-    }
-
 }
diff --git 
a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/IntegrationTest.java
 
b/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/IntegrationTest.java
index 8b2d7029a..66f774514 100644
--- 
a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/IntegrationTest.java
+++ 
b/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/IntegrationTest.java
@@ -13,8 +13,10 @@
 
 package org.apache.couchdb.nouveau;
 
+import static org.apache.couchdb.nouveau.api.LuceneVersion.LUCENE_9;
 import static org.assertj.core.api.Assertions.assertThat;
 
+import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 
@@ -24,10 +26,8 @@ import javax.ws.rs.core.Response;
 
 import org.apache.couchdb.nouveau.api.DocumentUpdateRequest;
 import org.apache.couchdb.nouveau.api.IndexDefinition;
-import static org.apache.couchdb.nouveau.api.LuceneVersion.*;
 import org.apache.couchdb.nouveau.api.SearchRequest;
 import org.apache.couchdb.nouveau.api.SearchResults;
-
 import org.apache.lucene.document.DoubleDocValuesField;
 import org.apache.lucene.document.DoublePoint;
 import org.apache.lucene.document.SortedSetDocValuesField;
@@ -112,12 +112,16 @@ public class IntegrationTest {
     }
 
     @Test
-    public void healthCheckShouldSucceed() {
+    public void healthCheckShouldSucceed() throws IOException {
         final Response healthCheckResponse =
                 APP.client().target("http://localhost:"; + APP.getAdminPort() + 
"/healthcheck")
                 .request()
                 .get();
 
+        //healthCheckResponse.bufferEntity();
+        //InputStream in = (InputStream) healthCheckResponse.getEntity();
+        //System.err.printf("health check response: %s\n", new 
String(in.readAllBytes()));
+
         assertThat(healthCheckResponse)
                 .extracting(Response::getStatus)
                 .isEqualTo(Response.Status.OK.getStatusCode());
diff --git 
a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/api/SearchRequestTest.java
 
b/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/api/SearchRequestTest.java
index 9544b6f6e..7bc886af2 100644
--- 
a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/api/SearchRequestTest.java
+++ 
b/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/api/SearchRequestTest.java
@@ -7,12 +7,12 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.couchdb.nouveau.core.ser.LuceneModule;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.lucene.facet.range.DoubleRange;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+
 public class SearchRequestTest {
 
     private static ObjectMapper mapper;
diff --git 
a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/core/IndexManagerTest.java
 
b/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/core/IndexManagerTest.java
index 64081d5c0..4487b69e3 100644
--- 
a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/core/IndexManagerTest.java
+++ 
b/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/core/IndexManagerTest.java
@@ -42,7 +42,7 @@ public class IndexManagerTest {
     public void setup() throws Exception {
         manager = new IndexManager();
         manager.setMetricRegistry(new MetricRegistry());
-        manager.setAnalyzerFactory(new AnalyzerFactory());
+        manager.setAnalyzerFactory(new Lucene9AnalyzerFactory());
         manager.setCommitIntervalSeconds(5);
         manager.setObjectMapper(new ObjectMapper());
         manager.setRootDir(tempDir);
diff --git 
a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/core/AnalyzerFactoryTest.java
 
b/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/core/Lucene9AnalyzerFactoryTest.java
similarity index 98%
rename from 
java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/core/AnalyzerFactoryTest.java
rename to 
java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/core/Lucene9AnalyzerFactoryTest.java
index 43bdb4e14..0adf18d45 100644
--- 
a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/core/AnalyzerFactoryTest.java
+++ 
b/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/core/Lucene9AnalyzerFactoryTest.java
@@ -56,7 +56,7 @@ import org.apache.lucene.analysis.th.ThaiAnalyzer;
 import org.apache.lucene.analysis.tr.TurkishAnalyzer;
 import org.junit.jupiter.api.Test;
 
-public class AnalyzerFactoryTest {
+public class Lucene9AnalyzerFactoryTest {
 
     @Test
     public void testkeyword() throws Exception {
@@ -249,7 +249,7 @@ public class AnalyzerFactoryTest {
     }
 
     private void assertAnalyzer(final String name, final Class<? extends 
Analyzer> clazz) throws Exception {
-        final AnalyzerFactory factory = new AnalyzerFactory();
+        final Lucene9AnalyzerFactory factory = new Lucene9AnalyzerFactory();
         assertThat(factory.newAnalyzer(name)).isInstanceOf(clazz);
     }
 

Reply via email to