Search and Filtering
This guide covers implementing product search and filtering in your storefront.
Overview
Unchained Engine provides a flexible search and filter system:
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Storefront │────▶│ FilterDirector │────▶│ Filter Adapters │
│ (Search) │◀────│ (Aggregation) │◀────│ (Search Logic) │
└─────────────┘ └──────────────────┘ └─────────────────┘
Basic Search
Text Search Query
query SearchProducts($query: String!) {
searchProducts(queryString: $query) {
filteredProductsCount
products {
_id
texts {
title
description
}
... on SimpleProduct {
simulatedPrice(currencyCode: "CHF") {
amount
currencyCode
}
}
media {
file {
url
}
}
}
}
}
Search with Pagination
query SearchWithPagination($query: String, $limit: Int, $offset: Int) {
searchProducts(
queryString: $query,
) {
filteredProductsCount
products(limit: $limit, offset: $offset) {
_id
texts {
title
}
}
}
}
Filters
Get Available Filters
query GetFilters {
filters {
texts {
title
}
options {
texts {
title
}
}
}
}
Search with Filters
query FilteredSearch($query: String, $filters: [FilterQueryInput!]) {
searchProducts(
queryString: $query
filterQuery: $filters
) {
filteredProductsCount
products {
_id
texts {
title
}
}
filters {
filteredProductsCount
isSelected
options {
filteredProductsCount
isSelected
}
}
}
}
Filter Query Input
// Example filter queries
const filters = [
// Single value
{ key: 'category', value: 'electronics' },
// Multiple values (OR)
{ key: 'brand', value: 'apple' },
{ key: 'brand', value: 'samsung' },
// Range filter
{ key: 'price', value: '100-500' },
];
Filter Types
| Type | Description | Example |
|---|---|---|
SINGLE_CHOICE | Select one option | Category |
MULTI_CHOICE | Select multiple options | Brand, Color |
RANGE | Numeric range | Price, Weight |
SWITCH | Boolean toggle | In Stock |
Creating Filters
mutation CreateFilter {
createFilter(
filter: {
key: "brand"
type: MULTI_CHOICE
options: ["apple", "samsung"]
}
texts: [
{ locale: "en", title: "Brand" }
]
) {
_id
key
type
texts {
title
}
}
}
Note: Filter option texts are managed separately via updateFilterTexts.
Assigning Filters to Products
Filters are typically assigned through the Assortment system. Products inherit filters from their assortments, and filter options are managed separately through filter configuration.
mutation LinkAssortmentFilter {
addAssortmentFilter(assortmentId: "product-assortment", filterId: "filter-id") {
_id
assortment {
_id
texts {
_id
title
}
}
filter {
_id
texts {
_id
title
}
}
}
}
mutation LinkAssortmentProduct {
addAssortmentProduct(assortmentId: "product-assortment", productId: "product-id") {
_id
assortment {
_id
texts {
_id
title
}
}
product {
_id
texts {
_id
title
}
}
}
}
query ProductFilters {
product(productId: "product-id") {
_id
texts {
title
}
assortmentPaths {
links {
assortmentId
}
}
}
}
Assortment-Based Filtering
Filter products within an assortment (category):
query AssortmentProducts($assortmentId: ID!, $filters: [FilterQueryInput!]) {
assortment(assortmentId: $assortmentId) {
_id
texts {
title
}
searchProducts(filterQuery: $filters) {
filteredProductsCount
products {
_id
texts {
title
}
}
filters {
filteredProductsCount
isSelected
options {
filteredProductsCount
}
}
}
}
}
Frontend Notes
The search result exposes everything a filter UI needs — for each filter, isSelected and filteredProductsCount per option (see the filters field in the queries above). Build your sidebar from that result instead of tracking counts client-side, debounce search input, and echo the active filterQuery pairs into the URL for shareable links.
External search (Elasticsearch, Algolia, …)
To delegate product search to an external engine, use the registerProductSearchFilter factory — its search callback receives the query and returns the matching product ids:
import { registerProductSearchFilter } from '@unchainedshop/core';
registerProductSearchFilter({
adapterId: 'elasticsearch',
search: async ({ queryString, locale }) => {
const results = await elasticsearch.search({
index: 'products',
body: buildESQuery(queryString, locale),
});
return results.hits.hits.map((hit) => hit._id);
},
});
For custom MongoDB selector / sort logic (rather than an external engine), build a FilterAdapter directly — see Filters.
The Product-ID Cache
Filtering is not computed per request. Each filter's matching product ids are precomputed and served from a cache (MongoDB-backed by default). The cache is rebuilt automatically:
- On startup — unless you pass
workQueueOptions: { skipInvalidationOnStartup: true }tostartPlatform - On filter changes — creating or updating filters and adding/removing filter options triggers
FilterDirector.invalidateProductIdCachefor the affected filter; removing a whole filter just purges its cached product ids - After a bulk import — unless the import sets
skipCacheInvalidation(see Bulk Import)
To swap the cache backend (e.g. Redis), provide setCachedProductIds, getCachedProductIds, and purgeCachedProductIds together in the filters module options — see Filters Module.
Related
- Filter Plugins - Filter adapters
- Custom Filter Adapter - Building custom adapters
- Search Behavior - Search customization