Score and relevance¶
Every search result has a score. It has always meant something different
depending on where it came from, and that is fine for what it is for, which is
putting results in order. It is no good for the other question people ask of
it: is this result any good?
| Where it came from | What score is |
Range |
|---|---|---|
| Dense, local index | Cosine similarity | -1 to 1 |
| Keyword | BM25 weight | 0 upward, no ceiling |
| Hybrid | Sum of reciprocal ranks, 0.5 / (60 + rank) per list, with a lift for being in both |
tops out near 0.018 |
| Azure AI Search, one vector | 1 / (1 + cosine_distance) |
0.333 to 1 |
| OpenSearch, one vector | 1 / (1 + d) or (2 - d) / 2, by version and engine |
0.333 to 1, or 0 to 1 |
| Either service, hybrid or two vectors | Fused ranks | tiny |
In a hybrid search the best result there can possibly be scores 0.018. That reads as terrible, and it is the top of the scale. Worse, the top of a ranking is the top whatever is in it: ask a collection of contracts about zebras and the first result still scores 0.018.
relevance¶
So every result also carries relevance, from 0 to 1, and relevance_kind,
which says what the number is.
relevance_kind |
What it is | When |
|---|---|---|
reranker |
A cross-encoder read the question and the chunk together and judged the pair. Strict: a chunk on the right topic that does not answer the question scores low. | A rerank ran: mode="hybrid" and above in the library by default, Rerank in the dashboard, Azure's semantic ranker. |
similarity |
The cosine similarity, with anything below zero shown as zero. The same on every engine. | Dense and hybrid searches that did not rerank. |
relative |
The score as a share of the best hit. The top hit is always 1.0, which says it won and not that it is good. | Keyword searches, because BM25 has no ceiling. |
distance |
1 / (1 + distance). Ordered and bounded, not comparable with a similarity. |
A collection whose metric is not cosine. |
| None | There is no honest number. | A store whose scoring is not known here. |
Result.similarity is the cosine on its own whenever it is known, whatever
relevance turned out to be. A reranker's verdict and a similarity live on
different scales, so if your searches sometimes rerank and sometimes do not,
put your threshold on similarity.
matched_by says which search found the chunk: ["meaning"],
["keywords"] or both. Being found by meaning is being among the nearest with
some likeness at all, not merely being in a wide prefetch.
What it is for¶
from vectrixdb import Vectrix
db = Vectrix("handbook", path="./relevance_demo")
db.add([
"Payment is due within thirty days of the invoice date.",
"Interest accrues monthly on any overdue balance.",
])
def answer(question, floor=0.55):
hits = list(db.search(question, mode="dense", limit=1))
best = hits[0] if hits else None
if best is None or (best.similarity or 0) < floor:
return "I could not find that in the documents."
return best.text
print(answer("When is payment due?")) # the first sentence
print(answer("How do zebras sleep?")) # still has a top result, and declines
db.close()
The right cut-off depends on the embedding model and on your documents. Similarities from the bundled models bunch between about 0.5 and 0.9, so measure it on your own questions rather than borrowing a number: see Measure retrieval.
In a hybrid search the relevances need not run downhill. Keywords can lift a
chunk above one that is closer in meaning, and matched_by is what explains
it. The order is still the better order; the numbers are still the truth about
each chunk.
Azure AI Search and OpenSearch¶
Both report a transform of the cosine, and both are turned back into it.
Azure documents @search.score = 1 / (1 + cosine_distance) and the way back,
and documents the semantic ranker's score as 4 (answers completely) to 0
(irrelevant), which becomes reranker on a 0 to 1 scale. A hybrid search and a
search over two vectors come back as fused ranks with no similarity in them, so
the store asks the collection's own vector one more question to get it. That is
one extra query; VectrixDB.with_azure_search(..., relevance=False) or
azure_search_relevance=False on the storage configuration saves it, and
relevance is then None for those searches.
OpenSearch documented 1 / (1 + d) for nmslib and faiss and (2 - d) / 2 for
Lucene through 2.17, and (2 - d) / 2 for every engine from 2.19. A managed or
serverless cluster may not say which it is, so the store works it out: the
first hit whose stored vector it can read gives the true cosine, and only one
of the two formulas turns that hit's score into it. Until a hit settles it
there is no number. opensearch_score_formula="half" or "reciprocal" states
it outright.
Where a store holds two vectors per chunk, the collection's own and the
service's, relevance is the collection's own, because a threshold is set
against one model and not a blend of two, and relevances carries both:
{"vectrixdb": 0.71, "bedrock": 0.64}.