Source: https://docs.any.org/search/hybrid.html # Hybrid ranking `hybrid` runs both legs and fuses them with reciprocal-rank fusion (k = 60). It is the default because it is the safe choice across corpora: it wins outright on lexical-friendly content and never collapses when one leg is weak — and when the embedder cannot help at all, it becomes full-text search by itself and says so in `vectorStatus`. ## The default call ```bash curl -s http://127.0.0.1:7001/v1/spaces/$SPACE/search \ -H 'content-type: application/json' \ -d '{"query": "what did we decide about the reranker?", "scopes": ["chat", "basic"], "limit": 10}' ``` ```bash any search $SPACE "what did we decide about the reranker?" --scopes chat,basic --limit 10 ``` ```json { "hits": [ { "scope": "chat", "objectId": "…", "dataset": "chat_messages", "recordId": "…", "data": "…", "dataTotal": 29, "score": 0.0328 } ], "mode": "hybrid", "vectorStatus": "used" } ``` `limit` counts records (one hit per record), defaults to 10 and caps at 100. `score` is the fused RRF score — small numbers, comparable only within this one response. ## Reciprocal-rank fusion Each leg returns a ranked list; a document's fused score is the sum over the legs that returned it of `weight / (k + rank)`. Rank, not raw score, is what gets combined, so BM25 and cosine never have to be put on one scale. A document found by both legs rises above one found by only one. ``` fused(doc) = ftsWeight / (60 + rank_fts) + vectorWeight / (60 + rank_vec) ``` Full-text operators (`"phrases"`, `prefix*`) shape the lexical leg's list; the vector leg always embeds the whole `query` verbatim, stop words included. `require` / `exclude` bind every hit regardless of leg: vector hits are post-filtered against the FTS index before fusion, so a fused result never contains a doc the lexical constraint would have refused. ## Read `vectorStatus` before trusting recall The reply's `mode` is the mode that actually ran, and `vectorStatus` says whether semantic recall participated and, if not, why. Agents in particular should branch on it before concluding that "nothing matched". | `vectorStatus` | Meaning | What to do | | --- | --- | --- | | `used` | the vector leg ran and contributed | trust the fused ranking | | `unavailable` | an embedder is configured but did not embed the query — unreachable, or not within `index.search.queryEmbedTimeout` — so results are lexical-only | retry later; results may differ | | `disabled` | this server has no embedder — vector can never run until config changes | adjust the query toward exact terms; don't retry | | `skipped` | the caller asked for `mode: "fts"`, or a `filter` matched no object so no leg ran | the echo of your own choice, or an empty filtered set | When a leg runs, `unavailable` or `disabled` makes `mode` in the reply read `fts` even though the request said `hybrid`. Pure `vector` mode does not degrade this way — it errors instead (see [Vector search](https://docs.any.org/search/vector.md.txt)). The query embedding is bounded by `index.search.queryEmbedTimeout` (default 5 s), so a model still loading, a wedged local embedder or a slow API turns into a lexical-only answer instead of a stalled search. The local embedder serves a search query ahead of queued index documents, so a busy re-index delays a query by at most the one decode in flight. Under `auto` the online API gets half the budget, leaving the local fallback time to answer. ## Filtering by host object `filter` keeps only hits whose host object matches, in the [objects query](https://docs.any.org/database/reading-data.md.txt) filter grammar verbatim — `any.type`, `any.collections`, `.`, `modifiedAt`, `id`. It binds every mode, like `require` / `exclude`, and checks the object's live row, so a property write counts at once. `limit` still counts matching records. ```bash curl -s http://127.0.0.1:7001/v1/spaces/$SPACE/search \ -H 'content-type: application/json' \ -d '{"query": "reranker", "filter": {"any.collections": {"$nin": ["bin"]}}}' ``` ```bash any search $SPACE reranker --filter '{"any.collections": {"$nin": ["bin"]}}' ``` Two replies need care: - A filter no object matches answers `hits: []` without running a leg: `mode` echoes the request, and `vectorStatus` reads `skipped` (`disabled` on a server without an embedder). - `truncated: true` means a leg's read budget ran out under the filter while the page held fewer than `limit` records. The index may hold more matches, so a short page is not exhaustive — narrow the filter or the query. Without a filter the field is absent. > **Why it matters.** The embedder is the one moving part that can be absent or temporarily down — a model still downloading, an API outage, a laptop without the shared libraries. Degrading to full-text keeps search answering; reporting it lets the caller decide how much to trust a thin result. ## Weighting knobs All under `index.search.*` in [configuration](https://docs.any.org/operations/configuration.md.txt); defaults reproduce the untuned behaviour, so an absent block changes nothing. | Key | Default | Effect | | --- | --- | --- | | `ftsWeight` | 1 | RRF weight of the lexical leg | | `vectorWeight` | 1 | RRF weight of the dense leg | | `adaptiveWeights` | false | per query, down-weight the FTS leg when its BM25 scores are flat/weak | | `stopWords` | true | strip stop words from the FTS-leg query | | `defaultOperator` | `or` | how bare FTS terms combine | | `minVectorSim` | 0 | cosine floor applied to vector hits before fusion | | `queryEmbedTimeout` | `5s` | budget for embedding the query; past it hybrid answers lexical-only | Equal weights (1/1) are optimal on the lexical-friendly reference corpus and a safe default elsewhere. Corpora where one leg is much weaker want a tilt: on paraphrastic Q&A the dense leg alone beats equal-weight hybrid, because a weak BM25 list drags the fusion down. `adaptiveWeights` automates that tilt from the query's own score distribution — a big win where the lexical leg is weak, a small cost where it is strong, hence off by default. It is asymmetric on purpose: only the FTS leg is ever down-weighted, since cosine scores are not calibrated enough to judge. ```yaml index: search: adaptiveWeights: true # semantic / conversational corpora ``` ## Choosing a mode - **`hybrid`** (default) for anything a person or agent types. - **`fts`** for identifiers, error codes, exact names, and property-value lookups (`props` is full-text only anyway). - **`vector`** when paraphrase recall matters more than precision and you want a hard failure rather than a silent degrade. The measurements behind these recommendations are on [Evaluation](https://docs.any.org/search/evaluation.md.txt).