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: RankFusion gives 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 with add(documents, embeddings=...) and search with Query(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

Query(vector=...)

Query(text=..., vector=...)

pgvector, Qdrant, Milvus, MongoDB, Weaviate, Redis, Elasticsearch, OpenSearch, LanceDB, Vespa, FAISS, Cassandra, Pinecone

embedded with embed

the vector

the vector

The same in mode="keyword" (all but FAISS, Cassandra and Pinecone, which search vectors only)

the text

InvalidQuery (a ValueError)

the text

Chroma

embed, or the collection’s embedding function

the vector

the vector

InMemoryBackend (word matching)

the text

InvalidQuery

the text

LangChainVectorStoreBackend

similarity_search

similarity_search_by_vector

similarity_search_by_vector

LlamaIndexBackend

the index embeds it

a QueryBundle with the embedding

a QueryBundle with both

RankFusion

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.