Hybrid retrieval
PyAIStack can combine cosine retrieval with keyword ranking. SQLite uses FTS5;
PostgreSQL uses built-in full-text search with pgvector. Hybrid retrieval is
optional and normal RAG() calls keep vector retrieval.
| Store | Vector branch | Keyword branch |
|---|---|---|
SQLiteVectorStore |
Exact cosine scan | SQLite FTS5 with BM25 ordering |
PgVectorStore |
Exact or approximate pgvector cosine search | PostgreSQL full-text rank ordering |
Quick start
Pull the models, then run the separate indexing and question examples from the repository root:
ollama pull embeddinggemma
ollama pull gemma3:4b
python examples/hybrid_retrieval/index.py
python examples/hybrid_retrieval/query.py "What does policy FIN-042 require?"
The SQLite index example refuses to append to a populated database. Use another
--db filename for a fresh demo. The question example only queries the index.
For PostgreSQL setup and runnable index/query examples, see PostgreSQL + pgvector.
Configure retrieval
from pyaistack import RAG, RAGConfig
from pyaistack.vectorstores import SQLiteVectorStore
with SQLiteVectorStore("hybrid_knowledge.db") as store:
rag = RAG(
vector_store=store,
config=RAGConfig(
retrieval_mode="hybrid",
candidate_k=20,
top_k=5,
min_score=None,
include_sources=True,
),
)
answer = rag.ask("What does policy FIN-042 require?")
print(answer.text)
for source in answer.context_sources:
print(source.document.text) # exact excerpt sent to the model
An existing index must use the same embedding model. SQLite and PostgreSQL are
hybrid-capable. The in-memory store
supports vector search only; requesting hybrid mode with it raises
ConfigurationError.
How ranking works
Both branches apply the metadata filter before limiting their candidates.
Each branch retains up to max(candidate_k, top_k) results. SQLite FTS5 uses
BM25 ordering; PostgreSQL uses ts_rank_cd. Rank fusion adds
1 / (60 + rank) from each branch, using one-based ranks. Equal fusion scores
are ordered by document ID.
Queries are treated as plain text, not executable SQL. SQLite bounds lexical
input to 64 unique word/number terms joined with OR. PostgreSQL uses a bounded
plain-text query with the simple text-search configuration. Queries without
lexical matches still retain vector candidates.
result.scoreis always cosine similarity, including keyword-only candidates.result.ranking_scoreis the fusion score in hybrid results; it is normallyNonefor vector results.min_scoreremains a cosine threshold, applied to combined candidates before final selection. A high value can remove useful keyword matches.answer.sourcescontains retrieved chunks, including ones that may not fit.answer.context_sourcescontains only the actual text excerpts supplied.- The optional displayed Sources block uses context sources. It identifies context provenance, not proof that each generated claim is supported.
Filter behavior
results = rag.search(
"purchase policy",
metadata_filter={"department": "finance", "tags": ["purchasing", "approval"]},
)
Every field must match. A scalar filter matches the same scalar or an element of a stored array. An array filter against a stored array requires all requested elements; against a stored scalar it matches any requested element. Comparisons are case-sensitive. Missing keys do not equal JSON null.
SQLite uses parameterized SQL for scalar filters and a shared Python comparison function executed within SQLite for complex filters. PostgreSQL compiles the same public matching behavior into parameterized JSONB predicates. Metadata filtering is not an authorization system.
SQLite databases and cleanup
Back up the database with SQLite's backup API (or copy it while all connections are closed) before upgrading. The first hybrid query creates and backfills an FTS5 index transactionally from existing text; no re-embedding occurs. Inserts, updates and clears keep the keyword index synchronized through database triggers. Unknown newer FTS schemas are rejected.
Vector-only mode does not require FTS5. If the SQLite build lacks FTS5, hybrid
search raises VectorStoreError; use an FTS5-enabled Python/SQLite build.
Migration failures roll back. Prefer the context manager above; close() is
idempotent. The application owns injected stores and providers.
Performance and limitations
SQLite exact vector search remains linear in matching vectors. PostgreSQL can use exact search or a compatible cosine ANN index. Hybrid retrieval adds work and is not guaranteed to improve every query. Benchmark the selected store, filters, candidate counts, and index settings with representative data.
Run python benchmarks/retrieval.py for the checked-in corpus, query-level
results, Recall@5, MRR@5, latency, and Python allocation measurements. Use
--fixture only for an offline pipeline check, not semantic accuracy evidence.
See benchmark results.
Hybrid retrieval can be followed by an application-provided reranker. See reranking and verified citations. Inline claim-level citations, resumable ingestion, and duplicate detection are not implemented.