orion-search / meilisearch
Orion engine adapter for Meilisearch on the official client: the neutral schema as index settings with Orion's declaration beside them, versioned writes with tombstones, aliases emulated by core, async tasks awaited
Requires
- php: >=8.3
- guzzlehttp/guzzle: ^7.9
- meilisearch/meilisearch-php: ^1.17
- orion-search/core: ^1.0
Requires (Dev)
- orion-search/testing: ^1.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-27 19:15:57 UTC
README
Orion's engine adapter for Meilisearch 1.13 and later, on the official meilisearch/meilisearch-php client over
Guzzle.
composer require orion-search/meilisearch
use Orion\Meilisearch\MeilisearchEngine; use Orion\Registry\EngineRegistry; $engines = new EngineRegistry(meilisearch: MeilisearchEngine::connect($url, $apiKey));
- Meilisearch is schemaless: the neutral schema becomes index settings plus a declaration document Orion keeps in
the index (
orion-schema, a tombstone no search returns).inspect()reads the declaration and keeps each field only where the settings still serve it; a field they no longer serve is unknown and fails a plan closed. - Documents are validated against the declaration before they are sent, so an undeclared field, a wrong type, a
keyword over 250 bytes or an id the engine cannot hold fails alone, as a strict mapping would; a task the engine
refuses as a whole fails every document it carried, with the engine's reason. Every write is a task;
BulkResult::await()waits for it,flush($index, await: true)waits for every task of the index. - Versions are Orion's: a write or delete is compared with the stored version first (stale when not newer), after the engine's own queued write from this instance has settled, and a delete leaves a tombstone every search excludes. One writer per index, which the outbox is: two processes writing the same index at once can race.
- Lexical clauses are filters on shadow attributes (a text field's tokens as hex-encoded, letter-delimited strings
of at most 500 bytes, chunked with an overlap longer than any phrase; a keyword's value hex-encoded; hex because
the engine's filters are case-insensitive), never
q: Meilisearch'sqrequires every word or drops some, tolerates typos and completes the last word, none of which is the portable match.qstays available through the escape hatch, with the host's own typo, synonym and ranking settings. - Vectors are user-provided embeddings, ranked by cosine similarity (the one metric available); hybrid fusion is core's. Facets count within the k nearest, as on every engine, read from the window itself.
- No native aliases: core keeps the indirection. No nested documents. One geo field (
_geo). A prefix clause on a number is refused. IndexSchema::native('meilisearch', ['settings' => [...]])merges over the generated settings, except what Orion owns (filterable, sortable and displayed attributes, embedders, pagination, faceting, distinct).raw()takescallable(): arrayreturning a search body; scope filters and the tombstone exclusion are ANDed into itsfilter. Nothing in a search body reads past its filter, so no key is refused.- The
containsFilterexperimental feature is turned on atensure(): Orion's clauses areCONTAINSandSTARTS WITHfilters.
Proven by Orion\Testing\EngineConformanceTestCase against Meilisearch 1.56 and 1.13, with the tenancy isolation,
determinism, fault-injection and endpoint-fixture suites on both. The property-based query equivalence suite, which
holds a vector search to the exact neighbours within a slack, runs on the current line only: 1.13's approximate
index strays further and differently per build, so on that line a vector search returns neighbours the engine
ranks nearest, not provably the k nearest.
Connecting it and how it compares with the other engines: the Engines guide.