Skip to content

Commit e95e3ad

Browse files
committed
Merge branch 'AddFTS' into AddTypeSafetyPropertyReferences
2 parents e35d597 + 081a434 commit e95e3ad

9 files changed

Lines changed: 77 additions & 24 deletions

src/main/java/org/springframework/data/couchbase/core/ExecutableFindBySearchOperation.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ default Optional<T> first() {
9292

9393
/**
9494
* Get all matching elements, hydrated as entities via KV GET.
95+
* <p>
96+
* If no limit is specified via {@code withLimit(...)} or {@code withOptions(...)}, a default limit of 10,000 is
97+
* applied (the FTS service would otherwise return only its default of 10 hits).
9598
*
9699
* @return never {@literal null}.
97100
*/

src/main/java/org/springframework/data/couchbase/core/ExecutableFindBySearchOperationSupport.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,11 @@ public Stream<T> stream() {
121121

122122
@Override
123123
public long count() {
124-
return reactiveSupport.count().block();
124+
Long count = reactiveSupport.count().block();
125+
if (count == null) {
126+
throw new CouchbaseQueryExecutionException("search count query did not return a count, index: " + indexName);
127+
}
128+
return count;
125129
}
126130

127131
@Override

src/main/java/org/springframework/data/couchbase/core/ReactiveFindBySearchOperation.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ interface TerminatingFindBySearch<T> {
7373

7474
/**
7575
* Get all matching elements, hydrated as entities via KV GET.
76+
* <p>
77+
* If no limit is specified via {@code withLimit(...)} or {@code withOptions(...)}, a default limit of 10,000 is
78+
* applied (the FTS service would otherwise return only its default of 10 hits).
7679
*
7780
* @return never {@literal null}.
7881
*/
@@ -121,6 +124,9 @@ interface FindBySearchWithQuery<T> extends TerminatingFindBySearch<T>, WithSearc
121124

122125
/**
123126
* Fluent method to specify options.
127+
* <p>
128+
* The given {@link SearchOptions} are passed to the SDK as provided (aside from collection routing) and must not be
129+
* combined with the individual {@code with*} configuration methods of this fluent API.
124130
*
125131
* @param <T> the entity type to use.
126132
*/

src/main/java/org/springframework/data/couchbase/core/ReactiveFindBySearchOperationSupport.java

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ public class ReactiveFindBySearchOperationSupport implements ReactiveFindBySearc
4949
private final ReactiveCouchbaseTemplate template;
5050
private static final Logger LOG = LoggerFactory.getLogger(ReactiveFindBySearchOperationSupport.class);
5151

52+
/**
53+
* Limit applied when neither {@code withLimit(...)} nor {@code withOptions(...)} is used. Without an explicit size
54+
* the FTS service returns only its default of 10 hits, which would silently truncate {@code all()} results.
55+
*/
56+
static final int DEFAULT_LIMIT = 10_000;
57+
5258
public ReactiveFindBySearchOperationSupport(final ReactiveCouchbaseTemplate template) {
5359
this.template = template;
5460
}
@@ -243,7 +249,7 @@ public Flux<T> all() {
243249
return TransactionalSupport.verifyNotInTransaction("findBySearch")
244250
.thenMany(executeSearch()
245251
.flatMapMany(ReactiveSearchResult::rows))
246-
.concatMap(this::hydrateRow)
252+
.flatMapSequential(this::hydrateRow)
247253
.onErrorMap(throwable -> {
248254
if (throwable instanceof RuntimeException) {
249255
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
@@ -260,9 +266,10 @@ public Mono<Long> count() {
260266
Assert.notNull(indexName, "Index name must be specified via withIndex()");
261267
Assert.notNull(searchRequest, "SearchRequest must be specified via matching()");
262268

269+
// rows must be drained before the SDK emits the metadata trailer, even with limit 0
263270
return TransactionalSupport.verifyNotInTransaction("findBySearch")
264-
.then(executeSearch())
265-
.flatMap(ReactiveSearchResult::metaData)
271+
.then(executeSearch(true))
272+
.flatMap(result -> result.rows().then(result.metaData()))
266273
.map(metaData -> metaData.metrics().totalRows())
267274
.onErrorMap(throwable -> {
268275
if (throwable instanceof RuntimeException) {
@@ -329,7 +336,7 @@ private Mono<SearchResult<T>> collectFullResult(ReactiveSearchResult reactiveRes
329336
java.util.Map<String, SearchFacetResult> facetResults = tuple.getT3();
330337

331338
return Flux.fromIterable(searchRows)
332-
.concatMap(row -> hydrateRow(row))
339+
.flatMapSequential(this::hydrateRow)
333340
.collectList()
334341
.map(entities -> new SearchResult<>(entities, searchRows, metaData, facetResults));
335342
});
@@ -343,18 +350,23 @@ private Mono<SearchResult<T>> collectFullResult(ReactiveSearchResult reactiveRes
343350
* misses typically indicate an out-of-sync FTS index.
344351
*/
345352
private Mono<T> hydrateRow(SearchRow row) {
353+
// findById already maps DocumentNotFoundException to an empty Mono, so misses surface as emptiness
346354
return template.findById(returnType)
347355
.inScope(scope)
348356
.inCollection(collection)
349357
.one(row.id())
350-
.onErrorResume(com.couchbase.client.core.error.DocumentNotFoundException.class, ex -> {
358+
.switchIfEmpty(Mono.defer(() -> {
351359
LOG.warn("Skipping stale FTS result for document id '{}': document not found in KV "
352360
+ "(index '{}' may be out of sync)", row.id(), indexName);
353361
return Mono.empty();
354-
});
362+
}));
355363
}
356364
private Mono<ReactiveSearchResult> executeSearch() {
357-
SearchOptions opts = buildSearchOptions();
365+
return executeSearch(false);
366+
}
367+
368+
private Mono<ReactiveSearchResult> executeSearch(boolean metadataOnly) {
369+
SearchOptions opts = buildSearchOptions(metadataOnly);
358370
if (scope != null) {
359371
return template.getCouchbaseClientFactory()
360372
.withScope(scope).getScope().reactive()
@@ -366,8 +378,19 @@ private Mono<ReactiveSearchResult> executeSearch() {
366378
}
367379
}
368380

369-
private SearchOptions buildSearchOptions() {
370-
SearchOptions opts = options != null ? options : SearchOptions.searchOptions();
381+
private SearchOptions buildSearchOptions(boolean metadataOnly) {
382+
if (options != null) {
383+
if (hasFluentOptions()) {
384+
throw new IllegalArgumentException("withOptions() cannot be combined with withConsistency(), withSort(), "
385+
+ "withHighlight(), withFacets(), withFields(), withLimit() or withSkip(); "
386+
+ "set those directly on the SearchOptions instead");
387+
}
388+
if (collection != null) {
389+
options.collections(collection);
390+
}
391+
return options;
392+
}
393+
SearchOptions opts = SearchOptions.searchOptions();
371394
if (scanConsistency != null) {
372395
opts.scanConsistency(scanConsistency);
373396
}
@@ -390,15 +413,24 @@ private SearchOptions buildSearchOptions() {
390413
if (fields != null && fields.length > 0) {
391414
opts.fields(fields);
392415
}
393-
if (limitSkip != null) {
394-
if (limitSkip[0] != null) {
416+
if (metadataOnly) {
417+
opts.limit(0);
418+
} else {
419+
if (limitSkip != null && limitSkip[0] != null) {
395420
opts.limit(limitSkip[0]);
421+
} else {
422+
opts.limit(DEFAULT_LIMIT);
396423
}
397-
if (limitSkip[1] != null) {
424+
if (limitSkip != null && limitSkip[1] != null) {
398425
opts.skip(limitSkip[1]);
399426
}
400427
}
401428
return opts;
402429
}
430+
431+
private boolean hasFluentOptions() {
432+
return scanConsistency != null || (sort != null && sort.length > 0) || highlightStyle != null
433+
|| (facets != null && !facets.isEmpty()) || (fields != null && fields.length > 0) || limitSkip != null;
434+
}
403435
}
404436
}

src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ public RepositoryQuery resolveQuery(final Method method, final RepositoryMetadat
168168
CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, factory, mappingContext);
169169

170170
if (queryMethod.hasSearchAnnotation()) {
171+
if (queryMethod.hasN1qlAnnotation()) {
172+
throw new IllegalArgumentException(
173+
"Method " + method + " must not be annotated with both @Search and @Query");
174+
}
171175
return new SearchBasedCouchbaseQuery(queryMethod, couchbaseOperations);
172176
} else if (queryMethod.hasN1qlAnnotation()) {
173177
return new StringBasedCouchbaseQuery(queryMethod, couchbaseOperations,

src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata,
157157
mappingContext);
158158

159159
if (queryMethod.hasSearchAnnotation()) {
160+
if (queryMethod.hasN1qlAnnotation()) {
161+
throw new IllegalArgumentException(
162+
"Method " + method + " must not be annotated with both @Search and @Query");
163+
}
160164
return new ReactiveSearchBasedCouchbaseQuery(queryMethod, couchbaseOperations);
161165
} else if (queryMethod.hasN1qlAnnotation()) {
162166
return new ReactiveStringBasedCouchbaseQuery(queryMethod, couchbaseOperations,

src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateFtsIntegrationTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
3333
import org.springframework.data.couchbase.repository.Collection;
3434
import org.springframework.data.couchbase.repository.Scope;
35+
import org.springframework.data.couchbase.util.Capabilities;
3536
import org.springframework.data.couchbase.util.ClusterType;
3637
import org.springframework.data.couchbase.util.IgnoreWhen;
3738
import org.springframework.data.couchbase.util.JavaIntegrationTests;
@@ -58,7 +59,7 @@
5859
* @author Emilien Bevierre
5960
* @since 6.2
6061
*/
61-
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
62+
@IgnoreWhen(clusterTypes = ClusterType.MOCKED, missesCapabilities = Capabilities.SEARCH)
6263
class CouchbaseTemplateFtsIntegrationTests extends JavaIntegrationTests {
6364

6465
private static final String INDEX_NAME = "sd-fts-airport-idx";

src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateSearchIntegrationTests.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.beans.factory.annotation.Autowired;
2828
import org.springframework.data.couchbase.domain.Config;
2929
import org.springframework.data.couchbase.domain.User;
30+
import org.springframework.data.couchbase.util.Capabilities;
3031
import org.springframework.data.couchbase.util.ClusterType;
3132
import org.springframework.data.couchbase.util.IgnoreWhen;
3233
import org.springframework.data.couchbase.util.JavaIntegrationTests;
@@ -51,7 +52,7 @@
5152
*
5253
* @since 6.2
5354
*/
54-
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
55+
@IgnoreWhen(clusterTypes = ClusterType.MOCKED, missesCapabilities = Capabilities.SEARCH)
5556
@SpringJUnitConfig(Config.class)
5657
@DirtiesContext
5758
class CouchbaseTemplateSearchIntegrationTests extends JavaIntegrationTests {
@@ -341,16 +342,13 @@ private static void waitForFtsIndex(Cluster cluster, String indexName) {
341342
cluster.searchQuery(indexName, SearchQuery.queryString("*"));
342343
return;
343344
} catch (Exception ex) {
344-
if (i < maxRetries - 1 && (ex.getMessage().contains("no planPIndexes")
345-
|| ex.getMessage().contains("pindex_consistency")
346-
|| ex.getMessage().contains("pindex not available")
347-
|| ex.getMessage().contains("index not found"))) {
348-
sleepMs(2000);
349-
continue;
350-
}
351-
if (i >= maxRetries - 1) {
345+
String msg = ex.getMessage() != null ? ex.getMessage() : "";
346+
boolean retryable = msg.contains("no planPIndexes") || msg.contains("pindex_consistency")
347+
|| msg.contains("pindex not available") || msg.contains("index not found");
348+
if (!retryable || i >= maxRetries - 1) {
352349
throw new RuntimeException("FTS index " + indexName + " did not become ready in time", ex);
353350
}
351+
sleepMs(2000);
354352
}
355353
}
356354
}

src/test/java/org/springframework/data/couchbase/repository/ReactiveSearchRepositoryFtsIntegrationTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
3030
import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping;
3131
import org.springframework.data.couchbase.repository.support.ReactiveCouchbaseRepositoryFactory;
32+
import org.springframework.data.couchbase.util.Capabilities;
3233
import org.springframework.data.couchbase.util.ClusterType;
3334
import org.springframework.data.couchbase.util.IgnoreWhen;
3435
import org.springframework.data.couchbase.util.JavaIntegrationTests;
@@ -55,7 +56,7 @@
5556
* @author Emilien Bevierre
5657
* @since 6.2
5758
*/
58-
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
59+
@IgnoreWhen(clusterTypes = ClusterType.MOCKED, missesCapabilities = Capabilities.SEARCH)
5960
class ReactiveSearchRepositoryFtsIntegrationTests extends JavaIntegrationTests {
6061

6162
private static final String INDEX_NAME = "sd-fts-airport-idx";

0 commit comments

Comments
 (0)