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

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

commit c4c16bd8fee2d9cfc9ac453926f5d80bad6ecb2f
Author: Robert Newson <[email protected]>
AuthorDate: Sat Mar 21 20:27:47 2026 +0000

    convert Search api classes to records
---
 .../org/apache/couchdb/nouveau/api/SearchHit.java  |  39 +---
 .../apache/couchdb/nouveau/api/SearchRequest.java  | 207 +++++++++------------
 .../apache/couchdb/nouveau/api/SearchResults.java  |  74 +-------
 .../org/apache/couchdb/nouveau/core/Index.java     |   2 +-
 .../couchdb/nouveau/health/IndexHealthCheck.java   |  12 +-
 .../apache/couchdb/nouveau/lucene/LuceneIndex.java | 154 +++++++--------
 .../couchdb/nouveau/api/SearchRequestTest.java     |  15 +-
 .../couchdb/nouveau/core/IndexManagerTest.java     |   5 +-
 .../couchdb/nouveau/lucene/LuceneIndexTest.java    |  70 +++----
 9 files changed, 225 insertions(+), 353 deletions(-)

diff --git 
a/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchHit.java 
b/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchHit.java
index 2e575fef1..9a0d8229b 100644
--- a/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchHit.java
+++ b/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchHit.java
@@ -18,43 +18,8 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming;
 import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import java.util.Collection;
-import java.util.Objects;
 import org.apache.couchdb.nouveau.core.ser.PrimitiveWrapper;
 
 @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
-public class SearchHit {
-
-    @NotEmpty
-    private String id;
-
-    @NotNull
-    private PrimitiveWrapper<?>[] order;
-
-    @NotNull
-    private Collection<@NotNull StoredField> fields;
-
-    public SearchHit() {}
-
-    public SearchHit(final String id, final PrimitiveWrapper<?>[] order, final 
Collection<StoredField> fields) {
-        this.id = id;
-        this.order = Objects.requireNonNull(order);
-        this.fields = Objects.requireNonNull(fields);
-    }
-
-    public String getId() {
-        return id;
-    }
-
-    public PrimitiveWrapper<?>[] getOrder() {
-        return order;
-    }
-
-    public Collection<StoredField> getFields() {
-        return fields;
-    }
-
-    @Override
-    public String toString() {
-        return "SearchHit [id=" + id + ", order=" + order + ", fields=" + 
fields + "]";
-    }
-}
+public record SearchHit(
+        @NotEmpty String id, @NotNull PrimitiveWrapper<?>[] order, @NotNull 
Collection<@NotNull StoredField> fields) {}
diff --git 
a/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchRequest.java 
b/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchRequest.java
index 8e4ebbf8d..948b080a7 100644
--- 
a/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchRequest.java
+++ 
b/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchRequest.java
@@ -13,7 +13,6 @@
 
 package org.apache.couchdb.nouveau.api;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.databind.PropertyNamingStrategies;
 import com.fasterxml.jackson.databind.annotation.JsonNaming;
 import jakarta.validation.constraints.Max;
@@ -25,162 +24,126 @@ import jakarta.validation.constraints.PositiveOrZero;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
+import java.util.OptionalInt;
 import org.apache.couchdb.nouveau.core.ser.PrimitiveWrapper;
 
 @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
-public class SearchRequest {
+public record SearchRequest(
+        @NotNull String query,
+        @PositiveOrZero long minUpdateSeq,
+        @PositiveOrZero long minPurgeSeq,
+        Optional<Locale> locale,
+        Optional<String> partition,
+        @Positive OptionalInt limit,
+        @NotNull Optional<List<@NotEmpty String>> sort,
+        @NotNull Optional<List<@NotEmpty String>> counts,
+        @NotNull Optional<Map<@NotEmpty String, List<@NotNull DoubleRange>>> 
ranges,
+        @NotNull Optional<PrimitiveWrapper<?>[]> after,
+        @Min(1) @Max(1000) OptionalInt topN) {
 
-    @NotNull
-    private String query;
-
-    @PositiveOrZero
-    private long minUpdateSeq;
-
-    @PositiveOrZero
-    private long minPurgeSeq;
-
-    private Locale locale;
-
-    private String partition;
-
-    @Positive
-    private int limit = 25;
-
-    private List<@NotEmpty String> sort;
-
-    private List<@NotEmpty String> counts;
-
-    private Map<@NotEmpty String, List<@NotNull DoubleRange>> ranges;
-
-    private PrimitiveWrapper<?>[] after;
-
-    @Min(1)
-    @Max(1000)
-    private int topN = 10;
-
-    public SearchRequest() {
-        // Jackson deserialization
+    public int limitAsInt() {
+        return limit.orElse(25);
     }
 
-    public void setQuery(final String query) {
-        this.query = query;
+    public int topNAsInt() {
+        return topN.orElse(10);
     }
 
-    @JsonProperty
-    public String getQuery() {
-        return query;
+    public boolean hasCounts() {
+        return !counts.isEmpty();
     }
 
-    public void setMinUpdateSeq(final long minUpdateSeq) {
-        this.minUpdateSeq = minUpdateSeq;
+    public boolean hasRanges() {
+        return !ranges.isEmpty();
     }
 
-    @JsonProperty
-    public long getMinUpdateSeq() {
-        return minUpdateSeq;
+    public Locale getLocale() {
+        return locale.orElse(Locale.getDefault());
     }
 
-    public void setMinPurgeSeq(final long minPurgeSeq) {
-        this.minPurgeSeq = minPurgeSeq;
-    }
+    public static class Builder {
 
-    @JsonProperty
-    public long getMinPurgeSeq() {
-        return minPurgeSeq;
-    }
+        private String query;
 
-    public void setLocale(final Locale locale) {
-        this.locale = locale;
-    }
+        private long minUpdateSeq;
 
-    @JsonProperty
-    public Locale getLocale() {
-        return locale;
-    }
+        private long minPurgeSeq;
 
-    public void setPartition(final String partition) {
-        this.partition = partition;
-    }
+        private Optional<Locale> locale = Optional.empty();
 
-    @JsonProperty
-    public String getPartition() {
-        return partition;
-    }
+        private Optional<String> partition = Optional.empty();
 
-    public boolean hasPartition() {
-        return partition != null;
-    }
+        private OptionalInt limit = OptionalInt.empty();
 
-    public void setLimit(final int limit) {
-        this.limit = limit;
-    }
+        private Optional<List<String>> sort = Optional.empty();
 
-    @JsonProperty
-    public int getLimit() {
-        return limit;
-    }
+        private Optional<List<String>> counts = Optional.empty();
 
-    public boolean hasSort() {
-        return sort != null;
-    }
+        private Optional<Map<String, List<DoubleRange>>> ranges = 
Optional.empty();
 
-    @JsonProperty
-    public List<String> getSort() {
-        return sort;
-    }
+        private Optional<PrimitiveWrapper<?>[]> after = Optional.empty();
 
-    public void setSort(List<String> sort) {
-        this.sort = sort;
-    }
+        private OptionalInt topN = OptionalInt.empty();
 
-    public boolean hasCounts() {
-        return counts != null;
-    }
+        public Builder setQuery(final String query) {
+            this.query = query;
+            return this;
+        }
 
-    public void setCounts(final List<String> counts) {
-        this.counts = counts;
-    }
+        public Builder setMinUpdateSeq(final long minUpdateSeq) {
+            this.minUpdateSeq = minUpdateSeq;
+            return this;
+        }
 
-    @JsonProperty
-    public List<String> getCounts() {
-        return counts;
-    }
+        public Builder setMinPurgeSeq(final long minPurgeSeq) {
+            this.minPurgeSeq = minPurgeSeq;
+            return this;
+        }
 
-    public boolean hasRanges() {
-        return ranges != null;
-    }
+        public Builder setLocale(final Locale locale) {
+            this.locale = Optional.of(locale);
+            return this;
+        }
 
-    public void setRanges(final Map<String, List<DoubleRange>> ranges) {
-        this.ranges = ranges;
-    }
+        public Builder setPartition(final String partition) {
+            this.partition = Optional.of(partition);
+            return this;
+        }
 
-    @JsonProperty
-    public Map<String, List<DoubleRange>> getRanges() {
-        return ranges;
-    }
+        public Builder setLimit(final int limit) {
+            this.limit = OptionalInt.of(limit);
+            return this;
+        }
 
-    public void setTopN(final int topN) {
-        this.topN = topN;
-    }
+        public Builder setSort(List<String> sort) {
+            this.sort = Optional.of(sort);
+            return this;
+        }
 
-    @JsonProperty
-    public int getTopN() {
-        return topN;
-    }
+        public Builder setCounts(final List<String> counts) {
+            this.counts = Optional.of(counts);
+            return this;
+        }
 
-    public void setAfter(final PrimitiveWrapper<?>[] after) {
-        this.after = after;
-    }
+        public Builder setRanges(final Map<String, List<DoubleRange>> ranges) {
+            this.ranges = Optional.of(ranges);
+            return this;
+        }
 
-    @JsonProperty
-    public PrimitiveWrapper<?>[] getAfter() {
-        return after;
-    }
+        public Builder setTopN(final int topN) {
+            this.topN = OptionalInt.of(topN);
+            return this;
+        }
+
+        public Builder setAfter(final PrimitiveWrapper<?>[] after) {
+            this.after = Optional.of(after);
+            return this;
+        }
 
-    @Override
-    public String toString() {
-        return "SearchRequest [query=" + query + ", min_update_seq=" + 
minUpdateSeq + ", min_purge_seq=" + minPurgeSeq
-                + ", locale=" + locale + ", sort=" + sort + ", limit=" + limit 
+ ", after=" + after + ", counts="
-                + counts + ", ranges=" + ranges + "]";
+        public SearchRequest build() {
+            return new SearchRequest(
+                    query, minUpdateSeq, minPurgeSeq, locale, partition, 
limit, sort, counts, ranges, after, topN);
+        }
     }
 }
diff --git 
a/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchResults.java 
b/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchResults.java
index a273e6ef2..dec725e8a 100644
--- 
a/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchResults.java
+++ 
b/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/api/SearchResults.java
@@ -13,7 +13,6 @@
 
 package org.apache.couchdb.nouveau.api;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.databind.PropertyNamingStrategies;
 import com.fasterxml.jackson.databind.annotation.JsonNaming;
 import jakarta.validation.constraints.NotNull;
@@ -23,70 +22,9 @@ import java.util.Map;
 import org.apache.lucene.search.TotalHits.Relation;
 
 @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
-public class SearchResults {
-
-    @PositiveOrZero
-    private long totalHits;
-
-    @NotNull
-    private Relation totalHitsRelation;
-
-    @NotNull
-    private List<@NotNull SearchHit> hits;
-
-    private Map<@NotNull String, Map<@NotNull String, Number>> counts;
-
-    private Map<@NotNull String, Map<@NotNull String, Number>> ranges;
-
-    public SearchResults() {}
-
-    public void setTotalHits(final long totalHits) {
-        this.totalHits = totalHits;
-    }
-
-    @JsonProperty
-    public long getTotalHits() {
-        return totalHits;
-    }
-
-    public Relation getTotalHitsRelation() {
-        return totalHitsRelation;
-    }
-
-    public void setTotalHitsRelation(Relation relation) {
-        this.totalHitsRelation = relation;
-    }
-
-    public void setHits(final List<SearchHit> hits) {
-        this.hits = hits;
-    }
-
-    @JsonProperty
-    public List<SearchHit> getHits() {
-        return hits;
-    }
-
-    public void setCounts(final Map<String, Map<String, Number>> counts) {
-        this.counts = counts;
-    }
-
-    @JsonProperty
-    public Map<String, Map<String, Number>> getCounts() {
-        return counts;
-    }
-
-    public void setRanges(final Map<String, Map<String, Number>> ranges) {
-        this.ranges = ranges;
-    }
-
-    @JsonProperty
-    public Map<String, Map<String, Number>> getRanges() {
-        return ranges;
-    }
-
-    @Override
-    public String toString() {
-        return "SearchResults [hits=" + hits + ", totalHits=" + totalHits + ", 
counts=" + counts + ", ranges=" + ranges
-                + "]";
-    }
-}
+public record SearchResults(
+        @PositiveOrZero long totalHits,
+        @NotNull Relation totalHitsRelation,
+        @NotNull List<@NotNull SearchHit> hits,
+        Map<@NotNull String, Map<@NotNull String, Number>> counts,
+        Map<@NotNull String, Map<@NotNull String, Number>> ranges) {}
diff --git 
a/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/core/Index.java 
b/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/core/Index.java
index e7693702c..c90cc6aaa 100644
--- a/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/core/Index.java
+++ b/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/core/Index.java
@@ -77,7 +77,7 @@ 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 {
-        assertMinSeqs(request.getMinUpdateSeq(), request.getMinPurgeSeq());
+        assertMinSeqs(request.minUpdateSeq(), request.minPurgeSeq());
         return doSearch(request);
     }
 
diff --git 
a/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/health/IndexHealthCheck.java
 
b/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/health/IndexHealthCheck.java
index 81bca3094..5b4e7beb8 100644
--- 
a/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/health/IndexHealthCheck.java
+++ 
b/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/health/IndexHealthCheck.java
@@ -53,16 +53,16 @@ public final class IndexHealthCheck extends HealthCheck {
                     name,
                     new BulkUpdateRequest(List.of(new DocumentUpdate(
                             "foo", new DocumentUpdateRequest(0, 1, null, 
Collections.emptyList())))));
-            final SearchRequest searchRequest = new SearchRequest();
-            searchRequest.setQuery("_id:foo");
-            searchRequest.setMinUpdateSeq(1);
-
+            final SearchRequest searchRequest = new SearchRequest.Builder()
+                    .setQuery("_id:foo")
+                    .setMinUpdateSeq(1)
+                    .build();
             final SearchResults searchResults = 
indexResource.searchIndex(name, searchRequest);
-            if (searchResults.getTotalHits() == 1) {
+            if (searchResults.totalHits() == 1) {
                 return Result.healthy();
             } else {
                 return Result.unhealthy(
-                        "Wrong number of search results, expected 1, got %d", 
searchResults.getTotalHits());
+                        "Wrong number of search results, expected 1, got %d", 
searchResults.totalHits());
             }
         } finally {
             indexResource.deletePath(name, null);
diff --git 
a/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/lucene/LuceneIndex.java
 
b/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/lucene/LuceneIndex.java
index 1f6a735ff..14cc672e9 100644
--- 
a/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/lucene/LuceneIndex.java
+++ 
b/extra/nouveau/src/main/java/org/apache/couchdb/nouveau/lucene/LuceneIndex.java
@@ -25,9 +25,9 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.NoSuchFileException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
@@ -223,21 +223,20 @@ public class LuceneIndex extends Index {
 
     private CollectorManager<?, ? extends TopDocs> hitCollector(final 
SearchRequest searchRequest) {
         final Sort sort = toSort(searchRequest);
+        final FieldDoc fieldDoc = searchRequest
+                .after()
+                .map(after -> {
+                    var result = toFieldDoc(after);
+                    if (getLastSortField(sort).getReverse()) {
+                        result.doc = 0;
+                    } else {
+                        result.doc = Integer.MAX_VALUE;
+                    }
+                    return result;
+                })
+                .orElse(null);
 
-        final PrimitiveWrapper<?>[] after = searchRequest.getAfter();
-        final FieldDoc fieldDoc;
-        if (after != null) {
-            fieldDoc = toFieldDoc(after);
-            if (getLastSortField(sort).getReverse()) {
-                fieldDoc.doc = 0;
-            } else {
-                fieldDoc.doc = Integer.MAX_VALUE;
-            }
-        } else {
-            fieldDoc = null;
-        }
-
-        return new TopFieldCollectorManager(sort, searchRequest.getLimit(), 
fieldDoc, 1000);
+        return new TopFieldCollectorManager(sort, searchRequest.limitAsInt(), 
fieldDoc, 1000);
     }
 
     private SortField getLastSortField(final Sort sort) {
@@ -248,17 +247,20 @@ public class LuceneIndex extends Index {
     private SearchResults toSearchResults(
             final SearchRequest searchRequest, final IndexSearcher searcher, 
final Object[] reduces)
             throws IOException {
-        final SearchResults result = new SearchResults();
-        collectHits(searcher, (TopDocs) reduces[0], result);
+        final TopDocs topDocs = (TopDocs) reduces[0];
+        final var hits = collectHits(searcher, topDocs);
+        Map<String, Map<String, Number>> counts = Collections.emptyMap();
+        Map<String, Map<String, Number>> ranges = Collections.emptyMap();
+
         if (reduces.length == 2) {
-            collectFacets(searchRequest, searcher, (FacetsCollector) 
reduces[1], result);
+            counts = collectCounts(searchRequest, searcher, (FacetsCollector) 
reduces[1]);
+            ranges = collectRanges(searchRequest, searcher, (FacetsCollector) 
reduces[1]);
         }
-        return result;
+        return new SearchResults(topDocs.totalHits.value(), 
topDocs.totalHits.relation(), hits, counts, ranges);
     }
 
-    private void collectHits(final IndexSearcher searcher, final TopDocs 
topDocs, final SearchResults searchResults)
-            throws IOException {
-        final List<SearchHit> hits = new 
ArrayList<SearchHit>(topDocs.scoreDocs.length);
+    private List<SearchHit> collectHits(final IndexSearcher searcher, final 
TopDocs topDocs) throws IOException {
+        final List<SearchHit> result = new 
ArrayList<SearchHit>(topDocs.scoreDocs.length);
         final StoredFields storedFields = searcher.storedFields();
 
         for (final ScoreDoc scoreDoc : topDocs.scoreDocs) {
@@ -287,42 +289,40 @@ public class LuceneIndex extends Index {
             }
 
             final PrimitiveWrapper<?>[] after = toAfter(((FieldDoc) scoreDoc));
-            hits.add(new SearchHit(doc.get("_id"), after, fields));
+            result.add(new SearchHit(doc.get("_id"), after, fields));
         }
-
-        searchResults.setTotalHits(topDocs.totalHits.value());
-        searchResults.setTotalHitsRelation(topDocs.totalHits.relation());
-        searchResults.setHits(hits);
+        return result;
     }
 
-    private void collectFacets(
-            final SearchRequest searchRequest,
-            final IndexSearcher searcher,
-            final FacetsCollector fc,
-            final SearchResults searchResults)
+    private Map<String, Map<String, Number>> collectCounts(
+            final SearchRequest searchRequest, final IndexSearcher searcher, 
final FacetsCollector fc)
             throws IOException {
-        if (searchRequest.hasCounts()) {
-            final Map<String, Map<String, Number>> countsMap = new 
HashMap<String, Map<String, Number>>(
-                    searchRequest.getCounts().size());
-            for (final String field : searchRequest.getCounts()) {
-                final StringDocValuesReaderState state =
-                        new 
StringDocValuesReaderState(searcher.getIndexReader(), field);
-                final StringValueFacetCounts counts = new 
StringValueFacetCounts(state, fc);
-                countsMap.put(field, collectFacets(counts, 
searchRequest.getTopN(), field));
-            }
-            searchResults.setCounts(countsMap);
+        if (!searchRequest.hasCounts()) {
+            return Collections.emptyMap();
+        }
+        var c = searchRequest.counts().get();
+        final Map<String, Map<String, Number>> result = new HashMap<String, 
Map<String, Number>>(c.size());
+        for (final String field : c) {
+            final StringDocValuesReaderState state = new 
StringDocValuesReaderState(searcher.getIndexReader(), field);
+            final StringValueFacetCounts counts = new 
StringValueFacetCounts(state, fc);
+            result.put(field, collectFacets(counts, searchRequest.topNAsInt(), 
field));
         }
+        return result;
+    }
 
-        if (searchRequest.hasRanges()) {
-            final Map<String, Map<String, Number>> rangesMap = new 
HashMap<String, Map<String, Number>>(
-                    searchRequest.getRanges().size());
-            for (final Entry<String, List<DoubleRange>> entry :
-                    searchRequest.getRanges().entrySet()) {
-                final DoubleRangeFacetCounts counts = 
toDoubleRangeFacetCounts(fc, entry.getKey(), entry.getValue());
-                rangesMap.put(entry.getKey(), collectFacets(counts, 
searchRequest.getTopN(), entry.getKey()));
-            }
-            searchResults.setRanges(rangesMap);
+    private Map<String, Map<String, Number>> collectRanges(
+            final SearchRequest searchRequest, final IndexSearcher searcher, 
final FacetsCollector fc)
+            throws IOException {
+        if (!searchRequest.hasRanges()) {
+            return Collections.emptyMap();
         }
+        var r = searchRequest.ranges().get();
+        final Map<String, Map<String, Number>> result = new HashMap<String, 
Map<String, Number>>(r.size());
+        for (final Entry<String, List<DoubleRange>> entry : r.entrySet()) {
+            final DoubleRangeFacetCounts counts = toDoubleRangeFacetCounts(fc, 
entry.getKey(), entry.getValue());
+            result.put(entry.getKey(), collectFacets(counts, 
searchRequest.topNAsInt(), entry.getKey()));
+        }
+        return result;
     }
 
     private DoubleRangeFacetCounts toDoubleRangeFacetCounts(
@@ -353,21 +353,22 @@ public class LuceneIndex extends Index {
 
     // Ensure _id is final sort field so we can paginate.
     private Sort toSort(final SearchRequest searchRequest) {
-        if (!searchRequest.hasSort()) {
-            return DEFAULT_SORT;
-        }
-
-        final List<String> sort = new 
ArrayList<String>(searchRequest.getSort());
-        final String last = sort.get(sort.size() - 1);
-        // Append _id field if not already present.
-        switch (last) {
-            case "-_id":
-            case "_id":
-                break;
-            default:
-                sort.add("_id");
-        }
-        return convertSort(sort);
+        return searchRequest
+                .sort()
+                .map(s -> {
+                    final List<String> sort = new ArrayList<String>(s);
+                    final String last = sort.get(sort.size() - 1);
+                    // Append _id field if not already present.
+                    switch (last) {
+                        case "-_id":
+                        case "_id":
+                            break;
+                        default:
+                            sort.add("_id");
+                    }
+                    return convertSort(sort);
+                })
+                .orElse(DEFAULT_SORT);
     }
 
     private Sort convertSort(final List<String> sort) {
@@ -518,23 +519,22 @@ public class LuceneIndex extends Index {
     }
 
     private Query parse(final SearchRequest request) {
-        var locale = request.getLocale() != null ? request.getLocale() : 
Locale.getDefault();
-        var pointsConfigMap = schema.toPointsConfigMap(locale);
+        var pointsConfigMap = schema.toPointsConfigMap(request.getLocale());
         var queryParser = new NouveauQueryParser(analyzer, pointsConfigMap);
 
-        Query result;
         try {
-            result = queryParser.parse(request.getQuery(), "default");
-            if (request.hasPartition()) {
-                final BooleanQuery.Builder builder = new 
BooleanQuery.Builder();
-                builder.add(new TermQuery(new Term("_partition", 
request.getPartition())), Occur.MUST);
-                builder.add(result, Occur.MUST);
-                result = builder.build();
-            }
+            var result = queryParser.parse(request.query(), "default");
+            return request.partition()
+                    .map(partition -> {
+                        final BooleanQuery.Builder builder = new 
BooleanQuery.Builder();
+                        builder.add(new TermQuery(new Term("_partition", 
partition)), Occur.MUST);
+                        builder.add(result, Occur.MUST);
+                        return (Query) builder.build();
+                    })
+                    .orElse(result);
         } catch (final QueryNodeException e) {
             throw new WebApplicationException(e.getMessage(), e, 
Status.BAD_REQUEST);
         }
-        return result;
     }
 
     private LuceneIndexSchema initSchema(IndexWriter writer) {
diff --git 
a/extra/nouveau/src/test/java/org/apache/couchdb/nouveau/api/SearchRequestTest.java
 
b/extra/nouveau/src/test/java/org/apache/couchdb/nouveau/api/SearchRequestTest.java
index 9269f11e8..9c02589d8 100644
--- 
a/extra/nouveau/src/test/java/org/apache/couchdb/nouveau/api/SearchRequestTest.java
+++ 
b/extra/nouveau/src/test/java/org/apache/couchdb/nouveau/api/SearchRequestTest.java
@@ -28,6 +28,7 @@ public class SearchRequestTest {
     @BeforeAll
     public static void setupMapper() {
         mapper = new ObjectMapper();
+        mapper.findAndRegisterModules();
     }
 
     @Test
@@ -47,12 +48,12 @@ public class SearchRequestTest {
     }
 
     private SearchRequest asObject() {
-        final SearchRequest result = new SearchRequest();
-        result.setQuery("*:*");
-        result.setLimit(10);
-        result.setCounts(List.of("bar"));
-        result.setTopN(5);
-        result.setRanges(Map.of("foo", List.of(new DoubleRange("0 to 100 inc", 
0.0, true, 100.0, true))));
-        return result;
+        return new SearchRequest.Builder()
+                .setQuery("*:*")
+                .setLimit(10)
+                .setCounts(List.of("bar"))
+                .setTopN(5)
+                .setRanges(Map.of("foo", List.of(new DoubleRange("0 to 100 
inc", 0.0, true, 100.0, true))))
+                .build();
     }
 }
diff --git 
a/extra/nouveau/src/test/java/org/apache/couchdb/nouveau/core/IndexManagerTest.java
 
b/extra/nouveau/src/test/java/org/apache/couchdb/nouveau/core/IndexManagerTest.java
index e1d867bd2..b76ff1268 100644
--- 
a/extra/nouveau/src/test/java/org/apache/couchdb/nouveau/core/IndexManagerTest.java
+++ 
b/extra/nouveau/src/test/java/org/apache/couchdb/nouveau/core/IndexManagerTest.java
@@ -66,10 +66,9 @@ public class IndexManagerTest {
     public void managerReturnsUsableIndex() throws Exception {
         final IndexDefinition indexDefinition = new IndexDefinition();
         manager.create("foo", indexDefinition);
-        var searchRequest = new SearchRequest();
-        searchRequest.setQuery("*:*");
+        var searchRequest = new 
SearchRequest.Builder().setQuery("*:*").build();
         var searchResults = manager.with("foo", (index) -> 
index.search(searchRequest));
-        assertThat(searchResults.getTotalHits()).isEqualTo(0);
+        assertThat(searchResults.totalHits()).isEqualTo(0);
     }
 
     @Test
diff --git 
a/extra/nouveau/src/test/java/org/apache/couchdb/nouveau/lucene/LuceneIndexTest.java
 
b/extra/nouveau/src/test/java/org/apache/couchdb/nouveau/lucene/LuceneIndexTest.java
index 83ee15268..e8375184f 100644
--- 
a/extra/nouveau/src/test/java/org/apache/couchdb/nouveau/lucene/LuceneIndexTest.java
+++ 
b/extra/nouveau/src/test/java/org/apache/couchdb/nouveau/lucene/LuceneIndexTest.java
@@ -79,11 +79,12 @@ public class LuceneIndexTest {
                 final DocumentUpdateRequest request = new 
DocumentUpdateRequest(i - 1, i, null, fields);
                 index.update("doc" + i, request);
             }
-            final SearchRequest request = new SearchRequest();
-            request.setMinUpdateSeq(count);
-            request.setQuery("*:*");
+            final SearchRequest request = new SearchRequest.Builder()
+                    .setMinUpdateSeq(count)
+                    .setQuery("*:*")
+                    .build();
             final SearchResults results = index.search(request);
-            assertThat(results.getTotalHits()).isEqualTo(count);
+            assertThat(results.totalHits()).isEqualTo(count);
         } finally {
             cleanup(index);
         }
@@ -99,12 +100,13 @@ public class LuceneIndexTest {
                 final DocumentUpdateRequest request = new 
DocumentUpdateRequest(i - 1, i, null, fields);
                 index.update("doc" + i, request);
             }
-            final SearchRequest request = new SearchRequest();
-            request.setMinUpdateSeq(count);
-            request.setQuery("*:*");
-            request.setSort(List.of("foo"));
+            final SearchRequest request = new SearchRequest.Builder()
+                    .setMinUpdateSeq(count)
+                    .setQuery("*:*")
+                    .setSort(List.of("foo"))
+                    .build();
             final SearchResults results = index.search(request);
-            assertThat(results.getTotalHits()).isEqualTo(count);
+            assertThat(results.totalHits()).isEqualTo(count);
         } finally {
             cleanup(index);
         }
@@ -120,12 +122,13 @@ public class LuceneIndexTest {
                 final DocumentUpdateRequest request = new 
DocumentUpdateRequest(i - 1, i, null, fields);
                 index.update("doc" + i, request);
             }
-            final SearchRequest request = new SearchRequest();
-            request.setMinUpdateSeq(count);
-            request.setQuery("*:*");
-            request.setCounts(List.of("bar"));
+            final SearchRequest request = new SearchRequest.Builder()
+                    .setMinUpdateSeq(count)
+                    .setQuery("*:*")
+                    .setCounts(List.of("bar"))
+                    .build();
             final SearchResults results = index.search(request);
-            assertThat(results.getCounts()).isEqualTo(Map.of("bar", 
Map.of("baz", count)));
+            assertThat(results.counts()).isEqualTo(Map.of("bar", Map.of("baz", 
count)));
         } finally {
             cleanup(index);
         }
@@ -141,16 +144,17 @@ public class LuceneIndexTest {
                 final DocumentUpdateRequest request = new 
DocumentUpdateRequest(i - 1, i, null, fields);
                 index.update("doc" + i, request);
             }
-            final SearchRequest request = new SearchRequest();
-            request.setMinUpdateSeq(count);
-            request.setQuery("*:*");
-            request.setRanges(Map.of(
-                    "bar",
-                    List.of(
-                            new DoubleRange("low", 0.0, true, (double) count / 
2, true),
-                            new DoubleRange("high", (double) count / 2, true, 
(double) count, true))));
+            final SearchRequest request = new SearchRequest.Builder()
+                    .setMinUpdateSeq(count)
+                    .setQuery("*:*")
+                    .setRanges(Map.of(
+                            "bar",
+                            List.of(
+                                    new DoubleRange("low", 0.0, true, (double) 
count / 2, true),
+                                    new DoubleRange("high", (double) count / 
2, true, (double) count, true))))
+                    .build();
             final SearchResults results = index.search(request);
-            assertThat(results.getRanges()).isEqualTo(Map.of("bar", 
Map.of("low", count / 2, "high", count / 2 + 1)));
+            assertThat(results.ranges()).isEqualTo(Map.of("bar", Map.of("low", 
count / 2, "high", count / 2 + 1)));
         } finally {
             cleanup(index);
         }
@@ -173,13 +177,14 @@ public class LuceneIndexTest {
                 index.update("doc" + i, request);
             }
 
-            final SearchRequest request = new SearchRequest();
-            request.setMinUpdateSeq(count);
-            request.setQuery("*:*");
-            request.setCounts(List.of("bar"));
-            request.setTopN(1);
+            final SearchRequest request = new SearchRequest.Builder()
+                    .setMinUpdateSeq(count)
+                    .setQuery("*:*")
+                    .setCounts(List.of("bar"))
+                    .setTopN(1)
+                    .build();
             final SearchResults results = index.search(request);
-            assertThat(results.getCounts()).isEqualTo(Map.of("bar", 
Map.of("baz", count + 5)));
+            assertThat(results.counts()).isEqualTo(Map.of("bar", Map.of("baz", 
count + 5)));
         } finally {
             cleanup(index);
         }
@@ -214,9 +219,10 @@ public class LuceneIndexTest {
         try {
             // Require min seq 1 on new, empty index should fail.
             assertThrows(StaleIndexException.class, () -> {
-                final SearchRequest request = new SearchRequest();
-                request.setMinUpdateSeq(1);
-                request.setQuery("*:*");
+                final SearchRequest request = new SearchRequest.Builder()
+                        .setMinUpdateSeq(1)
+                        .setQuery("*:*")
+                        .build();
                 index.search(request);
             });
         } finally {

Reply via email to