VikingRAG
Structure-aware retrieval for documents that actually have structure.
Independent Postgres/pgvector VikingRAG with a Python SDK and explicit control
over retrieval budgets and experience learning.
Inspired by the
VikingRAG paper
· research artifacts:
rucdatascience/VikingRAG
(AGPL — not vendored here).
Install · Features · Architecture · Quick start · SDK · API · Docs · Cite
Why VikingRAG?
Classic RAG often flattens documents into unrelated chunks. Structure is lost - sections, subsections, and provenance disappear into a top-k bag of text.
VikingRAG (Gao et al., 2026) showed that hierarchy-preserving storage, multi-granular indexing, evidence-gap tools (Search / List / Grep / Read), and adaptive escalation can improve accuracy while cutting tokens.
This project turns those ideas into a production-oriented stack:
| Research idea | This repo today |
|---|---|
| Hierarchical documents | document → section → subsection → chunk with stable + canonical object URIs |
| Multi-granular indexing | Bottom-up abstracts + embeddings at document/section/chunk |
| Semantic Search | pgvector cosine discovery returning viking:// URIs |
| Cheap path / agents | Algorithm 1 agentic loop; E+ one-round fast path then escalate |
| Evidence-first grounding | Authoritative Reads, citations, POST /v1/answers |
| Experience edges | Algorithm 2 construction + Algorithm 3 Search+ (γ-gated) |
| Controllable learning | LearningPolicy: off / record_only / learn / frozen + snapshots |
| Operational constraints | Auth allowlist, migrations, prod Compose, eval smoke, typed settings |
Independent Apache-2.0 implementation — not a fork of the AGPL research artifacts. Paper charts below are paper experiments, not measurements from this repository. Score claims require regenerable exports under
docs/EVALUATION.md.
Charts redrawn from Gao et al., arXiv:2609.11390 Table 3 / abstract.
Paper experiments — not measurements from this repository.
Regenerate: uv run python scripts/render_paper_comparison_charts.py
Install
Option A — PyPI (fastest on any laptop)
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -U vikingrag
# optional richer DOCX parsing:
# pip install "vikingrag[docx]"
You still need Postgres + pgvector and Redis, plus a .env (copy from the repo .env.example):
export VIKINGRAG_DATABASE_URL=postgresql+asyncpg://vikingrag:vikingrag@localhost:5432/vikingrag
export VIKINGRAG_REDIS_URL=redis://localhost:6379/0
# Offline wiring only (not semantic quality):
export VIKINGRAG_LLM_PROVIDER=fake
export VIKINGRAG_EMBEDDING_PROVIDER=deterministic
# Real retrieval: openai | deepseek | gemini | anthropic (+ matching embeddings)
vikingrag-migrate upgrade head
vikingrag-api
# API: http://localhost:8000/docs
Option B — Clone + Docker (API already on :8000)
git clone https://github.com/mhuzaifadev/VikingRAG.git
cd VikingRAG
cp .env.example .env
# Python 3.12+, https://docs.astral.sh/uv/
make install && make docker-up && make migrate
# Compose already serves the API at http://localhost:8000 — do not also `make dev` (port conflict).
Host-only API with reload (when Compose API is not running):
make dev
Option C — pip from GitHub (pre-release / specific tag)
pip install "git+https://github.com/mhuzaifadev/VikingRAG.git@v0.5.2"
Option D — Docker Compose
git clone https://github.com/mhuzaifadev/VikingRAG.git && cd VikingRAG
cp .env.example .env
docker compose up -d --build
Production-shaped: docker compose -f docker-compose.prod.yml up -d --build — see docs/OPERATIONS.md.
Features
- Hierarchical documents - preserve section / subsection / chunk structure instead of flattening everything
- Stable URIs - address any node with
viking://documents/...(storage-independent) - Structure-aware ingestion - Markdown, TXT, and PDF with region-local chunking
- Idempotent uploads -
skip_identical,replace, orcreate_version - Structural navigation - parent, children, ancestors, descendants, and URI resolve
- Hierarchical abstracts - bottom-up summaries with token budgets and idempotent reuse
- Multi-granular embeddings - chunk content + structural summaries with explicit embedding identity
- Semantic Search - pgvector cosine retrieval with granularity weights and URI-addressable hits
- List / Grep / Read - structural children, scoped literal Grep, authoritative Read with provenance
- Evidence collection - bounded Search→List/Read pipeline assembling deduplicated evidence bundles
- Evidence sufficiency - structured assessment (
sufficient/insufficient/unknown) with citation checks - Agentic Algorithm 1 - tool-calling Search/List/Grep/Read loop with shared budgets and finalization reserve
- Answers API -
POST /v1/answersandPOST /v1/querywith modesvikingrag/vikingrag_e/vikingrag_e_plus - Experience edges - Algorithm 2 learning jobs + Algorithm 3 Search+ expansion
- Learning policy - hold-out scoring with
frozen; warm-up withlearn; snapshots freeze/rollback - Explain / replay -
GET /v1/answers/{id}/explainandrag.explain(query_id)(offline, no new LLM calls) - Evaluation scaffolding -
flat_rag+ three VikingRAG modes; Voca FAQ fixtures; CSV/JSON exports; offline smoke - Auth - optional API-key + server document allowlist (single-tenant)
- Shared budgets - cumulative tool/embedding/read/LLM limits across a request
- Replaceable providers -
openai/deepseek/gemini/anthropic/vllm; fakes rejected in production - SDK facade -
async with VikingRAGClient.from_settings()→ingest/index/ask/explain - Production foundation - FastAPI, PostgreSQL + pgvector, Redis, object store, typed settings, health checks
Docs hub: docs/README.md · docs/RETRIEVAL.md · docs/PAPER_PARITY.md · AGENTS.md
Architecture
flowchart TB
Client[Client / SDK / UI]
API[FastAPI Gateway]
subgraph Ingest[Ingestion]
Parse[Parsers TXT / MD / PDF]
Hier[Hierarchy builder]
Chunk[Structure-aware chunker]
Persist[(PostgreSQL + object store)]
end
subgraph Retrieve[Retrieval]
Search[Search]
List[List]
Grep[Grep]
Read[Read]
Judge[Evidence sufficiency]
Agent[Bounded agent]
end
Client --> API
API --> Ingest
API --> Retrieve
Parse --> Hier --> Chunk --> Persist
Search --> Persist
List --> Persist
Grep --> Persist
Read --> Persist
Judge -->|insufficient| Agent
Document model
Documents are not flat chunk lists:
flowchart TD
D["DOCUMENT<br/>viking://documents/{id}"]
S1[SECTION · Storage]
S2[SECTION · Retrieval]
S3[SECTION · Operations]
SS1[SUBSECTION · PostgreSQL]
SS2[SUBSECTION · Object Storage]
SS3[SUBSECTION · Semantic Search]
C1[CHUNK]
C2[CHUNK]
D --> S1
D --> S2
D --> S3
S1 --> SS1
S1 --> SS2
S2 --> SS3
SS1 --> C1
SS1 --> C2
Stable URIs:
viking://documents/{document_id}
viking://documents/{document_id}/nodes/{node_id}
Ingestion pipeline
flowchart LR
A[Upload] --> B[Hash / idempotency]
B --> C[Parse]
C --> D[Structure extract]
D --> E[Normalize]
E --> F[Hierarchy]
F --> G[Chunk in-region]
G --> H[Persist nodes + blob]
Layout
src/vikingrag/
api/ FastAPI routes & schemas
application/ Use-cases (ingest, navigation)
domain/ Models, URIs, errors
ingestion/ Parse → hierarchy → chunk pipeline
infrastructure/ Postgres, Redis, object store
providers/ LLM / embedding / rerank Protocols
observability/ Structured logging + correlation
settings/ Typed configuration
Quick start
After Install, check health:
curl -s http://localhost:8000/health/live | jq
curl -s http://localhost:8000/health/ready | jq
Ingest a document, index it, and search. Defaults below are offline wiring (fake LLM + deterministic embeddings) — fine for API plumbing, not real semantic quality:
curl -s -X POST http://localhost:8000/v1/documents \
-F "file=@tests/fixtures/system_architecture.md;type=text/markdown" \
-F "strategy=skip_identical" | jq
DOC_ID="<document_id from response>"
curl -s "http://localhost:8000/v1/documents/${DOC_ID}/tree" | jq
curl -s -X POST "http://localhost:8000/v1/documents/${DOC_ID}/index" \
-H "Content-Type: application/json" \
-d '{"force_summaries":false,"force_embeddings":false}' | jq
curl -s -X POST http://localhost:8000/v1/search \
-H "Content-Type: application/json" \
-d '{"query":"How do we verify retrieved evidence?","top_k":5}' | jq
curl -s -X POST http://localhost:8000/v1/answers \
-H "Content-Type: application/json" \
-d "{\"query\":\"How do we verify retrieved evidence?\",\"document_ids\":[\"${DOC_ID}\"],\"execution_mode\":\"vikingrag\"}" | jq
Interactive API docs: http://localhost:8000/docs · Ops: docs/OPERATIONS.md
SDK
Embed VikingRAG in-process (no FastAPI server required). Still needs Postgres + Redis + env:
import asyncio
from vikingrag import VikingRAGClient
async def main() -> None:
async with VikingRAGClient.from_settings() as rag:
doc = await rag.ingest("policy.md")
await rag.index(doc.id)
answer = await rag.ask(
"What is the cancellation policy?",
document_ids=[doc.id],
mode="vikingrag",
learning_policy="record_only", # held-out scoring: use "frozen"
)
print(answer.status, (answer.answer or "")[:200])
run_id = answer.metadata.get("experience_query_run_id") or answer.query_id
print(await rag.explain(run_id))
asyncio.run(main())
See also examples/voca_faq/. Advanced services remain on the client (search, answer_generator(), …).
Providers via env: VIKINGRAG_LLM_PROVIDER=openai|deepseek|gemini|anthropic|vllm (and matching embeddings). Empty VIKINGRAG_LLM_MODEL uses the provider preset (e.g. Gemini → gemini-2.5-flash). Production rejects fake / deterministic / scripted.
API
| Method | Path | Purpose |
|---|---|---|
GET |
/health/live |
Liveness |
GET |
/health/ready |
Readiness (Postgres + Redis) |
POST |
/v1/documents |
Multipart ingest (.md / .txt / .pdf / .docx) |
GET |
/v1/documents/{id} |
Document metadata |
GET |
/v1/documents/{id}/tree |
Structural tree |
POST |
/v1/documents/{id}/index |
Summarize + embed (idempotent) |
GET |
/v1/documents/{id}/index-status |
Index stage and counts |
POST |
/v1/search |
Semantic Search (scope_uri optional) |
POST |
/v1/retrieval/list |
List direct children |
POST |
/v1/retrieval/grep |
Scoped Grep |
POST |
/v1/retrieval/read |
Authoritative Read |
POST |
/v1/retrieval/evidence |
Collect evidence + assess sufficiency |
POST |
/v1/answers |
Cited answer (vikingrag / vikingrag_e / vikingrag_e_plus; learning_policy) |
GET |
/v1/answers/{query_run_id}/explain |
Offline replay of a persisted run (auth-scoped) |
POST |
/v1/query |
Mode-switched query orchestration |
GET |
/v1/nodes/{id} |
Node metadata (+ optional content) |
GET |
/v1/uris/resolve?uri= |
Resolve a viking://… URI |
Development
| Command | Purpose |
|---|---|
make install |
Sync deps (uv) + pre-commit |
make dev |
API with reload |
make lint |
Ruff |
make typecheck |
mypy (strict) |
make test |
Unit tests |
make test-integration |
Integration tests (Postgres + Redis) |
make migrate |
Apply Alembic migrations |
make docker-up / make docker-down |
Compose stack (volumes preserved on down) |
uv run vikingrag-eval smoke --offline |
Offline evaluation scaffolding smoke |
Citation
If you use ideas from VikingRAG or this software in research, please cite the paper:
@article{gao2026vikingrag,
title = {VikingRAG: Accurate and Token-efficient Retrieval-augmented
Generation over Structured Documents},
author = {Gao, Peiyuan and others},
year = {2026},
eprint = {2609.11390},
archivePrefix = {arXiv},
primaryClass = {cs.CL},
url = {https://arxiv.org/abs/2609.11390}
}
Paper: arXiv:2609.11390 · Machine-readable: CITATION.cff · Legal notices: NOTICE
License
Apache License 2.0 - see LICENSE.
VikingRAG is independently authored for production use. Do not copy code from AGPL research repositories into this tree unless you intentionally accept those obligations.
Contributing
Issues and PRs are welcome. Please:
- Keep layers clean (API ≠ domain ≠ adapters)
- Prefer typed contracts over
dict[str, Any] - Add tests with features
- Run
make lint && make typecheck && make testbefore opening a PR
⭐ If this project helps you, starring the repo helps others find it.
LinkedIn · mhuzaifa.com · GitHub
Metadata
Release files for vikingrag 0.5.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| vikingrag-0.5.2.tar.gz | 156.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vikingrag-0.5.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 383.5 kB
Release files / vikingrag-0.5.2.tar.gz
| Download URL | vikingrag-0.5.2.tar.gz |
|---|---|
| Size | 156.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f9e1a744454c807af3ff589853f85db914478bb9d7ee9b91015650e00e69d267
|
|
BLAKE2b-256 checksum How to use checksums |
9287513dd82265f38472dad764d866806d5e47c9f305793acea986c41e0253d4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / vikingrag-0.5.2-py3-none-any.whl
| Download URL | vikingrag-0.5.2-py3-none-any.whl |
|---|---|
| Size | 227.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
616acc3d5349c93912e2138b92861b1f3eaa795ea69b35c0708f2b3a479e4adb
|
|
BLAKE2b-256 checksum How to use checksums |
7c1423c753c81cdb2728db18ba24a95a3ee7c6ef03c0c67ad18311189a1ef1e3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|