Vector stores & filters¶
pgvector requires the pgvector extra: pip install "byoai-runtime[pgvector]". Qdrant and
Pinecone need no extra — both are built on the core httpx dependency.
Zero-migration schema mapping¶
byoai.vector adapters query existing vector tables/collections/indexes directly — no
migrations, no re-indexing, no data duplication. A schema_map tells the adapter which existing
columns/fields hold each logical slot.
pgvector¶
runtime = Runtime(
vector_store={
"provider": "pgvector", # or "postgres" / "postgresql"
"dsn": "postgresql://user:pass@localhost:5432/production_db",
"table": "enterprise_knowledge",
"schema_map": {
"id": "uuid",
"embedding": "vector_768",
"content": "body_text",
"metadata": "attributes_json",
},
},
)
byoai.vector.pgvector.PgVectorStore also accepts pool (an existing asyncpg pool) instead of
dsn, min_pool_size/max_pool_size, a command_timeout, and arbitrary **pool_kwargs
forwarded to asyncpg.create_pool() (e.g. server_settings={"statement_timeout": "..."}, ssl=...).
metric selects the pgvector distance operator — it must match whatever operator class the
table's index was actually built with, or pgvector silently falls back to a sequential scan:
"cosine"(default) —<=>, for avector_cosine_opsindex."l2"—<->, for avector_l2_opsindex."inner_product"—<#>, for avector_ip_opsindex.
Document.score is always "higher = more similar" regardless of metric: cosine reports
1 - distance; l2/inner_product report the negated distance.
Qdrant¶
Reads an existing collection over Qdrant's REST API — points are never written.
runtime = Runtime(
vector_store={
"provider": "qdrant",
"url": "http://qdrant.internal:6333",
"collection": "documents",
"api_key": "...",
"schema_map": {"content": "body_text", "metadata": None}, # None = whole payload
},
)
byoai.vector.qdrant.QdrantVectorStore also takes with_vectors, a score_threshold (drop
results below a similarity floor before top_k applies), and search_params (HNSW knobs like
{"hnsw_ef": 128, "exact": False}) — Qdrant-specific, so exposed directly rather than through
the cross-provider filter dialect.
Pinecone¶
Queries an existing index via its data-plane REST API — vectors are never upserted. host is
the index's data-plane host from the Pinecone console.
runtime = Runtime(
vector_store={
"provider": "pinecone",
"host": "https://my-index-abc123.svc.us-east-1-aws.pinecone.io",
"api_key": "...",
"namespace": "",
"schema_map": {"content": "content"}, # Pinecone stores text in metadata
},
)
byoai.vector.pinecone.PineconeVectorStore also takes include_values and a fixed
sparse_vector for hybrid dense+sparse search.
Bring your own function¶
search() is the vector store protocol's only operation, so a custom retrieval backend needs
no class at all — vector_store= accepts a bare async function directly:
async def my_search(embedding, *, top_k=5, filters=None) -> list[Document]:
return await my_existing_index.query(vector=embedding, limit=top_k)
runtime = Runtime(vector_store=my_search)
Auto-wrapped in byoai.vector.base.FunctionVectorStore — the same pattern providers= uses for
bare provider functions.
Custom adapters via plugins¶
An unrecognized provider is resolved through Python entry points under the
byoai.vector_stores group before raising ConfigurationError — pip installing a package that
registers a factory there adds a new vector store without a code change here. The same plugin
mechanism applies to cache= (byoai.caches), llm= (byoai.providers), embedder=
(byoai.embedders), and semantic_cache= (byoai.semantic_caches).
Cross-provider AST filter translation¶
Pass one Mongo-style filter dialect; byoai.vector.filters parses it into a small AST, and each
adapter compiles that AST to its own native query form:
{"field": "value"} # implicit $eq
{"field": {"$eq" | "$ne" | "$gt" | "$gte" | "$lt" | "$lte": value}}
{"field": {"$in" | "$nin": [values]}}
{"$and": [expr, ...]}
{"$or": [expr, ...]}
{"$not": expr}
For pgvector, filters compile to a SQL predicate over a JSONB metadata column
(attributes_json->>'department' = 'legal'). Adding a new vector backend means adding one
dialect compiler against the shared AST, not another parser.
RAG retrieval in the pipeline¶
vector_store= alone does not add retrieval to the default pipeline — Runtime only wires up
context resolution, caching, and the provider call, in that order, ending with the provider call
as the terminal stage. To retrieve documents and inject them into the prompt, add
byoai.stages.VectorRetrieve before the terminal ProviderCall stage, with an embedder that
turns the query into a vector (see the Providers guide for
embedder=). Pipeline.add() only appends, so insert VectorRetrieve by removing and
re-adding ProviderCall after it:
from byoai import Runtime
from byoai.stages import ProviderCall, VectorRetrieve
runtime = Runtime(
llm={"provider": "openai", "model": "gpt-4o"},
vector_store={"provider": "pgvector", "dsn": "...", "table": "enterprise_knowledge"},
embedder={"provider": "openai", "model": "text-embedding-3-small"},
)
runtime.pipeline.remove(ProviderCall)
runtime.pipeline.add(VectorRetrieve(runtime.vector_store, runtime.embedder, top_k=5))
runtime.pipeline.add(ProviderCall(runtime.router))
result = await runtime.execute(
"What are our enterprise SLA terms?",
filters={"department": {"$eq": "legal"}}, # read from ctx.state by VectorRetrieve
)
filters= passed to runtime.execute() only has an effect once a stage that reads it (like
VectorRetrieve) is on the pipeline — the default pipeline ignores it.
Search directly¶
Adapters also implement search() for retrieval outside the pipeline entirely:
docs = await runtime.vector_store.search(
embedding=[0.01, 0.02, ...],
top_k=5,
filters={"department": {"$eq": "legal"}},
)