Index Into More Than One Engine
Every index picks its own engine, so one project can hold the same data on Meilisearch, Typesense, Elasticsearch and Visor side by side: to compare them on your own data, to run an A/B test, or to serve different jobs from the engine that suits each. This page shows how to load the same documents into each, which settings mean the same thing on each, and what does not carry over.
The multi-engine example does all of it in one script and prints the results side by side.
1. One Index per Engine
Pass engine when you create each index. The uid is yours to choose; a suffix per engine keeps them apart.
for (const engine of ["meilisearch", "typesense", "elasticsearch", "visor"]) {
await hb("POST", "/v1/indexes", ADMIN, { uid: `products_${engine}`, primaryKey: "id", engine });
}Each index goes on your project's engine of that type. If the project has none yet, Headband creates one on first use, within your plan's engines per type.
2. The Same Documents
Send the same array to each index with POST /v1/documents?index=<uid> (or /v1/bulk). Give every document a string id: it works the same on all four, and Typesense hands ids back as strings anyway.
| Meilisearch | Typesense | Elasticsearch | Visor | |
|---|---|---|---|---|
| When the write is done | Later: poll the taskUid | When it answers | When it answers | When it answers |
| Some documents rejected | Shown in the task | 422 partial_failure, the rest written | 422 partial_failure, the rest written | 400 for the batch, nothing written |
3. Settings That Mean the Same Thing
Each engine applies a different part of the settings vocabulary. To make four indexes behave alike, send each the settings it applies, and leave out the ones it refuses.
| To get | Meilisearch | Typesense | Elasticsearch | Visor |
|---|---|---|---|---|
| Search only some fields, in order | searchableAttributes | searchableAttributes (Typesense 27 and later) | Not possible: every field is searched. Refused. | searchableAttributes |
| Filter on a field | filterableAttributes | Any field, no setting | Any field, no setting; exact text through <field>.keyword | filterableAttributes |
| Facet counts on a field | filterableAttributes | filterableAttributes | Facet on <field>.keyword for text, no setting | filterableAttributes |
| Sort by a field | sortableAttributes | Numbers without a setting; text needs sortableAttributes | Any field; text through <field>.keyword | sortableAttributes |
| Synonyms | synonyms | synonyms | Refused | synonyms |
| Stop words | stopWords | Refused | Refused | stopWords |
| Typo tolerance | typoTolerance | Refused (Typesense's own defaults apply) | typoTolerance.enabled only | typoTolerance |
| Ranking rules | rankingRules | Refused | Refused | Built-in rules only |
"Refused" means the settings call answers 400 (unsupported_setting, or feature_not_supported on Visor) and changes nothing. Sending back the settings you read from an index never fails. Two more rules:
- On Typesense, add the documents first. A field can be made sortable only once a document has given it a type; before that the call answers
409 settings_need_documents. - On Elasticsearch, send none of the attribute lists or synonyms. It searches, filters and sorts every field without them.
const shared = {
searchableAttributes: ["title", "brand", "category"],
filterableAttributes: ["brand", "category", "price"],
sortableAttributes: ["price"],
synonyms: { coat: ["jacket"] },
};
const SETTINGS = { meilisearch: shared, visor: shared, typesense: shared, elasticsearch: null };
for (const [engine, settings] of Object.entries(SETTINGS)) {
if (!settings) continue; // Elasticsearch: nothing to send
await hb("PATCH", `/v1/indexes/products_${engine}/settings`, ADMIN, settings);
}4. The Same Search
A search request is the same on every engine, with one exception: on Elasticsearch an exact filter, a facet or a sort on a text field names <field>.keyword. Elasticsearch stores a text field as its analysed words, so brand = "Contoso" finds nothing there (while brand = "contoso" happens to match a lower-cased word). Numbers, booleans and dates need no suffix.
const field = (engine, name) => (engine === "elasticsearch" ? `${name}.keyword` : name);
await hb("POST", "/v1/search", SEARCH, {
index: `products_${engine}`,
q: "",
filter: `${field(engine, "brand")} = "Contoso"`,
facets: [field(engine, "brand")],
});The example's output on Meilisearch, Typesense and Visor:
q "running", sort price:asc
meilisearch 2 hit(s) [1,2]
typesense 2 hit(s) [1,2]
visor 2 hit(s) [1,2]
brand = "Contoso", facets brand
meilisearch 4 hit(s) [1,3,5,8] facets={"brand":{"Contoso":4}}
typesense 4 hit(s) [8,5,3,1] facets={"brand":{"Contoso":4}}
visor 4 hit(s) [1,3,5,8] facets={"brand":{"Contoso":4}}
price < 100
meilisearch 3 hit(s) [6,7,8]
typesense 3 hit(s) [8,7,6]
visor 3 hit(s) [6,7,8]
q "coat" (synonym of jacket)
meilisearch 3 hit(s) [6,4,5]
typesense 6 hit(s) [6,5,4,8,3,1]
visor 6 hit(s) [6,4,5,1,3,8]Filters, facets and a sorted search agree. Without a sort, the order differs. coat finds the three jackets through the synonym on every engine, but Typesense and Visor also match Contoso for it: their typo and prefix matching is looser on short words than Meilisearch's, which allows no typo in a word under 5 letters. On Elasticsearch the facet comes back under brand.keyword.
What Does Not Carry Over
- Relevance. Typo rules, prefix matching and ranking differ, so the same query can return different hits in a different order. Sort explicitly when the order matters.
- Tasks. Only Meilisearch queues writes. On the others
GET /v1/taskshas nothing to wait for. - Scores.
relevanceScorein AI search is set on Meilisearch and Visor only, and hybrid search's _scoring means something different on each engine. - Search parameters.
attributesToCrop,cropLength,matchingStrategyandshowMatchesPositionare ignored on Typesense and Elasticsearch, and Visor accepts only its own values.modeis Visor's alone. - Totals and paging. Visor counts a search's hits exactly only within its first 1000 (past that,
totalHitsExactis false). Listing documents by offset stops at 10,000 on Elasticsearch and 1000 on Visor; walk bycursorto read everything. - Vectors. Hybrid search needs a vector field set up differently on each engine, and Visor has none. See Hybrid Search.
Moving an Index Instead
To move one index to another engine rather than keep copies, use an engine switch (POST /v1/indexes/:uid/engine, or headband indexes switch-engine). It keeps the uid and keys, and carries the settings the new engine applies. It lists the rest as drops and refuses until you accept them.
Set these again after a switch
A switch to Typesense drops searchableAttributes, filterableAttributes and sortableAttributes: the new index is set up before the documents arrive, and Typesense cannot make an untyped field sortable. Set them on the index again once the switch is done. A switch to Elasticsearch drops them for good, and its notes remind you to use <field>.keyword.