public final class SearchDocumentation extends Base
Even though the Query API
lets you query for resources with certain attribute values, its performance is seriously affected by some attributes, specially by custom attributes, when manipulating data (i.e. filtering, sorting). Moreover, some typical operations over a list of resources, such as full-text search and facets, are simply not supported.
The Search API is specially designed to support those uses cases where the Query API is not powerful enough, by providing not only full-text search, filtering, and sorting, but also statistical analysis of the data with facets.
The search endpoints are supposed to be faster than the query endpoints, but for the price of eventual consistency. In other words, whenever the name of a product was changed, it will still take some seconds to propagate this change to the search index.
Currently only products have a search endpoint for ProductProjection
only. Therefore, be aware that the class to create a search request for products is called ProductProjectionSearch
.
The following examples in this document are based on the search for products. The product data defined in Composable Commerce used for the following code examples are:
Product | Product1 "shoe" | Product2 "shirt" | Product3 "dress" | |||
---|---|---|---|---|---|---|
Variant | 1 | 2 | 1 | 2 | 1 | 2 |
Color | blue | red | blue | |||
Size | 38 | 46 | 36 | 44 | 40 | 42 |
With ProductProjectionSearch
you can perform a full-text search for a specific language. On the Full-Text Search documentation page you can explore which fields are included for the search and other additional information.
The following example uses SearchDsl.withText(java.util.Locale, String)
to search for all products containing the word "shoe" in English:
final ProductProjectionSearch search = ProductProjectionSearch.ofStaged().withText(ENGLISH, "shoe"); testResultIds(search, resultIds -> assertThat(resultIds).containsOnly(product1.getId()));
See the test code.
Use SearchDsl.withOffset(Long)
and SearchDsl.withLimit(Long)
for pagination. An extended explanation about how pagination works in the API can be found in QueryDocumentation
.
The following request skips the first 50 products and limits the result set to only 25 products:
final long offset = 10; final long limit = 25; final ProductProjectionSearch search = ProductProjectionSearch.ofStaged() .withOffset(offset) .withLimit(limit); testResult(search, result -> { final long remainingProducts = max(result.getTotal() - offset, 0); final long expectedProducts = min(limit, remainingProducts); assertThat(result.getCount()).as("size").isEqualTo(expectedProducts); assertThat(result.getOffset()).as("offset").isEqualTo(offset); });
See the test code.
Any attribute you can sort by, allows both sort directions, ascending and descending. On the Sorting documentation page you can explore for which fields you can sort for. Use ProductProjectionSortSearchModel
class to build sort expressions easily.
The code sample below shows a request for all products which will be sorted by size in an ascending direction:
final ProductProjectionSearch search = ProductProjectionSearch.ofStaged() .withSort(productModel -> productModel.allVariants().attribute().ofNumber(ATTR_NAME_SIZE).asc()); testResultIds(search, resultIds -> assertThat(resultIds).containsExactly(product2.getId(), product1.getId(), product3.getId()));
See the test code.
When sorting on multi-valued attributes, such as attributes from product variants, you can also choose which value should be used for sorting. By default, the value selected is the best-matching according to the sorting direction, or in other words, the minimum value for ascending and the maximum value for descending sort. This behaviour can be easily inverted, as explained in the Sorting by Attributes documentation page.
In the following example the products are sorted by size in an ascending direction, but sorted using the highest size value within each product variants instead:
final ProductProjectionSearch search = ProductProjectionSearch.ofStaged() .withSort(productModel -> productModel.allVariants().attribute().ofNumber(ATTR_NAME_SIZE).ascWithMaxValue()); testResultIds(search, resultIds -> assertThat(resultIds).containsExactly(product3.getId(), product2.getId(), product1.getId()));
See the test code.
Alternatively, you can provide the sort request directly, even though this method is unsafe and therefore not recommended.
Here is the alternative code equivalent to the previous sorting by size:
final SortExpression<ProductProjection> sort = SortExpression.of("variants.attributes." + ATTR_NAME_SIZE + " asc.max"); final ProductProjectionSearch search = ProductProjectionSearch.ofStaged().withSort(sort); testResultIds(search, resultIds -> assertThat(resultIds).containsExactly(product3.getId(), product2.getId(), product1.getId()));
See the test code.
Providing a list of sort expressions has the effect of multi-sorting, as it is demonstrated in the following example:
final ProductProjectionSearch singleSortedRequest = ProductProjectionSearch.ofStaged() .plusSort(productModel -> productModel.allVariants().attribute().ofString(ATTR_NAME_COLOR).asc()); final ProductProjectionSearch searchWithNameAsc = singleSortedRequest .plusSort(productModel -> productModel.name().locale(ENGLISH).asc()); testResultIds(searchWithNameAsc, resultIds -> assertThat(resultIds).containsExactly(product3.getId(), product1.getId(), product2.getId())); final ProductProjectionSearch searchWithNameDesc = singleSortedRequest .plusSort(productModel -> productModel.name().locale(ENGLISH).desc()); testResultIds(searchWithNameDesc, resultIds -> assertThat(resultIds).containsExactly(product1.getId(), product3.getId(), product2.getId()));
See the test code.
Facets calculate statistical counts based on the values associated with an attribute. Building facet expressions is very easy thanks to the ProductProjectionFacetSearchModel
class. You can consult the Facets HTTP API documentation for more information.
There are three types of facets: TermFacetResult
, RangeFacetResult
and FilteredFacetResult
. Next are presented example codes of each type to better illustrate how these facet types work.
The Term Facet result obtained with the following code contains all different size values found in the products, along with the statistical count of the amount of product variants with that value associated:
final TermFacetExpression<ProductProjection> facetExpr = PRODUCT_MODEL.allVariants().attribute().ofString(ATTR_NAME_COLOR).allTerms(); final ProductProjectionSearch search = ProductProjectionSearch.ofStaged().plusFacets(facetExpr); testResult(search, result -> { final TermFacetResult termFacetResult = result.getFacetResult(facetExpr); assertThat(termFacetResult.getMissing()).isGreaterThanOrEqualTo(3); assertThat(termFacetResult.getTotal()).isEqualTo(3); assertThat(termFacetResult.getOther()).isEqualTo(0); assertThat(termFacetResult.getTerms()).isEqualTo(asList(TermStats.of("blue", 2L), TermStats.of("red", 1L))); });
See the test code.
In contrast, the Filtered Facet result contains the statistical count for some certain values specified in the request, in this case the amount of variants with blue color:
final FilteredFacetExpression<ProductProjection> facetExpr = PRODUCT_MODEL.allVariants().attribute().ofString(ATTR_NAME_COLOR).onlyTerm("blue"); final ProductProjectionSearch search = ProductProjectionSearch.ofStaged().plusFacets(facetExpr); testResult(search, result -> { final FilteredFacetResult filteredFacetResult = result.getFacetResult(facetExpr); assertThat(filteredFacetResult.getCount()).isEqualTo(2); });
See the test code.
You can also request statistics (i.e. count, minimum and maximum values, as well as the sum and arithmetic mean) about a range of values, like it is done with size in the following example within the range [0, +∞):
final RangeFacetExpression<ProductProjection> facetExpr = PRODUCT_MODEL.allVariants().attribute().ofNumber(ATTR_NAME_SIZE).onlyGreaterThanOrEqualTo(ZERO); final ProductProjectionSearch search = ProductProjectionSearch.ofStaged().plusFacets(facetExpr); testResult(search, result -> { final RangeStats rangeStats = result.getFacetResult(facetExpr).getRanges().get(0); assertThat(rangeStats.getLowerEndpoint()).isEqualTo("0.0"); assertThat(rangeStats.getUpperEndpoint()).isNull(); assertThat(rangeStats.getCount()).isEqualTo(6L); assertThat(rangeStats.getMin()).isEqualTo("36.0"); assertThat(rangeStats.getMax()).isEqualTo("46.0"); assertThat(rangeStats.getSum()).isEqualTo("246.0"); assertThat(rangeStats.getMean()).isEqualTo(41.0); });
See the test code.
There is also the possibility to provide the facet expression directly, although it is unsafe and thus not recommended.
In the following code the same request as in the Term Facet example is reproduced, but providing a facet expression now instead:
final TermFacetExpression<ProductProjection> facetExpr = TermFacetExpression.of("variants.attributes." + ATTR_NAME_COLOR); final ProductProjectionSearch search = ProductProjectionSearch.ofStaged().plusFacets(facetExpr); testResult(search, result -> { final TermFacetResult termFacetResult = result.getFacetResult(facetExpr); assertThat(termFacetResult.getMissing()).isGreaterThanOrEqualTo(3); assertThat(termFacetResult.getTotal()).isEqualTo(3); assertThat(termFacetResult.getOther()).isEqualTo(0); assertThat(termFacetResult.getTerms()).isEqualTo(asList(TermStats.of("blue", 2L), TermStats.of("red", 1L))); });
See the test code.
Additionally, facets allow to specify an alias which will then replace the attribute path in the result. This functionality allows to calculate different types of facets on the same attribute. There is an extended explanation with examples in the API documentation for Facets.
In order to use it, the SDK offers a method that allows to define the desired alias:
final String allFacetAlias = "my-facet"; final String blueFacetAlias = "my-blue-facet"; final TermFacetExpression<ProductProjection> allFacetExpr = PRODUCT_MODEL.allVariants().attribute().ofString(ATTR_NAME_COLOR).withAlias(allFacetAlias).allTerms(); final FilteredFacetExpression<ProductProjection> blueFacetExpr = PRODUCT_MODEL.allVariants().attribute().ofString(ATTR_NAME_COLOR).withAlias(blueFacetAlias).onlyTerm("blue"); final ProductProjectionSearch search = ProductProjectionSearch.ofStaged() .plusFacets(allFacetExpr) .plusFacets(blueFacetExpr); testResult(search, result -> { final TermFacetResult allFacetResult = result.getFacetResult(allFacetExpr); final FilteredFacetResult blueFacetResult = result.getFilteredFacetResult(blueFacetAlias); assertThat(allFacetExpr.resultPath()).isEqualTo(allFacetAlias); assertThat(blueFacetExpr.resultPath()).isEqualTo(blueFacetAlias); assertThat(allFacetResult.getTerms()).isEqualTo(asList(TermStats.of("blue", 2L), TermStats.of("red", 1L))); assertThat(blueFacetResult.getCount()).isEqualTo(2); });
See the test code.
When searching for products, there are three stages of the process where filters can be applied, as shown in the following drawing:
FilterDsl.withQueryFilters(java.util.List)
.FilterDsl.withResultFilters(java.util.List)
FilterDsl.withFacetFilters(java.util.List)
Does it filter results? | Does it filter facets? | |
---|---|---|
Filter Query | Yes | Yes |
Filter Results | Yes | No |
Filter Facets | No | Yes |
No filter | No | No |
For further explanation, some diagrams regarding the filter mechanism can be found in the Filters and Facets presentation.
You can easily build filter expressions with the ProductProjectionFilterSearchModel
class. For more details, check the HTTP API documentation for Filters.
In the following example only the products with red color are returned:
final ProductProjectionSearch search = ProductProjectionSearch.ofStaged() .plusQueryFilters(productModel -> productModel.allVariants().attribute().ofString(ATTR_NAME_COLOR).is("red")); testResultIds(search, resultIds -> assertThat(resultIds).containsOnly(product2.getId()));
See the test code.
Besides filtering by terms, you can also filter by a range of values, as shown in the following code.
Here we are requesting only those products with at least one variant having the size attribute greater than or equals to 44:
final ProductProjectionSearch search = ProductProjectionSearch.ofStaged() .plusQueryFilters(productModel -> productModel.allVariants().attribute().ofNumber(ATTR_NAME_SIZE).isGreaterThanOrEqualTo(valueOf(44))); testResultIds(search, resultIds -> assertThat(resultIds).containsOnly(product1.getId(), product2.getId()));
See the test code.
There is also the possibility to provide the filter expression directly, although it is unsafe and thus not recommended.
In the following example the same filter by size is requested as previously, but now in this way:
final FilterExpression<ProductProjection> filterExpr = FilterExpression.of("variants.attributes." + ATTR_NAME_SIZE + ":range(44 to *)"); final ProductProjectionSearch search = ProductProjectionSearch.ofStaged().plusQueryFilters(singletonList(filterExpr)); testResultIds(search, resultIds -> assertThat(resultIds).containsOnly(product1.getId(), product2.getId()));
See the test code.
The Search Model for products is currently in an experimental status, so please be aware it will probably suffer breaking changes in the future.
The ProductProjectionSearchModel
class offers a domain-specific language (DSL) to build expressions accepted by the search endpoint, which can be otherwise a bit complex to write. In particular, this DSL allows you to build expressions for sorting, filtering and faceting, as it is demonstrated in the corresponding sections.
In order to build an expression, first you will be required to select the action to apply (i.e. ProductProjectionSearchModel.filter()
, ProductProjectionSearchModel.facet()
and ProductProjectionSearchModel.sort()
).
Then you need to build the path of the attribute you want to apply that action to, for example ProductProjectionFilterSearchModel.lastModifiedAt()
represents the lastModifiedAt
attribute on a product. In the case of custom attributes, you also need to provide the attribute name and select the primary type of the attribute.
Finally, you need to indicate the criteria used for the desired operation. For example you would use TermFilterSearchModel.isIn(java.lang.Iterable<V>)
if you want to filter by any of the given values.
Notice that even though the Search Model expects values of a certain type based on the attribute, you can force it to accept simple strings as shown in the following example:
final ProductProjectionSearch search = ProductProjectionSearch.ofStaged() .plusQueryFilters(productModel -> productModel.allVariants().attribute().ofNumber(ATTR_NAME_SIZE).containsAnyAsString(asList("36", "38"))); testResultIds(search, resultIds -> assertThat(resultIds).containsOnly(product1.getId(), product2.getId()));
See the test code.
A faceted search is the most typical application of facets and filters when browsing products, which consists of using some of the facet result values as filter parameters. This enables the user to see all possible attribute values of the current product list (using facets) and search by these exact values (using filters).
This behaviour is achieved by using on the same attribute a Facet
for all terms or ranges, together with a Filter Facets
and Filter Results
, both with the same filter parameters.
The ProductProjectionSearchModel
simplifies all this process by allowing you to build faceted search expressions directly, just providing the required filter parameters.
The following example demonstrates how to perform a faceted search with the model, involving two different attributes:
final ProductAttributeFacetedSearchSearchModel attributeModel = ProductProjectionSearchModel.of().facetedSearch().allVariants().attribute(); final ProductProjectionSearch search = ProductProjectionSearch.ofStaged() .plusFacetedSearch(attributeModel.ofNumber(ATTR_NAME_SIZE).allTerms()) .plusFacetedSearch(attributeModel.ofString(ATTR_NAME_COLOR).is("red")); testResult(search, ids -> assertThat(ids).containsOnly(product2.getId()), colors -> assertThat(colors).containsOnly(TermStats.of("blue", 2L), TermStats.of("red", 1L)), sizes -> assertThat(sizes).containsOnly(TermStats.of("36.0", 1L)));
See the test code.
You can see now how the same request is performed without the faceted search model:
final ProductProjectionSearch search = ProductProjectionSearch.ofStaged() .plusFacets(facet -> facet.allVariants().attribute().ofString(ATTR_NAME_COLOR).allTerms()) .plusFacets(facet -> facet.allVariants().attribute().ofNumber(ATTR_NAME_SIZE).allTerms()) .plusResultFilters(filter -> filter.allVariants().attribute().ofString(ATTR_NAME_COLOR).is("red")) .plusFacetFilters(filter -> filter.allVariants().attribute().ofString(ATTR_NAME_COLOR).is("red")); testResult(search, ids -> assertThat(ids).containsOnly(product2.getId()), colors -> assertThat(colors).containsOnly(TermStats.of("blue", 2L), TermStats.of("red", 1L)), sizes -> assertThat(sizes).containsOnly(TermStats.of("36.0", 1L)));
See the test code.