@priemskiyyy/scout-meilisearch
v0.1.0
Published
Meilisearch adapter for Scout: filter translation, honest totals, and safe highlight parts behind the portable search contract.
Maintainers
Readme
@priemskiyyy/scout-meilisearch
The Meilisearch adapter for
Scout. It translates one portable
search request onto one Meilisearch index and maps the answer back, and it does
nothing else. Requires meilisearch 0.50 or newer.
pnpm add @priemskiyyy/scout@next @priemskiyyy/scout-meilisearch@next meilisearchmeilisearch is a peer dependency and the client is yours. This package
imports no SDK at runtime, so one build serves the browser, Node and React
Native. src/meilisearch.contracts.ts pins its structural types to the real
ones.
Use
import { index, Scout } from "@priemskiyyy/scout";
import { meilisearch } from "@priemskiyyy/scout-meilisearch";
import { Meilisearch } from "meilisearch";
const client = new Meilisearch({ host, apiKey: searchKey });
const products = index({
schema: ProductSchema,
id: (product) => product.id,
searchable: ["title", "brand"],
filterable: ["brand", "price", "inStock"],
facetable: { brand: "string", price: "number", inStock: "boolean" },
highlightable: ["title"],
sorts: { priceAsc: { price: "asc" } },
profiles: { titles: { searchable: ["title"] } },
});
const scout = new Scout({
indexes: { products },
adapters: {
products: meilisearch({
index: client.index("products"),
profiles: { titles: { matchingStrategy: "all" } },
}),
},
});The index says what a search can do, and the adapter only says how Meilisearch does it:
indexis the index client the application built. The adapter never closes it, andscout.index("products").nativehands it back.profilestunes the index's profiles, keyed by profile name, with any Meilisearch parameter Scout does not write, such asmatchingStrategy,rankingScoreThreshold,localesor the cropping options. The fields a profile searches come from the index.paginationis"page", the default, or"offset". See below.maxTotalHitsis the index's configuredpagination.maxTotalHits, 1000 by default in Meilisearch.
A search may pass the same parameters per search through .provider(), and
they win over the profile's.
What it translates
- Query. The query is the SDK's first argument, and an empty one is a
placeholder search that browses the index.
attributesToSearchOnis the profile's fields. - Filters. The normalised filter becomes a Meilisearch filter expression:
field = value,field != value,field > n,field IN [a, b],field NOT IN [a, b],field min TO max, andAND,ORandNOTwith every member parenthesised. A null comparison becomesIS NULLorIS NOT NULL, because Meilisearch has no null literal. String values are double quoted with backslashes and quotes escaped, so a value cannot end its clause and start another. - Sorts. A sort's fields become the
sortarray, such as["price:asc", "title:desc"], and relevance sends none. Each field has to be sortable in the index settings. - Facets. Requested facets are read from
facetDistribution, and the field'sfacetStatsis kept on the facet'snative. Meilisearch reports every value as a string, and Scout reads it back as the type the index declared. - Highlights.
attributesToHighlightwith ahighlightPreTagand ahighlightPostTagmade for each search, and_formattedsplit into{ text, matched }parts. The adapter never reads_matchesPosition, whose offsets count UTF-8 bytes rather than JavaScript characters. - Time.
processingTimeMsis the engine's owntook. - Cancellation. The signal goes to the SDK's request options, so the HTTP request is cancelled.
The document is the hit without Meilisearch's own keys, such as _formatted
and _rankingScore. hit.native keeps the whole hit and result.native the
whole response.
How sure a total is
pagination: "page"sendspageandhitsPerPageand readstotalHits, which Meilisearch counts exhaustively, so the total is"exact". It counts up tomaxTotalHitsand stops, so a total that reaches the cap is a"lower-bound"with scope"retrievable": how many documents paging can reach, not how many matched.pagination: "offset"sendsoffsetandlimitand readsestimatedTotalHits, so the total is"estimated"however small it is.- A response with neither block has an
"unknown"total.
A facet's exhaustive is "unknown", because Meilisearch never says whether
maxValuesPerFacet cut a distribution short. A highlight fragment is
truncated: false unless the search crops, and "unknown" when it does,
because Meilisearch never says which values it cut.
What it refuses
Each refusal rejects the search before anything is sent.
- A page larger than
maxTotalHits, which is the most Meilisearch will fill, with the codeunsupported-feature. - A filter field that is not a plain attribute name, with the code
configuration. A filter's values need no check here: each operator's value is typed, and Scout refusesNaNand infinite numbers. - A provider option Scout writes from the search itself, with the code
configuration:q,attributesToSearchOn,filter,sort,facets,page,hitsPerPage,offset,limit,attributesToHighlight,highlightPreTagandhighlightPostTag. These are the adapter'sreservedOptions, so Scout refuses them before the adapter sees the search.
Multi search, federated search, searchForFacetValues, and hybrid and vector
search are outside the portable contract. Reach for the client directly when
you need them.
fingerprint() is the index uid. An API key never takes part in query
identity, so it cannot reach a cache key or devtools.
Tests
The adapter runs the shared conformance suite from @priemskiyyy/scout/testing
against an in-process fake of the index client. The fake evaluates the query,
the searched attributes, pagination, facets and highlights. It does not
evaluate filters or sorts, so their translation is pinned string by string in
src/meilisearch.test.ts instead. src/meilisearch.contracts.ts pins the fake
and the structural types to the real meilisearch 0.62.0 types.
src/meilisearch.backend.test.ts runs the same suite against a real
Meilisearch 1.54.0 server started from its official image, where filters,
sorts and pages are checked on the documents it returned. Run it with
pnpm test:backends meilisearch, which needs Docker.
MIT
