How search works#
A search takes a question and returns the passages of a base that answer it best, each with a citation. It runs on the base's machine, against the base's index, in a few steps you can see — each result carries its score at every step, and each search says how long each step took. This page explains the steps, so you can read results, choose a mode and know what to change when the right passage does not come first.
flowchart LR
Q[Question] --> E[Embedded by the<br/>embedding model]
Q --> W[Its words]
E --> D["By meaning:<br/>closest chunks (cosine)"]
W --> K["By words:<br/>best chunks (BM25)"]
D --> F[Fused by rank]
K --> F
F --> R[Reranker orders<br/>the best 20–50]
R --> T[top_k passages,<br/>each cited]
What is searched#
A base searches chunks: passages of its documents of at most 1,200
characters by default, cut within one section, each keeping its document's
title and the headings above it, its page (PDF) or its line (text and
Markdown) — see Chunks. A chunk is embedded
with its title and headings in front (Handbook > Leave policy >
Vacation), so the topic of its section counts even when the passage
itself does not name it. The reranker reads the chunk the same way.
By meaning#
The question is turned into a vector by the base's embedding model
(with the model's query prefix, when it expects one: query: for E5,
search_query: for Nomic, an instruction for Qwen3 Embedding — set by
default from the model, embedding.query_prefix to change it). It is
compared with the vector of every chunk of the base (cosine
similarity), and the closest are kept: 50, or 4 × top_k when that is
more.
This finds a passage that says the same thing in other words: how many holidays do I get finds 25 days of paid vacation. With a multilingual model such as BGE-M3, a question in one language finds a passage in another. It blurs what has no meaning to the model: a product code, an error number, a rare name.
Comparing with every chunk is exact — no approximate index, nothing missed — and fits the bases Metis is built for, up to about 100,000 chunks each. The vectors are held in the base's memory.
By words#
The question's words are looked up in an inverted index of every chunk,
ranked with BM25: a chunk scores higher for holding more of the
question's words, rarer words counting more, in a shorter chunk. The best
50 (or 4 × top_k) are kept.
- Words are compared in lower case with accents folded: configuracao finds Configuração.
- A word is a run of letters and digits; one letter alone is left out,
one digit kept (
step 2). - There is no stemming and no stop-word list: travel does not find travelling, and very common words count for little anyway. Words are compared as written in any language — and languages written without spaces between words (Chinese, Japanese, Thai) are found by meaning much better than by words.
This finds what meaning blurs: Xid 79, nvidia-smi, PO-2231, a
person's name.
Fusion#
The two lists are merged by reciprocal-rank fusion: each chunk scores
the sum of 1 / (60 + its rank) over the lists it is in. Ranks, not raw
scores, are combined, so a cosine and a BM25 score need no calibration;
the constant 60 keeps one list's first place from deciding alone. A chunk
both lists found ranks above one only one list found first.
Reranking#
When the base has a reranker (spec.reranker), it reads the question
together with each of the best fused candidates — 3 × top_k, at least
20 and at most 50 — and scores how well each answers it. Results are then
in the reranker's order. A reranker reads question and passage together,
so it judges them more precisely than either retriever, which compare
them apart. Without a reranker, results come in the fused order.
Memories#
In a memory, facts that still hold come before facts a newer one replaced, after reranking: what does Ana work on? answers what holds now first, and what was Ana's role in September? still finds the old fact, below.
Filters#
A search may keep only some documents, before anything is ranked:
| Filter | API | CLI | Keeps |
|---|---|---|---|
| Folder | filters.path_prefix |
--prefix |
Documents whose path starts with it (policies/) |
| Kind | filters.kinds |
--kind (repeatable) |
md, txt, html, pdf, docx |
| Source | filters.sources |
--source (repeatable) |
Documents of these drives (a base's own notes: metis-<base>) |
What a result holds#
| Field | What it says |
|---|---|
score |
The reranker's score when reranked, else the fused score — the order of the results |
text |
The whole chunk |
citation |
source (the drive), document (its path), title, kind, page or line, headings, and snippet — up to 240 characters around the sentence holding most of the question's words |
scores.dense, scores.dense_rank |
Its cosine with the question and its rank by meaning (absent: not among the closest) |
scores.keyword, scores.keyword_rank |
Its BM25 score and its rank by words (absent: not among the best by words) |
scores.fused |
Its fused score |
scores.rerank |
The reranker's score (absent: not reranked) |
The answer also says the mode used, whether it was reranked, its
warnings, and took: milliseconds to embed the question, search by
meaning, by words, fuse and rerank, and in all. The console shows the same
under each result: meaning 0.830 (#1) · words 7.41 (#2) · fused 0.0328 ·
reranker 0.970.
When a model does not answer#
A search answers whatever its models do, and says what it could not do:
| What happens | The search | Its warning |
|---|---|---|
| The embedding model does not answer within 10 s | By words only | … : results by keywords only |
| The reranker does not answer within 10 s | In the fused order | … : results in the fused order |
| Chunks were indexed while the embedding model was down | By words, for those chunks | no chunk has a vector yet (indexed while the embedding model did not answer): results by keywords |
| Nothing is indexed yet | No results | nothing is indexed yet: the index is … |
| The sources hold nothing a base reads | No results | nothing is indexed: the sources hold no document Metis reads … |
mode: dense is the exception: without its embedding model it is refused
(503 EMBEDDING_MODEL_UNAVAILABLE), since by-words results would not be
what was asked.
The three modes#
A search may ask for one retriever only:
mode |
Uses | Console | Use it to |
|---|---|---|---|
hybrid (default) |
Meaning and words, fused, reranked | hybrid (both on the Search page) | Answer questions |
dense |
Meaning only, reranked | by meaning only | See what meaning alone finds |
keyword |
Words only, reranked; needs no embedding model | by words only | Find exact terms; search while the embedding model is down |
rerank: false (rerank cleared, --no-rerank) skips the reranker in
any mode.
Metis → Search asks one question of one or several bases. With compare by meaning, by words, both ticked, it asks it three ways and lists every passage any mode found with its rank in each — Both, Meaning, Words — and Why it came up:
- close in meaning (#2) and shares words (#1) — both agree;
- shares words (#1); not among the closest in meaning — it names a term of the question exactly;
- close in meaning (#1); its words do not match — it says the same thing in other words;
- followed by the reranker scored it … when it was reranked.
A passage found only by words often names a term exactly; one found only by meaning says the same thing in other words. To compare modes over many questions rather than one, ask an evaluation set each way (Measure and improve search quality).
Where a search runs#
A search reaches the base's index service on its machine, which embeds the
question with the base's embedding model and reranks with its reranker —
Eos deployments of the workspace, called on the workspace's network. The
question and the passages stay on your machines; Astralyx counts searches
per day, never what was asked. Searches are answered while the base is
Indexing (from what is indexed so far) and while it is Waiting.