# How search works

> BM25, vector, default bounded graph expansion, fusion, and reranking, end to end.

Section: Guides  
Canonical: https://gno.sh/docs/how-search-works  
Markdown: https://gno.sh/docs/how-search-works.md

GNO hybrid search runs BM25 and vector retrieval, fuses by rank, optionally expands one hop of wiki/markdown links, then cross-encoder-reranks. Exact identifiers and quoted phrases skip HyDE; `--fast` returns fused BM25+vector; default mode adds bounded graph expansion and rerank. Latency depends on the corpus, model, hardware, and whether native models must reload after idle.

## The pipeline

1. **Query expansion** — optionally expand the query with HyDE (hypothetical document embeddings) and lexical variants. GNO skips expansion when it has a strong signal.
2. **BM25 retrieval** — classical full-text search over the inverted index. Fast and precise for exact terms, technical identifiers, and quoted phrases.
3. **Vector retrieval** — cosine similarity over dense embeddings. Handles synonyms and conceptual matches.
4. **Reciprocal rank fusion** — merges BM25 and vector results by rank position, not raw score, so the two score scales never have to be comparable.
5. **Bounded graph expansion** — by default, GNO resolves only outgoing wiki/markdown links and backlinks touching the top seeds, then adds those bounded one-hop neighbors after initial fusion. Active filters still apply, existing chunk positions are preserved, and explicit links are weighted above inferred or ambiguous matches. Semantic similarity remains in the vector stage; query-time graph expansion never rebuilds the full collection graph. `--no-graph`and `--fast` skip the stage.
6. **Second fusion pass** — graph candidates join the BM25/vector ranked inputs before reranking, so linked evidence can boost the right chunk instead of duplicating a document-level hit.
7. **Cross-encoder reranking** — rescores the fused top-N with a cross-encoder over query+document together. This stage runs in Thorough and default modes, not `--fast`.
8. **Return** — the reranked top-K is returned with scores, snippets, and optionally the retrieval pipeline trace.

Supported active-owner, collection/path, tag, date, category, author, exclusion, visibility, and memory filters apply before the relevant candidate budget. Vector and hybrid retrieval also select matching-language chunks; standalone lexical language remains reserved. Whole-document exclusions inspect every chunk, including other languages. Results that fail your filters never push matching results out. Thresholds, deduplication, or few matching documents can still return fewer than K results.

## Speed modes

- **Fast** — skip query expansion, graph expansion, and reranking; return fused BM25+vector.
- **Balanced** — default. Hybrid retrieval with moderate budgets and bounded graph expansion.
- **Thorough** — larger query expansion budget, bounded graph expansion, and deeper rerank.

```
gno query "topic" --fast
gno query "topic" # balanced
gno query "topic" --thorough
gno query "topic" --no-graph
```

## Document hits and complete evidence

Finding the right document does not prove that the passage needed to answer survived into the final context. A question comparing several sources or dates needs every required passage, not just one relevant hit. Open the sources, check their exact lines, and cite each source used. A graph connection helps discover a source; it does not replace the source text supporting a claim.

If a known source is missing, use `gno query diagnose "your question" --target <uri>` to inspect document presence and filter state before retrying. A diagnostic hit is not a guarantee of complete passage coverage. Context budgets can omit or clip useful evidence; preserve Capsule gaps and omissions, state unanswered parts, and abstain when support is missing. Verified Ask checks claims against retained evidence, not the completeness or truth of the whole corpus.

## Benchmark fixtures

For repeatable retrieval checks, create a fixture with queries and relevant document URIs, then run `gno bench <fixture>`. The command evaluates BM25, vector, and hybrid modes against the same corpus and prints metrics that make model or pipeline regressions visible before you switch defaults.

```
gno bench docs/examples/bench-fixture.json
gno bench fixture.json --modes bm25,vector,hybrid --json
```

The [frozen Context Capsule agent-outcome demo](https://gno.sh/features/benchmarks#context-capsule-agent-outcome) runs one exact-identifier task three ways (a lexical-only baseline, GNO query and get, and a Context Capsule) with the same agent, corpus, and settings, and publishes the full receipts. The task is the only one of 24 where plain GNO failed and the Capsule succeeded, so it shows that difference and is not a representative sample. The Capsule lane in this demo is a lexical research prototype, so its latency does not reflect the shipped Context Capsule.

Capsule result: `INC-4827` from `gno://c001/d001.md:3` in `1` agent call and `1295` model-visible UTF-8 bytes. Token counts are not reported because the three lanes do not share one tokenizer.

## --explain

Pass `--explain` to Query or Ask to see exactly what happened: expansion output, BM25 hits, vector hits, graph expansion status, fusion order, rerank scores, and stage timings including `graphMs`. When a non-neutral content-type rule applies, the receipt also shows the raw/base score, configured factor, bounded contribution, shared auxiliary cap, final score, rule source, and full ranking-rules fingerprint. Explain output does not change Context Capsule contents.

```
gno query "topic" --explain
gno ask "summarize the meeting" --verify --explain
```

## Bounded auxiliary ranking

One configured `contentTypes[].searchBoost` can contribute `-0.05..+0.05`. Trusted local CLI project affinity can contribute up to `+0.03`. They compose once under a shared `±0.08` cap, and the final score remains in `0..1`. BM25 and vector only adjust candidates that already survived retrieval and `minScore`; content-type boosts alone never widen either candidate set.

Hybrid applies the composed auxiliary score to normalized fusion before rerank blending. Rerank ordering and lexical top-hit protection remain the final authority; content-type preference cannot override either safeguard.

Boost factors accept `0.5..2`; omitted or `1` is neutral. A canonical configured frontmatter type wins over longest-prefix matching. Categories, unknown type text, and overlapping prefixes cannot stack or manufacture a boost. The signal never creates a candidate and never bypasses collection, tag, date, category, author, or exclude filters.

## Strong signal detection

When your query contains exact identifiers or quoted phrases, the pipeline recognizes the strong signal and skips expansion — no point hallucinating a HyDE document when the user already told you exactly what they want.

## Multilingual scope and evidence

Query-language classification uses an explicit 34-language allowlist to choose prompt language. Indexed-document detection is a separate seven-language path covering `en`, `de`, `fr`, `it`, `zh`, `ja`, and `ko`. Neither count is a retrieval-quality guarantee.

The immutable April 6, 2026 FastAPI-docs fixture used 15 documents in five corpus languages and 13 queries. It measured bge-m3 at vector nDCG\@10 `0.3503` and hybrid `0.642` in the [bge-m3 evidence](https://github.com/gmickel/gno/blob/main/evals/fixtures/general-embedding-benchmark/2026-04-06-bge-m3-incumbent.md), versus Qwen3 Embedding 0.6B at vector `0.8594` and hybrid `0.947` in the [Qwen evidence](https://github.com/gmickel/gno/blob/main/evals/fixtures/general-embedding-benchmark/2026-04-06-qwen3-embedding-0-6b.md).

A separate [July 21, 2026 screen](https://github.com/gmickel/gno/blob/main/research/embeddings/2026-07-21-nemotron-3-embed-1b.md) reran the same 13-query lane after runtime/profile changes: Qwen measured `0.9891` vector / `0.9891` hybrid, and Nemotron 3 Embed 1B measured `0.9023` / `0.9461`. Nemotron used a temporary PyTorch HTTP adapter while Qwen used GNO’s production GGUF path, so timings are not comparable and no official production Nemotron GGUF was validated. These small fixtures support keeping Qwen as the default; they do not prove general language superiority.

Keyword search is weak for Chinese, Japanese, and Korean text. The [July 22, 2026 CJK benchmark](https://github.com/gmickel/gno/blob/main/evals/fixtures/cjk-lexical-benchmark/2026-07-22.md) measured current BM25 keyword search on 25 same-language queries (Recall\@10, nDCG\@10, and the share of queries with zero results):

- **Chinese:** Recall\@10 `0.2222`, nDCG\@10 `0.1481`, zero-result `0.7778`.
- **Japanese:** Recall\@10 `0.125`, nDCG\@10 `0.125`, zero-result `0.875`.
- **Korean:** Recall\@10 `0.5`, nDCG\@10 `0.5`, zero-result `0.5`.

## Related

- [Hybrid search feature](https://gno.sh/features/hybrid-search)
- [Advanced retrieval](https://gno.sh/features/advanced-retrieval)
- [Benchmarks](https://gno.sh/features/benchmarks)

## Typed metadata filters

Typed predicates apply before lexical/vector candidate limits, fusion, graph expansion, reranking, and packing. Invalid or pending metadata is ineligible under every predicate, including negation. An incomplete-coverage warning prevents interpreting an empty result as definitive absence.

[Read the metadata guide: examples, operators, missing fields, limits, and recovery.](https://gno.sh/docs/typed-metadata)
