Search / Words and vectors together
Words and vectors together
A search with both text and vector asks the words and the vectors at once.
Words find the exact term, a model number or a name; vectors find the
meaning, a question asked in other words. Together they find both.
const question = 'how do I keep a starter alive'
const { hits } = await docs.search({ text: question, vector: await embed(question) })question = "how do I keep a starter alive"
result = await docs.search(question, vector=await embed(question))question := "how do I keep a starter alive"
result, err := docs.Search(ctx, question, search.Vector(embed(question)))let question = "how do I keep a starter alive";
let found = docs.search(question).vector(&embed(question)).run()?;How the two are combined
By default the scores are added:
- the words give their best hundred by BM25, the vectors their best hundred
by cosine, or
offset + limiteach when that is more; - each list's scores are scaled to 0–1, its lowest becoming 0 and its highest 1;
- a document's score is
(1 − w) × words + w × vectors, withw, the vector weight, 0.5 unless set; a document one half did not find scores 0 there.
So a document both halves found near the top comes first, and one that only the vectors found, the recipe that never says "starter", still makes the page.
await docs.search({ text: question, vector: q, hybrid: { vectorWeight: 0.8 } }) // lean on meaningawait docs.search(question, vector=q, hybrid=tinystore.Hybrid(vector_weight=0.8)) # lean on meaningresult, err = docs.Search(ctx, question, search.Vector(q), search.Hybrid{VectorWeight: 0.8}) // lean on meaninglet found = docs.search(question).vector(&q).vector_weight(0.8).run()?; // lean on meaningvectorWeight | Ranks by |
|---|---|
| 0 | words alone; vectors only fill places |
| 0.5 | both equally, the default |
| 1 | vectors alone; words only fill places |
The weight can be set where the index opens, hybrid: { vectorWeight }, as
the index's default, and per search, which wins. Changing the index's default
applies at once.
Rerank
rerank takes the words' best hundred and orders them by their vectors
alone. Every hit holds a word of the question; the vectors decide which of
them is meant.
await docs.search({ text: 'jaguar', vector: await embed('jaguar the car'), hybrid: { rerank: true } })await docs.search("jaguar", vector=await embed("jaguar the car"), hybrid=tinystore.Hybrid(rerank=True))result, err = docs.Search(ctx, "jaguar", search.Vector(embed("jaguar the car")), search.Hybrid{Rerank: true})let found = docs.search("jaguar").vector(&embed("jaguar the car")).rerank().run()?;A hit's score is then its cosine, and a document without a vector comes last,
scored −1.
rerank needs both text and vector, and is invalid beside a
vectorWeight.
Which to use
| You want | Use |
|---|---|
| a search box that understands both terms and questions | both, the default sum |
| exact terms first, meaning to break ties: codes, names, jargon | sum, vectorWeight low |
| questions in the user's own words: support, docs, notes | sum, vectorWeight high |
| only hits that hold a word, ordered by what was meant | rerank |
Why this design?
Why add scores rather than fuse ranks. Reciprocal rank fusion, a common
way to combine two lists, reads only each hit's position, so a hit that one
half scored far above the rest counts the same as one barely ahead. In the
search-hybrid research round (the bge-m3 model on Amazon's products and
twelve StackExchange forums, 600 judged questions each), the scaled sum
ranked better than fusion by rank in all four comparisons, so it is the
default. The research reports
have every number and the command that reproduces it.
Why each half's best hundred. Scaling needs a best and a worst. A hundred is more than a page ever shows, and few enough that both halves stay exact and fast. A document past the hundredth of both halves would score near zero in any case.
See also
- Search: the engine's overview.
- Vectors: the vectors half on its own.
- Fields and weights: the words half on its own.