Your own vectors#
A text query is embedded by the backend’s embed function. When you already have the query’s
vector, pass a Query instead of a string:
from needtoknow import Query
vector = embedding_service.embed(question) # your model, service or cache
result = retriever.retrieve(Query(text=question, vector=vector), subject, k=5)
The vector is searched with as it is; the backend does not embed the text again. Permissions apply
exactly as for a text query: the same filter, the same re-check, the same RetrievalResult.
This covers:
Embedding outside the retriever: a batching embedding service, a GPU worker, a cache of frequent questions, or a model the backend cannot call.
Other modalities: an image, audio clip or document page embedded by a multimodal model, and searched against chunks embedded by the same model.
Query(vector=...)needs no text.Hybrid search with one embedding:
RankFusiongives every backend the same query, so vector backends use the vector and keyword backends use the text.Backends without an embedding function: if your ingestion pipeline computes the vectors, build the backend without
embed, write withadd(documents, embeddings=...)and search withQuery(vector=...).
A store whose vectors you compute#
from needtoknow import PermissionedRetriever, Query
from needtoknow.backends.qdrant import QdrantBackend
backend = QdrantBackend(client, "pages") # no embed function
backend.add(pages, embeddings=[image_model.embed(p.image) for p in pages])
retriever = PermissionedRetriever(backend, authorizer)
result = retriever.retrieve(Query(vector=image_model.embed(photo)), subject, k=10)
image_model stands for whatever multimodal model you use; needtoknow only sees the numbers. The
query vector must come from the same model, and have the same dimensions, as the stored vectors.
On a vector backend built without embed, a text query raises ValueError rather than being
searched some other way (Chroma is the exception: it falls back to the collection’s own embedding
function).
Hybrid search with one embedding#
from needtoknow import PermissionedRetriever, Query, RankFusion
from needtoknow.backends.pgvector import PgVectorBackend
vector = PgVectorBackend(pool) # vectors come with each query
keyword = PgVectorBackend(pool, mode="keyword")
retriever = PermissionedRetriever(RankFusion(vector, keyword), authorizer)
result = retriever.retrieve(Query(text=question, vector=embed(question)), subject)
What each backend uses#
Backend |
Text query |
|
|
|---|---|---|---|
pgvector, Qdrant, Milvus, MongoDB, Weaviate, Redis, Elasticsearch, OpenSearch |
embedded with |
the vector |
the vector |
The same in |
the text |
|
the text |
Chroma |
|
the vector |
the vector |
|
the text |
|
the text |
|
|
|
|
|
the index embeds it |
a |
a |
|
passed to every backend |
passed to every backend |
passed to every backend |
The framework integrations pass vectors on too: the Haystack component has a query_embedding
input for a text embedder’s output, and the LlamaIndex retriever forwards a QueryBundle’s
embedding when one is set.
Audit fingerprints#
Events never contain the query’s text or vector. With an audit_key, query_hmac fingerprints a
query by its text when it has text, so the same question matches whether or not a vector came
with it, and by its vector otherwise. Equal vectors give equal fingerprints; see
Audit events.
In your own backend#
search receives the query exactly as the caller passed it: a string, or a Query. A backend that
takes only strings keeps working with string queries. To accept vectors:
from needtoknow import Query
class MyBackend:
supports_filters = True
def search(self, query: str | Query, k: int, filter: Filter | None = None) -> list[Hit]:
q = Query.of(query) # a string becomes Query(text=...)
vector = q.embedded(self.embed) # the given vector, or embed(text)
...
q.keywords() gives the text for keyword search and raises ValueError for a vector-only query.
check_backend(factory, query=Query(vector=...)) runs the conformance kit with
vector queries.