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

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

commit 045ab2fff7c1d7bf99caefb19b5a4b6a40baaaa0
Author: Robert Newson <[email protected]>
AuthorDate: Thu Aug 28 18:32:35 2025 +0100

    add configurable search result caching
---
 nouveau/build.gradle                               |  2 +-
 .../apache/couchdb/nouveau/NouveauApplication.java |  6 ++-
 .../nouveau/NouveauApplicationConfiguration.java   | 10 +++++
 .../apache/couchdb/nouveau/api/SearchRequest.java  | 51 ++++++++++++++++++++++
 .../org/apache/couchdb/nouveau/core/Index.java     |  8 ++++
 .../couchdb/nouveau/core/SearchResultsCache.java   | 31 +++++++++++++
 .../couchdb/nouveau/resources/IndexResource.java   | 19 +++++++-
 .../nouveau/health/IndexHealthCheckTest.java       |  2 +-
 8 files changed, 124 insertions(+), 5 deletions(-)

diff --git a/nouveau/build.gradle b/nouveau/build.gradle
index 356295ee1..8719a931c 100644
--- a/nouveau/build.gradle
+++ b/nouveau/build.gradle
@@ -46,7 +46,7 @@ group = 'org.apache.couchdb'
 version = '1.0-SNAPSHOT'
 
 java {
-    sourceCompatibility = "11"
+    sourceCompatibility = "21"
 }
 
 jar {
diff --git 
a/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java
index 7179eadc0..5625d4c6c 100644
--- a/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java
+++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java
@@ -21,6 +21,7 @@ import java.util.EnumSet;
 import java.util.concurrent.ForkJoinPool;
 import java.util.concurrent.ScheduledExecutorService;
 import org.apache.couchdb.nouveau.core.IndexManager;
+import org.apache.couchdb.nouveau.core.SearchResultsCache;
 import org.apache.couchdb.nouveau.core.UserAgentFilter;
 import org.apache.couchdb.nouveau.health.AnalyzeHealthCheck;
 import org.apache.couchdb.nouveau.health.IndexHealthCheck;
@@ -64,6 +65,9 @@ public class NouveauApplication extends 
Application<NouveauApplicationConfigurat
         indexManager.setRootDir(configuration.getRootDir());
         environment.lifecycle().manage(indexManager);
 
+        // configure cache
+        var cache = new 
SearchResultsCache(configuration.getSearchResultCacheSpec());
+
         // Serialization classes
         environment.getObjectMapper().registerModule(new Lucene9Module());
 
@@ -72,7 +76,7 @@ public class NouveauApplication extends 
Application<NouveauApplicationConfigurat
         environment.jersey().register(analyzeResource);
 
         // IndexResource
-        final IndexResource indexResource = new IndexResource(indexManager);
+        final IndexResource indexResource = new IndexResource(indexManager, 
cache);
         environment.jersey().register(indexResource);
 
         // Health checks
diff --git 
a/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
index dce6fe6da..13ae0969b 100644
--- 
a/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
+++ 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
@@ -36,6 +36,8 @@ public class NouveauApplicationConfiguration extends 
Configuration {
     @Min(2)
     private int schedulerThreadCount = 5;
 
+    private String searchResultCacheSpec = "maximumSize = 10000";
+
     @JsonProperty
     public void setMaxIndexesOpen(int maxIndexesOpen) {
         this.maxIndexesOpen = maxIndexesOpen;
@@ -79,4 +81,12 @@ public class NouveauApplicationConfiguration extends 
Configuration {
     public void setSchedulerThreadCount(int schedulerThreadCount) {
         this.schedulerThreadCount = schedulerThreadCount;
     }
+
+    public String getSearchResultCacheSpec() {
+        return searchResultCacheSpec;
+    }
+
+    public void setSearchResultCacheSpec(String searchResultCacheSpec) {
+        this.searchResultCacheSpec = searchResultCacheSpec;
+    }
 }
diff --git 
a/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchRequest.java 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchRequest.java
index 8e4ebbf8d..06fca14d3 100644
--- a/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchRequest.java
+++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchRequest.java
@@ -22,6 +22,7 @@ import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Positive;
 import jakarta.validation.constraints.PositiveOrZero;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -177,6 +178,56 @@ public class SearchRequest {
         return after;
     }
 
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((query == null) ? 0 : query.hashCode());
+        result = prime * result + (int) (minUpdateSeq ^ (minUpdateSeq >>> 32));
+        result = prime * result + (int) (minPurgeSeq ^ (minPurgeSeq >>> 32));
+        result = prime * result + ((locale == null) ? 0 : locale.hashCode());
+        result = prime * result + ((partition == null) ? 0 : 
partition.hashCode());
+        result = prime * result + limit;
+        result = prime * result + ((sort == null) ? 0 : sort.hashCode());
+        result = prime * result + ((counts == null) ? 0 : counts.hashCode());
+        result = prime * result + ((ranges == null) ? 0 : ranges.hashCode());
+        result = prime * result + Arrays.hashCode(after);
+        result = prime * result + topN;
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null) return false;
+        if (getClass() != obj.getClass()) return false;
+        SearchRequest other = (SearchRequest) obj;
+        if (query == null) {
+            if (other.query != null) return false;
+        } else if (!query.equals(other.query)) return false;
+        if (minUpdateSeq != other.minUpdateSeq) return false;
+        if (minPurgeSeq != other.minPurgeSeq) return false;
+        if (locale == null) {
+            if (other.locale != null) return false;
+        } else if (!locale.equals(other.locale)) return false;
+        if (partition == null) {
+            if (other.partition != null) return false;
+        } else if (!partition.equals(other.partition)) return false;
+        if (limit != other.limit) return false;
+        if (sort == null) {
+            if (other.sort != null) return false;
+        } else if (!sort.equals(other.sort)) return false;
+        if (counts == null) {
+            if (other.counts != null) return false;
+        } else if (!counts.equals(other.counts)) return false;
+        if (ranges == null) {
+            if (other.ranges != null) return false;
+        } else if (!ranges.equals(other.ranges)) return false;
+        if (!Arrays.equals(after, other.after)) return false;
+        if (topN != other.topN) return false;
+        return true;
+    }
+
     @Override
     public String toString() {
         return "SearchRequest [query=" + query + ", min_update_seq=" + 
minUpdateSeq + ", min_purge_seq=" + minPurgeSeq
diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/core/Index.java 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/core/Index.java
index 35013bf8d..d1d870958 100644
--- a/nouveau/src/main/java/org/apache/couchdb/nouveau/core/Index.java
+++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/core/Index.java
@@ -44,6 +44,14 @@ public abstract class Index implements Closeable {
         this.purgeSeq = purgeSeq;
     }
 
+    public final synchronized long getUpdateSeq() {
+        return updateSeq;
+    }
+
+    public final synchronized long getPurgeSeq() {
+        return purgeSeq;
+    }
+
     public final IndexInfo info() throws IOException {
         final int numDocs = doNumDocs();
         final long diskSize = doDiskSize();
diff --git 
a/nouveau/src/main/java/org/apache/couchdb/nouveau/core/SearchResultsCache.java 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/core/SearchResultsCache.java
new file mode 100644
index 000000000..3f1a5e114
--- /dev/null
+++ 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/core/SearchResultsCache.java
@@ -0,0 +1,31 @@
+package org.apache.couchdb.nouveau.core;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.apache.couchdb.nouveau.api.SearchRequest;
+import org.apache.couchdb.nouveau.api.SearchResults;
+
+public class SearchResultsCache {
+
+    record Key(String name, SearchRequest request, long updateSeq, long 
purgeSeq) {}
+
+    private final Cache<Key, SearchResults> cache;
+
+    public SearchResultsCache(final String cacheSpec) {
+        cache = Caffeine.from(cacheSpec).build();
+    }
+
+    public SearchResults get(
+            final String name, final SearchRequest request, final long 
updateSeq, final long purgeSeq) {
+        return cache.getIfPresent(new Key(name, request, updateSeq, purgeSeq));
+    }
+
+    public void put(
+            final String name,
+            final SearchRequest request,
+            final long updateSeq,
+            final long purgeSeq,
+            final SearchResults results) {
+        cache.put(new Key(name, request, updateSeq, purgeSeq), results);
+    }
+}
diff --git 
a/nouveau/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java
index a52e00da9..4a75ee30b 100644
--- 
a/nouveau/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java
+++ 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java
@@ -39,6 +39,7 @@ import org.apache.couchdb.nouveau.api.Ok;
 import org.apache.couchdb.nouveau.api.SearchRequest;
 import org.apache.couchdb.nouveau.api.SearchResults;
 import org.apache.couchdb.nouveau.core.IndexManager;
+import org.apache.couchdb.nouveau.core.SearchResultsCache;
 
 @Path("/index/{name}")
 @Metered
@@ -50,8 +51,11 @@ public final class IndexResource {
 
     private final IndexManager indexManager;
 
-    public IndexResource(final IndexManager indexManager) {
+    private final SearchResultsCache cache;
+
+    public IndexResource(final IndexManager indexManager, final 
SearchResultsCache cache) {
         this.indexManager = Objects.requireNonNull(indexManager);
+        this.cache = cache;
     }
 
     @PUT
@@ -110,7 +114,18 @@ public final class IndexResource {
     public SearchResults searchIndex(@PathParam("name") String name, @NotNull 
@Valid SearchRequest request)
             throws Exception {
         return indexManager.with(name, (index) -> {
-            return index.search(request);
+            if (cache == null) {
+                return index.search(request);
+            }
+            final long updateSeq = index.getUpdateSeq();
+            final long purgeSeq = index.getPurgeSeq();
+            SearchResults result = cache.get(name, request, updateSeq, 
purgeSeq);
+            if (result != null) {
+                return result;
+            }
+            result = index.search(request);
+            cache.put(name, request, updateSeq, purgeSeq, result);
+            return result;
         });
     }
 
diff --git 
a/nouveau/src/test/java/org/apache/couchdb/nouveau/health/IndexHealthCheckTest.java
 
b/nouveau/src/test/java/org/apache/couchdb/nouveau/health/IndexHealthCheckTest.java
index 0c777fbab..3da541efd 100644
--- 
a/nouveau/src/test/java/org/apache/couchdb/nouveau/health/IndexHealthCheckTest.java
+++ 
b/nouveau/src/test/java/org/apache/couchdb/nouveau/health/IndexHealthCheckTest.java
@@ -39,7 +39,7 @@ public class IndexHealthCheckTest {
         manager.start();
 
         try {
-            var resource = new IndexResource(manager);
+            var resource = new IndexResource(manager, null);
             var check = new IndexHealthCheck(resource);
             var result = check.check();
             assertTrue(result.isHealthy(), result.toString());

Reply via email to