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 wardowl 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 wardowl import PermissionedRetriever, Query
from wardowl.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; wardowl 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).
A query vector with the wrong number of dimensions makes the retrieval raise, and nothing is
returned. FaissBackend checks the length itself and raises ValueError before searching. Vespa
and Pinecone refuse the query, and their backends raise RuntimeError with the store’s message.
Every other backend hands the vector to its store’s client, and the store’s own error comes back
unchanged (an exception from psycopg, redis-py, the Qdrant client and so on), since the retriever
does not catch errors from a backend. The same holds when embed returns vectors of another
model’s size.
Hybrid search with one embedding#
from wardowl import PermissionedRetriever, Query, RankFusion
from wardowl.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, LanceDB, Vespa, FAISS, Cassandra, Pinecone |
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.
InvalidQuery is a ValueError that says the query, not the system, is at fault: only a vector
for keyword search, only text for a backend without an embed function, or k beyond
max_candidates. FaissBackend raises it for a query vector whose length is not its dim; other
stores report a vector of the wrong length with their own errors. The
HTTP service answers it with 422.
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 wardowl 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 InvalidQuery for a vector-only query;
raise InvalidQuery yourself for other queries your store cannot search, such as a vector of the
wrong length.
check_backend(factory, query=Query(vector=...)) runs the conformance kit with
vector queries.