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.

admin key, on your server
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.

MeilisearchTypesenseElasticsearchVisor
When the write is doneLater: poll the taskUidWhen it answersWhen it answersWhen it answers
Some documents rejectedShown in the task422 partial_failure, the rest written422 partial_failure, the rest written400 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 getMeilisearchTypesenseElasticsearchVisor
Search only some fields, in ordersearchableAttributessearchableAttributes (Typesense 27 and later)Not possible: every field is searched. Refused.searchableAttributes
Filter on a fieldfilterableAttributesAny field, no settingAny field, no setting; exact text through <field>.keywordfilterableAttributes
Facet counts on a fieldfilterableAttributesfilterableAttributesFacet on <field>.keyword for text, no settingfilterableAttributes
Sort by a fieldsortableAttributesNumbers without a setting; text needs sortableAttributesAny field; text through <field>.keywordsortableAttributes
SynonymssynonymssynonymsRefusedsynonyms
Stop wordsstopWordsRefusedRefusedstopWords
Typo tolerancetypoToleranceRefused (Typesense's own defaults apply)typoTolerance.enabled onlytypoTolerance
Ranking rulesrankingRulesRefusedRefusedBuilt-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.
settings per engine
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);
}

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/tasks has nothing to wait for.
  • Scores. relevanceScore in AI search is set on Meilisearch and Visor only, and hybrid search's _scoring means something different on each engine.
  • Search parameters. attributesToCrop, cropLength, matchingStrategy and showMatchesPosition are ignored on Typesense and Elasticsearch, and Visor accepts only its own values. mode is Visor's alone.
  • Totals and paging. Visor counts a search's hits exactly only within its first 1000 (past that, totalHitsExact is false). Listing documents by offset stops at 10,000 on Elasticsearch and 1000 on Visor; walk by cursor to 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.