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:21:23 UTC

[couchdb] branch import-nouveau-great-shuffle-wip updated (aaa4ed618 -> 7a5a8a2b5)

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

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


 discard aaa4ed618 WIP the great shuffle
 discard c2f37cf72 better bundle bootstrapping
 discard 7e4771707 fix mango/nouveau that I broke in 'redo field abstraction'
 discard 2da5e63f6 WIP. we can deserialize IndexableField directly for both lucene versions with same objectmapper
 discard bb32c5425 enable-preview not needed
 discard d59db2006 redo field abstraction
     new 65cf09cd9 enable-preview not needed
     new ddff1ecbf redo field abstraction
     new e719b15db WIP. we can deserialize IndexableField directly for both lucene versions with same objectmapper
     new 405e54aff fix mango/nouveau that I broke in 'redo field abstraction'
     new 6ef0e8e10 better bundle bootstrapping
     new 7a5a8a2b5 WIP the great shuffle

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (aaa4ed618)
            \
             N -- N -- N   refs/heads/import-nouveau-great-shuffle-wip (7a5a8a2b5)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

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


Summary of changes:


[couchdb] 05/06: better bundle bootstrapping

Posted by rn...@apache.org.
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 6ef0e8e1057926af9be2ef490d87282002639212
Author: Robert Newson <rn...@apache.org>
AuthorDate: Tue Feb 28 18:24:07 2023 +0000

    better bundle bootstrapping
---
 java/nouveau/server/nouveau.yaml                   |  4 --
 java/nouveau/server/pom.xml                        |  2 +
 .../apache/couchdb/nouveau/NouveauApplication.java | 65 +++++++++-------------
 .../nouveau/NouveauApplicationConfiguration.java   | 14 +----
 .../apache/couchdb/nouveau/IntegrationTest.java    |  6 +-
 5 files changed, 30 insertions(+), 61 deletions(-)

diff --git a/java/nouveau/server/nouveau.yaml b/java/nouveau/server/nouveau.yaml
index 73a211e9f..1dcf51e23 100644
--- a/java/nouveau/server/nouveau.yaml
+++ b/java/nouveau/server/nouveau.yaml
@@ -3,10 +3,6 @@ commitIntervalSeconds: 30
 idleSeconds: 60
 rootDir: target/indexes
 
-luceneBundlePaths:
-  - file://${HOME}/.m2/repository/org/apache/couchdb/nouveau/lucene4/1.0-SNAPSHOT/lucene4-1.0-SNAPSHOT-dist.jar
-  - file://${HOME}/.m2/repository/org/apache/couchdb/nouveau/lucene9/1.0-SNAPSHOT/lucene9-1.0-SNAPSHOT-dist.jar
-
 server:
   applicationConnectors:
     - type: h2c
diff --git a/java/nouveau/server/pom.xml b/java/nouveau/server/pom.xml
index 1e6ed315d..696f8f274 100644
--- a/java/nouveau/server/pom.xml
+++ b/java/nouveau/server/pom.xml
@@ -135,6 +135,8 @@
           <executable>java</executable>
           <arguments>
             <arguments>-server</arguments>
+            <argument>-Dnouveau.bundle.4=file://${user.home}/.m2/repository/org/apache/couchdb/nouveau/lucene4/${project.version}/lucene4-${project.version}-dist.jar</argument>
+            <argument>-Dnouveau.bundle.9=file://${user.home}/.m2/repository/org/apache/couchdb/nouveau/lucene9/${project.version}/lucene9-${project.version}-dist.jar</argument>
             <argument>-classpath</argument>
             <classpath/>
             <argument>org.apache.couchdb.nouveau.NouveauApplication</argument>
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 72f5469ca..8357fc4e6 100644
--- a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java
+++ b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java
@@ -13,9 +13,12 @@
 
 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;
@@ -38,8 +41,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import io.dropwizard.Application;
 import io.dropwizard.Configuration;
 import io.dropwizard.ConfiguredBundle;
-import io.dropwizard.configuration.EnvironmentVariableSubstitutor;
-import io.dropwizard.configuration.SubstitutingSourceProvider;
 import io.dropwizard.setup.Bootstrap;
 import io.dropwizard.setup.Environment;
 
@@ -47,6 +48,8 @@ public class NouveauApplication extends Application<NouveauApplicationConfigurat
 
     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);
     }
@@ -58,12 +61,23 @@ public class NouveauApplication extends Application<NouveauApplicationConfigurat
 
     @Override
     public void initialize(Bootstrap<NouveauApplicationConfiguration> bootstrap) {
-        // Enable variable substitution with environment variables
-        bootstrap.setConfigurationSourceProvider(
-                new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(),
-                                                   new EnvironmentVariableSubstitutor(false)
-                )
-        );
+        // Find Lucene bundles
+        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);
+                    }
+                }
+                } catch (final MalformedURLException e) {
+                    throw new Error(e);
+                }
+            }
+        }
     }
 
     @Override
@@ -73,29 +87,11 @@ public class NouveauApplication extends Application<NouveauApplicationConfigurat
 
         final ObjectMapper objectMapper = environment.getObjectMapper();
 
-        Class<?> dummy4Class = null;
-        Class<?> dummy9Class = null;
-
-        // The clever bit.
         final Map<Integer, Lucene> lucenes = new HashMap<Integer, Lucene>();
-        for (final URL luceneBundlePath : configuration.getLuceneBundlePaths()) {
-            final ClassLoader classLoader = URLClassLoader.newInstance(new URL[]{luceneBundlePath});
-            final ServiceLoader<ConfiguredBundle> bundleLoader = ServiceLoader.load(ConfiguredBundle.class, classLoader);
-            for (final ConfiguredBundle<Configuration> bundle : bundleLoader) {
-                if (bundle instanceof LuceneBundle) {
-                    bundle.run(configuration, environment);
-                    final Lucene lucene = ((LuceneBundle)bundle).getLucene();
-                    lucenes.put(lucene.getMajor(), lucene);
-                    LOGGER.info("Loaded bundle for Lucene {} from {}", lucene.getMajor(), luceneBundlePath);
-
-                    if (lucene.getMajor() == 4) {
-                        dummy4Class = classLoader.loadClass("org.apache.couchdb.nouveau.core.lucene4.Dummy4");
-                    }
-                    if (lucene.getMajor() == 9) {
-                        dummy9Class = classLoader.loadClass("org.apache.couchdb.nouveau.core.lucene9.Dummy9");
-                    }
-                }
-            }
+        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()) {
@@ -127,15 +123,6 @@ public class NouveauApplication extends Application<NouveauApplicationConfigurat
         // health checks
         environment.healthChecks().register("analyzeResource", new AnalyzeHealthCheck(analyzeResource));
         environment.healthChecks().register("indexManager", new IndexManagerHealthCheck(indexManager));
-
-        LOGGER.info("object mapper:" + objectMapper.getRegisteredModuleIds());
-
-        if (dummy4Class != null) {
-            LOGGER.info("4: {}", objectMapper.readValue("{\"field\":12}", dummy4Class));
-        }
-        if (dummy9Class != null) {
-            LOGGER.info("9: {}", objectMapper.readValue("{\"field\":12}", dummy9Class));
-        }
     }
 
 }
diff --git a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
index 4395506a3..5be8c5a21 100644
--- a/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
+++ b/java/nouveau/server/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
@@ -39,9 +39,6 @@ public class NouveauApplicationConfiguration extends Configuration {
     @NotNull
     private Path rootDir = null;
 
-    @NotEmpty
-    private URL[] luceneBundlePaths;
-
     @JsonProperty
     public void setMaxIndexesOpen(int maxIndexesOpen) {
         this.maxIndexesOpen = maxIndexesOpen;
@@ -78,20 +75,11 @@ public class NouveauApplicationConfiguration extends Configuration {
         return rootDir;
     }
 
-    @JsonProperty
-    public void setLuceneBundlePaths(final URL... luceneBundlePaths) {
-        this.luceneBundlePaths = luceneBundlePaths;
-    }
-
-    public URL[] getLuceneBundlePaths() {
-        return luceneBundlePaths;
-    }
-
     @Override
     public String toString() {
         return "NouveauApplicationConfiguration [maxIndexesOpen=" + maxIndexesOpen + ", commitIntervalSeconds="
                 + commitIntervalSeconds + ", idleSeconds=" + idleSeconds + ", rootDir=" + rootDir
-                + ", luceneBundlePaths=" + Arrays.toString(luceneBundlePaths) + "]";
+                + "]";
     }
 
 }
diff --git a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/IntegrationTest.java b/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/IntegrationTest.java
index fd82d240b..13f9048e3 100644
--- a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/IntegrationTest.java
+++ b/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/IntegrationTest.java
@@ -58,11 +58,7 @@ public class IntegrationTest {
             String.format("file://%s/.m2/repository/org/apache/couchdb/nouveau/lucene9/1.0-SNAPSHOT/lucene9-1.0-SNAPSHOT-dist.jar",
             System.getProperty("user.home"));
 
-        try {
-            CONFIG.setLuceneBundlePaths(new URL(path));
-        } catch (MalformedURLException e) {
-            throw new Error(e);
-        }
+        System.setProperty("nouveau.bundle.9", path);
 
         APP = new DropwizardAppExtension<>(NouveauApplication.class, CONFIG);
     }


[couchdb] 03/06: WIP. we can deserialize IndexableField directly for both lucene versions with same objectmapper

Posted by rn...@apache.org.
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 e719b15db635f115f3ce4d6074a30931b62adc07
Author: Robert Newson <rn...@apache.org>
AuthorDate: Tue Feb 28 17:23:52 2023 +0000

    WIP. we can deserialize IndexableField directly for both lucene versions with same objectmapper
---
 java/nouveau/lucene4/pom.xml                       |  5 ++++
 .../couchdb/nouveau/core/lucene4/Dummy4.java       | 25 ++++++++++++++++++
 .../core/lucene4/IndexableFieldDeserializer.java   | 30 ++++++++++++++++++++++
 .../nouveau/core/lucene4/Lucene4Bundle.java        |  2 ++
 .../nouveau/core/lucene4/Lucene4Module.java        | 15 +++++++++++
 java/nouveau/lucene9/pom.xml                       |  5 ++++
 .../couchdb/nouveau/core/lucene9/Dummy9.java       | 25 ++++++++++++++++++
 .../core/lucene9/IndexableFieldDeserializer.java   | 30 ++++++++++++++++++++++
 .../nouveau/core/lucene9/Lucene9Bundle.java        |  2 ++
 .../nouveau/core/lucene9/Lucene9Module.java        | 16 ++++++++++++
 .../apache/couchdb/nouveau/NouveauApplication.java | 19 ++++++++++++++
 11 files changed, 174 insertions(+)

diff --git a/java/nouveau/lucene4/pom.xml b/java/nouveau/lucene4/pom.xml
index 0a3e0b4c5..9d51fbd97 100644
--- a/java/nouveau/lucene4/pom.xml
+++ b/java/nouveau/lucene4/pom.xml
@@ -33,6 +33,11 @@
       <artifactId>dropwizard-core</artifactId>
     </dependency>
 
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+    </dependency>
+
     <!-- Lucene -->
     <dependency>
       <groupId>org.apache.lucene</groupId>
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
new file mode 100644
index 000000000..2c6c287a2
--- /dev/null
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Dummy4.java
@@ -0,0 +1,25 @@
+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
new file mode 100644
index 000000000..c01d9e554
--- /dev/null
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/IndexableFieldDeserializer.java
@@ -0,0 +1,30 @@
+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/Lucene4Bundle.java b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4Bundle.java
index 78c77394a..2976f7057 100644
--- 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
@@ -13,6 +13,8 @@ public final class Lucene4Bundle<Configuration> implements LuceneBundle<Configur
     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
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
new file mode 100644
index 000000000..4ef2a411e
--- /dev/null
+++ b/java/nouveau/lucene4/src/main/java/org/apache/couchdb/nouveau/core/lucene4/Lucene4Module.java
@@ -0,0 +1,15 @@
+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/lucene9/pom.xml b/java/nouveau/lucene9/pom.xml
index dca2a32d2..2094f3985 100644
--- a/java/nouveau/lucene9/pom.xml
+++ b/java/nouveau/lucene9/pom.xml
@@ -33,6 +33,11 @@
       <artifactId>dropwizard-core</artifactId>
     </dependency>
 
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+    </dependency>
+
     <!-- Lucene -->
     <dependency>
       <groupId>org.apache.lucene</groupId>
diff --git a/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/Dummy9.java b/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/Dummy9.java
new file mode 100644
index 000000000..6891c5b63
--- /dev/null
+++ b/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/Dummy9.java
@@ -0,0 +1,25 @@
+package org.apache.couchdb.nouveau.core.lucene9;
+
+import org.apache.lucene.index.IndexableField;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class Dummy9 {
+
+    private IndexableField field;
+
+    @JsonProperty
+    public IndexableField getField() {
+        return field;
+    }
+
+    public void setField(IndexableField field) {
+        this.field = field;
+    }
+
+    @Override
+    public String toString() {
+        return "Dummy9 [field=" + field + "]";
+    }
+
+}
diff --git a/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/IndexableFieldDeserializer.java b/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/IndexableFieldDeserializer.java
new file mode 100644
index 000000000..baa67db7a
--- /dev/null
+++ b/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/IndexableFieldDeserializer.java
@@ -0,0 +1,30 @@
+package org.apache.couchdb.nouveau.core.lucene9;
+
+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("foo9", "bar9", Store.NO);
+    }
+
+}
diff --git a/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/Lucene9Bundle.java b/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/Lucene9Bundle.java
index 110e5b94d..6f5a04956 100644
--- a/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/Lucene9Bundle.java
+++ b/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/Lucene9Bundle.java
@@ -13,6 +13,8 @@ public final class Lucene9Bundle<Configuration> implements LuceneBundle<Configur
     public void run(final Configuration configuration, final Environment environment) throws Exception {
         lucene = new Lucene9();
         lucene.setExecutor(environment.lifecycle().executorService("nouveau-lucene9-%d").build());
+
+        environment.getObjectMapper().registerModule(new Lucene9Module());
     }
 
     @Override
diff --git a/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/Lucene9Module.java b/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/Lucene9Module.java
new file mode 100644
index 000000000..937710535
--- /dev/null
+++ b/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/Lucene9Module.java
@@ -0,0 +1,16 @@
+package org.apache.couchdb.nouveau.core.lucene9;
+
+import org.apache.lucene.index.IndexableField;
+
+import com.fasterxml.jackson.core.Version;
+
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+public class Lucene9Module extends SimpleModule {
+
+    public Lucene9Module() {
+        super("lucene9", Version.unknownVersion());
+        addDeserializer(IndexableField.class, new IndexableFieldDeserializer());
+    }
+
+}
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 e2c818151..72f5469ca 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
@@ -73,6 +73,9 @@ public class NouveauApplication extends Application<NouveauApplicationConfigurat
 
         final ObjectMapper objectMapper = environment.getObjectMapper();
 
+        Class<?> dummy4Class = null;
+        Class<?> dummy9Class = null;
+
         // The clever bit.
         final Map<Integer, Lucene> lucenes = new HashMap<Integer, Lucene>();
         for (final URL luceneBundlePath : configuration.getLuceneBundlePaths()) {
@@ -84,6 +87,13 @@ public class NouveauApplication extends Application<NouveauApplicationConfigurat
                     final Lucene lucene = ((LuceneBundle)bundle).getLucene();
                     lucenes.put(lucene.getMajor(), lucene);
                     LOGGER.info("Loaded bundle for Lucene {} from {}", lucene.getMajor(), luceneBundlePath);
+
+                    if (lucene.getMajor() == 4) {
+                        dummy4Class = classLoader.loadClass("org.apache.couchdb.nouveau.core.lucene4.Dummy4");
+                    }
+                    if (lucene.getMajor() == 9) {
+                        dummy9Class = classLoader.loadClass("org.apache.couchdb.nouveau.core.lucene9.Dummy9");
+                    }
                 }
             }
         }
@@ -117,6 +127,15 @@ public class NouveauApplication extends Application<NouveauApplicationConfigurat
         // health checks
         environment.healthChecks().register("analyzeResource", new AnalyzeHealthCheck(analyzeResource));
         environment.healthChecks().register("indexManager", new IndexManagerHealthCheck(indexManager));
+
+        LOGGER.info("object mapper:" + objectMapper.getRegisteredModuleIds());
+
+        if (dummy4Class != null) {
+            LOGGER.info("4: {}", objectMapper.readValue("{\"field\":12}", dummy4Class));
+        }
+        if (dummy9Class != null) {
+            LOGGER.info("9: {}", objectMapper.readValue("{\"field\":12}", dummy9Class));
+        }
     }
 
 }


[couchdb] 04/06: fix mango/nouveau that I broke in 'redo field abstraction'

Posted by rn...@apache.org.
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 405e54affa7283a8679f0bda4ff3ff587ef3380c
Author: Robert Newson <rn...@apache.org>
AuthorDate: Tue Feb 28 18:23:35 2023 +0000

    fix mango/nouveau that I broke in 'redo field abstraction'
---
 share/server/nouveau.js             | 4 ++--
 src/mango/src/mango_native_proc.erl | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/share/server/nouveau.js b/share/server/nouveau.js
index 9f9834164..c7f706324 100644
--- a/share/server/nouveau.js
+++ b/share/server/nouveau.js
@@ -79,13 +79,13 @@ var Nouveau = (function() {
         case 'string':
         case 'text':
           var value = arguments[2];
-          var options = arguments[3];
+          var options = arguments[3] || {};
           assertType('value', type == 'double' ? 'number' : 'string', value);
           index_results.push({
             '@type': type,
             'name': name,
             'value': value,
-            'store': options.store|| false
+            'store': options.store|| false,
             'facet': options.facet|| false,
             'sortable': options.sortable|| true
           });
diff --git a/src/mango/src/mango_native_proc.erl b/src/mango/src/mango_native_proc.erl
index de7431c6e..8e04ab4b8 100644
--- a/src/mango/src/mango_native_proc.erl
+++ b/src/mango/src/mango_native_proc.erl
@@ -367,7 +367,7 @@ convert_nouveau_fields([{Name, <<"string">>, Value} | Rest]) ->
 convert_nouveau_fields([{Name, <<"number">>, Value} | Rest]) ->
     Field =
         {[
-            {<<"@type">>, <<"double">},
+            {<<"@type">>, <<"double">>},
             {<<"name">>, Name},
             {<<"value">>, Value}
         ]},


[couchdb] 06/06: WIP the great shuffle

Posted by rn...@apache.org.
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 7a5a8a2b51693f26cd986e845f9f01561b38c6ef
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:


[couchdb] 02/06: redo field abstraction

Posted by rn...@apache.org.
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 ddff1ecbf4ef1ba5a52d8ef2baaeebd0ed4b7d09
Author: Robert Newson <rn...@apache.org>
AuthorDate: Sun Feb 26 23:50:32 2023 +0000

    redo field abstraction
---
 .../nouveau/api/document/DoubleDocValuesField.java |  40 --------
 .../couchdb/nouveau/api/document/DoubleField.java  |  43 ++++++--
 .../couchdb/nouveau/api/document/DoublePoint.java  |  41 --------
 .../apache/couchdb/nouveau/api/document/Field.java |   4 -
 .../nouveau/api/document/SortedDocValuesField.java |  40 --------
 .../api/document/SortedSetDocValuesField.java      |  40 --------
 .../nouveau/api/document/StoredDoubleField.java    |   4 +-
 .../nouveau/api/document/StoredStringField.java    |   4 +-
 .../couchdb/nouveau/api/document/StringField.java  |  39 ++++++--
 .../couchdb/nouveau/api/document/TextField.java    |  42 ++++++--
 .../nouveau/api/DocumentUpdateRequestTest.java     |   9 +-
 .../resources/fixtures/DocumentUpdateRequest.json  |   8 +-
 .../couchdb/nouveau/core/lucene4/Lucene4Index.java |  63 +++++-------
 .../couchdb/nouveau/core/lucene9/Lucene9Index.java |  72 ++++++-------
 java/nouveau/server/README.md                      |   8 +-
 .../apache/couchdb/nouveau/IntegrationTest.java    |  11 +-
 .../nouveau/core/ser/SerializationTest.java        |  42 ++++----
 share/server/nouveau.js                            | 111 ++++++++++-----------
 src/mango/src/mango_native_proc.erl                |  18 +---
 19 files changed, 252 insertions(+), 387 deletions(-)

diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/DoubleDocValuesField.java b/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/DoubleDocValuesField.java
deleted file mode 100644
index 64a706e39..000000000
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/DoubleDocValuesField.java
+++ /dev/null
@@ -1,40 +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.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public final class DoubleDocValuesField extends Field {
-
-    private final double value;
-
-    @JsonCreator
-    public DoubleDocValuesField(@JsonProperty("name") final String name,
-            @JsonProperty("value") final double value) {
-        super(name);
-        this.value = value;
-    }
-
-    @JsonProperty
-    public double getValue() {
-        return value;
-    }
-
-    @Override
-    public String toString() {
-        return "DoubleDocValuesField [name=" + name + ", value=" + value + "]";
-    }
-
-}
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
index b11a4bd2d..11f662233 100644
--- 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
@@ -13,21 +13,39 @@
 
 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 stored;
+    private final boolean store;
+
+    private final boolean facet;
+
+    private final boolean sortable;
 
     @JsonCreator
-    public DoubleField(@JsonProperty("name") final String name,
-            @JsonProperty("value") final double value, @JsonProperty("stored") final boolean stored) {
+    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.stored = stored;
+        this.store = store;
+        this.facet = facet;
+        this.sortable = sortable;
     }
 
     @JsonProperty
@@ -36,13 +54,24 @@ public final class DoubleField extends Field {
     }
 
     @JsonProperty
-    public boolean isStored() {
-        return stored;
+    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 + ", stored=" + stored + "]";
+        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/DoublePoint.java b/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/DoublePoint.java
deleted file mode 100644
index 9733301a5..000000000
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/DoublePoint.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 java.util.Arrays;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public final class DoublePoint extends Field {
-
-    private final double[] value;
-
-    @JsonCreator
-    public DoublePoint(@JsonProperty("name") final String name, @JsonProperty("value") final double... value) {
-        super(name);
-        this.value = value;
-    }
-
-    @JsonProperty
-    public double[] getValue() {
-        return value;
-    }
-
-    @Override
-    public String toString() {
-        return "DoublePoint [name=" + name + ", value=" + Arrays.toString(value) + "]";
-    }
-
-}
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
index 0eb9bcd4e..2fc90b29e 100644
--- 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
@@ -25,11 +25,7 @@ import io.dropwizard.jackson.JsonSnakeCase;
     include = JsonTypeInfo.As.PROPERTY,
     property = "@type")
 @JsonSubTypes({
-    @JsonSubTypes.Type(value = DoubleDocValuesField.class, name = "double_dv"),
     @JsonSubTypes.Type(value = DoubleField.class, name = "double"),
-    @JsonSubTypes.Type(value = DoublePoint.class, name = "double_point"),
-    @JsonSubTypes.Type(value = SortedDocValuesField.class, name = "sorted_dv"),
-    @JsonSubTypes.Type(value = SortedSetDocValuesField.class, name = "sorted_set_dv"),
     @JsonSubTypes.Type(value = StoredDoubleField.class, name = "stored_double"),
     @JsonSubTypes.Type(value = StoredStringField.class, name = "stored_string"),
     @JsonSubTypes.Type(value = StringField.class, name = "string"),
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/SortedDocValuesField.java b/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/SortedDocValuesField.java
deleted file mode 100644
index d5cfe9bcc..000000000
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/SortedDocValuesField.java
+++ /dev/null
@@ -1,40 +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 java.util.Arrays;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public final class SortedDocValuesField extends Field {
-
-    private final byte[] value;
-
-    @JsonCreator
-    public SortedDocValuesField(@JsonProperty("name") final String name, @JsonProperty("value") final byte[] value) {
-        super(name);
-        this.value = value;
-    }
-
-    @JsonProperty
-    public byte[] getValue() {
-        return value;
-    }
-
-    @Override
-    public String toString() {
-        return "SortedDocValuesField [name=" + name + ", value=" + Arrays.toString(value) + "]";
-    }
-
-}
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/SortedSetDocValuesField.java b/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/SortedSetDocValuesField.java
deleted file mode 100644
index bdf432a70..000000000
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/SortedSetDocValuesField.java
+++ /dev/null
@@ -1,40 +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 java.util.Arrays;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public final class SortedSetDocValuesField extends Field {
-
-    private final byte[] value;
-
-    @JsonCreator
-    public SortedSetDocValuesField(@JsonProperty("name") final String name, @JsonProperty("value") final byte[] value) {
-        super(name);
-        this.value = value;
-    }
-
-    @JsonProperty
-    public byte[] getValue() {
-        return value;
-    }
-
-    @Override
-    public String toString() {
-        return "SortedSetDocValuesField [name=" + name + ", value=" + Arrays.toString(value) + "]";
-    }
-
-}
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
index 32210d19b..b880f1742 100644
--- 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
@@ -13,6 +13,8 @@
 
 package org.apache.couchdb.nouveau.api.document;
 
+import javax.validation.constraints.NotNull;
+
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
@@ -21,7 +23,7 @@ public final class StoredDoubleField extends Field {
     private final double value;
 
     @JsonCreator
-    public StoredDoubleField(@JsonProperty("name") final String name, @JsonProperty("value") final double value) {
+    public StoredDoubleField(@NotNull @JsonProperty("name") final String name, @NotNull @JsonProperty("value") final double value) {
         super(name);
         this.value = value;
     }
diff --git a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/StoredStringField.java b/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/StoredStringField.java
index 6bddc5f0f..a93e3fade 100644
--- a/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/StoredStringField.java
+++ b/java/nouveau/api/src/main/java/org/apache/couchdb/nouveau/api/document/StoredStringField.java
@@ -13,6 +13,8 @@
 
 package org.apache.couchdb.nouveau.api.document;
 
+import javax.validation.constraints.NotNull;
+
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
@@ -21,7 +23,7 @@ public final class StoredStringField extends Field {
     private String value;
 
     @JsonCreator
-    public StoredStringField(@JsonProperty("name") final String name, @JsonProperty("value") final String value) {
+    public StoredStringField(@NotNull @JsonProperty("name") final String name, @NotNull @JsonProperty("value") final String value) {
         super(name);
         this.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
index 7e7565815..d03a6f38a 100644
--- 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
@@ -16,21 +16,33 @@ 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 {
 
-    @NotNull
     private final String value;
 
-    private final boolean stored;
+    private final boolean store;
+
+    private final boolean facet;
+
+    private final boolean sortable;
 
     @JsonCreator
-    public StringField(@JsonProperty("name") final String name, @JsonProperty("value") final String value,
-            @JsonProperty("stored") final boolean stored) {
+    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.stored = stored;
+        this.store = store;
+        this.facet = facet;
+        this.sortable = sortable;
     }
 
     @JsonProperty
@@ -39,13 +51,24 @@ public final class StringField extends Field {
     }
 
     @JsonProperty
-    public boolean isStored() {
-        return stored;
+    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 + ", stored=" + stored + "]";
+        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
index 5c0604bb5..0c8903c77 100644
--- 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
@@ -13,19 +13,36 @@
 
 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 stored;
+    private final boolean store;
+
+    private final boolean facet;
+
+    private final boolean sortable;
 
-    public TextField(@JsonProperty("name") final String name, @JsonProperty("value") final String value,
-            @JsonProperty("stored") final boolean stored) {
+    @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.stored = stored;
+        this.store = store;
+        this.facet = facet;
+        this.sortable = sortable;
     }
 
     @JsonProperty
@@ -34,13 +51,24 @@ public final class TextField extends Field {
     }
 
     @JsonProperty
-    public boolean isStored() {
-        return stored;
+    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 + ", stored=" + stored + "]";
+        return "TextField [name=" + name + ", value=" + value + ", store=" + store + ", facet=" + facet + ", sortable=" + sortable
+                + "]";
     }
 
 }
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
index 4ee310ea5..1c79f8fe1 100644
--- 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
@@ -19,10 +19,11 @@ import static org.assertj.core.api.Assertions.assertThat;
 import java.util.ArrayList;
 import java.util.List;
 
-import org.apache.couchdb.nouveau.api.document.DoublePoint;
+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;
@@ -55,9 +56,9 @@ public class DocumentUpdateRequestTest {
 
     private DocumentUpdateRequest asObject() {
         final List<Field> fields = new ArrayList<Field>();
-        fields.add(new StringField("stringfoo", "bar", true));
-        fields.add(new TextField("textfoo", "hello there", true));
-        fields.add(new DoublePoint("doublefoo", 12));
+        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/src/test/resources/fixtures/DocumentUpdateRequest.json b/java/nouveau/api/src/test/resources/fixtures/DocumentUpdateRequest.json
index ee2069542..a22e322d4 100644
--- a/java/nouveau/api/src/test/resources/fixtures/DocumentUpdateRequest.json
+++ b/java/nouveau/api/src/test/resources/fixtures/DocumentUpdateRequest.json
@@ -5,18 +5,18 @@
       "@type": "string",
       "name": "stringfoo",
       "value": "bar",
-      "stored": true
+      "store": true
     },
     {
       "@type": "text",
       "name": "textfoo",
       "value": "hello there",
-      "stored": true
+      "store": true
     },
     {
-      "@type": "double_point",
+      "@type": "double",
       "name": "doublefoo",
-      "value": [12]
+      "value": 12
     }
   ]
 }
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/core/lucene4/Lucene4Index.java
index ec3ccaa01..d6986c6ef 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/core/lucene4/Lucene4Index.java
@@ -34,11 +34,8 @@ 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.DoubleDocValuesField;
 import org.apache.couchdb.nouveau.api.document.DoubleField;
 import org.apache.couchdb.nouveau.api.document.Field;
-import org.apache.couchdb.nouveau.api.document.SortedDocValuesField;
-import org.apache.couchdb.nouveau.api.document.SortedSetDocValuesField;
 import org.apache.couchdb.nouveau.api.document.StoredDoubleField;
 import org.apache.couchdb.nouveau.api.document.StoredStringField;
 import org.apache.couchdb.nouveau.api.document.StringField;
@@ -323,12 +320,13 @@ class Lucene4Index extends Index {
         final Document result = new Document();
 
         // id
-        result.add(new org.apache.lucene.document.StringField("_id", docId, Store.YES));
-        result.add(new org.apache.lucene.document.SortedDocValuesField("_id", new BytesRef(docId)));
+        addIndexableFields(result,
+                new StringField("_id", docId, true, false, true));
 
         // partition (optional)
         if (request.hasPartition()) {
-            result.add(new org.apache.lucene.document.StringField("_partition", request.getPartition(), Store.NO));
+            addIndexableFields(result,
+                    new StringField("_partition", request.getPartition(), false, false, false));
         }
 
         for (Field field : request.getFields()) {
@@ -336,48 +334,35 @@ class Lucene4Index extends Index {
             if (field.getName().startsWith("_")) {
                 continue;
             }
-            result.add(toIndexableField(field));
+            addIndexableFields(result, field);
         }
 
         return result;
     }
 
-    private static IndexableField toIndexableField(final Field field) {
-        if (field instanceof DoubleDocValuesField) {
-            final DoubleDocValuesField f = (DoubleDocValuesField) field;
-            return new org.apache.lucene.document.DoubleDocValuesField(f.getName(), f.getValue());
-        }
+    private static void addIndexableFields(final Document doc, final Field field) {
         if (field instanceof DoubleField) {
             final DoubleField f = (DoubleField) field;
-            return new org.apache.lucene.document.DoubleField(f.getName(), f.getValue(), f.isStored() ? Store.YES : Store.NO);
-        }
-        if (field instanceof SortedDocValuesField) {
-            final SortedDocValuesField f = (SortedDocValuesField) field;
-            return new org.apache.lucene.document.SortedDocValuesField(f.getName(), new BytesRef(f.getValue()));
-        }
-        if (field instanceof SortedSetDocValuesField) {
-            final SortedSetDocValuesField f = (SortedSetDocValuesField) field;
-            return new org.apache.lucene.document.SortedSetDocValuesField(f.getName(), new BytesRef(f.getValue()));
-        }
-        if (field instanceof StoredDoubleField) {
-            final StoredDoubleField f = (StoredDoubleField) field;
-            return new org.apache.lucene.document.StoredField(f.getName(), f.getValue());
-        }
-        if (field instanceof StoredStringField) {
-            final StoredStringField f = (StoredStringField) field;
-            return new org.apache.lucene.document.StoredField(f.getName(), f.getValue());
-        }
-        if (field instanceof StringField) {
-            final StringField f = (StringField) field;
-            return new org.apache.lucene.document.StringField(f.getName(), f.getValue(),
-                f.isStored() ? Store.YES : Store.NO);
-        }
-        if (field instanceof TextField) {
+            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;
-            return new org.apache.lucene.document.TextField(f.getName(), f.getValue(),
-                f.isStored() ? Store.YES : Store.NO);
+            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);
         }
-        throw new WebApplicationException(field.getClass() + " is not valid", Status.BAD_REQUEST);
     }
 
     private static Field toField(final IndexableField field) {
diff --git a/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/Lucene9Index.java b/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/Lucene9Index.java
index 79b801032..705fbac11 100644
--- a/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/Lucene9Index.java
+++ b/java/nouveau/lucene9/src/main/java/org/apache/couchdb/nouveau/core/lucene9/Lucene9Index.java
@@ -34,12 +34,8 @@ 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.DoubleDocValuesField;
 import org.apache.couchdb.nouveau.api.document.DoubleField;
-import org.apache.couchdb.nouveau.api.document.DoublePoint;
 import org.apache.couchdb.nouveau.api.document.Field;
-import org.apache.couchdb.nouveau.api.document.SortedDocValuesField;
-import org.apache.couchdb.nouveau.api.document.SortedSetDocValuesField;
 import org.apache.couchdb.nouveau.api.document.StoredDoubleField;
 import org.apache.couchdb.nouveau.api.document.StoredStringField;
 import org.apache.couchdb.nouveau.api.document.StringField;
@@ -314,12 +310,13 @@ class Lucene9Index extends Index {
         final Document result = new Document();
 
         // id
-        result.add(new org.apache.lucene.document.StringField("_id", docId, Store.YES));
-        result.add(new org.apache.lucene.document.SortedDocValuesField("_id", new BytesRef(docId)));
+        addIndexableFields(result,
+            new StringField("_id", docId, true, false, true));
 
         // partition (optional)
         if (request.hasPartition()) {
-            result.add(new org.apache.lucene.document.StringField("_partition", request.getPartition(), Store.NO));
+            addIndexableFields(result,
+                new StringField("_partition", request.getPartition(), false, false, false));
         }
 
         for (Field field : request.getFields()) {
@@ -334,47 +331,40 @@ class Lucene9Index extends Index {
     }
 
     private static void addIndexableFields(final Document doc, final Field field) {
-        if (field instanceof DoubleDocValuesField) {
-            final DoubleDocValuesField f = (DoubleDocValuesField) field;
-            doc.add(new org.apache.lucene.document.DoubleDocValuesField(f.getName(), f.getValue()));
-            return;
-        } else if (field instanceof DoubleField) {
+        if (field instanceof DoubleField) {
             final DoubleField f = (DoubleField) field;
-            doc.add(new org.apache.lucene.document.DoubleField(f.getName(), f.getValue()));
-            if (f.isStored()) {
+            if (f.isSortable() && f.isFacet()) {
+                doc.add(new org.apache.lucene.document.DoubleField(f.getName(), f.getValue()));
+            } else {
+                if (f.isSortable()) {
+                    doc.add(new org.apache.lucene.document.DoublePoint(f.getName(), f.getValue()));
+                }
+                if (f.isFacet()) {
+                    doc.add(new org.apache.lucene.document.DoubleDocValuesField(f.getName(), f.getValue()));
+                }
+            }
+            if (f.isStore()) {
                 doc.add(new org.apache.lucene.document.StoredField(f.getName(), f.getValue()));
             }
-            return;
-        } else if (field instanceof DoublePoint) {
-            final DoublePoint f = (DoublePoint) field;
-            doc.add(new org.apache.lucene.document.DoublePoint(f.getName(), f.getValue()));
-            return;
-        } else if (field instanceof SortedDocValuesField) {
-            final SortedDocValuesField f = (SortedDocValuesField) field;
-            doc.add(new org.apache.lucene.document.SortedDocValuesField(f.getName(), new BytesRef(f.getValue())));
-            return;
-        } else if (field instanceof SortedSetDocValuesField) {
-            final SortedSetDocValuesField f = (SortedSetDocValuesField) field;
-            doc.add(new org.apache.lucene.document.SortedSetDocValuesField(f.getName(), new BytesRef(f.getValue())));
-            return;
-        } else if (field instanceof StoredDoubleField) {
-            final StoredDoubleField f = (StoredDoubleField) field;
-            doc.add(new org.apache.lucene.document.StoredField(f.getName(), f.getValue()));
-            return;
-        } else if (field instanceof StoredStringField) {
-            final StoredStringField f = (StoredStringField) field;
-            doc.add(new org.apache.lucene.document.StoredField(f.getName(), f.getValue()));
-            return;
         } else if (field instanceof StringField) {
             final StringField f = (StringField) field;
             doc.add(new org.apache.lucene.document.StringField(f.getName(), f.getValue(),
-                f.isStored() ? Store.YES : Store.NO));
-            return;
+                    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())));
+            }
+            if (f.isStore()) {
+                doc.add(new org.apache.lucene.document.StoredField(f.getName(), f.getValue()));
+            }
         } else if (field instanceof TextField) {
-            final StringField f = (StringField) field;
-            doc.add(new org.apache.lucene.document.TextField(f.getName(), f.getValue(),
-                f.isStored() ? Store.YES : Store.NO));
-            return;
+            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())));
+            }
+            if (f.isStore()) {
+                doc.add(new org.apache.lucene.document.StoredField(f.getName(), f.getValue()));
+            }
         } else {
             throw new WebApplicationException(field.getClass() + " is not valid", Status.BAD_REQUEST);
         }
diff --git a/java/nouveau/server/README.md b/java/nouveau/server/README.md
index cd56a8de3..696932ba9 100644
--- a/java/nouveau/server/README.md
+++ b/java/nouveau/server/README.md
@@ -98,8 +98,6 @@ To ease migration nouveau functions can use the 'index' function exactly as it e
 | index("foo", "bar");                               | adds a TextField.
 | index("foo", "bar", {"store":true});               | adds a TextField and a StoredField.
 | index("foo", "bar", {"store":true, "facet":true}); | adds a TextField, a StoredField and a SortedSetDocValuesField.
-| index("foo", "bar", "text");                       | adds a TextField.
-| index("foo", "bar", "string");                     | adds a StringField.
-| index("foo", "bar", "stored_string");              | adds a StoredField.
-| index("foo", "bar", "sorted_set_dv");              | adds a SortedSetDocValuesField.
-| index("foo", "bar", "string", true);               | adds a TextField with Store.YES
+| index("text", "foo", "bar");                       | adds a TextField.
+| index("text", "foo", "bar", {"store":true});       | adds a TextField with Store.YES
+| index("string", "foo", "bar");                     | adds a StringField.
diff --git a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/IntegrationTest.java b/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/IntegrationTest.java
index d38f04525..fd82d240b 100644
--- a/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/IntegrationTest.java
+++ b/java/nouveau/server/src/test/java/org/apache/couchdb/nouveau/IntegrationTest.java
@@ -32,9 +32,8 @@ 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.DoubleDocValuesField;
-import org.apache.couchdb.nouveau.api.document.DoublePoint;
-import org.apache.couchdb.nouveau.api.document.SortedSetDocValuesField;
+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;
 
@@ -93,9 +92,9 @@ public class IntegrationTest {
         for (int i = 0; i < 10; i++) {
             final DocumentUpdateRequest docUpdate = new DocumentUpdateRequest(i + 1, null,
                 List.of(
-                    new DoublePoint("foo", i),
-                    new DoubleDocValuesField("baz", i),
-                    new SortedSetDocValuesField("bar", new byte[]{'b', 'a', 'z'})));
+                    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()
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
index b383f123d..c45cdb758 100644
--- 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
@@ -19,7 +19,7 @@ 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.DoublePoint;
+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;
@@ -40,53 +40,45 @@ public class SerializationTest {
 
     @Test
     public void testSerializeStringFieldStoreYES() throws Exception {
-        final String expected = "{\"@type\":\"string\",\"name\":\"foo\",\"value\":\"bar\",\"stored\":true}";
-        final String actual = mapper.writeValueAsString(new StringField("foo", "bar", true));
+        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\",\"stored\":false}";
-        final String actual = mapper.writeValueAsString(new StringField("foo", "bar", false));
+        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\",\"stored\":true}";
-        final String actual = mapper.writeValueAsString(new TextField("foo", "bar", true));
+        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\",\"stored\":false}";
-        final String actual = mapper.writeValueAsString(new TextField("foo", "bar", false));
+        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 testSerializeDoublePoint() throws Exception {
-        final String expected = "{\"@type\":\"double_point\",\"name\":\"foo\",\"value\":[12.5]}";
-        final String actual = mapper.writeValueAsString(new DoublePoint("foo", 12.5));
+    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 testDeserializeDoublePoint1D() throws Exception {
-        final String json = "{\"@type\":\"double_point\",\"name\":\"foo\",\"value\":[12.5]}";
-        final DoublePoint point = mapper.readValue(json, DoublePoint.class);
-        assertEquals("foo", point.getName());
-        assertArrayEquals(new double[]{12.5}, point.getValue());
-    }
-
-    @Test
-    public void testDeserializeDoublePoint2D() throws Exception {
-        final String json = "{\"@type\":\"double_point\",\"name\":\"foo\",\"value\":[12.5,13.6]}";
-        final DoublePoint point = mapper.readValue(json, DoublePoint.class);
-        assertEquals("foo", point.getName());
-        assertArrayEquals(new double[]{12.5, 13.6}, point.getValue());
+    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
diff --git a/share/server/nouveau.js b/share/server/nouveau.js
index 90e7ac9a9..9f9834164 100644
--- a/share/server/nouveau.js
+++ b/share/server/nouveau.js
@@ -32,78 +32,67 @@ var Nouveau = (function() {
   };
 
   return {
-    index: function(name, value) {
-      assertType('name', 'string', name);
-
-      if (name.substring(0, 1) === '_') {
-        throw({name: 'ReservedName', message: 'name must not start with an underscore'});
-      }
+    index: function() {
 
       // Dreyfus compatibility.
       if (arguments.length == 2 || (arguments.length == 3 && typeof arguments[2] == 'object')) {
-        options = arguments[2] || {};
+
+        var name = arguments[0];
+        var value = arguments[1];
+        var options = arguments[2] || {};
+
+        assertType('name', 'string', name);
+
+        if (name.substring(0, 1) === '_') {
+          throw({name: 'ReservedName', message: 'name must not start with an underscore'});
+        }
+
         if (typeof value == 'boolean') {
           // coerce to string as handling is the same.
           value = value ? 'true' : 'false'
         }
-        switch (typeof value) {
-        case 'string':
-          index_results.push({'@type': 'text', 'name': name, 'value': value, 'stored': options.store || false});
-          index_results.push({'@type': 'sorted_dv', 'name': name, 'value': value}); // for sorting.
-          if (options.facet) {
-            index_results.push({'@type': 'sorted_set_dv', 'name': name, 'value': value});
-          }
-          break;
-        case 'number':
-          index_results.push({'@type': 'double_point', 'name': name, 'value': [value]});
-          if (options.store) {
-            index_results.push({'@type': 'stored_double', 'name': name, 'value': value});
-          }
-          if (options.facet) {
-            index_results.push({'@type': 'double_dv', 'name': name, 'value': value});
-          }
-          break;
-        default:
+
+        if (!(typeof value == 'string' || typeof value == 'number')) {
           throw({name: 'TypeError', message: 'value must be a string, a number or boolean not ' + typeof value});
         }
-        return;
-      }
 
-      var type = arguments[2];
-      assertType('type', 'string', type);
-
-      switch (type) {
-      case 'sorted_set_dv':
-      case 'sorted_dv':
-        assertType('value', 'string', value);
-        index_results.push({'@type': type, 'name': name, 'value': value});
-        break;
-      case 'double_point':
-        assertType('value', 'number', value);
-        index_results.push({'@type': type, 'name': name, 'value': [value]});
-        break;
-      case 'double_dv':
-        assertType('value', 'number', value);
-        index_results.push({'@type': type, 'name': name, 'value': value});
-        break;
-      case 'string':
-      case 'text':
-        assertType('value', 'string', value);
-        if (arguments.length === 4) {
-          assertType('boolean', arguments[3]);
+        index_results.push({
+          '@type': typeof value == 'string' ? 'string' : 'double',
+          'name': name,
+          'value': value,
+          'store': options.store|| false,
+          'facet': options.facet|| false
+        });
+      } else { // nouveau api
+        var type = arguments[0];
+        var name = arguments[1];
+
+        assertType('type', 'string', type);
+        assertType('name', 'string', name);
+
+        if (name.substring(0, 1) === '_') {
+          throw({name: 'ReservedName', message: 'name must not start with an underscore'});
+        }
+
+        switch (type) {
+        case 'double':
+        case 'string':
+        case 'text':
+          var value = arguments[2];
+          var options = arguments[3];
+          assertType('value', type == 'double' ? 'number' : 'string', value);
+          index_results.push({
+            '@type': type,
+            'name': name,
+            'value': value,
+            'store': options.store|| false
+            'facet': options.facet|| false,
+            'sortable': options.sortable|| true
+          });
+          break;
+        default:
+          throw({name: 'TypeError', message: type + ' not supported'});
         }
-        index_results.push({'@type': type, 'name': name, 'value': value, 'stored': arguments[3] || false});
-        break;
-      case 'stored_double':
-        assertType('value', 'number', value);
-        index_results.push({'@type': type, 'name': name, 'value': value});
-        break;
-      case 'stored_string':
-        assertType('value', 'string', value);
-        index_results.push({'@type': type, 'name': name, 'value': value});
-        break;
-      default:
-        throw({name: 'TypeError', message: type + ' not supported'});
       }
     },
 
diff --git a/src/mango/src/mango_native_proc.erl b/src/mango/src/mango_native_proc.erl
index 5b0600021..de7431c6e 100644
--- a/src/mango/src/mango_native_proc.erl
+++ b/src/mango/src/mango_native_proc.erl
@@ -351,8 +351,7 @@ convert_to_nouveau_string_field([Name, Value, []]) when is_binary(Name), is_bina
     {[
         {<<"@type">>, <<"string">>},
         {<<"name">>, Name},
-        {<<"value">>, Value},
-        {<<"stored">>, false}
+        {<<"value">>, Value}
     ]}.
 
 convert_nouveau_fields([]) ->
@@ -362,24 +361,17 @@ convert_nouveau_fields([{Name, <<"string">>, Value} | Rest]) ->
         {[
             {<<"@type">>, <<"text">>},
             {<<"name">>, Name},
-            {<<"value">>, Value},
-            {<<"stored">>, false}
+            {<<"value">>, Value}
         ]},
     [Field | convert_nouveau_fields(Rest)];
 convert_nouveau_fields([{Name, <<"number">>, Value} | Rest]) ->
-    PointField =
-        {[
-            {<<"@type">>, <<"double_point">>},
-            {<<"name">>, Name},
-            {<<"value">>, Value}
-        ]},
-    DocValuesField =
+    Field =
         {[
-            {<<"@type">>, <<"double_dv">>},
+            {<<"@type">>, <<"double">},
             {<<"name">>, Name},
             {<<"value">>, Value}
         ]},
-    [PointField, DocValuesField | convert_nouveau_fields(Rest)];
+    [Field | convert_nouveau_fields(Rest)];
 convert_nouveau_fields([{Name, <<"boolean">>, true} | Rest]) ->
     Field =
         {[


[couchdb] 01/06: enable-preview not needed

Posted by rn...@apache.org.
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 65cf09cd9a3d19a13e7f924adac6fe3ce7d83d3d
Author: Robert Newson <rn...@apache.org>
AuthorDate: Tue Feb 28 17:28:21 2023 +0000

    enable-preview not needed
---
 java/nouveau/server/pom.xml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/java/nouveau/server/pom.xml b/java/nouveau/server/pom.xml
index c52a6499d..1e6ed315d 100644
--- a/java/nouveau/server/pom.xml
+++ b/java/nouveau/server/pom.xml
@@ -135,7 +135,6 @@
           <executable>java</executable>
           <arguments>
             <arguments>-server</arguments>
-            <arguments>--enable-preview</arguments>
             <argument>-classpath</argument>
             <classpath/>
             <argument>org.apache.couchdb.nouveau.NouveauApplication</argument>