Advanced, efficient, lightning-fast search with support for fuzzy matching, synonyms, curation, analytics and more
Be the first to know when the new marketplace is available. We'll send you an email as soon as it launches.
This plugin enables lightning-fast, memory-efficient search backed by Typesense, as well as powerful search analytics.
This is the most advanced and feature-rich search solution available for Vendure.
You'll need an instance of Typesense available, which is the underlying search engine used by this plugin.
You can quickly set up a local instance of Typesense using Docker:
or with Docker Compose:
Or you can set up an account with Typesense Cloud for a hosted solution.
For the best support for search analytics, we strongly recommend Clickhouse:
docker pull yandex/clickhouse-server:24-alpine
Note: If you are self-hosting Clickhouse, the default configuration can use a lot of disk space due to the logging settings. Read Calming down Clickhouse for some tips on how to reduce disk usage.
If you do not want to use Clickhouse, we also support SQL-based analytics which will store the analytics data in your database.
It is also possible to use this plugin without analytics - see the Search Analytics guide.
Add the plugin to your VendureConfig:
If not already installed, install the @vendure/ui-devkit
package.
Then add the UI extensions. This plugin requires the ui-devkit in order to compile a custom Admin UI including the advanced search extensions. See our guide to extending the Admin UI.
Generate a database migration for the new custom field that will be set on the GlobalSettings entity.
It is possible to override the indexed value of the built-in fields on the SearchResult
type. For example,
let's say you have a customField which stores a "sale price" which you want
to use in your search results. Here's how you can configure it:
The override function will receive an object as it's single argument which looks like this:
The Advanced Search Plugin is a drop-in replacement for the DefaultSearchPlugin
that comes with a standard
Vendure installation. Therefore, the same search
query is used to perform searches, and the same response shape is returned.
However, the Advanced Search Plugin provides a number of additional features which can be used to enhance the search experience.
The regular SearchInput
type is extended with the following additional fields:
The same search
query is used to search in the Shop API, but the input object is extended as follows:
The search response is also extended to include data on facet counts, price ranges, top collections, and more.
For reference, here are the definitions of the new types:
The heart of the search experience is the fulltext search. Typesense provides powerful, typo-tolerant and extremely fast fulltext search capabilities,
which you use by providing a term
input to the search
query.
The prefixMode
input can be used to perform a prefix search, which is useful for live search (autocomplete) use cases.
When set to true
, the last word of the query will be treated as a prefix, rather than a whole word:
Usually this would return no results, as there is no product with the name "baske". However, with prefixMode
set to true
,
the search will return products with names such as "basketball", "basket", etc.
The highlights
field in the SearchResult
type provides information about which fields matched the search query.
This can be useful for displaying search results in a user-friendly way, e.g. by highlighting the matched terms in the product name.
The result will look like this:
You can use the snippet
field directly in your client application to display the highlighted text by defining a
CSS style for the <mark>
tag.
As with the DefaultSearchPlugin, the search
query results can be sorted by price
or name
:
It is also possible to define extra fields by which to sort, by using the sortableFields
option.
With the above configuration, we will then be able to perform queries such as:
Filtering of the search results can be done via the built-in filter inputs of the SearchInput
object such as
collectionSlug
, facetValueFilters
, inStock
, priceRangeWithTax
.
Search results can be filtered in various ways using dedicated input fields:
For even more control over filtering
you can use the filterBy
input to provide expressions directly as Typesense filter_by parameters.
Custom mappings can be filtered by prepending the name with customMapping_
:
Faceted search is a powerful feature that allows users to refine search results by applying multiple filters.
This can be implemented using the facetsFilter
argument of the SearchInput
object. This argument accepts the code of
the facet and the codes of the selected facet values, and returns the updated facets with the remaining counts of their
facet values in the facets
property of the SearchResponse
object.
Note: facets
vs facetValues
vs facetCounts
The SearchResponse
type contains 3 fields related to facets, which can be a source of confusion. Here's what they mean:
facetValues
: This contains a flat list of all the facet values that are present in the search results. This exists for
backwards compatibility with the default search plugin, but is not recommended.facetCounts
: This contains the raw counts of faceted values from Typesense. This is mainly useful for debugging, as the
returned values contain limited information, e.g. just the IDs for facet values.facets
: This is the recommended way to get faceted values. It groups the facet values by facet, and gives you more
control over the results based on the facetsFilter
input.Likewise, the SearchInput
object has 2 fields related to facets:
facetValueFilters
: The backward-compatible way of filtering by facet value ID and is not recommended.facetsFilter
: This is the recommended way to implemented faceted search together with the facets
field in the SearchResponse
.By setting includeNullValues: true
in the facetsFilter
argument, the response will also include facet values with a count of zero.
Here's an example of a GraphQL query implementing a faceted search:
This query will return a list of products filtered by the selected facets, along with the total number of items and the updated facets with their counts.
The results returned in the facets
object will include the updated counts of the facet values for the selected facets.
These can be used in the frontend to display the available facet values and their counts to the user.
This would then be used as the basis for a faceted search UI, where the user can select multiple facets to refine the search results.
The topCollections
field in the SearchResponse
object returns the top collections based on the search input.
If differs from the collections
field in the following way:
collections
is a backward-compatible field that returns all collections in the result set. So if you are returning the
first 10 results, you will get the collections of those 10 results. This means that the collections returned
are affected by the pagination (skip, take) of the search query.topCollections
is more powerful because it allows you to return the collections related to the top n results, regardless
of the pagination, by using the topCollectionsFromTop
input.Custom mappings allow you to index arbitrary data alongside your product variants. For example, if you have defined custom fields on your products or variants, you can index these fields and make them searchable.
Here are some example use cases for custom mappings:
For more information on each specific option, see:
Primitive custom mappings return a primitive value (a GraphQL scalar or list of scalars such as Int
or [String!]
). The examples
above are of this type, and these are suitable for most cases.
Sometimes, however, you may want to return a more complex type from a custom mapping. In this case, you can use an object custom mapping, which can return any kind of GraphQL object type.
An object custom mapping must:
typesenseType
which is one of the Typesense field types. This is required because we cannot infer the Typesense type from the GraphQL type, since the GraphQL type can be anything.outputFn
which has the task of transforming the data stored in Typesense into the correct shape according to the chosen graphQlType
. The values
it receives will be an array, and in the case that the search has been performed with groupByProduct: true
, it will contain all the values for each variant in the given product. The groupByProduct
argument will be true
if the search was performed with groupByProduct: true
, otherwise it will be false
.graphQlSchemaExtension
property.In this example, we have defined a custom rrp
(recommended retail price) field on the Product, and we want to expose it
as a min/max range:
Custom mappings are exposed via the customMappings
file in the search response type.
Search results can be sorted by custom mappings by configuring the sortableFields config option.
Then, in your GraphQL query, you can sort by the custom mapping:
Results can be filtered by custom mappings by using the filterBy
input field and specifying the custom mapping name
in the format customMapping_<name>
. The value should be a string in the format of the
Typesense filter syntax.
Typesense supports powerful geosearch capabilities, which you use to sort results based on distance from a given point. Here's an example of how this would work.
First, we need to store coordinates somewhere. In this example, we will store them in custom fields on the Product
entity:
Next, we need to define a custom mapping for the latitude
and longitude
fields:
(here we are assuming that the custom fields have been correctly typed)
We also need to specify that the location custom mapping is sortable:
Finally, we can use the sort
input to sort by distance from a given point, filter by distance from that point,
and use the geoDistance
field to get the distance in meters.
sort
input is used to sort results by distance from a given latitude and longitude.filterBy
input is used to only include results within a certain distance from a given latitude and longitude.An external index allows you to index and search any dataset alongside your product data. This data can come from any source, such as within Vendure itself or from an external system.
Here's an example where we index Vendure collections, so we can display them in the search results:
As another example, you might want to index blog posts or information pages. This is done by defining an ExternalIndex
instance.
Here's an example which indexes articles from a CMS:
You then pass this ExternalIndex
instance to the AdvancedSearchPlugin
:
For each external index defined, the plugin will automatically generate a new query in the GraphQL API.
The name of the query will be search<Name>
, where Name
is the name of the index converted to PascalCase.
This can be overridden by specifying the queryName
property when defining the ExternalIndex
.
It is also possible to search multiple external indexes in a single query using the searchExternal
query. The response will be a union type of all the external indexes you have defined. The
plugin will automatically create a GraphQL type for your external index, which will be named as a PascalCase version of the name
property with a Response
suffix. In the example above, the name is 'cms-articles'
, so the
resulting GraphQL type would be CmsArticlesResponse
.
The ExternalSearchInput
object must then specify the name of the external index(es) to search:
In this example we are also providing some custom filterBy
data, which allows your external index to support arbitrary
filtering expressions supported by Typesense.
Analytics on searches can be collected based on the configured analytics strategy. The plugin comes with two strategies out of the box:
SqlAnalyticsStrategy
: This strategy stores analytics data in a SQL database. This is the default strategy.ClickhouseAnalyticsStrategy
: This strategy stores analytics data in a Clickhouse database.When performing a search, setting the logAnalytics
input field to true
will record that search in the analytics store.
Note that the analytics engine will automatically "debounce" intermediate searches during an autocomplete, so that only
the final search is recorded. For example, if the user types "red", then "red sh" and then "red shoes",
only the "red shoes" search will be recorded. The window of time during which intermediate searches are ignored
can be configured via the analytics.aggregationWindowMs
option.
In your storefront, you should then implement the following mutations in order to track search result views and click-throughs:
The logSearchListViewed
mutation allows us to register the fact that the search results were viewed. This can be used
for both a full results page, or an autocomplete list.
The logSearchResultClicked
mutation allows us to register the fact that a specific search result was clicked. This in
turn allows the analytics engine to calculate click rate and average click position for each search term.
If you do not need the analytics features, they can be disabled with the following configuration (since v1.4.0):