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());
