Vector Stores Guide¶
Use these capability snippets inside your application’s native runtime. Installation is through PyFly Agentic extras.
Copyright 2026 Firefly Software Foundation. Licensed under the Apache License 2.0.
The Vector Stores module provides pluggable storage and retrieval backends for embedding vectors. It ships six backends -- in-memory, ChromaDB, Pinecone, Qdrant, PostgreSQL/pgvector, and sqlite-vec -- behind a unified API for upserting, searching, and deleting documents, plus a tenant/workspace-scoped wrapper. Applications must authorize scopes and qualify each backend's isolation behavior.
Architecture¶
graph TD
VP[VectorStoreProtocol] --> BVS[BaseVectorStore]
BVS --> IM[InMemoryVectorStore]
BVS --> CH[ChromaVectorStore]
BVS --> PC[PineconeVectorStore]
BVS --> QD[QdrantVectorStore]
BVS --> PG[PgVectorVectorStore]
BVS --> SV[SqliteVecVectorStore]
REG[VectorStoreRegistry] --> VP
BVS --> |auto-embed| EMB[EmbeddingProtocol]
TS[TenantScopedVectorStore] --> |wraps| VP
- VectorStoreProtocol -- Duck-typed protocol with
upsert(),search(),search_text(),delete(). - BaseVectorStore -- Abstract base providing auto-embedding,
search_textconvenience, and error wrapping. - Concrete Backends -- Backend-specific implementations that override
_upsert(),_search(),_delete(). - VectorStoreRegistry -- Named registry for managing multiple store instances.
- TenantScopedVectorStore -- Requires
(tenant_id, workspace_id)and maps that scope to a backend namespace.
Quick Start¶
from pyfly.agentic.embeddings.providers import OpenAIEmbedder
from pyfly.agentic.vectorstores import InMemoryVectorStore, VectorDocument
# Create a store with auto-embedding
embedder = OpenAIEmbedder()
store = InMemoryVectorStore(embedder=embedder)
# Upsert documents (auto-embeds if embedding is None)
docs = [
VectorDocument(id="1", text="Python is great for AI"),
VectorDocument(id="2", text="JavaScript runs in browsers"),
VectorDocument(id="3", text="Rust is fast and safe"),
]
await store.upsert(docs)
# Search by text (embeds the query, then searches)
results = await store.search_text("programming languages for machine learning", top_k=2)
for r in results:
print(f"{r.document.text} (score: {r.score:.3f})")
Backends¶
In-Memory¶
Zero-dependency, brute-force cosine similarity. Ideal for development and testing.
from pyfly.agentic.vectorstores import InMemoryVectorStore
store = InMemoryVectorStore(embedder=my_embedder)
No extra install required.
ChromaDB¶
from pyfly.agentic.vectorstores import ChromaVectorStore
# Ephemeral (in-process)
store = ChromaVectorStore(collection_name="my_docs", embedder=my_embedder)
# With a persistent/remote client
import chromadb
client = chromadb.HttpClient(host="localhost", port=8000)
store = ChromaVectorStore(collection_name="my_docs", client=client, embedder=my_embedder)
Install: python -m pip install -e ".[agentic,agentic-vectorstores-chroma]"
ChromaDB 1.5.9 retains known upstream advisories. Keep embedded use within one trust domain and restrict any remote server to trusted application access. Metadata namespaces, caller-provided filters and collection-wide document IDs do not provide the PostgreSQL adapter's namespace-write guarantee. Read the security restrictions before using Chroma for shared data.
Pinecone¶
from pyfly.agentic.vectorstores import PineconeVectorStore
store = PineconeVectorStore(
index_name="my-index",
# Reads PINECONE_API_KEY from the environment.
embedder=my_embedder,
)
Install: python -m pip install -e ".[agentic,agentic-vectorstores-pinecone]"
Qdrant¶
from pyfly.agentic.vectorstores import QdrantVectorStore
store = QdrantVectorStore(
collection_name="my_collection",
url="http://localhost:6333", # default
# For Qdrant Cloud, pass api_key from your secret store.
vector_size=1536, # must match your embedder dimensions
embedder=my_embedder,
)
Install: python -m pip install -e ".[agentic,agentic-vectorstores-qdrant]"
PostgreSQL / pgvector¶
PgVectorVectorStore borrows PyFly Data's datasource and unit of work. Install
pyfly[agentic-vectorstores-pgvector], composed with native relational and
PostgreSQL dependencies. ANN search remains pgvector/HNSW over cosine distance.
from pyfly.agentic.vectorstores import PgVectorVectorStore
# datasource is injected from the application's native PyFly Data context.
store = PgVectorVectorStore(
datasource,
dimension=1536,
table_name="vector_documents",
hnsw_m=16,
hnsw_ef_construction=64,
hnsw_ef_search=200,
create_schema=None,
embedder=my_embedder,
)
await store.initialise() # startup on the application's owning loop
await store.upsert(docs)
results = await store.search_text("query")
await store.close() # resets adapter state; never disposes the datasource
The first argument accepts a native DataSource, transaction manager or
registered name; None resolves the installed default at startup. Initialization
captures that owner so another application registry cannot redirect it. Explicit
injection is preferred with concurrent contexts. start()/stop() delegate to
initialise()/close(). Historic url=, positional URL and pool-size arguments
fail with migration guidance; move them to pyfly.data.relational configuration.
create_schema=None follows native ddl-auto: create/create-drop permit
creation; none/validate require the extension/table to exist. Explicit True
selects deliberate vector bootstrap; False always validates. Initialize before
business transactions; first initialization inside an ambient transaction is
rejected. Migrations must also qualify dimensions, indexes, privileges and RLS.
Retry and restart retain the original loop/owner; cross-loop use fails safely.
Operations use native guarded sessions and join the caller's unit of work, so business writes and vectors commit or roll back together. Read-only units reject writes. An active unit from another application manager is refused. The adapter creates no asyncpg pool or worker loop and leaves the borrowed engine usable after close.
async _prepare_session(conn, *, namespace) remains the per-transaction hook for
SET LOCAL/RLS configuration. conn is now a narrow SQLAlchemy-backed facade
with execute(sql, *values), fetchval and fetch; existing $1/$2 binds
remain supported. It exposes no raw connection/commit/pool API and execute
returns a SQLAlchemy result rather than an asyncpg command tag. Session settings
and hnsw.ef_search share the operation transaction and reset when it finishes.
Subclass _create_schema(conn) hooks use the same SQL surface at startup.
Document IDs remain globally unique across the vector table. An ID owned by
another namespace raises VectorStoreError, marks the batch's whole unit
rollback-only and never moves or discloses that row. Repeated same-namespace IDs
within a batch keep the final value. All seven metadata filter operators remain
supported. Namespace scoping does not install RLS or authenticate callers. Use
TenantScopedVectorStore with authenticated scope
binding and deployment-managed permissions.
There is no supported PRISM selector or prism.nprobe setting in this adapter.
The pg_vectorsearch evaluation records the
compatibility and filtered-recall reasons for retaining pgvector/HNSW.
sqlite-vec¶
A sqlite-vec (vec0 virtual table) backend that co-resides inside an existing
SQLite file via the storage module's DatabaseStore. Useful for embedded,
file-based deployments.
from pyfly.agentic.vectorstores import SqliteVecVectorStore
# From a path (wrapped in a DatabaseStore/LocalBackend automatically)
store = SqliteVecVectorStore("vectors.db", dimension=1536, embedder=my_embedder)
# Or from a pre-built DatabaseStore (to share a SQLite file with other data)
from pyfly.agentic.storage import DatabaseStore, LocalBackend
db = DatabaseStore(LocalBackend("app.db"), store_id="local:app.db")
store = SqliteVecVectorStore(db, dimension=1536, table_name="vec_chunks", embedder=my_embedder)
await store.upsert(docs)
results = await store.search_text("query")
await store.close()
upsert() and delete() accept an optional keyword-only session: WriteSession
to enlist the operation in a coordinated DatabaseStore write batch. search()
returns results carrying only the document id and score (text/metadata are not
stored by this backend).
Install: python -m pip install -e ".[agentic,agentic-vectorstores-sqlite-vec]"
Core Types¶
VectorDocument¶
from pyfly.agentic.vectorstores import VectorDocument
doc = VectorDocument(
id="unique-id", # required
text="The document text", # required
embedding=[0.1, 0.2, 0.3], # all vectors must match the configured embedding dimension
metadata={"source": "web"}, # optional key-value metadata
namespace="default", # optional namespace scoping
)
SearchResult¶
from pyfly.agentic.vectorstores import SearchResult
# Returned by search() and search_text()
result.document # VectorDocument
result.score # float (0.0 to 1.0, higher = more similar)
SearchFilter¶
from pyfly.agentic.vectorstores import SearchFilter
# Supported operators: eq, ne, gt, lt, gte, lte, in
filters = [
SearchFilter(field="source", operator="eq", value="web"),
SearchFilter(field="year", operator="gte", value=2024),
]
results = await store.search(query_embedding, filters=filters)
Note: Filter operator support varies by backend. eq is universally supported.
InMemoryVectorStore and PgVectorVectorStore support all 7 operators;
PgVectorVectorStore binds both the metadata key and value as parameters
(no SQL interpolation). Other external backends support a narrower subset.
Namespaces¶
All operations accept a namespace parameter to scope documents. Use distinct
IDs when writing to different namespaces in a shared PostgreSQL table:
from pyfly.agentic.vectorstores import VectorDocument
project_a_docs = [VectorDocument(id="project-a:1", text="Project A notes")]
project_b_docs = [VectorDocument(id="project-b:1", text="Project B notes")]
await store.upsert(project_a_docs, namespace="project-a")
await store.upsert(project_b_docs, namespace="project-b")
# Search only within a namespace
results = await store.search_text("query", namespace="project-a")
# Delete only within a namespace
await store.delete(["project-a:1"], namespace="project-a")
Auto-Embedding¶
When a BaseVectorStore has an embedder configured, documents without embeddings
are automatically embedded during upsert():
store = InMemoryVectorStore(embedder=my_embedder)
# These docs have no embedding -- they'll be auto-embedded
docs = [VectorDocument(id="1", text="Hello")]
await store.upsert(docs) # embedder.embed() called automatically
The search_text() method embeds the query string before searching:
# Equivalent to: embedding = await embedder.embed_one(query); store.search(embedding)
results = await store.search_text("my query")
Registry¶
from pyfly.agentic.vectorstores import VectorStoreRegistry, InMemoryVectorStore
registry = VectorStoreRegistry()
registry.register("memory", InMemoryVectorStore(embedder=my_embedder))
store = registry.get("memory") # raises KeyError if the name is not registered
await store.upsert(docs)
registry.list_names() # -> ["memory"]
registry.unregister("memory") # remove (no-op if absent)
Tenant/Workspace Scoping¶
The VectorStoreProtocol is single-namespace: a store partitions documents by one
opaque namespace string. Multi-tenant applications need stronger guarantees --
every read and write must be confined to a (tenant_id, workspace_id) scope, and
forgetting the scope must fail loudly rather than silently leak across tenants.
TenantScopedVectorStore wraps any VectorStoreProtocol backend and folds the
scope into the canonical "t/<tenant_id>/w/<workspace_id>" namespace. The wrapper
requires explicit scope on each operation; the application must authorize that
scope and choose a backend whose storage behavior meets its isolation needs.
from pyfly.agentic.vectorstores import (
InMemoryVectorStore,
TenantScopedVectorStore,
)
inner = InMemoryVectorStore(embedder=my_embedder)
store = TenantScopedVectorStore(inner) # stamp_metadata=True by default
# tenant_id and workspace_id are required keyword-only arguments on every op
await store.upsert(docs, tenant_id="acme", workspace_id="prod")
results = await store.search(query_embedding, top_k=5, tenant_id="acme", workspace_id="prod")
await store.delete(["id-1"], tenant_id="acme", workspace_id="prod")
await store.initialise() # passes through to the wrapped store's lifecycle hook
await store.close()
On upsert, each document is copied (never mutating the caller's objects), its
namespace is set to the scope, and -- when stamp_metadata=True (default) -- the
scope is also stamped onto metadata["tenant_id"] / metadata["workspace_id"].
These stamps describe the scope; they are not an authorization boundary. A caller
that forgets tenant_id/workspace_id fails with TypeError. Document IDs are
preserved, so PostgreSQL's table-wide uniqueness rule still applies.
The namespace codec is exported directly:
from pyfly.agentic.vectorstores import scope_namespace, parse_scope_namespace
scope_namespace("acme", "prod") # -> "t/acme/w/prod"
parse_scope_namespace("t/acme/w/prod") # -> ("acme", "prod")
Both raise ValueError if a component is empty or contains /. ScopedVectorStore
is the fail-loud Protocol that TenantScopedVectorStore satisfies.
Configuration¶
Construct and inject a vector adapter explicitly, choose its embedder/model and
pass namespace= on operations or use a scoped adapter. Former
default_vector_store and vector_store_namespace settings are rejected; they
do not select or scope an adapter. Native pgvector connections/pools belong to
pyfly.data.relational. See typed configuration and
native cutover.
Pipeline Integration (RAG)¶
Build retrieval-augmented generation workflows by wiring a RetrievalStep into a
pipeline via PipelineBuilder / PipelineEngine:
from pyfly.agentic.pipeline import PipelineBuilder, RetrievalStep
# RetrievalStep embeds the query text (via embedder) and searches the store
retrieval = RetrievalStep(my_store, embedder=my_embedder, top_k=5)
engine = (
PipelineBuilder("rag")
.add_node("retrieve", retrieval)
# .add_node("answer", agent) # e.g. an AgentStep that consumes the results
# .chain("retrieve", "answer")
.build()
)
result = await engine.run(inputs="What is RAG?")
RetrievalStep(store, *, embedder=None, top_k=5, input_key="input") reads the
query from inputs[input_key] (falling back to the pipeline's initial inputs). It
embeds the query with embedder.embed_one(...) then calls store.search(...); if
no embedder is supplied, the input is assumed to already be an embedding.
Custom Backend¶
Subclass BaseVectorStore and implement _upsert, _search, and _delete for a
new storage driver. For an existing backend, you can extend its implemented hooks.
This complete in-memory variant validates every document before accepting the batch:
from pyfly.agentic.vectorstores import InMemoryVectorStore, VectorDocument
class AttributedVectorStore(InMemoryVectorStore):
async def _upsert(self, documents: list[VectorDocument], namespace: str) -> None:
missing = [doc.id for doc in documents if not doc.metadata.get("source")]
if missing:
raise ValueError(f"Documents require source metadata: {', '.join(missing)}")
await super()._upsert(documents, namespace)
store = AttributedVectorStore()
await store.upsert([
VectorDocument(id="guide-1", text="Bound retry attempts.", embedding=[1.0, 0.0], metadata={"source": "guide.md"}),
])
results = await store.search([1.0, 0.0], top_k=1)
assert results[0].document.id == "guide-1"
The inherited backend implements search, filtering, namespace isolation, and deletion.
The base class fills in the contract for you: all public ops default to
namespace="default"; search()/search_text() default to top_k=5; upsert()
auto-embeds documents lacking an embedding via embedder.embed(texts) (batch);
search_text() uses embedder.embed_one(query) and raises VectorStoreError if no
embedder is configured. Every public method wraps any non-VectorStoreError
exception from your _upsert/_search/_delete in a VectorStoreError.
See also: Embeddings Guide for embedding provider setup.