Drawer 03 · Ranking model
How the order is decided
Three stages produce the number a reader sees. Reciprocal Rank Fusion merges two ranked lists into one, normalised to a maximum of 1. A reranker replaces that score with a 30 / 70 blend of the fused position and a fresh query–document judgement. A separate four-term function converts the surviving list into a single confidence figure for chat answers.
03.1Constants of the model
| Symbol | Value | Declared at | Effect |
|---|---|---|---|
| DEFAULT_RRF_K | 60 | hybrid_search.py:49 | Rank damping. Larger flattens the difference between positions. |
| keyword_weight | 0.4 | main.py:196 | Multiplier on each Zoekt contribution. |
| semantic_weight | 0.6 | main.py:197 | Multiplier on each FAISS contribution. |
| use_reranking | True | main.py:198 | Reranking is on for every query that survives fusion with ≥2 rows. |
| use_query_expansion | True | main.py:199 | Keyword leg runs against up to three query variants. |
| ORIGINAL_WEIGHT | 0.3 | reranker.py:86, 274 | Share of the fused score retained after reranking. |
| RERANK_WEIGHT | 0.7 | reranker.py:87, 275 | Share taken from the cross-encoder or Cohere judgement. |
| DEFAULT_TOP_K | 50 / 100 | reranker.py:83, 273 | Depth reranked: 50 locally, 100 through Cohere. |
| tail penalty | × 0.5 | reranker.py:237, 461 | Applied to rows below the rerank depth so they sort under reranked rows. |
| HNSW_THRESHOLD | 50 000 | vector_store_faiss.py:54 | Declared; no code path constructs an HNSW index. |
03.2The score, worked through
03.3Reranker selection
get_reranker(prefer_cloud=True) at reranker.py:467
resolves at startup and never changes. The order of preference is Cohere if the
package is importable and COHERE_API_KEY is set, else the local
cross-encoder, else Cohere without a key (which degrades to pass-through), else
a cross-encoder that will fail to load.
| Property | CrossEncoderReranker | CohereReranker |
|---|---|---|
| Default model | BAAI/bge-reranker-v2-m3 | rerank-v3.5 |
| Fallback | cross-encoder/ms-marco-MiniLM-L-6-v2 | none — returns original order |
| Device | "cpu", hard default at reranker.py:92 | remote |
| Rerank depth | 50 | 100 |
| Score scale | min–max within the batch | provider's relevance score |
| Retries | none | 3, exponential back-off on rate limits |
| Failure mode | logs and returns fused order | logs and returns fused order |
| Loaded lazily | yes — first rerank() call | yes — first client use |
main.py:184 resolves a reranker through the factory and passes it
into HybridSearchService. But the service constructor also carries
a default, self.reranker = reranker or CrossEncoderReranker()
(hybrid_search.py:81). Because the factory always returns an
object, the default never fires — the two are consistent. The same is not true
of device: nothing ever hands the detected GPU device to
CrossEncoderReranker, so on a CUDA host the embedding model runs
on the GPU and the reranker runs on the CPU.
03.4Query expansion
Expansion widens the keyword leg only. QueryExpander.expand() holds
a 25-entry synonym table, builds a reverse index of it at construction, and
substitutes at most two synonyms per matched word by plain string replacement.
It then adds a case variant if the query contains an underscore or an interior
capital. The result is capped at five variants, of which
hybrid_search.py:133 uses the first three.
| Head word | Substitutions offered | First two used |
|---|---|---|
| function | func · method · def · fn · procedure | func, method |
| class | struct · type · interface · object | struct, type |
| error | exception · err · failure · bug | exception, err |
| auth | authentication · authorization · login · credentials | authentication, authorization |
| api | endpoint · route · handler · controller | endpoint, route |
| database | db · storage · repository · datastore | db, storage |
| get | fetch · retrieve · find · load · read | fetch, retrieve |
| async | await · promise · future · concurrent | await, promise |
queries = [query]
words = query.lower().split()
for word in words:
if word in self.SYNONYMS:
for syn in self.SYNONYMS[word][:2]:
expanded = query.replace(word, syn) # case-sensitive replace
if expanded not in queries:
queries.append(expanded)
str.replace against the original query, so
"Get user" matches nothing on the head word get.
And replacement is substring-wide, so expanding get in
"widget config" also rewrites the inside of
widget. The reverse index built at
reranker.py:542 is a set, so the two synonyms taken
from it are chosen in unspecified order.
03.5What the reader is shown
- score
-
The blended value after reranking — or the normalised fused value if
reranking was skipped or failed. The web client renders it as
Math.round(result.score * 100)followed by a percent sign (web/src/components/search/search-result-card.tsx), which reads as a probability but is a relative position within one result set. - match_type
-
Set from the request's
search_type, not from which leg actually produced the row (cross_repo_search.py:101-111). A hybrid query labels every rowhybrid, including rows only the semantic leg returned. - keyword_score / semantic_score
-
Carried on
HybridResultand onCrossRepoResult, but dropped bymodels/schemas.py:25— the HTTP response has no field for them, so the leg breakdown never reaches a client. - confidence
-
Present on
ChatResponseonly. Rounded to two decimals and computed from the chunks, not from the generated answer. - search_metadata
-
The full
RoutingDecision, including the prosereasoningstring with the three raw pattern scores, e.g."(Scores: symbol=0.00, concept=0.90, architecture=0.00)".