Every result carries a cosine similarity score in [-1, 1], raw and unrescaled: 1.0 means the vectors point the same direction, 0 means unrelated, negative means opposed. Higher is closer. By default a query returns its top_k best matches whatever their scores — no hidden cutoff — and you impose one with min_score when you'd rather have fewer, stronger results than a fixed count. There is no universally correct threshold: the score a "good match" earns depends on the embedding model that produced the vectors, so calibrate on your own data rather than copying a number from a different stack.
Watch the scores on a live query against the 5,000-film sample corpus (pip install "veep[samples]"):
from veep import VP, samples
df = samples.dataframe()
vp = VP.from_creds()
vp.collections.create("score-threshold-demo", tier="hot")
vp.vectors.upsert("score-threshold-demo", dataframe=df)
qv = samples.encode("a young wizard discovers magical powers")
for hit in vp.vectors.query("score-threshold-demo", vector=qv, top_k=5):
print(f"{hit.score:.4f} {hit.metadata['title']}")
0.5097 The Flight of Dragons
0.3391 FairyTale: A True Story
0.3298 The Godless Girl
0.3234 Smurfs: The Lost Village
0.3232 Idle Hands
A clear gap separates the top hit from the rest of the field. min_score turns that gap into a cutoff:
hits = vp.vectors.query("score-threshold-demo", vector=qv, top_k=5, min_score=0.4)
print(len(hits))
for hit in hits:
print(f"{hit.score:.4f} {hit.metadata['title']}")
1
0.5097 The Flight of Dragons
Why there is no universal threshold
The same pair of texts scores differently under different embedding models. Under all-MiniLM-L6-v2 (the model behind the sample corpus), strong plot-level matches land in the 0.4–0.6 range and near-duplicates above that — a 0.7 cutoff here would discard genuinely excellent results. Other model families run hotter and routinely score loose matches above 0.7. Neither is wrong; the absolute number just isn't comparable across models.
So pick a threshold empirically: run a handful of representative queries with no min_score, look at where good results stop and noise begins in your score distribution, and set the cutoff in that gap. Revisit it if you change embedding models — and consider declaring your model on the collection so it can't drift silently (see How do I keep my embedding model consistent?).
Two more things worth knowing:
- RAG retrieval usually wants
top_k, not a threshold. Retrieving a fixed number of chunks and letting the LLM judge relevance is more robust than tuning a cutoff. Reach formin_scorewhen acting on weak matches has a real cost — deduplication, automated routing, recommendations. - Other metrics, other ranges.
metric="euclidean"andmetric="dot_product"score in their natural ranges (for euclidean, lower is closer). Everything above is about the default cosine metric.
vp.collections.delete("score-threshold-demo")
