You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by rn...@apache.org on 2023/03/01 19:20:12 UTC

[couchdb] 07/07: WIP the great shuffle

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

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

commit aaa4ed6185825d92fe481b3220661665eff15cd0
Author: Robert Newson <rn...@apache.org>
AuthorDate: Wed Mar 1 19:19:41 2023 +0000

    WIP the great shuffle
---
 .../couchdb/nouveau/api/document/DoubleField.java  |  77 -----------
 .../apache/couchdb/nouveau/api/document/Field.java |  47 -------
 .../nouveau/api/document/StoredDoubleField.java    |  41 ------
 .../couchdb/nouveau/api/document/StringField.java  |  74 -----------
 .../couchdb/nouveau/api/document/TextField.java    |  74 -----------
 .../apache/couchdb/nouveau/core/LuceneBundle.java  |   9 --
 .../nouveau/api/DocumentUpdateRequestTest.java     |  65 ---------
 java/nouveau/{api => base}/pom.xml                 |   4 +-
 .../couchdb/nouveau/IndexManagerFactory.java}      |  65 +++++----
 .../nouveau/NouveauApplicationConfiguration.java}  |  28 ++--
 .../java/org/apache/couchdb/nouveau/api/After.java |   0
 .../apache/couchdb/nouveau/api/AnalyzeRequest.java |   0
 .../couchdb/nouveau/api/AnalyzeResponse.java       |   0
 .../couchdb/nouveau/api/DocumentDeleteRequest.java |  22 ---
 .../couchdb/nouveau/api/DocumentUpdateRequest.java |  44 +-----
 .../apache/couchdb/nouveau/api/DoubleRange.java    |   0
 .../couchdb/nouveau/api/IndexDefinition.java       |   0
 .../org/apache/couchdb/nouveau/api/IndexInfo.java  |  14 +-
 .../java/org/apache/couchdb/nouveau/api/Range.java |   0
 .../org/apache/couchdb/nouveau/api/SearchHit.java  |  10 +-
 .../apache/couchdb/nouveau/api/SearchRequest.java  |   0
 .../apache/couchdb/nouveau/api/SearchResults.java  |   8 +-
 .../org/apache/couchdb/nouveau/core/IOUtils.java   |   0
 .../org/apache/couchdb/nouveau/core/Index.java     |  10 +-
 .../apache/couchdb/nouveau/core/IndexLoader.java}  |  14 +-
 .../apache/couchdb/nouveau/core/IndexManager.java  |  47 +++----
 .../nouveau/core/UpdatesOutOfOrderException.java   |   0
 .../nouveau/core/ser/AfterDeserializer.java        |   0
 .../couchdb/nouveau/core/ser/AfterSerializer.java  |   0
 .../nouveau/resources/BaseAnalyzeResource.java}    |  24 +---
 .../nouveau/resources/BaseIndexResource.java}      |  41 +++---
 .../couchdb/nouveau/api/SearchRequestTest.java     |   0
 .../resources/fixtures/DocumentUpdateRequest.json  |   0
 .../src/test/resources/fixtures/SearchRequest.json |   0
 java/nouveau/lucene4/pom.xml                       |   4 +-
 .../couchdb/nouveau/core/lucene4/Dummy4.java       |  25 ----
 .../core/lucene4/IndexableFieldDeserializer.java   |  30 -----
 .../couchdb/nouveau/core/lucene4/Lucene4.java      |  99 --------------
 .../nouveau/core/lucene4/Lucene4Bundle.java        |  25 ----
 .../nouveau/core/lucene4/Lucene4Module.java        |  15 ---
 .../lucene4/core/IndexableFieldDeserializer.java   |  78 +++++++++++
 .../lucene4/core/IndexableFieldSerializer.java     |  59 ++++++++
 .../core}/Lucene4AnalyzerFactory.java              |   4 +-
 .../nouveau/lucene4/core/Lucene4Bundle.java        |  45 +++++++
 .../lucene4 => lucene4/core}/Lucene4Index.java     |  78 +++--------
 .../nouveau/lucene4/core/Lucene4Module.java        |  35 +++++
 .../core}/Lucene4QueryParser.java                  |   2 +-
 .../core}/ParallelSearcherFactory.java             |   2 +-
 .../lucene4 => lucene4/core}/PerFieldAnalyzer.java |   2 +-
 .../nouveau/lucene4/core/QueryDeserializer.java    |  41 ++++++
 .../{core/lucene4 => lucene4/core}/Utils.java      |   6 +-
 .../nouveau/lucene4/resources/AnalyzeResource.java |  62 +++++++++
 .../nouveau/lucene4/resources/IndexResource.java   |  68 ++++++++++
 .../core}/Lucene4AnalyzerFactoryTest.java          |   2 +-
 java/nouveau/lucene9/pom.xml                       |   4 +-
 java/nouveau/pom.xml                               |   2 +-
 java/nouveau/server/nouveau.yaml                   |   9 +-
 java/nouveau/server/pom.xml                        |   4 +-
 .../apache/couchdb/nouveau/NouveauApplication.java |  78 ++---------
 .../couchdb/nouveau/health/AnalyzeHealthCheck.java |  45 -------
 .../nouveau/health/IndexManagerHealthCheck.java    |  52 --------
 .../apache/couchdb/nouveau/IntegrationTest.java    | 137 -------------------
 .../couchdb/nouveau/core/IndexManagerTest.java     |  76 -----------
 .../nouveau/core/ser/SerializationTest.java        | 148 ---------------------
 share/server/nouveau.js                            |   9 +-
 65 files changed, 567 insertions(+), 1397 deletions(-)

diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/DoubleField.java b/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/DoubleField.java
deleted file mode 100644
index 11f662233..000000000
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/DoubleField.java
+++ /dev/null
@@ -1,77 +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.api.document;
-
-import javax.validation.constraints.NotNull;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonInclude.Include;
-
-import io.dropwizard.jackson.JsonSnakeCase;
-
-@JsonSnakeCase
-@JsonInclude(value=Include.NON_DEFAULT)
-public final class DoubleField extends Field {
-
-    private final double value;
-
-    private final boolean store;
-
-    private final boolean facet;
-
-    private final boolean sortable;
-
-    @JsonCreator
-    public DoubleField(
-        @NotNull @JsonProperty("name") final String name,
-        @NotNull @JsonProperty("value") final double value,
-        @JsonProperty("store") final boolean store,
-        @JsonProperty("facet") final boolean facet,
-        @JsonProperty("sortable") final boolean sortable) {
-        super(name);
-        this.value = value;
-        this.store = store;
-        this.facet = facet;
-        this.sortable = sortable;
-    }
-
-    @JsonProperty
-    public double getValue() {
-        return value;
-    }
-
-    @JsonProperty
-    public boolean isStore() {
-        return store;
-    }
-
-    @JsonProperty
-    public boolean isFacet() {
-        return facet;
-    }
-
-    @JsonProperty
-    public boolean isSortable() {
-        return sortable;
-    }
-
-    @Override
-    public String toString() {
-        return "DoubleField [name=" + name + ", value=" + value + ", store=" + store + ", facet=" + facet + ", sortable=" + sortable
-                + "]";
-    }
-
-}
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/Field.java b/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/Field.java
deleted file mode 100644
index 2fc90b29e..000000000
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/Field.java
+++ /dev/null
@@ -1,47 +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.api.document;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonSubTypes;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-
-import io.dropwizard.jackson.JsonSnakeCase;
-
-@JsonSnakeCase
-@JsonTypeInfo(
-    use = JsonTypeInfo.Id.NAME,
-    include = JsonTypeInfo.As.PROPERTY,
-    property = "@type")
-@JsonSubTypes({
-    @JsonSubTypes.Type(value = DoubleField.class, name = "double"),
-    @JsonSubTypes.Type(value = StoredDoubleField.class, name = "stored_double"),
-    @JsonSubTypes.Type(value = StoredStringField.class, name = "stored_string"),
-    @JsonSubTypes.Type(value = StringField.class, name = "string"),
-    @JsonSubTypes.Type(value = TextField.class, name = "text"),
-})
-public abstract class Field {
-
-    protected final String name;
-
-    protected Field(final String name) {
-        this.name = name;
-    }
-
-    @JsonProperty
-    public String getName() {
-        return name;
-    }
-
-}
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/StoredDoubleField.java b/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/StoredDoubleField.java
deleted file mode 100644
index b880f1742..000000000
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/StoredDoubleField.java
+++ /dev/null
@@ -1,41 +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.api.document;
-
-import javax.validation.constraints.NotNull;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public final class StoredDoubleField extends Field {
-
-    private final double value;
-
-    @JsonCreator
-    public StoredDoubleField(@NotNull @JsonProperty("name") final String name, @NotNull @JsonProperty("value") final double value) {
-        super(name);
-        this.value = value;
-    }
-
-    @JsonProperty
-    public double getValue() {
-        return value;
-    }
-
-    @Override
-    public String toString() {
-        return "StoredDoubleField [value=" + value + "]";
-    }
-
-}
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/StringField.java b/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/StringField.java
deleted file mode 100644
index d03a6f38a..000000000
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/StringField.java
+++ /dev/null
@@ -1,74 +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.api.document;
-
-import javax.validation.constraints.NotNull;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonInclude.Include;
-
-@JsonInclude(value=Include.NON_DEFAULT)
-public final class StringField extends Field {
-
-    private final String value;
-
-    private final boolean store;
-
-    private final boolean facet;
-
-    private final boolean sortable;
-
-    @JsonCreator
-    public StringField(
-        @NotNull @JsonProperty("name") final String name,
-        @NotNull @JsonProperty("value") final String value,
-        @JsonProperty("store") final boolean store,
-        @JsonProperty("facet") final boolean facet,
-        @JsonProperty("sortable") final boolean sortable) {
-        super(name);
-        this.value = value;
-        this.store = store;
-        this.facet = facet;
-        this.sortable = sortable;
-    }
-
-    @JsonProperty
-    public String getValue() {
-        return value;
-    }
-
-    @JsonProperty
-    public boolean isStore() {
-        return store;
-    }
-
-    @JsonProperty
-    public boolean isFacet() {
-        return facet;
-    }
-
-    @JsonProperty
-    public boolean isSortable() {
-        return sortable;
-    }
-
-    @Override
-    public String toString() {
-        return "StringField [name=" + name + ", value=" + value + ", store=" + store + ", facet=" + facet + ", sortable=" + sortable
-                + "]";
-    }
-
-}
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/TextField.java b/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/TextField.java
deleted file mode 100644
index 0c8903c77..000000000
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/TextField.java
+++ /dev/null
@@ -1,74 +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.api.document;
-
-import javax.validation.constraints.NotNull;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonInclude.Include;
-
-@JsonInclude(value=Include.NON_DEFAULT)
-public final class TextField extends Field {
-
-    private final String value;
-
-    private final boolean store;
-
-    private final boolean facet;
-
-    private final boolean sortable;
-
-    @JsonCreator
-    public TextField(
-        @NotNull @JsonProperty("name") final String name,
-        @NotNull @JsonProperty("value") final String value,
-        @JsonProperty("store") final boolean store,
-        @JsonProperty("facet") final boolean facet,
-        @JsonProperty("sortable") final boolean sortable) {
-        super(name);
-        this.value = value;
-        this.store = store;
-        this.facet = facet;
-        this.sortable = sortable;
-    }
-
-    @JsonProperty
-    public String getValue() {
-        return value;
-    }
-
-    @JsonProperty
-    public boolean isStore() {
-        return store;
-    }
-
-    @JsonProperty
-    public boolean isFacet() {
-        return facet;
-    }
-
-    @JsonProperty
-    public boolean isSortable() {
-        return sortable;
-    }
-
-    @Override
-    public String toString() {
-        return "TextField [name=" + name + ", value=" + value + ", store=" + store + ", facet=" + facet + ", sortable=" + sortable
-                + "]";
-    }
-
-}
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/LuceneBundle.java b/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/LuceneBundle.java
deleted file mode 100644
index c94e99e56..000000000
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/LuceneBundle.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.apache.couchdb.nouveau.core;
-
-import io.dropwizard.ConfiguredBundle;
-
-public interface LuceneBundle<T> extends ConfiguredBundle<T> {
-
-    Lucene getLucene();
-
-}
diff --git a/java/nouveau/api/src/test/java/org/apache/couchdb/nouveau/api/DocumentUpdateRequestTest.java b/java/nouveau/api/src/test/java/org/apache/couchdb/nouveau/api/DocumentUpdateRequestTest.java
deleted file mode 100644
index 1c79f8fe1..000000000
--- a/java/nouveau/api/src/test/java/org/apache/couchdb/nouveau/api/DocumentUpdateRequestTest.java
+++ /dev/null
@@ -1,65 +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.api;
-
-import static io.dropwizard.testing.FixtureHelpers.fixture;
-import static org.assertj.core.api.Assertions.assertThat;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.couchdb.nouveau.api.document.DoubleField;
-import org.apache.couchdb.nouveau.api.document.Field;
-import org.apache.couchdb.nouveau.api.document.StringField;
-import org.apache.couchdb.nouveau.api.document.TextField;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-
-public class DocumentUpdateRequestTest {
-
-    private static ObjectMapper mapper;
-
-    @BeforeAll
-    public static void setupMapper() {
-        System.err.println("farts");
-        mapper = new ObjectMapper();
-    }
-
-    @Test
-    public void testSerialisation() throws Exception {
-        DocumentUpdateRequest request = asObject();
-        final String expected = mapper.writeValueAsString(
-            mapper.readValue(fixture("fixtures/DocumentUpdateRequest.json"), DocumentUpdateRequest.class));
-        assertThat(mapper.writeValueAsString(request)).isEqualTo(expected);
-    }
-
-    @Test
-    public void testDeserialisation() throws Exception {
-        DocumentUpdateRequest request = asObject();
-        assertThat(mapper.readValue(fixture("fixtures/DocumentUpdateRequest.json"), DocumentUpdateRequest.class).toString())
-                .isEqualTo(request.toString());
-    }
-
-    private DocumentUpdateRequest asObject() {
-        final List<Field> fields = new ArrayList<Field>();
-        fields.add(new StringField("stringfoo", "bar", true, false, false));
-        fields.add(new TextField("textfoo", "hello there", true, false, false));
-        fields.add(new DoubleField("doublefoo", 12, false, false, false));
-        return new DocumentUpdateRequest(12, null, fields);
-    }
-
-}
diff --git a/java/nouveau/api/pom.xml b/java/nouveau/base/pom.xml
similarity index 95%
rename from java/nouveau/api/pom.xml
rename to java/nouveau/base/pom.xml
index ed1acd417..a54c844b8 100644
--- a/java/nouveau/api/pom.xml
+++ b/java/nouveau/base/pom.xml
@@ -2,10 +2,10 @@
 <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">
   <modelVersion>4.0.0</modelVersion>
   <groupId>org.apache.couchdb.nouveau</groupId>
-  <artifactId>api</artifactId>
+  <artifactId>base</artifactId>
   <name>${project.artifactId}</name>
   <version>1.0-SNAPSHOT</version>
-  <description>Nouveau API classes</description>
+  <description>Nouveau Base classes</description>
   <inceptionYear>2023</inceptionYear>
 
   <parent>
diff --git a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/IndexManagerFactory.java
similarity index 59%
rename from java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/IndexManagerFactory.java
index 5be8c5a21..3f5c38f9b 100644
--- a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
+++ b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/IndexManagerFactory.java
@@ -13,73 +13,90 @@
 
 package org.apache.couchdb.nouveau;
 
-import java.net.URL;
 import java.nio.file.Path;
-import java.util.Arrays;
+import java.util.concurrent.ScheduledExecutorService;
 
+import javax.validation.Valid;
 import javax.validation.constraints.Min;
-import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
 
+import org.apache.couchdb.nouveau.core.IndexManager;
+
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-import io.dropwizard.Configuration;
+import io.dropwizard.setup.Environment;
 
-public class NouveauApplicationConfiguration extends Configuration {
+public final class IndexManagerFactory {
 
     @Min(10)
-    private int maxIndexesOpen = -1;
+    private int maxIndexesOpen = 10;
 
     @Min(10)
-    private int commitIntervalSeconds = -1;
+    private int commitIntervalSeconds = 10;
 
     @Min(30)
-    private int idleSeconds = -1;
+    private int idleSeconds = 30;
 
+    @Valid
     @NotNull
-    private Path rootDir = null;
+    private Path rootDir;
 
     @JsonProperty
-    public void setMaxIndexesOpen(int maxIndexesOpen) {
-        this.maxIndexesOpen = maxIndexesOpen;
-    }
-
     public int getMaxIndexesOpen() {
         return maxIndexesOpen;
     }
 
     @JsonProperty
-    public void setCommitIntervalSeconds(int commitIntervalSeconds) {
-        this.commitIntervalSeconds = commitIntervalSeconds;
+    public void setMaxIndexesOpen(int maxIndexesOpen) {
+        this.maxIndexesOpen = maxIndexesOpen;
     }
 
+    @JsonProperty
     public int getCommitIntervalSeconds() {
         return commitIntervalSeconds;
     }
 
     @JsonProperty
-    public void setIdleSeconds(int idleSeconds) {
-        this.idleSeconds = idleSeconds;
+    public void setCommitIntervalSeconds(int commitIntervalSeconds) {
+        this.commitIntervalSeconds = commitIntervalSeconds;
     }
 
+    @JsonProperty
     public int getIdleSeconds() {
         return idleSeconds;
     }
 
     @JsonProperty
-    public void setRootDir(Path rootDir) {
-        this.rootDir = rootDir;
+    public void setIdleSeconds(int idleSeconds) {
+        this.idleSeconds = idleSeconds;
     }
 
+    @JsonProperty
     public Path getRootDir() {
         return rootDir;
     }
 
-    @Override
-    public String toString() {
-        return "NouveauApplicationConfiguration [maxIndexesOpen=" + maxIndexesOpen + ", commitIntervalSeconds="
-                + commitIntervalSeconds + ", idleSeconds=" + idleSeconds + ", rootDir=" + rootDir
-                + "]";
+    @JsonProperty
+    public void setRootDir(Path rootDir) {
+        this.rootDir = rootDir;
+    }
+
+    public IndexManager build(final Environment environment) {
+        final ScheduledExecutorService scheduler = environment.lifecycle()
+            .scheduledExecutorService("index-manager-scheduler-%d")
+            .threads(10)
+            .build();
+
+        final IndexManager result = new IndexManager();
+        result.setCommitIntervalSeconds(commitIntervalSeconds);
+        result.setIdleSeconds(idleSeconds);
+        result.setMaxIndexesOpen(maxIndexesOpen);
+        result.setObjectMapper(environment.getObjectMapper());
+        result.setRootDir(rootDir);
+        result.setScheduler(scheduler);
+
+        environment.lifecycle().manage(result);
+        return result;
     }
 
 }
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/StoredStringField.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
similarity index 56%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/StoredStringField.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
index a93e3fade..11602cdbc 100644
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/StoredStringField.java
+++ b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
@@ -11,31 +11,31 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package org.apache.couchdb.nouveau.api.document;
+package org.apache.couchdb.nouveau;
 
+import javax.validation.Valid;
 import javax.validation.constraints.NotNull;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public final class StoredStringField extends Field {
+import io.dropwizard.Configuration;
 
-    private String value;
+public class NouveauApplicationConfiguration extends Configuration {
 
-    @JsonCreator
-    public StoredStringField(@NotNull @JsonProperty("name") final String name, @NotNull @JsonProperty("value") final String value) {
-        super(name);
-        this.value = value;
-    }
+    @Valid
+    @NotNull
+    private IndexManagerFactory indexManagerFactory;
 
     @JsonProperty
-    public String getValue() {
-        return value;
+    public IndexManagerFactory getIndexManagerFactory() {
+        return indexManagerFactory;
     }
 
-    @Override
-    public String toString() {
-        return "StoredStringField [value=" + value + "]";
+    @JsonProperty
+    public void setIndexManagerFactory(IndexManagerFactory indexManagerFactory) {
+        this.indexManagerFactory = indexManagerFactory;
     }
 
+
+
 }
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/After.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/After.java
similarity index 100%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/After.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/After.java
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/AnalyzeRequest.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/AnalyzeRequest.java
similarity index 100%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/AnalyzeRequest.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/AnalyzeRequest.java
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/AnalyzeResponse.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/AnalyzeResponse.java
similarity index 100%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/AnalyzeResponse.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/AnalyzeResponse.java
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/DocumentDeleteRequest.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/DocumentDeleteRequest.java
similarity index 67%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/DocumentDeleteRequest.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/DocumentDeleteRequest.java
index f7a4c572b..045aa1944 100644
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/DocumentDeleteRequest.java
+++ b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/DocumentDeleteRequest.java
@@ -38,28 +38,6 @@ public class DocumentDeleteRequest {
         return seq;
     }
 
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + (int) (seq ^ (seq >>> 32));
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj)
-            return true;
-        if (obj == null)
-            return false;
-        if (getClass() != obj.getClass())
-            return false;
-        DocumentDeleteRequest other = (DocumentDeleteRequest) obj;
-        if (seq != other.seq)
-            return false;
-        return true;
-    }
-
     @Override
     public String toString() {
         return "DocumentDeleteRequest [seq=" + seq + "]";
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/DocumentUpdateRequest.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/DocumentUpdateRequest.java
similarity index 55%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/DocumentUpdateRequest.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/DocumentUpdateRequest.java
index e5871d68c..2e66fe4e2 100644
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/DocumentUpdateRequest.java
+++ b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/DocumentUpdateRequest.java
@@ -18,14 +18,12 @@ import java.util.Collection;
 import javax.validation.constraints.Min;
 import javax.validation.constraints.NotEmpty;
 
-import org.apache.couchdb.nouveau.api.document.Field;
-
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 import io.dropwizard.jackson.JsonSnakeCase;
 
 @JsonSnakeCase
-public class DocumentUpdateRequest {
+public class DocumentUpdateRequest<T> {
 
     @Min(1)
     private long seq;
@@ -33,13 +31,13 @@ public class DocumentUpdateRequest {
     private String partition;
 
     @NotEmpty
-    private Collection<Field> fields;
+    private Collection<T> fields;
 
     public DocumentUpdateRequest() {
         // Jackson deserialization
     }
 
-    public DocumentUpdateRequest(long seq, String partition, Collection<Field> fields) {
+    public DocumentUpdateRequest(long seq, String partition, Collection<T> fields) {
         this.seq = seq;
         this.partition = partition;
         this.fields = fields;
@@ -60,44 +58,10 @@ public class DocumentUpdateRequest {
     }
 
     @JsonProperty
-    public Collection<Field> getFields() {
+    public Collection<T> getFields() {
         return fields;
     }
 
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + (int) (seq ^ (seq >>> 32));
-        result = prime * result + ((partition == null) ? 0 : partition.hashCode());
-        result = prime * result + ((fields == null) ? 0 : fields.hashCode());
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj)
-            return true;
-        if (obj == null)
-            return false;
-        if (getClass() != obj.getClass())
-            return false;
-        DocumentUpdateRequest other = (DocumentUpdateRequest) obj;
-        if (seq != other.seq)
-            return false;
-        if (partition == null) {
-            if (other.partition != null)
-                return false;
-        } else if (!partition.equals(other.partition))
-            return false;
-        if (fields == null) {
-            if (other.fields != null)
-                return false;
-        } else if (!fields.equals(other.fields))
-            return false;
-        return true;
-    }
-
     @Override
     public String toString() {
         return "DocumentUpdateRequest [seq=" + seq + ", partition=" + partition + ", fields=" + fields + "]";
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/DoubleRange.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/DoubleRange.java
similarity index 100%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/DoubleRange.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/DoubleRange.java
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/IndexDefinition.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/IndexDefinition.java
similarity index 100%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/IndexDefinition.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/IndexDefinition.java
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/IndexInfo.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/IndexInfo.java
similarity index 80%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/IndexInfo.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/IndexInfo.java
index 2bfa096a6..55e7174ec 100644
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/IndexInfo.java
+++ b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/IndexInfo.java
@@ -13,8 +13,6 @@
 
 package org.apache.couchdb.nouveau.api;
 
-import javax.validation.constraints.NotNull;
-
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 import io.dropwizard.jackson.JsonSnakeCase;
@@ -22,27 +20,25 @@ import io.dropwizard.jackson.JsonSnakeCase;
 @JsonSnakeCase
 public class IndexInfo {
 
-    @NotNull
-    private Long updateSeq;
+    private long updateSeq;
 
-    @NotNull
-    private Integer numDocs;
+    private int numDocs;
 
     public IndexInfo() {
     }
 
-    public IndexInfo(final Long updateSeq, final Integer numDocs) {
+    public IndexInfo(final long updateSeq, final int numDocs) {
         this.updateSeq = updateSeq;
         this.numDocs = numDocs;
     }
 
     @JsonProperty
-    public Integer getNumDocs() {
+    public int getNumDocs() {
         return numDocs;
     }
 
     @JsonProperty
-    public Long getUpdateSeq() {
+    public long getUpdateSeq() {
         return updateSeq;
     }
 
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/Range.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/Range.java
similarity index 100%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/Range.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/Range.java
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/SearchHit.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/SearchHit.java
similarity index 87%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/SearchHit.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/SearchHit.java
index dbb6c7a28..a667fc7ba 100644
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/SearchHit.java
+++ b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/SearchHit.java
@@ -18,12 +18,10 @@ import java.util.Collection;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
 
-import org.apache.couchdb.nouveau.api.document.Field;
-
 import io.dropwizard.jackson.JsonSnakeCase;
 
 @JsonSnakeCase
-public class SearchHit {
+public class SearchHit<T> {
 
     @NotEmpty
     private String id;
@@ -32,12 +30,12 @@ public class SearchHit {
     private After order;
 
     @NotNull
-    private Collection<@NotNull Field> fields;
+    private Collection<@NotNull T> fields;
 
     public SearchHit() {
     }
 
-    public SearchHit(final String id, final After order, final Collection<Field> fields) {
+    public SearchHit(final String id, final After order, final Collection<T> fields) {
         this.id = id;
         this.order = order;
         this.fields = fields;
@@ -51,7 +49,7 @@ public class SearchHit {
         return order;
     }
 
-    public Collection<Field> getFields() {
+    public Collection<T> getFields() {
         return fields;
     }
 
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/SearchRequest.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/SearchRequest.java
similarity index 100%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/SearchRequest.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/SearchRequest.java
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/SearchResults.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/SearchResults.java
similarity index 92%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/SearchResults.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/SearchResults.java
index 7e68fe944..157572ee3 100644
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/SearchResults.java
+++ b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/api/SearchResults.java
@@ -24,7 +24,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import io.dropwizard.jackson.JsonSnakeCase;
 
 @JsonSnakeCase
-public class SearchResults {
+public class SearchResults<T> {
 
     @Min(0)
     private long totalHits;
@@ -33,7 +33,7 @@ public class SearchResults {
     private String totalHitsRelation;
 
     @NotNull
-    private List<@NotNull SearchHit> hits;
+    private List<@NotNull SearchHit<T>> hits;
 
     private Map<@NotNull String, Map<@NotNull String, Number>> counts;
 
@@ -59,12 +59,12 @@ public class SearchResults {
         this.totalHitsRelation = totalHitsRelation;
     }
 
-    public void setHits(final List<SearchHit> hits) {
+    public void setHits(final List<SearchHit<T>> hits) {
         this.hits = hits;
     }
 
     @JsonProperty
-    public List<SearchHit> getHits() {
+    public List<SearchHit<T>> getHits() {
         return hits;
     }
 
diff --git a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/IOUtils.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/IOUtils.java
similarity index 100%
rename from java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/IOUtils.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/IOUtils.java
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/Index.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/Index.java
similarity index 91%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/Index.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/Index.java
index b4ca00928..e037ddc95 100644
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/Index.java
+++ b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/Index.java
@@ -23,7 +23,7 @@ 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 {
+public abstract class Index<T> implements Closeable {
 
     private long updateSeq;
 
@@ -45,14 +45,14 @@ public abstract class Index implements Closeable {
 
     protected abstract int doNumDocs() throws IOException;
 
-    public final synchronized void update(final String docId, final DocumentUpdateRequest request) throws IOException {
+    public final synchronized void update(final String docId, final DocumentUpdateRequest<T> request) throws IOException {
         assertUpdateSeqIsLower(request.getSeq());
         updateLastUsed();
         doUpdate(docId, request);
         incrementUpdateSeq(request.getSeq());
     }
 
-    protected abstract void doUpdate(final String docId, final DocumentUpdateRequest request) throws IOException;
+    protected abstract void doUpdate(final String docId, final DocumentUpdateRequest<T> request) throws IOException;
 
     public final synchronized void delete(final String docId, final DocumentDeleteRequest request) throws IOException {
         assertUpdateSeqIsLower(request.getSeq());
@@ -63,12 +63,12 @@ public abstract class Index implements Closeable {
 
     protected abstract void doDelete(final String docId, final DocumentDeleteRequest request) throws IOException;
 
-    public final SearchResults search(final SearchRequest request) throws IOException {
+    public final SearchResults<T> search(final SearchRequest request) throws IOException {
         updateLastUsed();
         return doSearch(request);
     }
 
-    protected abstract SearchResults doSearch(final SearchRequest request) throws IOException;
+    protected abstract SearchResults<T> doSearch(final SearchRequest request) throws IOException;
 
     public final boolean commit() throws IOException {
         final long updateSeq;
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/Lucene.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/IndexLoader.java
similarity index 65%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/Lucene.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/IndexLoader.java
index f98335ae4..8fe2ecc76 100644
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/Lucene.java
+++ b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/IndexLoader.java
@@ -15,20 +15,12 @@ package org.apache.couchdb.nouveau.core;
 
 import java.io.IOException;
 import java.nio.file.Path;
-import java.util.List;
-
-import javax.ws.rs.WebApplicationException;
 
 import org.apache.couchdb.nouveau.api.IndexDefinition;
 
-public interface Lucene {
-
-    int getMajor();
-
-    List<String> analyze(final String analyzer, final String text) throws IOException;
-
-    void validate(final IndexDefinition indexDefinition) throws WebApplicationException;
+@FunctionalInterface
+public interface IndexLoader<T> {
 
-    Index open(final Path path, final IndexDefinition indexDefinition) throws IOException;
+    Index<T> apply(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/base/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java
similarity index 90%
rename from java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java
index d90fe2e90..8c8ac9c6b 100644
--- a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java
+++ b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java
@@ -13,6 +13,8 @@
 
 package org.apache.couchdb.nouveau.core;
 
+import static java.util.concurrent.TimeUnit.SECONDS;
+
 import java.io.IOException;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Files;
@@ -30,8 +32,6 @@ import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
-
-import static java.util.concurrent.TimeUnit.SECONDS;
 import java.util.stream.Stream;
 
 import javax.ws.rs.WebApplicationException;
@@ -58,12 +58,11 @@ public final class IndexManager implements Managed {
 
     private Path rootDir;
 
-    private Map<Integer, Lucene> lucenes;
-
     private ObjectMapper objectMapper;
 
     private ScheduledExecutorService scheduler;
 
+    @SuppressWarnings("rawtypes")
     private Map<String, Index> cache;
 
     private Map<String, Collection<ScheduledFuture<?>>> scheduledFutures;
@@ -72,7 +71,8 @@ public final class IndexManager implements Managed {
     // by the hashCode of the index's name.
     private ReadWriteLock[] locks;
 
-    public Index acquire(final String name) throws IOException {
+    @SuppressWarnings("rawtypes")
+    public Index acquire(final String name, final IndexLoader loader) throws IOException {
         if (!exists(name)) {
             throw new WebApplicationException("Index does not exist", Status.NOT_FOUND);
         }
@@ -106,9 +106,8 @@ public final class IndexManager implements Managed {
 
             LOGGER.info("opening {}", name);
             final Path path = indexPath(name);
-            final IndexDefinition indexDefinition = objectMapper.readValue(indexDefinitionPath(name).toFile(),
-                    IndexDefinition.class);
-            final Index newIndex = luceneFor(indexDefinition).open(path, indexDefinition);
+            final IndexDefinition indexDefinition = loadIndexDefinition(name);
+            final Index newIndex = loader.apply(path, indexDefinition);
 
             final Runnable committer = () -> {
                 try {
@@ -119,7 +118,8 @@ public final class IndexManager implements Managed {
                     LOGGER.error("I/O exception when committing " + name, e);
                 }
             };
-            final ScheduledFuture<?> committerFuture = scheduler.scheduleWithFixedDelay(committer, commitIntervalSeconds, commitIntervalSeconds, SECONDS);
+            final ScheduledFuture<?> committerFuture = scheduler.scheduleWithFixedDelay(committer,
+                    commitIntervalSeconds, commitIntervalSeconds, SECONDS);
 
             final Runnable idler = () -> {
                 if (newIndex.secondsSinceLastUse() >= idleSeconds) {
@@ -131,7 +131,8 @@ public final class IndexManager implements Managed {
                     }
                 }
             };
-            final ScheduledFuture<?> idlerFuture = scheduler.scheduleWithFixedDelay(idler, idleSeconds, idleSeconds, SECONDS);
+            final ScheduledFuture<?> idlerFuture = scheduler.scheduleWithFixedDelay(idler, idleSeconds, idleSeconds,
+                    SECONDS);
 
             synchronized (cache) {
                 cache.put(name, newIndex);
@@ -144,6 +145,7 @@ public final class IndexManager implements Managed {
         }
     }
 
+    @SuppressWarnings("rawtypes")
     public void release(final String name, final Index index) throws IOException {
         writeLock(name).lock();
         try {
@@ -168,6 +170,7 @@ public final class IndexManager implements Managed {
         }
     }
 
+    @SuppressWarnings("rawtypes")
     private void doRelease(final String name, final Index index) throws IOException {
         index.decRef();
         if (!index.isOpen()) {
@@ -196,7 +199,7 @@ public final class IndexManager implements Managed {
             throw new WebApplicationException("Index already exists", Status.EXPECTATION_FAILED);
         }
         // Validate index definiton
-        luceneFor(indexDefinition).validate(indexDefinition);
+        // TODO luceneFor(indexDefinition).validate(indexDefinition);
 
         // Persist definition
         final Path path = indexDefinitionPath(name);
@@ -231,6 +234,7 @@ public final class IndexManager implements Managed {
         }
     }
 
+    @SuppressWarnings("rawtypes")
     private void deleteIndex(final String name) throws IOException {
         final Index index;
         readLock(name).lock();
@@ -284,10 +288,6 @@ public final class IndexManager implements Managed {
         this.rootDir = rootDir;
     }
 
-    public void setLucenes(final Map<Integer, Lucene> lucenes) {
-        this.lucenes = lucenes;
-    }
-
     public void setObjectMapper(final ObjectMapper objectMapper) {
         this.objectMapper = objectMapper;
     }
@@ -297,6 +297,7 @@ public final class IndexManager implements Managed {
     }
 
     @Override
+    @SuppressWarnings("rawtypes")
     public void start() throws IOException {
         final int lockCount = Math.max(1000, maxIndexesOpen / 10);
         locks = new ReadWriteLock[lockCount];
@@ -324,7 +325,7 @@ public final class IndexManager implements Managed {
                 try {
                     release(evictee.getKey(), evictee.getValue());
                 } catch (final IOException e) {
-                    LOGGER.error("error evicting " +evictee.getKey(), e);
+                    LOGGER.error("error evicting " + evictee.getKey(), e);
                 }
             }
         };
@@ -332,6 +333,7 @@ public final class IndexManager implements Managed {
     }
 
     @Override
+    @SuppressWarnings("rawtypes")
     public void stop() throws IOException {
         synchronized (cache) {
             for (final Index index : cache.values()) {
@@ -353,6 +355,10 @@ public final class IndexManager implements Managed {
         return indexRootPath(name).resolve("index");
     }
 
+    private IndexDefinition loadIndexDefinition(final String name) throws IOException {
+        return objectMapper.readValue(indexDefinitionPath(name).toFile(), IndexDefinition.class);
+    }
+
     private Path indexRootPath(final String name) {
         final Path result = rootDir.resolve(name).normalize();
         if (result.startsWith(rootDir)) {
@@ -362,13 +368,4 @@ public final class IndexManager implements Managed {
                 Status.BAD_REQUEST);
     }
 
-    private Lucene luceneFor(final IndexDefinition indexDefinition) {
-        final int luceneMajor = indexDefinition.getLuceneMajor();
-        final Lucene result = lucenes.get(luceneMajor);
-        if (result == null) {
-            throw new WebApplicationException("Lucene major version " + luceneMajor + " not valid", Status.BAD_REQUEST);
-        }
-        return result;
-    }
-
 }
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/UpdatesOutOfOrderException.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/UpdatesOutOfOrderException.java
similarity index 100%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/UpdatesOutOfOrderException.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/UpdatesOutOfOrderException.java
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/ser/AfterDeserializer.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/ser/AfterDeserializer.java
similarity index 100%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/ser/AfterDeserializer.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/ser/AfterDeserializer.java
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/ser/AfterSerializer.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/ser/AfterSerializer.java
similarity index 100%
rename from java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/core/ser/AfterSerializer.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/core/ser/AfterSerializer.java
diff --git a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/AnalyzeResource.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/resources/BaseAnalyzeResource.java
similarity index 54%
rename from java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/AnalyzeResource.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/resources/BaseAnalyzeResource.java
index 4b532a8b7..a92a3d20d 100644
--- a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/AnalyzeResource.java
+++ b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/resources/BaseAnalyzeResource.java
@@ -14,45 +14,25 @@
 package org.apache.couchdb.nouveau.resources;
 
 import java.io.IOException;
-import java.util.List;
-import java.util.Map;
 
 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.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.AnalyzeRequest;
 import org.apache.couchdb.nouveau.api.AnalyzeResponse;
-import org.apache.couchdb.nouveau.core.Lucene;
 
 import com.codahale.metrics.annotation.Timed;
 
-@Path("/analyze")
 @Consumes(MediaType.APPLICATION_JSON)
 @Produces(MediaType.APPLICATION_JSON)
-public class AnalyzeResource {
-
-    private final Map<Integer, Lucene> lucenes;
-
-    public AnalyzeResource(Map<Integer, Lucene> lucenes) {
-        this.lucenes = lucenes;
-    }
+public abstract class BaseAnalyzeResource {
 
     @POST
     @Timed
-    public AnalyzeResponse analyzeText(@NotNull @Valid AnalyzeRequest analyzeRequest) throws IOException {
-        final Lucene lucene = lucenes.get(analyzeRequest.getLuceneMajor());
-        if (lucene == null) {
-            throw new WebApplicationException("Lucene major version " + analyzeRequest.getLuceneMajor() + " not valid", Status.BAD_REQUEST);
-        }
-        final List<String> tokens = lucene.analyze(analyzeRequest.getAnalyzer(), analyzeRequest.getText());
-        return new AnalyzeResponse(tokens);
-    }
+    public abstract AnalyzeResponse analyzeText(@NotNull @Valid AnalyzeRequest analyzeRequest) throws IOException;
 
 }
diff --git a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/resources/BaseIndexResource.java
similarity index 65%
rename from java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java
rename to java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/resources/BaseIndexResource.java
index d28ac2266..2b59665f7 100644
--- a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java
+++ b/java/nouveau/base/src/main/java/org/apache/couchdb/nouveau/resources/BaseIndexResource.java
@@ -14,7 +14,6 @@
 package org.apache.couchdb.nouveau.resources;
 
 import java.io.IOException;
-import java.util.concurrent.ExecutionException;
 
 import javax.validation.Valid;
 import javax.validation.constraints.NotNull;
@@ -35,26 +34,26 @@ import org.apache.couchdb.nouveau.api.IndexInfo;
 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.IndexLoader;
 import org.apache.couchdb.nouveau.core.IndexManager;
 
 import com.codahale.metrics.annotation.Timed;
 
-@Path("/index/{name}")
 @Consumes(MediaType.APPLICATION_JSON)
 @Produces(MediaType.APPLICATION_JSON)
-public class IndexResource {
+public abstract class BaseIndexResource<T> {
 
-    private final IndexManager indexManager;
+    protected final IndexManager indexManager;
 
-    public IndexResource(final IndexManager indexManager) {
+    protected BaseIndexResource(final IndexManager indexManager) {
         this.indexManager = indexManager;
     }
 
     @GET
     @SuppressWarnings("resource")
-    public IndexInfo indexInfo(@PathParam("name") String name)
-            throws IOException, InterruptedException, ExecutionException {
-        final Index index = indexManager.acquire(name);
+    public final IndexInfo indexInfo(@PathParam("name") String name)
+            throws Exception {
+        final Index<T> index = indexManager.acquire(name, indexLoader());
         try {
             return index.info();
         } finally {
@@ -63,12 +62,12 @@ public class IndexResource {
     }
 
     @DELETE
-    public void deletePath(@PathParam("name") String path) throws IOException {
+    public final void deletePath(@PathParam("name") String path) throws IOException {
         indexManager.deleteAll(path);
     }
 
     @PUT
-    public void createIndex(@PathParam("name") String name, @NotNull @Valid IndexDefinition indexDefinition)
+    public final void createIndex(@PathParam("name") String name, @NotNull @Valid IndexDefinition indexDefinition)
             throws IOException {
         indexManager.create(name, indexDefinition);
     }
@@ -76,10 +75,10 @@ public class IndexResource {
     @DELETE
     @Timed
     @Path("/doc/{docId}")
-    public void deleteDoc(@PathParam("name") String name, @PathParam("docId") String docId,
+    public final void deleteDoc(@PathParam("name") String name, @PathParam("docId") String docId,
             @NotNull @Valid final DocumentDeleteRequest request)
-            throws IOException, InterruptedException, ExecutionException {
-        final Index index = indexManager.acquire(name);
+            throws Exception {
+        final Index<T> index = indexManager.acquire(name, indexLoader());
         try {
             index.delete(docId, request);
         } finally {
@@ -90,10 +89,10 @@ public class IndexResource {
     @PUT
     @Timed
     @Path("/doc/{docId}")
-    public void updateDoc(@PathParam("name") String name, @PathParam("docId") String docId,
-            @NotNull @Valid final DocumentUpdateRequest request)
-            throws IOException, InterruptedException, ExecutionException {
-        final Index index = indexManager.acquire(name);
+    public final void updateDoc(@PathParam("name") String name, @PathParam("docId") String docId,
+            @NotNull @Valid final DocumentUpdateRequest<T> request)
+            throws Exception {
+        final Index<T> index = indexManager.acquire(name, indexLoader());
         try {
             index.update(docId, request);
         } finally {
@@ -104,9 +103,9 @@ public class IndexResource {
     @POST
     @Timed
     @Path("/search")
-    public SearchResults searchIndex(@PathParam("name") String name, @NotNull @Valid SearchRequest request)
-            throws IOException, InterruptedException, ExecutionException {
-        final Index index = indexManager.acquire(name);
+    public final SearchResults<T> searchIndex(@PathParam("name") String name, @NotNull @Valid SearchRequest request)
+            throws Exception {
+        final Index index = indexManager.acquire(name, indexLoader());
         try {
             return index.search(request);
         } finally {
@@ -114,4 +113,6 @@ public class IndexResource {
         }
     }
 
+    protected abstract IndexLoader<T> indexLoader();
+
 }
\ No newline at end of file
diff --git a/java/nouveau/api/src/test/java/org/apache/couchdb/nouveau/api/SearchRequestTest.java b/java/nouveau/base/src/test/java/org/apache/couchdb/nouveau/api/SearchRequestTest.java
similarity index 100%
rename from java/nouveau/api/src/test/java/org/apache/couchdb/nouveau/api/SearchRequestTest.java
rename to java/nouveau/base/src/test/java/org/apache/couchdb/nouveau/api/SearchRequestTest.java
diff --git a/java/nouveau/api/src/test/resources/fixtures/DocumentUpdateRequest.json b/java/nouveau/base/src/test/resources/fixtures/DocumentUpdateRequest.json
similarity index 100%
rename from java/nouveau/api/src/test/resources/fixtures/DocumentUpdateRequest.json
rename to java/nouveau/base/src/test/resources/fixtures/DocumentUpdateRequest.json
diff --git a/java/nouveau/api/src/test/resources/fixtures/SearchRequest.json b/java/nouveau/base/src/test/resources/fixtures/SearchRequest.json
similarity index 100%
rename from java/nouveau/api/src/test/resources/fixtures/SearchRequest.json
rename to java/nouveau/base/src/test/resources/fixtures/SearchRequest.json
diff --git a/java/nouveau/lucene4/pom.xml b/java/nouveau/lucene4/pom.xml
index 9d51fbd97..75cb27ebc 100644
--- a/java/nouveau/lucene4/pom.xml
+++ b/java/nouveau/lucene4/pom.xml
@@ -20,10 +20,10 @@
 
   <dependencies>
 
-    <!-- API -->
+    <!-- Base -->
     <dependency>
       <groupId>org.apache.couchdb.nouveau</groupId>
-      <artifactId>api</artifactId>
+      <artifactId>base</artifactId>
       <version>${project.version}</version>
     </dependency>
 
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Dummy4.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Dummy4.java
deleted file mode 100644
index 2c6c287a2..000000000
--- a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Dummy4.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.apache.couchdb.nouveau.core.lucene4;
-
-import org.apache.lucene.index.IndexableField;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public class Dummy4 {
-
-    private IndexableField field;
-
-    @JsonProperty
-    public IndexableField getField() {
-        return field;
-    }
-
-    public void setField(IndexableField field) {
-        this.field = field;
-    }
-
-    @Override
-    public String toString() {
-        return "Dummy4 [field=" + field + "]";
-    }
-
-}
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/IndexableFieldDeserializer.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/IndexableFieldDeserializer.java
deleted file mode 100644
index c01d9e554..000000000
--- a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/IndexableFieldDeserializer.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.apache.couchdb.nouveau.core.lucene4;
-
-import java.io.IOException;
-
-import org.apache.lucene.document.TextField;
-import org.apache.lucene.document.Field.Store;
-import org.apache.lucene.index.IndexableField;
-
-import com.fasterxml.jackson.core.JacksonException;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
-
-public class IndexableFieldDeserializer extends StdDeserializer<IndexableField> {
-
-    public IndexableFieldDeserializer() {
-        this(null);
-    }
-
-    public IndexableFieldDeserializer(Class<?> vc) {
-        super(vc);
-    }
-
-    @Override
-    public IndexableField deserialize(JsonParser p, DeserializationContext ctxt)
-            throws IOException, JacksonException {
-        return new TextField("foo4", "bar4", Store.NO);
-    }
-
-}
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4.java
deleted file mode 100644
index ca7362a31..000000000
--- a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4.java
+++ /dev/null
@@ -1,99 +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.lucene4;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
-
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.Response.Status;
-
-import org.apache.couchdb.nouveau.api.IndexDefinition;
-import org.apache.couchdb.nouveau.core.Index;
-import org.apache.couchdb.nouveau.core.Lucene;
-import org.apache.lucene.analysis.Analyzer;
-import org.apache.lucene.analysis.TokenStream;
-import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
-import org.apache.lucene.index.IndexWriter;
-import org.apache.lucene.index.IndexWriterConfig;
-import org.apache.lucene.search.SearcherFactory;
-import org.apache.lucene.search.SearcherManager;
-import org.apache.lucene.store.Directory;
-import org.apache.lucene.store.FSDirectory;
-
-final class Lucene4 implements Lucene {
-
-    private SearcherFactory searcherFactory;
-
-    public int getMajor() {
-        return 4;
-    }
-
-    public void setExecutor(ExecutorService executor) {
-        this.searcherFactory = new ParallelSearcherFactory(executor);
-    }
-
-    @Override
-    public List<String> analyze(String analyzer, String text) throws IOException {
-        try {
-            return tokenize(Lucene4AnalyzerFactory.newAnalyzer(analyzer), text);
-        } catch (IllegalArgumentException e) {
-            throw new WebApplicationException(analyzer + " not a valid analyzer",
-                    Status.BAD_REQUEST);
-        }
-    }
-
-    private List<String> tokenize(final Analyzer analyzer, final String text) throws IOException {
-        final List<String> result = new ArrayList<String>(10);
-        try (final TokenStream tokenStream = analyzer.tokenStream("default", text)) {
-            tokenStream.reset();
-            while (tokenStream.incrementToken()) {
-                final CharTermAttribute term = tokenStream.getAttribute(CharTermAttribute.class);
-                result.add(term.toString());
-            }
-            tokenStream.end();
-        }
-        return result;
-    }
-
-    @Override
-    public void validate(IndexDefinition indexDefinition) throws WebApplicationException {
-        Lucene4AnalyzerFactory.fromDefinition(indexDefinition);
-    }
-
-    @Override
-    public Index open(Path path, IndexDefinition indexDefinition) throws IOException {
-        final Analyzer analyzer = Lucene4AnalyzerFactory.fromDefinition(indexDefinition);
-        final Directory dir = FSDirectory.open(path.toFile());
-        final IndexWriterConfig config = new IndexWriterConfig(Utils.LUCENE_VERSION, analyzer);
-        config.setUseCompoundFile(false);
-        final IndexWriter writer = new IndexWriter(dir, config);
-        final long updateSeq = getUpdateSeq(writer);
-        final SearcherManager searcherManager = new SearcherManager(writer, true, searcherFactory);
-        return new Lucene4Index(analyzer, writer, updateSeq, searcherManager);
-    }
-
-    private static long getUpdateSeq(final IndexWriter writer) throws IOException {
-        final Map<String, String> commitData = writer.getCommitData();
-        if (commitData == null) {
-            return 0L;
-        }
-        return Long.parseLong(commitData.getOrDefault("update_seq", "0"));
-    }
-
-}
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4Bundle.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4Bundle.java
deleted file mode 100644
index 2976f7057..000000000
--- a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4Bundle.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.apache.couchdb.nouveau.core.lucene4;
-
-import org.apache.couchdb.nouveau.core.Lucene;
-import org.apache.couchdb.nouveau.core.LuceneBundle;
-
-import io.dropwizard.setup.Environment;
-
-public final class Lucene4Bundle<Configuration> implements LuceneBundle<Configuration> {
-
-    private Lucene4 lucene;
-
-    @Override
-    public void run(final Configuration configuration, final Environment environment) throws Exception {
-        lucene = new Lucene4();
-        lucene.setExecutor(environment.lifecycle().executorService("nouveau-lucene4-%d").build());
-
-        environment.getObjectMapper().registerModule(new Lucene4Module());
-    }
-
-    @Override
-    public Lucene getLucene() {
-        return lucene;
-    }
-
-}
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4Module.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4Module.java
deleted file mode 100644
index 4ef2a411e..000000000
--- a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4Module.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.apache.couchdb.nouveau.core.lucene4;
-
-import org.apache.lucene.index.IndexableField;
-
-import com.fasterxml.jackson.core.Version;
-import com.fasterxml.jackson.databind.module.SimpleModule;
-
-public class Lucene4Module extends SimpleModule {
-
-    public Lucene4Module() {
-        super("lucene4", Version.unknownVersion());
-        addDeserializer(IndexableField.class, new IndexableFieldDeserializer());
-    }
-
-}
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/IndexableFieldDeserializer.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/IndexableFieldDeserializer.java
new file mode 100644
index 000000000..90c5cbaa0
--- /dev/null
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/IndexableFieldDeserializer.java
@@ -0,0 +1,78 @@
+//
+// 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.lucene4.core;
+
+import java.io.IOException;
+
+import org.apache.lucene.document.DoubleField;
+import org.apache.lucene.document.Field.Store;
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.document.StringField;
+import org.apache.lucene.document.TextField;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.util.BytesRef;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+
+class IndexableFieldDeserializer extends StdDeserializer<IndexableField> {
+
+    IndexableFieldDeserializer() {
+        this(null);
+    }
+
+    IndexableFieldDeserializer(Class<?> vc) {
+        super(vc);
+    }
+
+    @Override
+    public IndexableField deserialize(final JsonParser parser, final DeserializationContext context)
+            throws IOException, JsonProcessingException {
+        JsonNode node = parser.getCodec().readTree(parser);
+
+        final String type = node.get("@type").asText();
+        final String name = node.get("name").asText();
+
+        switch (type) {
+            case "double":
+                return new DoubleField(name, node.get("value").doubleValue(),
+                        node.get("stored").asBoolean() ? Store.YES : Store.NO);
+            case "string":
+                return new StringField(name, node.get("value").asText(),
+                        node.get("stored").asBoolean() ? Store.YES : Store.NO);
+            case "text":
+                return new TextField(name, node.get("value").asText(),
+                        node.get("stored").asBoolean() ? Store.YES : Store.NO);
+            case "stored":
+                if (node.get("value").isDouble()) {
+                    return new StoredField(name, node.get("value").asDouble());
+                }
+                if (node.get("value").isTextual()) {
+                    final JsonNode value = node.get("value");
+                    if (node.has("encoded") && node.get("encoded").asBoolean()) {
+                        return new StoredField(name, new BytesRef(value.binaryValue()));
+                    } else {
+                        return new StoredField(name, value.asText());
+                    }
+                }
+                throw new JsonParseException(parser, node.get("value") + " not a valid value for a stored field");
+        }
+        throw new JsonParseException(parser, type + " not a valid type of field");
+    }
+
+}
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/IndexableFieldSerializer.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/IndexableFieldSerializer.java
new file mode 100644
index 000000000..a450a7a19
--- /dev/null
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/IndexableFieldSerializer.java
@@ -0,0 +1,59 @@
+//
+// 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.lucene4.core;
+
+import java.io.IOException;
+
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.util.BytesRef;
+
+import com.fasterxml.jackson.core.JsonGenerationException;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+class IndexableFieldSerializer extends StdSerializer<IndexableField> {
+
+    IndexableFieldSerializer() {
+        this(null);
+    }
+
+    IndexableFieldSerializer(Class<IndexableField> vc) {
+        super(vc);
+    }
+
+    @Override
+    public void serialize(final IndexableField field, final JsonGenerator gen, final SerializerProvider provider)
+            throws IOException {
+        if (!(field instanceof StoredField)) {
+            throw new JsonGenerationException(field.getClass() + " not supported", gen);
+        }
+        gen.writeStartObject();
+        gen.writeStringField("@type", "stored");
+        gen.writeStringField("name", field.name());
+        if (field.numericValue() != null) {
+            gen.writeNumberField("value", (double) field.numericValue());
+        } else if (field.stringValue() != null) {
+            gen.writeStringField("value", field.stringValue());
+        } else if (field.binaryValue() != null) {
+            final BytesRef bytesRef = field.binaryValue();
+            gen.writeFieldName("value");
+            gen.writeBinary(bytesRef.bytes, bytesRef.offset, bytesRef.length);
+            gen.writeBooleanField("encoded", true);
+        }
+        gen.writeEndObject();
+    }
+
+}
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4AnalyzerFactory.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4AnalyzerFactory.java
similarity index 98%
rename from java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4AnalyzerFactory.java
rename to java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4AnalyzerFactory.java
index 9067608c8..89d102c54 100644
--- a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4AnalyzerFactory.java
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4AnalyzerFactory.java
@@ -11,7 +11,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package org.apache.couchdb.nouveau.core.lucene4;
+package org.apache.couchdb.nouveau.lucene4.core;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -61,7 +61,7 @@ import org.apache.lucene.analysis.sv.SwedishAnalyzer;
 import org.apache.lucene.analysis.th.ThaiAnalyzer;
 import org.apache.lucene.analysis.tr.TurkishAnalyzer;
 
-final class Lucene4AnalyzerFactory {
+public final class Lucene4AnalyzerFactory {
 
     public Lucene4AnalyzerFactory() {
     }
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4Bundle.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4Bundle.java
new file mode 100644
index 000000000..2cb7482eb
--- /dev/null
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4Bundle.java
@@ -0,0 +1,45 @@
+//
+// 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.lucene4.core;
+
+import java.util.concurrent.ExecutorService;
+
+import org.apache.couchdb.nouveau.NouveauApplicationConfiguration;
+import org.apache.couchdb.nouveau.core.IndexManager;
+import org.apache.couchdb.nouveau.lucene4.resources.AnalyzeResource;
+import org.apache.couchdb.nouveau.lucene4.resources.IndexResource;
+import org.apache.lucene.search.SearcherFactory;
+
+import io.dropwizard.ConfiguredBundle;
+import io.dropwizard.setup.Environment;
+
+public final class Lucene4Bundle implements ConfiguredBundle<NouveauApplicationConfiguration> {
+
+    @Override
+    public void run(final NouveauApplicationConfiguration configuration, final Environment environment) throws Exception {
+
+        // Serialization classes
+        environment.getObjectMapper().registerModule(new Lucene4Module());
+
+        // AnalyzeResource
+        environment.jersey().register(new AnalyzeResource());
+
+        // IndexResource
+        final IndexManager indexManager = configuration.getIndexManagerFactory().build(environment);
+        final ExecutorService executorService = environment.lifecycle().executorService("nouveau-lucene4-%d").build();
+        final SearcherFactory searcherFactory = new ParallelSearcherFactory(executorService);
+        environment.jersey().register(new IndexResource(indexManager, searcherFactory));
+    }
+
+}
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4Index.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4Index.java
similarity index 79%
rename from java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4Index.java
rename to java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4Index.java
index d6986c6ef..01918fbb8 100644
--- a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4Index.java
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4Index.java
@@ -11,7 +11,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package org.apache.couchdb.nouveau.core.lucene4;
+package org.apache.couchdb.nouveau.lucene4.core;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -34,15 +34,10 @@ import org.apache.couchdb.nouveau.api.DoubleRange;
 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.api.document.DoubleField;
-import org.apache.couchdb.nouveau.api.document.Field;
-import org.apache.couchdb.nouveau.api.document.StoredDoubleField;
-import org.apache.couchdb.nouveau.api.document.StoredStringField;
-import org.apache.couchdb.nouveau.api.document.StringField;
-import org.apache.couchdb.nouveau.api.document.TextField;
 import org.apache.couchdb.nouveau.core.Index;
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.document.Document;
+import org.apache.lucene.document.StringField;
 import org.apache.lucene.document.Field.Store;
 import org.apache.lucene.facet.params.FacetSearchParams;
 import org.apache.lucene.facet.range.RangeAccumulator;
@@ -73,7 +68,7 @@ import org.apache.lucene.search.TopDocs;
 import org.apache.lucene.search.TopFieldCollector;
 import org.apache.lucene.util.BytesRef;
 
-class Lucene4Index extends Index {
+public class Lucene4Index extends Index<IndexableField> {
 
     private static final Sort DEFAULT_SORT = new Sort(SortField.FIELD_SCORE,
             new SortField("_id", SortField.Type.STRING));
@@ -84,7 +79,7 @@ class Lucene4Index extends Index {
     private final SearcherManager searcherManager;
     private volatile boolean isOpen = true;
 
-    Lucene4Index(final Analyzer analyzer, final IndexWriter writer, final long updateSeq,
+    public Lucene4Index(final Analyzer analyzer, final IndexWriter writer, final long updateSeq,
             final SearcherManager searcherManager) {
         super(updateSeq);
         this.analyzer = analyzer;
@@ -98,7 +93,7 @@ class Lucene4Index extends Index {
     }
 
     @Override
-    public void doUpdate(final String docId, final DocumentUpdateRequest request) throws IOException {
+    public void doUpdate(final String docId, final DocumentUpdateRequest<IndexableField> request) throws IOException {
         final Term docIdTerm = docIdTerm(docId);
         final Document doc = toDocument(docId, request);
         writer.updateDocument(docIdTerm, doc);
@@ -134,7 +129,7 @@ class Lucene4Index extends Index {
     }
 
     @Override
-    public SearchResults doSearch(final SearchRequest request) throws IOException {
+    public SearchResults<IndexableField> doSearch(final SearchRequest request) throws IOException {
         final Query query;
         try {
             query = newQueryParser().parse(request);
@@ -230,9 +225,9 @@ class Lucene4Index extends Index {
         return sortFields[sortFields.length - 1];
     }
 
-    private SearchResults toSearchResults(final SearchRequest searchRequest, final IndexSearcher searcher,
+    private SearchResults<IndexableField> toSearchResults(final SearchRequest searchRequest, final IndexSearcher searcher,
         TopFieldCollector hitCollector, FacetsCollector countsCollector, FacetsCollector rangesCollector) throws IOException {
-        final SearchResults result = new SearchResults();
+        final SearchResults<IndexableField> result = new SearchResults<IndexableField>();
         collectHits(searcher, hitCollector.topDocs(), result);
         if (searchRequest.hasCounts()) {
             result.setCounts(convertFacets(countsCollector));
@@ -243,22 +238,22 @@ class Lucene4Index extends Index {
         return result;
     }
 
-    private void collectHits(final IndexSearcher searcher, final TopDocs topDocs, final SearchResults searchResults)
+    private void collectHits(final IndexSearcher searcher, final TopDocs topDocs, final SearchResults<IndexableField> searchResults)
             throws IOException {
-        final List<SearchHit> hits = new ArrayList<SearchHit>(topDocs.scoreDocs.length);
+        final List<SearchHit<IndexableField>> hits = new ArrayList<SearchHit<IndexableField>>(topDocs.scoreDocs.length);
 
         for (final ScoreDoc scoreDoc : topDocs.scoreDocs) {
             final Document doc = searcher.doc(scoreDoc.doc);
 
-            final List<Field> fields = new ArrayList<Field>(doc.getFields().size());
+            final List<IndexableField> fields = new ArrayList<IndexableField>(doc.getFields().size());
             for (IndexableField field : doc.getFields()) {
                 if (!field.name().equals("_id")) {
-                    fields.add(toField(field));
+                    fields.add(field);
                 }
             }
 
             final After after = toAfter(((FieldDoc)scoreDoc));
-            hits.add(new SearchHit(doc.get("_id"), after, fields));
+            hits.add(new SearchHit<IndexableField>(doc.get("_id"), after, fields));
         }
 
         searchResults.setTotalHits(topDocs.totalHits);
@@ -316,63 +311,28 @@ class Lucene4Index extends Index {
         return new SortField(m.group(2), type, reverse);
     }
 
-    private static Document toDocument(final String docId, final DocumentUpdateRequest request) throws IOException {
+    private static Document toDocument(final String docId, final DocumentUpdateRequest<IndexableField> request) throws IOException {
         final Document result = new Document();
 
         // id
-        addIndexableFields(result,
-                new StringField("_id", docId, true, false, true));
+        result.add(new StringField("_id", docId, Store.YES));
 
         // partition (optional)
         if (request.hasPartition()) {
-            addIndexableFields(result,
-                    new StringField("_partition", request.getPartition(), false, false, false));
+            result.add(new StringField("_partition", request.getPartition(), Store.NO));
         }
 
-        for (Field field : request.getFields()) {
+        for (IndexableField field : request.getFields()) {
             // Underscore-prefix is reserved.
-            if (field.getName().startsWith("_")) {
+            if (field.name().startsWith("_")) {
                 continue;
             }
-            addIndexableFields(result, field);
+            result.add(field);
         }
 
         return result;
     }
 
-    private static void addIndexableFields(final Document doc, final Field field) {
-        if (field instanceof DoubleField) {
-            final DoubleField f = (DoubleField) field;
-            doc.add(new org.apache.lucene.document.DoubleField(f.getName(), f.getValue(), f.isStore() ? Store.YES : Store.NO));
-            if (f.isFacet()) {
-                doc.add(new org.apache.lucene.document.DoubleDocValuesField(f.getName(), f.getValue()));
-            }
-        } else if (field instanceof StringField) {
-            final StringField f = (StringField) field;
-            doc.add(new org.apache.lucene.document.StringField(f.getName(), f.getValue(),
-                    f.isStore() ? Store.YES : Store.NO));
-            if (f.isSortable() || f.isFacet()) {
-                doc.add(new org.apache.lucene.document.SortedDocValuesField(f.getName(), new BytesRef(f.getValue())));
-            }
-        } else if (field instanceof TextField) {
-            final TextField f = (TextField) field;
-            doc.add(new org.apache.lucene.document.TextField(f.getName(), f.getValue(), f.isStore() ? Store.YES : Store.NO));
-            if (f.isSortable() || f.isFacet()) {
-                doc.add(new org.apache.lucene.document.SortedDocValuesField(f.getName(), new BytesRef(f.getValue())));
-            }
-        } else {
-            throw new WebApplicationException(field.getClass() + " is not valid", Status.BAD_REQUEST);
-        }
-    }
-
-    private static Field toField(final IndexableField field) {
-        if (field.numericValue() != null) {
-            return new StoredDoubleField(field.name(), (double) field.numericValue());
-        } else {
-            return new StoredStringField(field.name(), field.stringValue());
-        }
-    }
-
     private FieldDoc toFieldDoc(final After after) {
         final Object[] fields = Arrays.copyOf(after.getFields(), after.getFields().length);
         for (int i = 0; i < fields.length; i++) {
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4Module.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4Module.java
new file mode 100644
index 000000000..377f92d11
--- /dev/null
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4Module.java
@@ -0,0 +1,35 @@
+//
+// 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.lucene4.core;
+
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.search.Query;
+
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+public class Lucene4Module extends SimpleModule {
+
+    public Lucene4Module() {
+        super("lucene4", Version.unknownVersion());
+
+        // IndexableField
+        addSerializer(IndexableField.class, new IndexableFieldSerializer());
+        addDeserializer(IndexableField.class, new IndexableFieldDeserializer());
+
+        // Query
+        addDeserializer(Query.class, new QueryDeserializer());
+    }
+
+}
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4QueryParser.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4QueryParser.java
similarity index 99%
rename from java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4QueryParser.java
rename to java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4QueryParser.java
index a9554ee5b..3753973a3 100644
--- a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4QueryParser.java
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4QueryParser.java
@@ -11,7 +11,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package org.apache.couchdb.nouveau.core.lucene4;
+package org.apache.couchdb.nouveau.lucene4.core;
 
 import java.util.regex.Pattern;
 
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/ParallelSearcherFactory.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/ParallelSearcherFactory.java
similarity index 95%
rename from java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/ParallelSearcherFactory.java
rename to java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/ParallelSearcherFactory.java
index a8d98ec47..fff84355f 100644
--- a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/ParallelSearcherFactory.java
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/ParallelSearcherFactory.java
@@ -11,7 +11,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package org.apache.couchdb.nouveau.core.lucene4;
+package org.apache.couchdb.nouveau.lucene4.core;
 
 import java.io.IOException;
 import java.util.concurrent.ExecutorService;
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/PerFieldAnalyzer.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/PerFieldAnalyzer.java
similarity index 96%
rename from java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/PerFieldAnalyzer.java
rename to java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/PerFieldAnalyzer.java
index 5a6d27b13..1952d9e32 100644
--- a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/PerFieldAnalyzer.java
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/PerFieldAnalyzer.java
@@ -11,7 +11,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package org.apache.couchdb.nouveau.core.lucene4;
+package org.apache.couchdb.nouveau.lucene4.core;
 
 import java.util.Map;
 
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/QueryDeserializer.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/QueryDeserializer.java
new file mode 100644
index 000000000..ca222605c
--- /dev/null
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/QueryDeserializer.java
@@ -0,0 +1,41 @@
+//
+// 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.lucene4.core;
+
+import java.io.IOException;
+
+import org.apache.lucene.search.Query;
+
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+
+class QueryDeserializer extends StdDeserializer<Query> {
+
+    QueryDeserializer() {
+        this(null);
+    }
+
+    QueryDeserializer(Class<?> vc) {
+        super(vc);
+    }
+
+    @Override
+    public Query deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
+        // TODO Auto-generated method stub
+        throw new UnsupportedOperationException("Unimplemented method 'deserialize'");
+    }
+
+}
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Utils.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Utils.java
similarity index 87%
rename from java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Utils.java
rename to java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Utils.java
index 0f72962f1..2fdc2cf02 100644
--- a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Utils.java
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/core/Utils.java
@@ -11,16 +11,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package org.apache.couchdb.nouveau.core.lucene4;
+package org.apache.couchdb.nouveau.lucene4.core;
 
 import org.apache.lucene.index.Term;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.NumericUtils;
 import org.apache.lucene.util.Version;
 
-class Utils {
+public class Utils {
 
-    static final Version LUCENE_VERSION = Version.LUCENE_46;
+    public static final Version LUCENE_VERSION = Version.LUCENE_46;
 
     static Term doubleToTerm(String field, Double value) {
         var bytesRef = new BytesRef();
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/resources/AnalyzeResource.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/resources/AnalyzeResource.java
new file mode 100644
index 000000000..eeb6e1426
--- /dev/null
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/resources/AnalyzeResource.java
@@ -0,0 +1,62 @@
+//
+// 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.lucene4.resources;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.Path;
+import javax.ws.rs.WebApplicationException;
+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.lucene4.core.Lucene4AnalyzerFactory;
+import org.apache.couchdb.nouveau.resources.BaseAnalyzeResource;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+
+@Path("/4/analyze")
+public class AnalyzeResource extends BaseAnalyzeResource {
+
+    @Override
+    public AnalyzeResponse analyzeText(@NotNull @Valid AnalyzeRequest request) throws IOException {
+        try {
+            final List<String> tokens = tokenize(Lucene4AnalyzerFactory.newAnalyzer(request.getAnalyzer()),
+                    request.getText());
+            return new AnalyzeResponse(tokens);
+        } catch (IllegalArgumentException e) {
+            throw new WebApplicationException(request.getAnalyzer() + " not a valid analyzer",
+                    Status.BAD_REQUEST);
+        }
+    }
+
+    private List<String> tokenize(final Analyzer analyzer, final String text) throws IOException {
+        final List<String> result = new ArrayList<String>(10);
+        try (final TokenStream tokenStream = analyzer.tokenStream("default", text)) {
+            tokenStream.reset();
+            while (tokenStream.incrementToken()) {
+                final CharTermAttribute term = tokenStream.getAttribute(CharTermAttribute.class);
+                result.add(term.toString());
+            }
+            tokenStream.end();
+        }
+        return result;
+    }
+
+}
diff --git a/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/resources/IndexResource.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/resources/IndexResource.java
new file mode 100644
index 000000000..061b57845
--- /dev/null
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/lucene4/resources/IndexResource.java
@@ -0,0 +1,68 @@
+//
+// 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.lucene4.resources;
+
+import java.io.IOException;
+import java.util.Map;
+
+import javax.ws.rs.Path;
+
+import org.apache.couchdb.nouveau.core.IndexLoader;
+import org.apache.couchdb.nouveau.core.IndexManager;
+import org.apache.couchdb.nouveau.lucene4.core.Lucene4AnalyzerFactory;
+import org.apache.couchdb.nouveau.lucene4.core.Lucene4Index;
+import org.apache.couchdb.nouveau.lucene4.core.Utils;
+import org.apache.couchdb.nouveau.resources.BaseIndexResource;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.search.SearcherFactory;
+import org.apache.lucene.search.SearcherManager;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;;
+
+@Path("/4/index/{name}")
+public class IndexResource extends BaseIndexResource<IndexableField> {
+
+    private final SearcherFactory searcherFactory;
+
+    public IndexResource(final IndexManager indexManager, final SearcherFactory searcherFactory) {
+        super(indexManager);
+        this.searcherFactory = searcherFactory;
+    }
+
+    @Override
+    protected IndexLoader<IndexableField> indexLoader() {
+        return (path, indexDefinition) -> {
+            final Analyzer analyzer = Lucene4AnalyzerFactory.fromDefinition(indexDefinition);
+            final Directory dir = FSDirectory.open(path.toFile());
+            final IndexWriterConfig config = new IndexWriterConfig(Utils.LUCENE_VERSION, analyzer);
+            config.setUseCompoundFile(false);
+            final IndexWriter writer = new IndexWriter(dir, config);
+            final long updateSeq = getUpdateSeq(writer);
+            final SearcherManager searcherManager = new SearcherManager(writer, true, searcherFactory);
+            return new Lucene4Index(analyzer, writer, updateSeq, searcherManager);
+        };
+    }
+
+    private static long getUpdateSeq(final IndexWriter writer) throws IOException {
+        final Map<String, String> commitData = writer.getCommitData();
+        if (commitData == null) {
+            return 0L;
+        }
+        return Long.parseLong(commitData.getOrDefault("update_seq", "0"));
+    }
+
+}
\ No newline at end of file
diff --git a/java/nouveau/lucene4/src/test/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4AnalyzerFactoryTest.java b/java/nouveau/lucene4/src/test/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4AnalyzerFactoryTest.java
similarity index 99%
rename from java/nouveau/lucene4/src/test/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4AnalyzerFactoryTest.java
rename to java/nouveau/lucene4/src/test/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4AnalyzerFactoryTest.java
index 67113d248..3a41189bd 100644
--- a/java/nouveau/lucene4/src/test/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4AnalyzerFactoryTest.java
+++ b/java/nouveau/lucene4/src/test/java/org/apache/couchdb/nouveau/lucene4/core/Lucene4AnalyzerFactoryTest.java
@@ -11,7 +11,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package org.apache.couchdb.nouveau.core.lucene4;
+package org.apache.couchdb.nouveau.lucene4.core;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
diff --git a/java/nouveau/lucene9/pom.xml b/java/nouveau/lucene9/pom.xml
index 2094f3985..2903efdf2 100644
--- a/java/nouveau/lucene9/pom.xml
+++ b/java/nouveau/lucene9/pom.xml
@@ -20,10 +20,10 @@
 
   <dependencies>
 
-    <!-- API -->
+    <!-- Base -->
     <dependency>
       <groupId>org.apache.couchdb.nouveau</groupId>
-      <artifactId>api</artifactId>
+      <artifactId>base</artifactId>
       <version>${project.version}</version>
     </dependency>
 
diff --git a/java/nouveau/pom.xml b/java/nouveau/pom.xml
index f62001d96..5f32aa759 100644
--- a/java/nouveau/pom.xml
+++ b/java/nouveau/pom.xml
@@ -52,7 +52,7 @@
   </dependencyManagement>
 
   <modules>
-    <module>api</module>
+    <module>base</module>
     <module>lucene4</module>
     <module>lucene9</module>
     <module>server</module>
diff --git a/java/nouveau/server/nouveau.yaml b/java/nouveau/server/nouveau.yaml
index 1dcf51e23..7ccc76737 100644
--- a/java/nouveau/server/nouveau.yaml
+++ b/java/nouveau/server/nouveau.yaml
@@ -1,7 +1,8 @@
-maxIndexesOpen: 100
-commitIntervalSeconds: 30
-idleSeconds: 60
-rootDir: target/indexes
+indexManager:
+  maxIndexesOpen: 100
+  commitIntervalSeconds: 30
+  idleSeconds: 60
+  rootDir: target/indexes
 
 server:
   applicationConnectors:
diff --git a/java/nouveau/server/pom.xml b/java/nouveau/server/pom.xml
index 696f8f274..505f4a1da 100644
--- a/java/nouveau/server/pom.xml
+++ b/java/nouveau/server/pom.xml
@@ -28,10 +28,10 @@
 
   <dependencies>
 
-    <!-- API -->
+    <!-- Base -->
     <dependency>
       <groupId>org.apache.couchdb.nouveau</groupId>
-      <artifactId>api</artifactId>
+      <artifactId>base</artifactId>
       <version>${project.version}</version>
     </dependency>
 
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 8357fc4e6..5086bdf4c 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
@@ -16,40 +16,17 @@ package org.apache.couchdb.nouveau;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLClassLoader;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
 import java.util.ServiceLoader;
-import java.util.concurrent.ScheduledExecutorService;
 
-import org.apache.couchdb.nouveau.core.IndexManager;
-import org.apache.couchdb.nouveau.core.Lucene;
-import org.apache.couchdb.nouveau.core.LuceneBundle;
 import org.apache.couchdb.nouveau.core.UpdatesOutOfOrderExceptionMapper;
-import org.apache.couchdb.nouveau.health.AnalyzeHealthCheck;
-import org.apache.couchdb.nouveau.health.IndexManagerHealthCheck;
-import org.apache.couchdb.nouveau.resources.AnalyzeResource;
-import org.apache.couchdb.nouveau.resources.IndexResource;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.codahale.metrics.MetricRegistry;
-import com.codahale.metrics.jersey2.InstrumentedResourceMethodApplicationListener;
-import com.fasterxml.jackson.databind.ObjectMapper;
 
 import io.dropwizard.Application;
-import io.dropwizard.Configuration;
 import io.dropwizard.ConfiguredBundle;
 import io.dropwizard.setup.Bootstrap;
 import io.dropwizard.setup.Environment;
 
 public class NouveauApplication extends Application<NouveauApplicationConfiguration> {
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(NouveauApplication.class);
-
-    private Collection<LuceneBundle> bundles = new HashSet<LuceneBundle>();
-
     public static void main(String[] args) throws Exception {
         new NouveauApplication().run(args);
     }
@@ -65,14 +42,15 @@ public class NouveauApplication extends Application<NouveauApplicationConfigurat
         for (String name : System.getProperties().stringPropertyNames()) {
             if (name.startsWith("nouveau.bundle.")) {
                 try {
-                    ClassLoader classLoader = URLClassLoader.newInstance(new URL[]{new URL(System.getProperty(name))});
-                    final ServiceLoader<ConfiguredBundle> bundleLoader = ServiceLoader.load(ConfiguredBundle.class, classLoader);
-                for (final ConfiguredBundle<Configuration> bundle : bundleLoader) {
-                    if (bundle instanceof LuceneBundle) {
-                        bootstrap.addBundle(bundle);
-                        bundles.add((LuceneBundle<?>)bundle);
+                    ClassLoader classLoader = URLClassLoader
+                            .newInstance(new URL[] { new URL(System.getProperty(name)) });
+                    final ServiceLoader<ConfiguredBundle> bundleLoader = ServiceLoader.load(ConfiguredBundle.class,
+                            classLoader);
+                    for (final ConfiguredBundle<NouveauApplicationConfiguration> bundle : bundleLoader) {
+                        if (bundle instanceof ConfiguredBundle) {
+                            bootstrap.addBundle(bundle);
+                        }
                     }
-                }
                 } catch (final MalformedURLException e) {
                     throw new Error(e);
                 }
@@ -82,47 +60,7 @@ public class NouveauApplication extends Application<NouveauApplicationConfigurat
 
     @Override
     public void run(NouveauApplicationConfiguration configuration, Environment environment) throws Exception {
-        final MetricRegistry metricsRegistry = new MetricRegistry();
-        environment.jersey().register(new InstrumentedResourceMethodApplicationListener(metricsRegistry));
-
-        final ObjectMapper objectMapper = environment.getObjectMapper();
-
-        final Map<Integer, Lucene> lucenes = new HashMap<Integer, Lucene>();
-        for (final LuceneBundle bundle : bundles) {
-            final Lucene lucene = ((LuceneBundle)bundle).getLucene();
-            lucenes.put(lucene.getMajor(), lucene);
-            LOGGER.info("Loaded bundle for Lucene {}", lucene.getMajor());
-        }
-
-        if (lucenes.isEmpty()) {
-            throw new IllegalStateException("No Lucene bundles configured");
-        }
-
-        final ScheduledExecutorService indexManagerScheduler =
-            environment.lifecycle()
-            .scheduledExecutorService("index-manager-scheduler-%d")
-            .threads(10)
-            .build();
-
-        final IndexManager indexManager = new IndexManager();
-        indexManager.setScheduler(indexManagerScheduler);
-        indexManager.setRootDir(configuration.getRootDir());
-        indexManager.setMaxIndexesOpen(configuration.getMaxIndexesOpen());
-        indexManager.setCommitIntervalSeconds(configuration.getCommitIntervalSeconds());
-        indexManager.setIdleSeconds(configuration.getIdleSeconds());
-        indexManager.setObjectMapper(objectMapper);
-        indexManager.setLucenes(lucenes);
-        environment.lifecycle().manage(indexManager);
-
         environment.jersey().register(new UpdatesOutOfOrderExceptionMapper());
-
-        final AnalyzeResource analyzeResource = new AnalyzeResource(lucenes);
-        environment.jersey().register(analyzeResource);
-        environment.jersey().register(new IndexResource(indexManager));
-
-        // health checks
-        environment.healthChecks().register("analyzeResource", new AnalyzeHealthCheck(analyzeResource));
-        environment.healthChecks().register("indexManager", new IndexManagerHealthCheck(indexManager));
     }
 
 }
diff --git a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/health/AnalyzeHealthCheck.java b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/health/AnalyzeHealthCheck.java
deleted file mode 100644
index ebe01d4d4..000000000
--- a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/health/AnalyzeHealthCheck.java
+++ /dev/null
@@ -1,45 +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.health;
-
-import java.util.Arrays;
-import java.util.List;
-
-import org.apache.couchdb.nouveau.api.AnalyzeRequest;
-import org.apache.couchdb.nouveau.api.AnalyzeResponse;
-import org.apache.couchdb.nouveau.resources.AnalyzeResource;
-import com.codahale.metrics.health.HealthCheck;
-
-public class AnalyzeHealthCheck extends HealthCheck {
-
-    private AnalyzeResource analyzeResource;
-
-    public AnalyzeHealthCheck(final AnalyzeResource analyzeResource) {
-        this.analyzeResource = analyzeResource;
-    }
-
-    @Override
-    protected Result check() throws Exception {
-        final AnalyzeRequest request = new AnalyzeRequest(9, "standard", "hello there");
-        final AnalyzeResponse response = analyzeResource.analyzeText(request);
-        final List<String> expected = Arrays.asList("hello", "there");
-        final List<String> actual = response.getTokens();
-        if (expected.equals(actual)) {
-            return Result.healthy();
-        } else {
-            return Result.unhealthy("Expected '{}' but got '{}'", expected, actual);
-        }
-    }
-
-}
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
deleted file mode 100644
index cbf73d505..000000000
--- a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/health/IndexManagerHealthCheck.java
+++ /dev/null
@@ -1,52 +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.health;
-
-import java.io.IOException;
-import java.util.Collections;
-
-import org.apache.couchdb.nouveau.api.DocumentUpdateRequest;
-import org.apache.couchdb.nouveau.api.IndexDefinition;
-import org.apache.couchdb.nouveau.core.Index;
-import org.apache.couchdb.nouveau.core.IndexManager;
-
-import com.codahale.metrics.health.HealthCheck;
-
-public class IndexManagerHealthCheck extends HealthCheck {
-
-    private IndexManager indexManager;
-
-    public IndexManagerHealthCheck(final IndexManager indexManager) {
-        this.indexManager = indexManager;
-    }
-
-    @Override
-    protected Result check() throws Exception {
-        final String name = "_____test";
-        try {
-            indexManager.deleteAll(name);
-        } catch (IOException e) {
-            // Ignored, index might not exist yet.
-        }
-
-        indexManager.create(name, new IndexDefinition(9, "standard", null));
-        final Index index = indexManager.acquire(name);
-        final DocumentUpdateRequest request = new DocumentUpdateRequest(1, null, Collections.emptyList());
-        index.update("foo", request);
-        index.commit();
-        index.setDeleteOnClose(true);
-        return Result.healthy();
-    }
-
-}
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
deleted file mode 100644
index 13f9048e3..000000000
--- a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/IntegrationTest.java
+++ /dev/null
@@ -1,137 +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;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.nio.file.Path;
-import java.util.List;
-import java.util.Map;
-
-import javax.ws.rs.client.Entity;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-
-import org.apache.couchdb.nouveau.api.After;
-import org.apache.couchdb.nouveau.api.DocumentUpdateRequest;
-import org.apache.couchdb.nouveau.api.DoubleRange;
-import org.apache.couchdb.nouveau.api.IndexDefinition;
-import org.apache.couchdb.nouveau.api.SearchRequest;
-import org.apache.couchdb.nouveau.api.SearchResults;
-import org.apache.couchdb.nouveau.api.document.DoubleField;
-import org.apache.couchdb.nouveau.api.document.StringField;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-
-import io.dropwizard.testing.junit5.DropwizardAppExtension;
-import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
-
-@ExtendWith(DropwizardExtensionsSupport.class)
-public class IntegrationTest {
-
-    static NouveauApplicationConfiguration CONFIG;
-    static DropwizardAppExtension<NouveauApplicationConfiguration> APP;
-
-    static {
-        CONFIG = new NouveauApplicationConfiguration();
-        CONFIG.setCommitIntervalSeconds(30);
-        CONFIG.setMaxIndexesOpen(10);
-        CONFIG.setIdleSeconds(60);
-        CONFIG.setRootDir(Path.of("target/indexes"));
-
-        // yuck
-        final String path =
-            String.format("file://%s/.m2/repository/org/apache/couchdb/nouveau/lucene9/1.0-SNAPSHOT/lucene9-1.0-SNAPSHOT-dist.jar",
-            System.getProperty("user.home"));
-
-        System.setProperty("nouveau.bundle.9", path);
-
-        APP = new DropwizardAppExtension<>(NouveauApplication.class, CONFIG);
-    }
-
-    @Test
-    public void indexTest() throws Exception{
-        final String url = "http://localhost:" + APP.getLocalPort();
-        final String indexName = "foo";
-        final IndexDefinition indexDefinition = new IndexDefinition(9, "standard", null);
-
-        // Clean up.
-        Response response =
-                APP.client().target(String.format("%s/index/%s", url, indexName))
-                .request()
-                .delete();
-
-        // Create index.
-        response =
-                APP.client().target(String.format("%s/index/%s", url, indexName))
-                .request()
-                .put(Entity.entity(indexDefinition, MediaType.APPLICATION_JSON_TYPE));
-
-        assertThat(response).extracting(Response::getStatus)
-        .isEqualTo(Response.Status.NO_CONTENT.getStatusCode());
-
-        // Populate index
-        for (int i = 0; i < 10; i++) {
-            final DocumentUpdateRequest docUpdate = new DocumentUpdateRequest(i + 1, null,
-                List.of(
-                    new DoubleField("foo", i, false, false, false),
-                    new DoubleField("baz", i, false, true, false),
-                    new StringField("bar", "baz", false, true, false)));
-            response =
-                APP.client().target(String.format("%s/index/%s/doc/doc%d", url, indexName, i))
-                .request()
-                .put(Entity.entity(docUpdate, MediaType.APPLICATION_JSON_TYPE));
-            assertThat(response).extracting(Response::getStatus)
-            .isEqualTo(Response.Status.NO_CONTENT.getStatusCode());
-        }
-
-        // Search index
-        final SearchRequest searchRequest = new SearchRequest();
-        searchRequest.setQuery("*:*");
-        searchRequest.setLimit(10);
-        searchRequest.setCounts(List.of("bar"));
-        searchRequest.setRanges(Map.of("baz", List.of(new DoubleRange("0 to 100 inc", 0.0, true, 100.0, true))));
-        searchRequest.setTopN(2);
-        searchRequest.setAfter(new After(1.0f, new byte[]{'a'}));
-
-        response =
-                APP.client().target(String.format("%s/index/%s/search", url, indexName))
-                .request()
-                .post(Entity.entity(searchRequest, MediaType.APPLICATION_JSON_TYPE));
-
-        assertThat(response).extracting(Response::getStatus).isEqualTo(Response.Status.OK.getStatusCode());
-        final SearchResults results = response.readEntity(SearchResults.class);
-        assertThat(results.getTotalHits()).isEqualTo(10);
-        assertThat(results.getTotalHitsRelation()).isEqualTo("EQUAL_TO");
-        assertThat(results.getCounts().size()).isEqualTo(1);
-        assertThat(results.getCounts().get("bar").get("baz")).isEqualTo(10);
-        assertThat(results.getRanges().get("baz").get("0 to 100 inc")).isEqualTo(10);
-    }
-
-    @Test
-    public void healthCheckShouldSucceed() throws IOException {
-        final Response healthCheckResponse =
-                APP.client().target("http://localhost:" + APP.getAdminPort() + "/healthcheck")
-                .request()
-                .get();
-
-        assertThat(healthCheckResponse)
-                .extracting(Response::getStatus)
-                .isEqualTo(Response.Status.OK.getStatusCode());
-    }
-
-}
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
deleted file mode 100644
index 1c247debc..000000000
--- a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/core/IndexManagerTest.java
+++ /dev/null
@@ -1,76 +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.lang.reflect.Method;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.nio.file.Path;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-
-import org.apache.couchdb.nouveau.api.IndexDefinition;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.api.io.TempDir;
-
-import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
-
-import static org.mockito.Mockito.*;
-
-@ExtendWith(DropwizardExtensionsSupport.class)
-public class IndexManagerTest {
-
-    private static final int LUCENE_MAJOR = 9;
-
-    @TempDir
-    static Path tempDir;
-
-    private IndexManager manager;
-
-    private ScheduledExecutorService scheduler;
-
-    @BeforeEach
-    public void setup() throws Exception {
-        scheduler = Executors.newScheduledThreadPool(1);
-        manager = new IndexManager();
-        manager.setScheduler(scheduler);
-        manager.setLucenes(Map.of(LUCENE_MAJOR, mock(Lucene.class)));
-        manager.setCommitIntervalSeconds(5);
-        manager.setObjectMapper(new ObjectMapper());
-        manager.setRootDir(tempDir);
-        manager.start();
-    }
-
-    @AfterEach
-    public void cleanup() throws Exception {
-        manager.stop();
-        scheduler.shutdown();
-    }
-
-    @Test
-    public void testCreate() throws Exception {
-        final IndexDefinition def = new IndexDefinition(LUCENE_MAJOR, "standard", null);
-        manager.create("foo", def);
-    }
-
-}
diff --git a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/core/ser/SerializationTest.java b/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/core/ser/SerializationTest.java
deleted file mode 100644
index c45cdb758..000000000
--- a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/core/ser/SerializationTest.java
+++ /dev/null
@@ -1,148 +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.ser;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-
-import org.apache.couchdb.nouveau.api.After;
-import org.apache.couchdb.nouveau.api.DoubleRange;
-import org.apache.couchdb.nouveau.api.document.DoubleField;
-import org.apache.couchdb.nouveau.api.document.StoredDoubleField;
-import org.apache.couchdb.nouveau.api.document.StoredStringField;
-import org.apache.couchdb.nouveau.api.document.StringField;
-import org.apache.couchdb.nouveau.api.document.TextField;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-public class SerializationTest {
-
-    private static ObjectMapper mapper;
-
-    @BeforeAll
-    public static void setupMapper() {
-        mapper = new ObjectMapper();
-    }
-
-    @Test
-    public void testSerializeStringFieldStoreYES() throws Exception {
-        final String expected = "{\"@type\":\"string\",\"name\":\"foo\",\"value\":\"bar\",\"store\":true}";
-        final String actual = mapper.writeValueAsString(new StringField("foo", "bar", true, false, false));
-        assertEquals(expected, actual);
-    }
-
-    @Test
-    public void testSerializeStringFieldStoreNO() throws Exception {
-        final String expected = "{\"@type\":\"string\",\"name\":\"foo\",\"value\":\"bar\"}";
-        final String actual = mapper.writeValueAsString(new StringField("foo", "bar", false, false, false));
-        assertEquals(expected, actual);
-    }
-
-    @Test
-    public void testSerializeTextFieldStoreYES() throws Exception {
-        final String expected = "{\"@type\":\"text\",\"name\":\"foo\",\"value\":\"bar\",\"store\":true}";
-        final String actual = mapper.writeValueAsString(new TextField("foo", "bar", true, false, false));
-        assertEquals(expected, actual);
-    }
-
-    @Test
-    public void testSerializeTextFieldStoreNO() throws Exception {
-        final String expected = "{\"@type\":\"text\",\"name\":\"foo\",\"value\":\"bar\"}";
-        final String actual = mapper.writeValueAsString(new TextField("foo", "bar", false, false, false));
-        assertEquals(expected, actual);
-    }
-
-    @Test
-    public void testSerializeDoubleField() throws Exception {
-        final String expected = "{\"@type\":\"double\",\"name\":\"foo\",\"value\":12.5}";
-        final String actual = mapper.writeValueAsString(new DoubleField("foo", 12.5, false, false, false));
-        assertEquals(expected, actual);
-    }
-
-    @Test
-    public void testDeserializeDoubleField1D() throws Exception {
-        final String json = "{\"@type\":\"double\",\"name\":\"foo\",\"value\":12.5}";
-        final DoubleField field = mapper.readValue(json, DoubleField.class);
-        assertEquals("foo", field.getName());
-        assertEquals(12.5, field.getValue());
-    }
-
-    @Test
-    public void testSerializeStoredFieldString() throws Exception {
-        final String expected = "{\"@type\":\"stored_string\",\"name\":\"foo\",\"value\":\"bar\"}";
-        final String actual = mapper.writeValueAsString(new StoredStringField("foo", "bar"));
-        assertEquals(expected, actual);
-    }
-
-    @Test
-    public void testSerializeStoredFieldDouble() throws Exception {
-        final String expected = "{\"@type\":\"stored_double\",\"name\":\"foo\",\"value\":12.5}";
-        final String actual = mapper.writeValueAsString(new StoredDoubleField("foo", 12.5));
-        assertEquals(expected, actual);
-    }
-
-    @Test
-    public void testSerializeAfter() throws Exception {
-        final After after = new After(
-                Float.valueOf(1),
-                Double.valueOf(2),
-                Integer.valueOf(3),
-                Long.valueOf(4),
-                "foo",
-                new byte[]{'b', 'a', 'r'});
-
-        final String expected = "[{\"@type\":\"float\",\"value\":1.0},{\"@type\":\"double\",\"value\":2.0},{\"@type\":\"int\",\"value\":3},{\"@type\":\"long\",\"value\":4},{\"@type\":\"string\",\"value\":\"foo\"},{\"@type\":\"bytes\",\"value\":\"YmFy\"}]";
-        final String actual = mapper.writeValueAsString(after);
-        assertEquals(expected, actual);
-
-        final After after2 = mapper.readValue(expected, After.class);
-
-        for (int i = 0; i < after.getFields().length; i++) {
-            assertThat(after.getFields()[i].getClass()).isEqualTo(after2.getFields()[i].getClass());
-        }
-    }
-
-    @Test
-    public void testSerializeDoubleRange() throws Exception {
-        final String expected = "{\"label\":\"foo\",\"min\":12.5,\"max\":52.1,\"min_inclusive\":false,\"max_inclusive\":false}";
-        final String actual = mapper.writeValueAsString(new DoubleRange("foo", 12.5, false, 52.1, false));
-        assertEquals(expected, actual);
-    }
-
-    @Test
-    public void testDeserializeDoubleRange() throws Exception {
-        final String expected = "{\"label\":\"foo\",\"min\":12.5,\"max\":52.1,\"min_inclusive\":false,\"max_inclusive\":false}";
-        final DoubleRange actual = mapper.readValue(expected, DoubleRange.class);
-        assertEquals("foo", actual.getLabel());
-        assertEquals(12.5, actual.getMin());
-        assertEquals(false, actual.isMinInclusive());
-        assertEquals(52.1, actual.getMax());
-        assertEquals(false, actual.isMaxInclusive());
-    }
-
-    @Test
-    public void testDeserializeDoubleRangeDefaults() throws Exception {
-        final String expected = "{\"label\":\"foo\",\"min\":12.5,\"max\":52.1}";
-        final DoubleRange actual = mapper.readValue(expected, DoubleRange.class);
-        assertEquals("foo", actual.getLabel());
-        assertEquals(12.5, actual.getMin());
-        assertEquals(true, actual.isMinInclusive());
-        assertEquals(52.1, actual.getMax());
-        assertEquals(true, actual.isMaxInclusive());
-    }
-
-}
diff --git a/share/server/nouveau.js b/share/server/nouveau.js
index c7f706324..101ab4632 100644
--- a/share/server/nouveau.js
+++ b/share/server/nouveau.js
@@ -60,10 +60,11 @@ var Nouveau = (function() {
           '@type': typeof value == 'string' ? 'string' : 'double',
           'name': name,
           'value': value,
-          'store': options.store|| false,
+          'stored': options.store|| false,
           'facet': options.facet|| false
         });
-      } else { // nouveau api
+      } else {
+        // Nouveau API.
         var type = arguments[0];
         var name = arguments[1];
 
@@ -85,9 +86,7 @@ var Nouveau = (function() {
             '@type': type,
             'name': name,
             'value': value,
-            'store': options.store|| false,
-            'facet': options.facet|| false,
-            'sortable': options.sortable|| true
+            'stored': options.store
           });
           break;
         default: