Skip to content

search-meilisearch API

Everything the package exports, by the path you import it from. Open an export for its signature and options.

@evolve-framework/search-meilisearch

Both adapters, the record shape, defaultIndexName, and the mappers toRecord and toProductDocument

Classes

MeilisearchIndexAdapterMeilisearch as an index adapter.
import { MeilisearchIndexAdapter } from "@evolve-framework/search-meilisearch";

One index per store and locale. beginFullReindex builds into a temporary index and swaps it in, which Meilisearch does atomically — so a rebuild never serves a half-filled index, and a failed rebuild leaves the live one untouched. That is the capability Algolia needs a temporary index and a move for, and it is the reason a full resync here is safe to run against production.

new MeilisearchIndexAdapter(options: MeilisearchIndexAdapterOptions)
OptionTypeDescription
hoststring

The Meilisearch URL, for example http://localhost:7700.

apiKeystring

Held server-side only; a browser must never get it.

resolveIndexName?IndexNameResolver

Defaults to defaultIndexName.

batchSize?number

Documents per request. Meilisearch has no hard cap; this bounds memory.

toRecord?((document: ProductDocument, locale: string) => MeilisearchProductRecord)

Inverse of the query adapter's reader. Defaults to toRecord.

filterableAttributes?string[]

Attributes that can be filtered and facetted on, beyond the ones every product document has. A project's own attribute names go here.

facetableAttributes?string[]

Which product attributes to offer as facets, by their own names — brand, not attr_brand. The prefix is this adapter's business.

More

This is the same lever Algolia calls attributesForFaceting: what the index will answer facet questions about, and therefore the whole of what a listing can offer. Empty means no attribute facets.

Curated rather than open. A wildcard works — and a real catalogue then returns 224 attribute facets on one page, including a raw id, a merchandiser's duplicate named Language (2), and the second translator of a book. Discovery is the right way to find out what is available; it is not a way to decide what is useful, and nothing in the data distinguishes the two.

Index-wide, because that is as far as Meilisearch's settings reach. Per category needs a layer above this one — Algolia does that with Rules, and Meilisearch has no equivalent.

sortableAttributes?string[]

Attributes that can be sorted on, beyond the price fields.

swapTimeoutMs?number

How long to wait for the index swap that ends a full rebuild.

More

The client's own default is five seconds, which is a rebuild of a small catalogue. A swap covering eight locale indexes of several thousand documents takes longer, and the wait then times out on work that goes on to succeed — so a reindex that worked reports failure, and a scheduled one alerts on nothing.

MethodDescription
init(): Promise<void>One-shot, idempotent initialization (build SDK clients, etc.).
upsertDocuments( scope: IndexScope, documents: ProductDocument[], ): Promise<void>Write documents into the scope's target for each one's type, replacing any stored document with the same id as a whole.
deleteDocuments( scope: IndexScope, ids: string[], documentTypes: string[], ): Promise<void>Remove documents by id, from the target of every type in documentTypes.
deleteBySource( scope: IndexScope, sourceIds: string[], options: { documentTypes: string[]; keep?: string[] | undefined; }, ): Promise<void>Remove everything that came from a source, except what is still current.
MeilisearchQueryAdapterAnswers product search from Meilisearch, with facets, filters and sorting by relevance, price, name, review rating or newest, reading the index per store and locale that MeilisearchIndexAdapter fills.
import { MeilisearchQueryAdapter } from "@evolve-framework/search-meilisearch";
new MeilisearchQueryAdapter(options: MeilisearchQueryAdapterOptions)
OptionTypeDescription
hoststring

The Meilisearch URL, for example http://localhost:7700.

apiKeystring

Held server-side only; a browser must never get it.

resolveIndexName?IndexNameResolver

Defaults to defaultIndexName.

facets?string[]

Attributes to return facet counts for.

distinct?string | null

The field results are grouped by, one hit per distinct value. Defaults to sourceId, which names the product a variant document belongs to. Pass null to search variants ungrouped.

toDocument?((record: MeilisearchProductRecord, locale: string) => ProductDocument)

Inverse of the index adapter's toRecord. Defaults to toProductDocument.

maxTotalHits?number

The index's pagination.maxTotalHits setting, when it was changed from Meilisearch's default of 1,000: hits past it cannot be paged to.

MethodDescription
init(): Promise<void>Create the Meilisearch client from host and apiKey.
search( query: ProductSearchQuery, ): Promise<SearchResponse<ProductDocument>>Run a query and return the documents that matched.

Functions

defaultIndexNameproducts-evolve-nl-nl.
import { defaultIndexName } from "@evolve-framework/search-meilisearch";

Lower-cased and dash-joined: Meilisearch index uids accept only letters, digits, hyphens and underscores, so a locale's underscore-free form is the one that survives.

function defaultIndexName(
scope: { storeKey: string; locale: string; },
context: IndexNameContext,
): string
OptionTypeDescription
storeKeystring
localestring
documentTypestring

Returns string

toProductDocumentA record back as a product document.
import { toProductDocument } from "@evolve-framework/search-meilisearch";

The record holds one locale's values, so the localized maps are rebuilt with a single entry. Anything the record does not carry is left absent rather than defaulted: a caller reading variants off a search hit should find nothing there and go to the catalogue, not find an empty list and believe it.

function toProductDocument(
record: MeilisearchProductRecord,
locale: string,
): ProductDocument
OptionTypeDescription
idstring
typestring
sourceIdstring
categoriesstring[]

Facet values: the category each path ends in, never its ancestors.

More

An ancestor is true of every product already on the page it would appear on — a shopper in "Boeken > Sport & Outdoor > Buitensport" is offered "Boeken (4)" beside "Hardlopen (2)", and the one that looks like the biggest refinement is the one that refines nothing. Filtering wants the ancestors; faceting wants only what narrows.

categoryIdsstring[]

Filter values: every node of every path, so a branch page matches.

skusstring[]
name?string
slug?string
description?string
brand?string
primaryCategory?string
rating?number

Average review rating; sortable, so it ranks the rating sort.

createdAt?number

Creation time in epoch milliseconds, for the newest sort. A number rather than the ISO string, which sorts by its characters and so misorders two dates written with different offsets or precision.

image?string
quantityRule?QuantityRuleDocument

The quantity rule of the variant skus names first.

Returns ProductDocument

toRecord
import { toRecord } from "@evolve-framework/search-meilisearch";
function toRecord(
document: ProductDocument,
locale: string,
): MeilisearchProductRecord

Returns MeilisearchProductRecord

Types

IndexNameContextIndex naming.
import { type IndexNameContext } from "@evolve-framework/search-meilisearch";

One index per store and locale, rather than one index holding every language. Meilisearch tokenises and stems per index, so a single index would have to pick one language's rules for all of them — which is what makes a Dutch search for "broeken" miss "broek" while the English stemmer is in charge.

Unlike Algolia there are no sort replicas: Meilisearch sorts any index by any sortableAttribute, so a sort is a query parameter rather than a separate index to route to.

export type IndexNameContext = {
documentType: string;
};
IndexNameResolver
import { type IndexNameResolver } from "@evolve-framework/search-meilisearch";
export type IndexNameResolver = (scope: {
storeKey: string;
locale: string;
}, context: IndexNameContext) => string;
MeilisearchProductRecordThe shared product document as a Meilisearch document.
import { type MeilisearchProductRecord } from "@evolve-framework/search-meilisearch";

Localized values are resolved to the index's own locale rather than kept as a map. An index serves exactly one locale (see index-name), so carrying every language would make the searchable text of a Dutch index include German and French — which is how a search for "short" starts matching trousers.

Prices are flattened to one field per currency (price_eur). Meilisearch sorts and filters on scalar attributes, not on objects inside an array, so a nested price list can be returned but never sorted by.

export type MeilisearchProductRecord = {
id: string;
type: string;
sourceId: string;
name?: string;
slug?: string;
description?: string;
brand?: string;
categories: string[];
categoryIds: string[];
primaryCategory?: string;
skus: string[];
rating?: number;
createdAt?: number;
image?: string;
quantityRule?: QuantityRuleDocument;
[key: string]: unknown;
};

@evolve-framework/search-meilisearch/indexing

MeilisearchIndexAdapter

Classes

MeilisearchIndexAdapterMeilisearch as an index adapter.
import { MeilisearchIndexAdapter } from "@evolve-framework/search-meilisearch/indexing";

One index per store and locale. beginFullReindex builds into a temporary index and swaps it in, which Meilisearch does atomically — so a rebuild never serves a half-filled index, and a failed rebuild leaves the live one untouched. That is the capability Algolia needs a temporary index and a move for, and it is the reason a full resync here is safe to run against production.

new MeilisearchIndexAdapter(options: MeilisearchIndexAdapterOptions)
OptionTypeDescription
hoststring

The Meilisearch URL, for example http://localhost:7700.

apiKeystring

Held server-side only; a browser must never get it.

resolveIndexName?IndexNameResolver

Defaults to defaultIndexName.

batchSize?number

Documents per request. Meilisearch has no hard cap; this bounds memory.

toRecord?((document: ProductDocument, locale: string) => MeilisearchProductRecord)

Inverse of the query adapter's reader. Defaults to toRecord.

filterableAttributes?string[]

Attributes that can be filtered and facetted on, beyond the ones every product document has. A project's own attribute names go here.

facetableAttributes?string[]

Which product attributes to offer as facets, by their own names — brand, not attr_brand. The prefix is this adapter's business.

More

This is the same lever Algolia calls attributesForFaceting: what the index will answer facet questions about, and therefore the whole of what a listing can offer. Empty means no attribute facets.

Curated rather than open. A wildcard works — and a real catalogue then returns 224 attribute facets on one page, including a raw id, a merchandiser's duplicate named Language (2), and the second translator of a book. Discovery is the right way to find out what is available; it is not a way to decide what is useful, and nothing in the data distinguishes the two.

Index-wide, because that is as far as Meilisearch's settings reach. Per category needs a layer above this one — Algolia does that with Rules, and Meilisearch has no equivalent.

sortableAttributes?string[]

Attributes that can be sorted on, beyond the price fields.

swapTimeoutMs?number

How long to wait for the index swap that ends a full rebuild.

More

The client's own default is five seconds, which is a rebuild of a small catalogue. A swap covering eight locale indexes of several thousand documents takes longer, and the wait then times out on work that goes on to succeed — so a reindex that worked reports failure, and a scheduled one alerts on nothing.

MethodDescription
init(): Promise<void>One-shot, idempotent initialization (build SDK clients, etc.).
upsertDocuments( scope: IndexScope, documents: ProductDocument[], ): Promise<void>Write documents into the scope's target for each one's type, replacing any stored document with the same id as a whole.
deleteDocuments( scope: IndexScope, ids: string[], documentTypes: string[], ): Promise<void>Remove documents by id, from the target of every type in documentTypes.
deleteBySource( scope: IndexScope, sourceIds: string[], options: { documentTypes: string[]; keep?: string[] | undefined; }, ): Promise<void>Remove everything that came from a source, except what is still current.

@evolve-framework/search-meilisearch/query

MeilisearchQueryAdapter, plus buildFilter and mapFacets for a query adapter of your own

Classes

MeilisearchQueryAdapterAnswers product search from Meilisearch, with facets, filters and sorting by relevance, price, name, review rating or newest, reading the index per store and locale that MeilisearchIndexAdapter fills.
import { MeilisearchQueryAdapter } from "@evolve-framework/search-meilisearch/query";
new MeilisearchQueryAdapter(options: MeilisearchQueryAdapterOptions)
OptionTypeDescription
hoststring

The Meilisearch URL, for example http://localhost:7700.

apiKeystring

Held server-side only; a browser must never get it.

resolveIndexName?IndexNameResolver

Defaults to defaultIndexName.

facets?string[]

Attributes to return facet counts for.

distinct?string | null

The field results are grouped by, one hit per distinct value. Defaults to sourceId, which names the product a variant document belongs to. Pass null to search variants ungrouped.

toDocument?((record: MeilisearchProductRecord, locale: string) => ProductDocument)

Inverse of the index adapter's toRecord. Defaults to toProductDocument.

maxTotalHits?number

The index's pagination.maxTotalHits setting, when it was changed from Meilisearch's default of 1,000: hits past it cannot be paged to.

MethodDescription
init(): Promise<void>Create the Meilisearch client from host and apiKey.
search( query: ProductSearchQuery, ): Promise<SearchResponse<ProductDocument>>Run a query and return the documents that matched.

Functions

buildFilterEverything that narrows a query, as one expression.
import { buildFilter } from "@evolve-framework/search-meilisearch/query";

Facet selections and prefilters are joined with AND, so a prefilter a project sets can never be widened by what a shopper picks. A prefilter matches any of its values.

function buildFilter(
options: { filters?: FacetFilter[] | undefined; prefilters?: FilterCondition[] | undefined; documentTypes?: string[] | undefined; categoryId?: string | undefined; skus?: string[] | undefined; },
): string | undefined
OptionTypeDescription
filters?FacetFilter[]
prefilters?FilterCondition[]
documentTypes?string[]
categoryId?string
skus?string[]

Returns string | undefined

mapFacets
import { mapFacets } from "@evolve-framework/search-meilisearch/query";
function mapFacets(
distribution: FacetDistribution | undefined,
stats: FacetStats | undefined,
selected: FacetFilter[] | undefined,
): FacetResult[]

Returns FacetResult[]

toProductDocumentA record back as a product document.
import { toProductDocument } from "@evolve-framework/search-meilisearch/query";

The record holds one locale's values, so the localized maps are rebuilt with a single entry. Anything the record does not carry is left absent rather than defaulted: a caller reading variants off a search hit should find nothing there and go to the catalogue, not find an empty list and believe it.

function toProductDocument(
record: MeilisearchProductRecord,
locale: string,
): ProductDocument
OptionTypeDescription
idstring
typestring
sourceIdstring
categoriesstring[]

Facet values: the category each path ends in, never its ancestors.

More

An ancestor is true of every product already on the page it would appear on — a shopper in "Boeken > Sport & Outdoor > Buitensport" is offered "Boeken (4)" beside "Hardlopen (2)", and the one that looks like the biggest refinement is the one that refines nothing. Filtering wants the ancestors; faceting wants only what narrows.

categoryIdsstring[]

Filter values: every node of every path, so a branch page matches.

skusstring[]
name?string
slug?string
description?string
brand?string
primaryCategory?string
rating?number

Average review rating; sortable, so it ranks the rating sort.

createdAt?number

Creation time in epoch milliseconds, for the newest sort. A number rather than the ISO string, which sorts by its characters and so misorders two dates written with different offsets or precision.

image?string
quantityRule?QuantityRuleDocument

The quantity rule of the variant skus names first.

Returns ProductDocument

Types

FacetDistributionMeilisearch's facet distribution as the contract's facet results.
import { type FacetDistribution } from "@evolve-framework/search-meilisearch/query";

Meilisearch reports counts per value and, for numeric attributes, the observed minimum and maximum. It does not say what kind of facet an attribute is, so the kind is inferred: a numeric stat is a range, two boolean values are a toggle, anything else is a list of options.

Inferring beats configuring here because the alternative is a second place that has to agree with the index settings — and when they disagree the facet silently renders as the wrong control.

export type FacetDistribution = Record<string, Record<string, number>>;
FacetStats
import { type FacetStats } from "@evolve-framework/search-meilisearch/query";
export type FacetStats = Record<string, {
min: number;
max: number;
}>;

Not listed: 4 options types. They appear as the tables under the constructors and functions they type.