Framework quickstart
The framework API uses one ClickHouse HTTP client and provisions one managed table for document revisions, passages, and their relationships. The same collection and metadata filters apply to text search, graph expansion and reasoning.
Start a compatible ClickHouse server as in the quickstart. In a checkout of this version, run the offline example. It uses deterministic fixtures for embeddings, extraction and generation.
uv run --locked python examples/graph_rag.py
uv run --locked python examples/graph_rag.py --evaluate
The program indexes two documents, returns a two-hop answer with links to both supporting documents, and runs a structured query against typed relationships. The evaluation flag adds distractors, a second service chain to the same database, and an independent group. It compares dense, lexical, hybrid, graph and mixed search, and exercises KAG intersections and corpus overviews on their own labelled questions. The bundled evaluation uses synthetic text and controlled providers. The benchmark guide describes the public HotpotQA comparison.
Use your models
Pass an EmbeddingProvider and its vector dimension to RAG. Add a
RelationExtractor for documents that should populate the graph. The adapter
extracts triples from a specific passage and can attach endpoint types. The
caller supplies the model API, timeouts and credentials. Pass an
AnswerProvider to use ask; the provider gets a context and returns text
and evidence IDs or an explicit abstention. Unknown citations are rejected.
Citation validation checks that the references were supplied to the generator.
It does not judge whether the generated claim entails the evidence.
index(Document(...))chunks a document by character count and overlap, batches the embeddings, and publishes the new passages and assertions together. Reusing the same document ID with changed input supersedes the entire previous revision. Repeating the current payload returns zero and skips the models.delete(document_id)hides the document and its facts.retrieve(question)defaults to hybrid text and vector search. Choosemode="mixed"to expand matching entity labels through directed relationships and fill the remaining budget with hybrid search. Useseedsinmixedorgraphmode for explicit entity IDs. To use existing relationships without an extractor, open the collection withgraph=True.reason(KnowledgeQuery(...))follows up to four predicates in order. Passschema=(Predicate(...), ...)to enable it. It requires matching stored endpoint types and returns the endpoints and complete supporting paths. Empty evidence means no matching path was returned within the configured traversal bounds. Pass aKnowledgePlannerand callask_knowledge(question)to translate a natural-language question to a typed plan before reasoning. Plans are validated against the schema and executed with bound parameters. An invalid chain proposed by a planner produces an abstention. If your application has a plan, pass it toask_knowledge(question, plan=plan)for a cited answer or toreason(plan)for the endpoints and paths. An invalid caller-supplied plan raisesKnowledgePlanError. UseKnowledgeIntersection((path_a, path_b))to find endpoints reachable from both paths. An answer must cite all passages in at least one complete proof for an endpoint. Raise the context limit if the proof spans more passages. The Ollama planner can propose either form. The framework validates types and paths. For questions that require precise semantics, supply a plan directly or evaluate the chosen model against your domain's questions.retrieve(question, mode="global")broadens the context beyond matching one entity. It groups the scoped facts into connected components and balances citations across groups and documents.askcan summarize those groups using the same answer provider. For bounded corpus reviews,overview(question, max_facts=...)exposes atruncatedflag when the fact scan exhausts its budget. A corpus dominated by one connected component needs finer clustering for a useful topical split.
Updates and deployment
Treat document IDs as stable within a namespace. For multiple writers or
replayed events, pass a monotonically increasing source revision as
Document(..., revision=n) or delete(id, revision=n). An older explicit
revision raises DocumentRevisionError. Repeating the latest revision and
payload is a no-op and a conflicting payload at the same visible revision
raises an error. If two different payloads race before either head is published,
ClickHouse deterministically chooses one generation at that revision. A
same-revision deletion wins over a live generation. Supply unique increasing
source revisions for intentional ordering across concurrent writers.
Publication follows successful staging. A failed operation can leave hidden
parts that consume storage. Logical deletion keeps stored history; manage
physical retention separately. On a single ClickHouse server, a fresh read
can see the whole old or whole new document. Queries that span several
statements recheck candidate evidence, which can reduce recall if a revision
changes during retrieval. Replica-wide visibility depends on deployment and
read routing. Provision a new table for the managed API when coming from the
low-level passage schema or the framework prototype, then re-index source
documents. The low-level ClickHouseStore still reads its own tables.
Evaluate against your corpus
Use EvalCase to label each question with the set of relevant passage IDs,
an optional set of accepted exact answers and a category. An empty relevance
set labels an unanswerable case. evaluate takes a callable returning
Context or Answer; it reports Recall@k, MRR@k, binary nDCG@k, plus exact
answer matching and citation metrics when available. It preserves the
per-case scores, reports aggregates by category, and measures wall-clock latency. Exact matching uses
case-folding and normalizes whitespace. For free-form answers use a separate
claim-level human or model-based judging process.
Keep model and corpus versions fixed across retrieval modes. Evaluate with a representative distribution of lookups, paraphrases, multi-hop and unanswerable questions. Measure project-appropriate p95 latency and report any truncated paths, retries or provider failures.
Model providers
See the model provider guide for mixing local and hosted embedding, extraction, planning and generation APIs.
Local models with Ollama
Use the supplied adapter to try arbitrary text with a local model server. Choose installed embedding and chat models; the chat model must support structured outputs. Set the dimension to a size supported by the embedding model.
import clickhouse_connect
from clickhouserag import Document, Predicate
from clickhouserag.ollama import Ollama
schema = (
Predicate("calls", "service", "service"),
Predicate("writes", "service", "database"),
)
model = Ollama(
embedding_model="embeddinggemma", chat_model="qwen2.5:1.5b", dimensions=768, schema=schema
)
with clickhouse_connect.get_client(
host="127.0.0.1",
autogenerate_session_id=False,
form_encode_query_params=True,
) as client:
rag = model.rag(client, table="service_runbooks")
rag.initialize()
rag.index(Document("gateway", "Gateway calls Ledger.", "https://example.org/gateway"))
rag.index(Document("ledger", "Ledger writes Vault.", "https://example.org/ledger"))
answer = rag.ask("What database is written to by a service that Gateway calls?", mode="mixed")
print(answer.text, [chunk.source for chunk in answer.citations])
The packaged example also accepts the ClickHouse connection variables from
the quickstart and OLLAMA_URL for a nondefault local endpoint.
ollama pull embeddinggemma
ollama pull qwen2.5:1.5b
uv run --locked python examples/ollama_rag.py
The adapter uses the configured Ollama origin directly and sends input text
to it. Use embedding_version to distinguish model revisions if reusing a tag.
Change the table when moving to a different embedding space. The local
offline example tests the RAG pipeline independently of Ollama. Model output
quality depends on the schema, corpus and selected models. The adapter requests
up to twelve relationships per passage by default. Set max_relations to
adjust extraction work for the density of your passages. When the model fails
to complete a response, the document stays at its previous published revision.
Use reason(plan) when the endpoint and its paths are sufficient. It does not
invoke the answer model. In the local example Qwen 2.5 1.5B returned the two-hop
answer in mixed mode. With full proof citation checks enabled, the generated
KAG answer abstained while the explicit plan still resolved the endpoint.
Customize document preparation
Pass a Chunker to RAG when you need section-based, token-based or semantic
splitting. Its split(text) method yields nonblank passages in source order.
ClickHouse RAG assigns the document ID, source URI, metadata and revision to
each passage. With a custom chunker you must provide index_revision.
Set an index_revision when changing the extraction model or schema too.
This lets a changed indexing pipeline republish an otherwise identical
document when using automatic revision assignment. With explicit revisions,
enqueue a new revision for each affected source document.
Combine several searches in one answer
Use retrieve_many for questions that benefit from multiple search terms or
strategies. The application can supply a known plan, user-selected searches,
or validated model-generated rewrites. The original question is passed to the
answer provider separately from the terms used for search.
from clickhouserag import SearchQuery
searches = (
SearchQuery("retention exceptions", mode="hybrid"),
SearchQuery("ERR-1044", mode="lexical"),
SearchQuery("archive service", mode="graph", seeds=("Archive",)),
)
answer = rag.ask_many(
"When can we override the archive retention policy?",
searches,
metadata={"team": "operations"},
candidates_per_query=12,
limit=8,
)
Up to eight searches share one namespace and metadata scope. The framework
fuses the ranked passages, checks current document revisions across all
searches, and gives complete graph paths priority within the final budget.
Corpus-wide summaries and KAG proofs remain available through overview
and reason or ask_knowledge.
For model-driven search, pass search_planner=StructuredLLM(backend) and
call rag.ask_planned(question). The planner sees the question and
available retrieval modes. Namespace and metadata come from the application.
The built-in model planner can propose up to four search terms. An empty
plan uses one hybrid search with the original question. Malformed plans
raise a model error. Each query result goes through the same revision and
citation checks as an explicitly composed plan.
The examples/planned_models.py program uses Ollama to plan the searches,
an OpenAI-compatible embedding endpoint and a separate Chat Completions
writer. Set RAG_EMBEDDING_URL and RAG_ANSWER_URL to the API base URLs,
and OLLAMA_URL to the planner's server. The example was verified locally
with EmbeddingGemma and Qwen 2.5 hosted in llama.cpp and Qwen 2.5 in Ollama.
It places matching documents in separate metadata scopes and cites the
document allowed by the requested scope.