From c0e8aa6c238d95918fdcf788f6bf52afb1d1546e Mon Sep 17 00:00:00 2001 From: honhimW Date: Tue, 24 Sep 2024 16:52:56 +0800 Subject: [PATCH] feat: utility multi-search on single index (#75) * fix: GsonJsonHandler duration & number deserialize * update * update: remove swagger annotations * update: @Schema Retention * CI: update github action server image:1.10.0 * feat: utility multi-search on single index --- .github/workflows/gradle-oss-publish.yml | 2 +- .github/workflows/gradle.yml | 2 +- .../java/io/github/honhimw/ms/api/Search.java | 22 +- .../honhimw/ms/api/TypedDetailsSearch.java | 23 +- .../io/github/honhimw/ms/api/TypedSearch.java | 20 +- .../api/reactive/ReactiveMSearchClient.java | 3 +- .../ms/api/reactive/ReactiveSearch.java | 24 +- .../reactive/ReactiveTypedDetailsSearch.java | 27 +- .../ms/api/reactive/ReactiveTypedSearch.java | 24 +- .../honhimw/ms/internal/SearchImpl.java | 10 + .../ms/internal/TypedDetailsSearchImpl.java | 17 +- .../honhimw/ms/internal/TypedSearchImpl.java | 10 + .../internal/reactive/ReactiveSearchImpl.java | 9 + .../ReactiveTypedDetailsSearchImpl.java | 7 + .../reactive/ReactiveTypedSearchImpl.java | 6 + .../ms/model/AttributeSearchRequest.java | 466 ++++++++++++++++++ .../honhimw/ms/support/CollectionUtils.java | 25 + .../java/io/github/honhimw/ms/MainRunner.java | 1 - .../github/honhimw/ms/client/SearchTests.java | 15 + 19 files changed, 693 insertions(+), 20 deletions(-) create mode 100644 src/main/java/io/github/honhimw/ms/model/AttributeSearchRequest.java diff --git a/.github/workflows/gradle-oss-publish.yml b/.github/workflows/gradle-oss-publish.yml index 1bb673c..67c0de4 100644 --- a/.github/workflows/gradle-oss-publish.yml +++ b/.github/workflows/gradle-oss-publish.yml @@ -27,7 +27,7 @@ jobs: distribution: 'temurin' - name: Start MeiliSearch - run: docker run -d -p 7700:7700 -e MEILI_ENV='development' -e MEILI_MASTER_KEY='MASTER_KEY' getmeili/meilisearch:v1.10.0-rc.1 + run: docker run -d -p 7700:7700 -e MEILI_ENV='development' -e MEILI_MASTER_KEY='MASTER_KEY' getmeili/meilisearch:v1.10.0 # Configure Gradle for optimal use in GiHub Actions, including caching of downloaded dependencies. # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index ec75054..a68b293 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -29,7 +29,7 @@ jobs: distribution: 'temurin' - name: Start MeiliSearch - run: docker run -d -p 7700:7700 -e MEILI_ENV='development' -e MEILI_MASTER_KEY='MASTER_KEY' getmeili/meilisearch:v1.10.0-rc.1 + run: docker run -d -p 7700:7700 -e MEILI_ENV='development' -e MEILI_MASTER_KEY='MASTER_KEY' getmeili/meilisearch:v1.10.0 # Configure Gradle for optimal use in GiHub Actions, including caching of downloaded dependencies. # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md diff --git a/src/main/java/io/github/honhimw/ms/api/Search.java b/src/main/java/io/github/honhimw/ms/api/Search.java index 8ba7ebb..69177d1 100644 --- a/src/main/java/io/github/honhimw/ms/api/Search.java +++ b/src/main/java/io/github/honhimw/ms/api/Search.java @@ -15,10 +15,10 @@ package io.github.honhimw.ms.api; import io.github.honhimw.ms.Experimental; +import io.github.honhimw.ms.api.annotation.Operation; import io.github.honhimw.ms.json.TypeRef; import io.github.honhimw.ms.model.*; import io.github.honhimw.ms.support.TypeRefs; -import io.github.honhimw.ms.api.annotation.Operation; import java.util.Map; import java.util.function.Consumer; @@ -151,6 +151,26 @@ public interface Search { @Operation(method = "POST", paths = "/indexes/{indexUid}/facet-search") FacetSearchResponse facetSearch(Consumer builder); + /** + * An utility api to search multiple queries in a single request on the same index. + * + * @param request search request + * @return search result + */ + @Operation(method = "POST", paths = "/multi-search") + SearchResponse> multiSearch(AttributeSearchRequest request); + + /** + * An utility api to search multiple queries in a single request on the same index. + * + * @param request search request + * @param typeRef type reference + * @param document type + * @return search result + */ + @Operation(method = "POST", paths = "/multi-search") + SearchResponse multiSearch(AttributeSearchRequest request, TypeRef typeRef); + /** * To retrieve similar documents in your datasets, two new routes have been introduced * diff --git a/src/main/java/io/github/honhimw/ms/api/TypedDetailsSearch.java b/src/main/java/io/github/honhimw/ms/api/TypedDetailsSearch.java index e6c8d0b..5c46585 100644 --- a/src/main/java/io/github/honhimw/ms/api/TypedDetailsSearch.java +++ b/src/main/java/io/github/honhimw/ms/api/TypedDetailsSearch.java @@ -14,11 +14,8 @@ package io.github.honhimw.ms.api; -import io.github.honhimw.ms.model.FacetSearchRequest; -import io.github.honhimw.ms.model.FacetSearchResponse; -import io.github.honhimw.ms.model.SearchDetailsResponse; -import io.github.honhimw.ms.model.SearchRequest; import io.github.honhimw.ms.api.annotation.Operation; +import io.github.honhimw.ms.model.*; import java.util.function.Consumer; @@ -96,4 +93,22 @@ default FacetSearchResponse facetSearch(Consumer bui return facetSearch(_builder.build()); } + /** + * An utility api to search multiple queries in a single request on the same index. + * + * @param request search request + * @return search result + */ + @Operation(method = "POST", paths = "/multi-search") + SearchDetailsResponse multiSearch(AttributeSearchRequest request); + + /** + * An utility api to search multiple queries in a single request on the same index. + * + * @param builder request builder + * @return search result + */ + @Operation(method = "POST", paths = "/multi-search") + SearchDetailsResponse multiSearch(Consumer builder); + } diff --git a/src/main/java/io/github/honhimw/ms/api/TypedSearch.java b/src/main/java/io/github/honhimw/ms/api/TypedSearch.java index e547a4b..f31e895 100644 --- a/src/main/java/io/github/honhimw/ms/api/TypedSearch.java +++ b/src/main/java/io/github/honhimw/ms/api/TypedSearch.java @@ -14,8 +14,8 @@ package io.github.honhimw.ms.api; -import io.github.honhimw.ms.model.*; import io.github.honhimw.ms.api.annotation.Operation; +import io.github.honhimw.ms.model.*; import java.util.function.Consumer; @@ -85,6 +85,24 @@ public interface TypedSearch { @Operation(method = "POST", paths = "/indexes/{indexUid}/facet-search") FacetSearchResponse facetSearch(Consumer builder); + /** + * An utility api to search multiple queries in a single request on the same index. + * + * @param request search request + * @return search result + */ + @Operation(method = "POST", paths = "/multi-search") + SearchResponse multiSearch(AttributeSearchRequest request); + + /** + * An utility api to search multiple queries in a single request on the same index. + * + * @param builder request builder + * @return search result + */ + @Operation(method = "POST", paths = "/multi-search") + SearchResponse multiSearch(Consumer builder); + /** * To retrieve similar documents in your datasets, two new routes have been introduced * diff --git a/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveMSearchClient.java b/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveMSearchClient.java index dbfd765..7b2ef58 100644 --- a/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveMSearchClient.java +++ b/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveMSearchClient.java @@ -15,9 +15,9 @@ package io.github.honhimw.ms.api.reactive; import io.github.honhimw.ms.MSearchConfig; +import io.github.honhimw.ms.api.annotation.Operation; import io.github.honhimw.ms.internal.reactive.ReactiveMSearchClientImpl; import io.github.honhimw.ms.model.*; -import io.github.honhimw.ms.api.annotation.Operation; import reactor.core.publisher.Mono; import java.util.Map; @@ -112,6 +112,7 @@ default R keys(Function operation) { * * @param request multi-search request with federation * @return multi-search result federated + * @since v1.10 */ @Operation(method = "POST", paths = "/multi-search") Mono>> multiSearch(MultiSearchWithFederationRequest request); diff --git a/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveSearch.java b/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveSearch.java index 0ea7e44..744d84d 100644 --- a/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveSearch.java +++ b/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveSearch.java @@ -15,10 +15,10 @@ package io.github.honhimw.ms.api.reactive; import io.github.honhimw.ms.Experimental; +import io.github.honhimw.ms.api.annotation.Operation; import io.github.honhimw.ms.json.TypeRef; import io.github.honhimw.ms.model.*; import io.github.honhimw.ms.support.TypeRefs; -import io.github.honhimw.ms.api.annotation.Operation; import reactor.core.publisher.Mono; import java.util.Map; @@ -167,6 +167,28 @@ default Mono facetSearch(Consumer>> multiSearch(AttributeSearchRequest request) { + return multiSearch(request, TypeRefs.StringObjectMapRef.INSTANCE); + } + + /** + * An utility api to search multiple queries in a single request on the same index. + * + * @param request search request + * @param typeRef type reference + * @param document type + * @return search result + */ + @Operation(method = "POST", paths = "/multi-search") + Mono> multiSearch(AttributeSearchRequest request, TypeRef typeRef); + /** * To retrieve similar documents in your datasets, two new routes have been introduced * diff --git a/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveTypedDetailsSearch.java b/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveTypedDetailsSearch.java index cae62da..bae442a 100644 --- a/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveTypedDetailsSearch.java +++ b/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveTypedDetailsSearch.java @@ -14,10 +14,7 @@ package io.github.honhimw.ms.api.reactive; -import io.github.honhimw.ms.model.FacetSearchRequest; -import io.github.honhimw.ms.model.FacetSearchResponse; -import io.github.honhimw.ms.model.SearchDetailsResponse; -import io.github.honhimw.ms.model.SearchRequest; +import io.github.honhimw.ms.model.*; import io.github.honhimw.ms.api.annotation.Operation; import reactor.core.publisher.Mono; @@ -96,4 +93,26 @@ default Mono facetSearch(Consumer> multiSearch(AttributeSearchRequest request); + + /** + * An utility api to search multiple queries in a single request on the same index. + * + * @param builder request builder + * @return search result + */ + @Operation(method = "POST", paths = "/multi-search") + default Mono> multiSearch(Consumer builder) { + AttributeSearchRequest.Builder _builder = AttributeSearchRequest.builder(); + builder.accept(_builder); + return multiSearch(_builder.build()); + } + } diff --git a/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveTypedSearch.java b/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveTypedSearch.java index 5e1bf10..67d185f 100644 --- a/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveTypedSearch.java +++ b/src/main/java/io/github/honhimw/ms/api/reactive/ReactiveTypedSearch.java @@ -14,8 +14,8 @@ package io.github.honhimw.ms.api.reactive; -import io.github.honhimw.ms.model.*; import io.github.honhimw.ms.api.annotation.Operation; +import io.github.honhimw.ms.model.*; import reactor.core.publisher.Mono; import java.util.function.Consumer; @@ -93,6 +93,28 @@ default Mono facetSearch(Consumer> multiSearch(AttributeSearchRequest request); + + /** + * An utility api to search multiple queries in a single request on the same index. + * + * @param builder request builder + * @return search result + */ + @Operation(method = "POST", paths = "/multi-search") + default Mono> multiSearch(Consumer builder) { + AttributeSearchRequest.Builder _builder = AttributeSearchRequest.builder(); + builder.accept(_builder); + return multiSearch(_builder.build()); + } + /** * To retrieve similar documents in your datasets, two new routes have been introduced * diff --git a/src/main/java/io/github/honhimw/ms/internal/SearchImpl.java b/src/main/java/io/github/honhimw/ms/internal/SearchImpl.java index 5f0e304..5b54c25 100644 --- a/src/main/java/io/github/honhimw/ms/internal/SearchImpl.java +++ b/src/main/java/io/github/honhimw/ms/internal/SearchImpl.java @@ -105,6 +105,16 @@ public FacetSearchResponse facetSearch(Consumer buil return ReactorUtils.blockNonNull(_search.facetSearch(builder)); } + @Override + public SearchResponse> multiSearch(AttributeSearchRequest request) { + return ReactorUtils.blockNonNull(_search.multiSearch(request)); + } + + @Override + public SearchResponse multiSearch(AttributeSearchRequest request, TypeRef typeRef) { + return ReactorUtils.blockNonNull(_search.multiSearch(request, typeRef)); + } + @Override public SearchResponse similar(SimilarSearchRequest request, TypeRef typeRef) { return ReactorUtils.blockNonNull(_search.similar(request, typeRef)); diff --git a/src/main/java/io/github/honhimw/ms/internal/TypedDetailsSearchImpl.java b/src/main/java/io/github/honhimw/ms/internal/TypedDetailsSearchImpl.java index aaeb3a4..c89d9f8 100644 --- a/src/main/java/io/github/honhimw/ms/internal/TypedDetailsSearchImpl.java +++ b/src/main/java/io/github/honhimw/ms/internal/TypedDetailsSearchImpl.java @@ -30,12 +30,11 @@ import io.github.honhimw.ms.api.TypedDetailsSearch; import io.github.honhimw.ms.api.reactive.ReactiveTypedDetailsSearch; -import io.github.honhimw.ms.model.FacetSearchRequest; -import io.github.honhimw.ms.model.FacetSearchResponse; -import io.github.honhimw.ms.model.SearchDetailsResponse; -import io.github.honhimw.ms.model.SearchRequest; +import io.github.honhimw.ms.model.*; import io.github.honhimw.ms.support.ReactorUtils; +import java.util.function.Consumer; + /** * @author hon_him * @since 2024-01-26 @@ -63,4 +62,14 @@ public SearchDetailsResponse find(SearchRequest request) { public FacetSearchResponse facetSearch(FacetSearchRequest request) { return ReactorUtils.blockNonNull(_search.facetSearch(request)); } + + @Override + public SearchDetailsResponse multiSearch(AttributeSearchRequest request) { + return ReactorUtils.blockNonNull(_search.multiSearch(request)); + } + + @Override + public SearchDetailsResponse multiSearch(Consumer builder) { + return ReactorUtils.blockNonNull(_search.multiSearch(builder)); + } } diff --git a/src/main/java/io/github/honhimw/ms/internal/TypedSearchImpl.java b/src/main/java/io/github/honhimw/ms/internal/TypedSearchImpl.java index c6a6ccd..4ee3811 100644 --- a/src/main/java/io/github/honhimw/ms/internal/TypedSearchImpl.java +++ b/src/main/java/io/github/honhimw/ms/internal/TypedSearchImpl.java @@ -73,6 +73,16 @@ public FacetSearchResponse facetSearch(Consumer buil return ReactorUtils.blockNonNull(_search.facetSearch(builder)); } + @Override + public SearchResponse multiSearch(AttributeSearchRequest request) { + return ReactorUtils.blockNonNull(_search.multiSearch(request)); + } + + @Override + public SearchResponse multiSearch(Consumer builder) { + return ReactorUtils.blockNonNull(_search.multiSearch(builder)); + } + @Override public SearchResponse similar(SimilarSearchRequest request) { return ReactorUtils.blockNonNull(_search.similar(request)); diff --git a/src/main/java/io/github/honhimw/ms/internal/reactive/ReactiveSearchImpl.java b/src/main/java/io/github/honhimw/ms/internal/reactive/ReactiveSearchImpl.java index 0be4aa1..a428b53 100644 --- a/src/main/java/io/github/honhimw/ms/internal/reactive/ReactiveSearchImpl.java +++ b/src/main/java/io/github/honhimw/ms/internal/reactive/ReactiveSearchImpl.java @@ -83,6 +83,15 @@ public Mono facetSearch(FacetSearchRequest request) { , TypeRefs.of(FacetSearchResponse.class)); } + @Override + public Mono> multiSearch(AttributeSearchRequest request, TypeRef typeRef) { + MultiSearchWithFederationRequest multiSearch = request.toMultiSearch(indexUid); + return post("/multi-search", configurer -> configurer + .body(payload -> payload.raw(raw -> raw.json(jsonHandler.toJson(multiSearch)))) + , new ComplexTypeRef>(typeRef) { + }); + } + @Override public Mono> similar(SimilarSearchRequest request, TypeRef typeRef) { return post(String.format("/indexes/%s/similar", indexUid), configurer -> configurer diff --git a/src/main/java/io/github/honhimw/ms/internal/reactive/ReactiveTypedDetailsSearchImpl.java b/src/main/java/io/github/honhimw/ms/internal/reactive/ReactiveTypedDetailsSearchImpl.java index 6408994..084fd29 100644 --- a/src/main/java/io/github/honhimw/ms/internal/reactive/ReactiveTypedDetailsSearchImpl.java +++ b/src/main/java/io/github/honhimw/ms/internal/reactive/ReactiveTypedDetailsSearchImpl.java @@ -62,6 +62,13 @@ public Mono facetSearch(FacetSearchRequest request) { return post(String.format("/indexes/%s/facet-search", indexUid), configurer -> json(configurer, jsonHandler.toJson(request)), TypeRefs.of(FacetSearchResponse.class)); } + @Override + public Mono> multiSearch(AttributeSearchRequest request) { + MultiSearchWithFederationRequest multiSearch = request.toMultiSearch(indexUid); + return post("/multi-search", configurer -> json(configurer, jsonHandler.toJson(multiSearch)), TypeRefs.StringObjectMapSearchResponseRef.INSTANCE) + .map(this::transform); + } + private SearchDetailsResponse transform(SearchResponse> searchResponse) { SearchDetailsResponse response = new SearchDetailsResponse<>(); response.setOffset(searchResponse.getOffset()); diff --git a/src/main/java/io/github/honhimw/ms/internal/reactive/ReactiveTypedSearchImpl.java b/src/main/java/io/github/honhimw/ms/internal/reactive/ReactiveTypedSearchImpl.java index aba0d09..77cedf3 100644 --- a/src/main/java/io/github/honhimw/ms/internal/reactive/ReactiveTypedSearchImpl.java +++ b/src/main/java/io/github/honhimw/ms/internal/reactive/ReactiveTypedSearchImpl.java @@ -60,6 +60,12 @@ public Mono facetSearch(FacetSearchRequest request) { return post(String.format("/indexes/%s/facet-search", indexUid), configurer -> json(configurer, jsonHandler.toJson(request)), TypeRefs.of(FacetSearchResponse.class)); } + @Override + public Mono> multiSearch(AttributeSearchRequest request) { + MultiSearchWithFederationRequest multiSearch = request.toMultiSearch(indexUid); + return post("/multi-search", configurer -> json(configurer, jsonHandler.toJson(multiSearch)), complexTypeRef); + } + @Override public Mono> similar(SimilarSearchRequest request) { return post(String.format("/indexes/%s/similar", indexUid), configurer -> json(configurer, jsonHandler.toJson(request)), complexTypeRef); diff --git a/src/main/java/io/github/honhimw/ms/model/AttributeSearchRequest.java b/src/main/java/io/github/honhimw/ms/model/AttributeSearchRequest.java new file mode 100644 index 0000000..14e4052 --- /dev/null +++ b/src/main/java/io/github/honhimw/ms/model/AttributeSearchRequest.java @@ -0,0 +1,466 @@ +/* + * 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 io.github.honhimw.ms.model; + +import io.github.honhimw.ms.api.annotation.Schema; +import io.github.honhimw.ms.support.CollectionUtils; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.*; + +/** + * Search request + * + * @author hon_him + * @since 2024-08-19 v1.10.0.1 + */ + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +public class AttributeSearchRequest extends FilterableAttributesRequest { + + /** + * Queries with specific attributes + */ + @Schema(description = "Queries with specific attributes") + private List queries; + + /** + * Attributes to display in the returned documents", defaultValue = "[\"*\"] + */ + @Schema(description = "Attributes to display in the returned documents", defaultValue = "[\"*\"]") + private List attributesToRetrieve; + + /** + * Attributes whose values have to be cropped + */ + @Schema(description = "Attributes whose values have to be cropped") + private List attributesToCrop; + + /** + * Maximum length of cropped value in words", defaultValue = "10 + */ + @Schema(description = "Maximum length of cropped value in words", defaultValue = "10") + private Integer cropLength; + + /** + * String marking crop boundaries", defaultValue = "\"...\" + */ + @Schema(description = "String marking crop boundaries", defaultValue = "\"...\"") + private String cropMarker; + + /** + * Highlight matching terms contained in an attribute + */ + @Schema(description = "Highlight matching terms contained in an attribute") + private List attributesToHighlight; + + /** + * String inserted at the start of a highlighted term" + */ + @Schema(description = "String inserted at the start of a highlighted term", defaultValue = "\"\"") + private String highlightPreTag; + + /** + * String inserted at the end of a highlighted term" + */ + @Schema(description = "String inserted at the end of a highlighted term", defaultValue = "\"\"") + private String highlightPostTag; + + /** + * Return matching terms location", defaultValue = "false + */ + @Schema(description = "Return matching terms location", defaultValue = "false") + private Boolean showMatchesPosition; + + /** + * Sort search results by an attribute's value + */ + @Schema(description = "Sort search results by an attribute's value", defaultValue = "null") + private List sort; + + /** + * Strategy used to match query terms within documents. + *

+ * Expected value: last or all + *

+ * last: returns documents containing all the query terms first. If there are not enough results containing all query terms to meet the requested limit, Meilisearch will remove one query term at a time, starting from the end of the query. + *

+ * all: only returns documents that contain all query terms. Meilisearch will not match any more documents even if there aren't enough to meet the requested limit. + */ + @Schema(description = "Strategy used to match query terms within documents", defaultValue = "last") + private MatchingStrategy matchingStrategy; + + /** + * Display the global ranking score of a document", defaultValue = "false + */ + @Schema(description = "Display the global ranking score of a document", defaultValue = "false") + private Boolean showRankingScore; + + /** + * Display the global ranking score of a document", defaultValue = "false + */ + @Schema(description = "Display the global ranking score of a document", defaultValue = "false") + private Boolean showRankingScoreDetails; + + /** + * @since v1.9.0 + */ + @Schema(description = "If a distinct attribute is already defined in the settings it'll be ignored in favor of the one defined at search time.") + private String distinct; + + /** + * Meilisearch does not return any documents below the configured threshold. Excluded results do not count towards estimatedTotalHits, totalHits, and facet distribution. + *

+ * For performance reasons, if the number of documents above rankingScoreThreshold is higher than limit, Meilisearch does not evaluate the ranking score of the remaining documents. Results ranking below the threshold are not immediately removed from the set of candidates. In this case, Meilisearch may overestimate the count of estimatedTotalHits, totalHits and facet distribution. + * + * @since v1.9.0 + */ + @Schema(description = "Exclude search results with low ranking scores") + private Number rankingScoreThreshold; + + /** + * If federation is empty ({}) default values of offset and limit are used, so respectively 0 and 20. + * If federation is null or missing, a classic multi-search will be applied, so a list of search result objects for each index will be returned. + */ + @Schema(description = "return a single search result object, whose list of hits is built by merging the hits coming from all the queries in descending ranking score order", defaultValue = "null") + private MultiSearchWithFederationRequest.Federation federation; + + /** + * allowing the end-user to define the language used in the current query. + *

+ * The locales parameter overrides eventual locales in the index settings. + * + * @since v1.10 + */ + @Schema(description = "allowing the end-user to define the language used in the current query") + private List locales; + + /** + * Build MultiSearchWithFederationRequest + * @param indexUid index + * @return MultiSearchWithFederationRequest + */ + public MultiSearchWithFederationRequest toMultiSearch(String indexUid) { + List queries = this.getQueries(); + MultiSearchWithFederationRequest.Builder builder = MultiSearchWithFederationRequest.builder(); + queries.forEach(query -> { + String q = query.getQ(); + List attributesToSearchOn = query.getAttributesToSearchOn(); + builder.addQuery(indexUid, SearchRequest.builder() + .q(q) + .attributesToSearchOn(attributesToSearchOn) + .attributesToRetrieve(this.getAttributesToRetrieve()) + .attributesToCrop(this.getAttributesToCrop()) + .cropLength(this.getCropLength()) + .cropMarker(this.getCropMarker()) + .attributesToHighlight(this.getAttributesToHighlight()) + .highlightPreTag(this.getHighlightPreTag()) + .highlightPostTag(this.getHighlightPostTag()) + .sort(this.getSort()) + .matchingStrategy(this.getMatchingStrategy()) + .distinct(this.getDistinct()) + .build() + ); + }); + MultiSearchWithFederationRequest.Federation federation = Optional.ofNullable(this.getFederation()).orElseGet(() -> new MultiSearchWithFederationRequest.Federation(0, 20)); + builder.federation(federation); + return builder.build(); + } + + @Data + @EqualsAndHashCode(callSuper = false) + @NoArgsConstructor + @AllArgsConstructor + public static class AttributeSearch implements Serializable { + /** + * Query string + */ + @Schema(description = "Query string") + private String q; + + /** + * Restrict search to the specified attributes", defaultValue = "[\"*\"] + */ + @Schema(description = "Restrict search to the specified attributes", defaultValue = "[\"*\"]") + private List attributesToSearchOn; + } + + private AttributeSearchRequest(Builder builder) { + setQueries(builder.queries); + setFilter(builder.filter); + setAttributesToRetrieve(builder.attributesToRetrieve); + setAttributesToCrop(builder.attributesToCrop); + setCropLength(builder.cropLength); + setCropMarker(builder.cropMarker); + setAttributesToHighlight(builder.attributesToHighlight); + setHighlightPreTag(builder.highlightPreTag); + setHighlightPostTag(builder.highlightPostTag); + setShowMatchesPosition(builder.showMatchesPosition); + setSort(builder.sort); + setMatchingStrategy(builder.matchingStrategy); + setShowRankingScore(builder.showRankingScore); + setShowRankingScoreDetails(builder.showRankingScoreDetails); + setDistinct(builder.distinct); + setRankingScoreThreshold(builder.rankingScoreThreshold); + setFederation(builder.federation); + } + + /** + * Creates and returns a new instance of the Builder class. + * + * @return a new instance of the Builder class + */ + public static Builder builder() { + return new Builder(); + } + + + /** + * {@code SearchRequest} builder static inner class. + */ + public static final class Builder { + private String filter; + private List queries; + private List attributesToRetrieve; + private List attributesToCrop; + private Integer cropLength; + private String cropMarker; + private List attributesToHighlight; + private String highlightPreTag; + private String highlightPostTag; + private Boolean showMatchesPosition; + private List sort; + private MatchingStrategy matchingStrategy; + private Boolean showRankingScore; + private Boolean showRankingScoreDetails; + private String distinct; + private Number rankingScoreThreshold; + private MultiSearchWithFederationRequest.Federation federation; + + private Builder() { + } + + /** + * Sets the {@code filter} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code filter} to set + * @return a reference to this Builder + */ + public Builder filter(String val) { + filter = val; + return this; + } + + /** + * Sets the {@code attributesToRetrieve} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code attributesToRetrieve} to set + * @return a reference to this Builder + */ + public Builder attributesToRetrieve(List val) { + attributesToRetrieve = val; + return this; + } + + /** + * Sets the {@code attributesToCrop} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code attributesToCrop} to set + * @return a reference to this Builder + */ + public Builder attributesToCrop(List val) { + attributesToCrop = val; + return this; + } + + /** + * Sets the {@code cropLength} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code cropLength} to set + * @return a reference to this Builder + */ + public Builder cropLength(Integer val) { + cropLength = val; + return this; + } + + /** + * Sets the {@code cropMarker} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code cropMarker} to set + * @return a reference to this Builder + */ + public Builder cropMarker(String val) { + cropMarker = val; + return this; + } + + /** + * Sets the {@code attributesToHighlight} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code attributesToHighlight} to set + * @return a reference to this Builder + */ + public Builder attributesToHighlight(List val) { + attributesToHighlight = val; + return this; + } + + /** + * Sets the {@code highlightPreTag} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code highlightPreTag} to set + * @return a reference to this Builder + */ + public Builder highlightPreTag(String val) { + highlightPreTag = val; + return this; + } + + /** + * Sets the {@code highlightPostTag} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code highlightPostTag} to set + * @return a reference to this Builder + */ + public Builder highlightPostTag(String val) { + highlightPostTag = val; + return this; + } + + /** + * Sets the {@code showMatchesPosition} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code showMatchesPosition} to set + * @return a reference to this Builder + */ + public Builder showMatchesPosition(Boolean val) { + showMatchesPosition = val; + return this; + } + + /** + * Sets the {@code sort} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code sort} to set + * @return a reference to this Builder + */ + public Builder sort(List val) { + sort = val; + return this; + } + + /** + * Sets the {@code matchingStrategy} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code matchingStrategy} to set + * @return a reference to this Builder + */ + public Builder matchingStrategy(MatchingStrategy val) { + matchingStrategy = val; + return this; + } + + /** + * Sets the {@code showRankingScore} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code showRankingScore} to set + * @return a reference to this Builder + */ + public Builder showRankingScore(Boolean val) { + showRankingScore = val; + return this; + } + + /** + * Sets the {@code showRankingScoreDetails} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code showRankingScoreDetails} to set + * @return a reference to this Builder + */ + public Builder showRankingScoreDetails(Boolean val) { + showRankingScoreDetails = val; + return this; + } + + /** + * Search on specific attributes, at least one attribute is required + * @param q content to search + * @param attribute attribute to search + * @param attributes attributes to search + * @return a reference to this Builder + */ + public Builder queryOn(String q, String attribute, String... attributes) { + if (Objects.isNull(this.queries)) { + this.queries = new ArrayList<>(); + } + + AttributeSearch attributeSearch = new AttributeSearch(); + attributeSearch.setQ(q); + Set attributesToSearchOn = new HashSet<>(); + attributesToSearchOn.add(attribute); + if (CollectionUtils.isNotEmpty(attributes)) { + Collections.addAll(attributesToSearchOn, attributes); + } + attributeSearch.setAttributesToSearchOn(new ArrayList<>(attributesToSearchOn)); + queries.add(attributeSearch); + return this; + } + + /** + * Sets the {@code distinct} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code distinct} to set + * @return a reference to this Builder + */ + public Builder distinct(String val) { + distinct = val; + return this; + } + + /** + * Sets the {@code rankingScoreThreshold} and returns a reference to this Builder enabling method chaining. + * + * @param val the {@code rankingScoreThreshold} to set + * @return a reference to this Builder + */ + public Builder rankingScoreThreshold(Number val) { + rankingScoreThreshold = val; + return this; + } + + public Builder federation(MultiSearchWithFederationRequest.Federation federation) { + this.federation = federation; + return this; + } + + /** + * Returns a {@code SearchRequest} built from the parameters previously set. + * + * @return a {@code SearchRequest} built with parameters of this {@code SearchRequest.Builder} + */ + public AttributeSearchRequest build() { + Objects.requireNonNull(this.queries, "queries cannot be null"); + return new AttributeSearchRequest(this); + } + } +} diff --git a/src/main/java/io/github/honhimw/ms/support/CollectionUtils.java b/src/main/java/io/github/honhimw/ms/support/CollectionUtils.java index 74d90bd..29200e9 100644 --- a/src/main/java/io/github/honhimw/ms/support/CollectionUtils.java +++ b/src/main/java/io/github/honhimw/ms/support/CollectionUtils.java @@ -14,6 +14,7 @@ package io.github.honhimw.ms.support; +import java.lang.reflect.Array; import java.util.Collection; import java.util.Map; @@ -26,6 +27,30 @@ public class CollectionUtils { + /** + * Null-safe check if the specified array is empty. + *

+ * Null returns true. + * + * @param arr the array to check, may be null + * @return true if empty or null + */ + public static boolean isEmpty(Object[] arr) { + return arr == null || Array.getLength(arr) == 0; + } + + /** + * Null-safe check if the specified array is not empty. + *

+ * Null returns false. + * + * @param arr the array to check, may be null + * @return true if non-null and non-empty + */ + public static boolean isNotEmpty(Object[] arr) { + return !isEmpty(arr); + } + /** * Null-safe check if the specified collection is empty. *

diff --git a/src/test/java/io/github/honhimw/ms/MainRunner.java b/src/test/java/io/github/honhimw/ms/MainRunner.java index 26dd01d..3c147ec 100644 --- a/src/test/java/io/github/honhimw/ms/MainRunner.java +++ b/src/test/java/io/github/honhimw/ms/MainRunner.java @@ -36,7 +36,6 @@ public class MainRunner { public static void main(String[] args) { - JsonHandler jsonHandler = new JacksonJsonHandler(); SearchResponse strings = jsonHandler.fromJson("{\"hits\": [\"hello\", \"world\"]}", type(new TypeRef() { })); diff --git a/src/test/java/io/github/honhimw/ms/client/SearchTests.java b/src/test/java/io/github/honhimw/ms/client/SearchTests.java index 3e0da94..a678603 100644 --- a/src/test/java/io/github/honhimw/ms/client/SearchTests.java +++ b/src/test/java/io/github/honhimw/ms/client/SearchTests.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.*; import java.util.*; +import java.util.stream.Collectors; /** * @author hon_him @@ -242,6 +243,20 @@ void filterWithContains() { assert movieHitDetails.getSource().getId() == 18; } + @Order(10) + @Test + @EnabledOnVersion("1.10") + void utilityMultiSearch() { + TypedDetailsSearch search = indexes.searchWithDetails(INDEX, Movie.class); + SearchDetailsResponse response = search.multiSearch(builder -> builder + .queryOn("girl", "overview") + .queryOn("Metropolis", "title") + ); + List> hits = response.getHits(); + List collect = hits.stream().map(movieHitDetails -> movieHitDetails.getSource().getId()).collect(Collectors.toList()); + assert collect.containsAll(toList(17, 18, 19)); + } + @AfterEach void resetSetting() { TaskInfo reset = indexes.settings(INDEX).reset();