Skip to content

search-meilisearch

@evolve-framework/search-meilisearch · v1.0.0

Meilisearch as the search provider for an Evolve shop. It has two parts that deploy separately. The index adapter writes the catalog into Meilisearch, with atomic full rebuilds, and the query adapter answers product search from it, with facets, filters and sorting. Both plug into @evolve-framework/search, so the indexing and the storefront's queries work the same as with any other provider. Meilisearch is self-hosted, so choosing it puts no part of a catalog behind a third party.

When to use

  • A shop's product search and listings come from a Meilisearch you run.
  • An indexer service keeps Meilisearch in step with the catalog: give its SearchIndexerModule or IndexSyncService a MeilisearchIndexAdapter.
  • You are moving from Algolia: Meilisearch sorts any index by any sortable attribute, so the replica per sort order goes away.

A service usually needs only one of the two: the job that fills the index does not answer queries, and the subgraph that answers queries does not write.

Get started

  1. Install the package with the peer dependencies it needs. It comes from the Evolve registry, which needs a one-time sign-in.

    pnpm add @evolve-framework/search-meilisearch @evolve-framework/search
    Requirements2
    @evolve-framework/search>= 0.2.0 < 2.0.0
    @types/node>=22optional

For example:

import { MeilisearchIndexAdapter } from "@evolve-framework/search-meilisearch/indexing";
import { MeilisearchQueryAdapter } from "@evolve-framework/search-meilisearch/query";
const credentials = {
host: process.env.MEILISEARCH_URL ?? "http://localhost:7700",
apiKey: process.env.MEILISEARCH_API_KEY ?? "",
};
// The indexer: only the attributes named here become facets.
export const indexAdapter = new MeilisearchIndexAdapter({
...credentials,
facetableAttributes: ["brand", "colour", "size"],
});
// The service answering product search.
export const queryAdapter = new MeilisearchQueryAdapter(credentials);

Adapters

MeilisearchIndexAdapter

Meilisearch as an index adapter.

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.

import { MeilisearchIndexAdapter } from "@evolve-framework/search-meilisearch";
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.

MeilisearchQueryAdapter

Answers 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.

Entry points

@evolve-framework/search-meilisearchBoth adapters, the record shape, defaultIndexName, and the mappers toRecord and toProductDocument8 exports
@evolve-framework/search-meilisearch/indexingMeilisearchIndexAdapter1 export
@evolve-framework/search-meilisearch/queryMeilisearchQueryAdapter, plus buildFilter and mapFacets for a query adapter of your own6 exports