Skip to main content

vs-rag

Enterprise RAG framework with pluggable layers. Drop in your embedder, vector store, LLM, and chunker — the pipeline handles retrieval, reranking, faithfulness checking, citation verification, caching, and Graph RAG automatically.


Why vs-rag

Building a RAG system from scratch means wiring together 8–10 components and debugging failures across all of them. vs-rag gives you a production-ready pipeline out of the box:

  • Hybrid retrieval — vector search + BM25 fused with configurable weights, so keyword-heavy and semantic queries both work well
  • Reranking — cross-encoder reranker pushes the most relevant chunks to the top before generation
  • Faithfulness guard — LLM-scored faithfulness check prevents hallucinated answers from reaching the user; triggers fallback or abstain automatically
  • Graph RAG — entity extraction at ingest time + graph traversal at query time surfaces related chunks that vector search alone would miss (great for multi-hop questions)
  • Caching — query-level and embedding-level caching to avoid redundant LLM and embedding calls
  • Citations — every answer comes with chunk-level citations so you know exactly where each claim came from
  • Pluggable everything — every layer (embedder, store, chunker, reranker, LLM, cache) is an abstract base class; swap implementations without touching the pipeline

Installation

pip install vs-rag

Infrastructure dependencies (run locally or in Docker):

Service Used for Default URL
Qdrant Vector store http://localhost:6333
Ollama Embeddings + LLM (optional) http://localhost:11434

Start Qdrant:

docker run -p 6333:6333 qdrant/qdrant

Start Ollama with the default embedding model:

ollama pull nomic-embed-text:latest

Quickstart

1. Config file (config.ini)

[embedder]
provider = my_embedder
model = nomic-embed-text:latest
url = http://localhost:11434
dimension = 768

[vector_store]
provider = qdrant
url = http://localhost:6333

[chunker]
strategy = recursive
chunk_size = 512
chunk_overlap = 50

[retriever]
top_k = 20
vector_weight = 0.7
bm25_weight = 0.3

[reranker]
provider = cross_encoder
top_n = 5

[faithfulness]
threshold = 0.75
fallback = abstain

2. Implement VsLLMClient

vs-rag doesn't ship with an LLM client — you bring your own. Implement two methods:

from vs_rag import VsLLMClient

class MyLLMClient(VsLLMClient):

    async def rephrase_query(self, query: str, num_variations: int) -> list[str]:
        # Call your LLM to generate query variations for multi-query retrieval
        ...

    async def check_faithfulness(self, query: str, answer: str, context: str) -> float:
        # Call your LLM to score how grounded the answer is in the context,
        # in relation to what was actually asked. Return a float between 0.0 and 1.0
        ...

3. Build the pipeline

import asyncio
from vs_common.config.vs_ini_config import VsIniConfig
from vs_rag import RagFactory, Document

config = VsIniConfig("config.ini")
pipeline = RagFactory.from_config(config, llm=MyLLMClient())

async def main():
    # Ingest a document
    doc = Document(filename="handbook.md", content=open("handbook.md").read(), content_type="markdown")
    result = await pipeline.ingest(doc, user_ref="user_123")
    print(f"Ingested {result.chunks_created} chunks into '{result.collection}'")

    # Query
    async def generate(prompt: str) -> str:
        # call your LLM with the prompt that already contains the retrieved context
        return my_llm_call(prompt)

    response = await pipeline.query(
        query="What is the parental leave policy?",
        user_ref="user_123",
        generate_fn=generate,
    )

    print(response.answer)
    print(f"Confidence: {response.confidence.overall:.2f}")
    for c in response.citations:
        print(f"  [{c.source}] {c.text[:60]}...")

asyncio.run(main())

Response shape

class RagResponse:
    answer: str
    confidence: ConfidenceScore   # retrieval, faithfulness, overall — all 0.0–1.0
    citations: list[Citation]     # per-sentence source attribution
    fallback_triggered: bool      # True if faithfulness check failed
    cached: bool                  # True if result came from query cache

Features

Chunking strategies

Set chunker.strategy in config:

Strategy Best for
recursive (default) General text, tries paragraph → sentence splits
fixed Uniform token budgets
semantic Splits on semantic similarity boundaries (requires embedder)
document_aware Markdown/structured docs — splits on headings, preserves section context
[chunker]
strategy = document_aware
chunk_size = 1024

Hybrid retrieval tuning

Vector search finds semantically similar chunks; BM25 finds keyword-matching chunks. Adjust the blend:

[retriever]
vector_weight = 0.7   # increase for semantic/conceptual queries
bm25_weight = 0.3     # increase for exact keyword / filter queries
top_k = 20

Reranking

The cross-encoder reranker re-scores the top-k retrieved chunks using a more accurate (but slower) model before sending them to the generator. Reduce top_n to send fewer, higher-quality chunks:

[reranker]
provider = cross_encoder
top_n = 5   # chunks sent to the LLM after reranking

To skip reranking (faster, lower quality):

[reranker]
provider = passthrough

Faithfulness guard

The pipeline scores the generated answer against the retrieved context. If the score is below threshold, it triggers the fallback:

[faithfulness]
threshold = 0.75

# abstain: immediately return "I don't know" — safest
# retry: re-run generation up to max_retries times before abstaining
fallback = abstain

# only relevant when fallback = retry
max_retries = 2

If no VsLLMClient is provided, faithfulness checking is skipped and all answers pass through.

Caching

Two independent cache layers:

Query cache — caches the retrieved + reranked chunks for a user_ref:query key. The same query from the same user skips the entire retrieval pipeline on subsequent calls.

Embedding cache — caches embedding vectors so repeated texts (e.g. chunks that appear across multiple documents) are not re-embedded.

Enabling caching

Step 1 — set the backend in config.ini:

[cache]
query_backend = vs
query_prefix = rag:query:
embedding_backend = vs
embedding_prefix = rag:embed:

Step 2 — build the pipeline via RagFactory.from_config(). TTLs are optional (defaults shown):

pipeline = RagFactory.from_config(
    config,
    llm=MyLLMClient(),
    query_cache_ttl=3600,       # seconds — how long query results are cached
    embedding_cache_ttl=86400,  # seconds — how long embedding vectors are cached
)

The built-in vs backend delegates to VsCacheManager from vs-common. By default it uses an in-memory cache (lost on restart). To persist query results across restarts, point it at Redis.

Enabling Redis:

Install the Redis extra:

pip install vs-common[redis]

Add Redis config to config.ini:

[cache]
enabled = true
backend = redis
host = localhost
port = 6379
db = 0
password =
default_ttl = 3600

query_backend = vs
query_prefix = rag:query:
embedding_backend = vs
embedding_prefix = rag:embed:

Initialise VsCacheManager at startup before building the pipeline:

from vs_common.cache.vs_cache_manager import VsCacheManager
from vs_common.schema.vs_cache_config import VsCacheConfig

VsCacheManager.init(VsCacheConfig(
    enabled=config.get("cache.enabled", data_type=bool, default=False),
    backend=config.get("cache.backend", default="memory"),
    host=config.get("cache.host", default="localhost"),
    port=config.get("cache.port", data_type=int, default=6379),
    password=config.get("cache.password"),
))

pipeline = RagFactory.from_config(config, llm=MyLLMClient())

With Redis enabled, the same query from the same user_ref will be served from cache on all subsequent calls without hitting the vector store or embedder.

Cache key

The query cache key is user_ref:query. Two things must be identical for a cache hit:

  • user_ref passed to pipeline.query()
  • The exact query string

If user_ref changes between calls (e.g. a session ID), the cache will never hit. Use a stable identifier such as a user ID.

Custom cache backend

Implement VsQueryCache or VsEmbeddingCache and register it:

from vs_rag.cache.base import VsQueryCache
from vs_rag.pipeline.rag_registry import RagComponentRegistry

class MyQueryCache(VsQueryCache):
    async def get(self, key): ...
    async def set(self, key, results, ttl): ...

RagComponentRegistry.register_query_cache("my_backend", lambda cfg: MyQueryCache(cfg))
[cache]
query_backend = my_backend

Graph RAG

Graph RAG extracts entities and relationships from each chunk at ingest time and builds a knowledge graph. At query time, entities mentioned in the question are used to traverse the graph and pull in related chunks that vector search alone would not find — essential for multi-hop questions.

Implement VsEntityExtractor

from vs_rag import VsEntityExtractor
from vs_rag.schema.graph import Entity, Relationship

class MyEntityExtractor(VsEntityExtractor):

    async def extract(self, text: str, chunk_id: str, collection: str):
        # Call your LLM to extract entities and relationships from text
        # Return (List[Entity], List[Relationship])
        ...

Enable it

pipeline = RagFactory.from_config(
    config,
    llm=MyLLMClient(),
    entity_extractor=MyEntityExtractor(),
    # graph_store defaults to InMemoryGraphStore if not provided
)

Graph traversal is automatic — the HybridRetriever enriches results with graph-connected chunks transparently. No changes to query calls.

Bring your own graph store

The default InMemoryGraphStore is lost on restart. For persistence, implement VsGraphStore:

from vs_rag import VsGraphStore
from vs_rag.schema.graph import Entity, Relationship, GraphResult

class Neo4jGraphStore(VsGraphStore):
    async def add_entities(self, entities): ...
    async def add_relationships(self, relationships): ...
    async def traverse(self, entity_names, top_k): ...
    async def clear(self): ...
pipeline = RagFactory.from_config(config, llm=..., entity_extractor=..., graph_store=Neo4jGraphStore())

Customization

Every component is an abstract base class. Register your own implementation and vs-rag will use it automatically.

Custom embedder

from vs_rag import VsEmbedder
from vs_rag.pipeline.rag_registry import RagComponentRegistry

class MyEmbedder(VsEmbedder):
    async def embed(self, texts): ...
    async def embed_one(self, text): ...
    def dimension(self): return 1536

RagComponentRegistry.register_embedder("my_embedder", lambda cfg: MyEmbedder())
[embedder]
provider = my_embedder

Custom vector store

from vs_rag import VsVectorStore
from vs_rag.pipeline.rag_registry import RagComponentRegistry

class PineconeStore(VsVectorStore):
    ...

RagComponentRegistry.register_vector_store("pinecone", lambda cfg, embedder: PineconeStore(cfg))
[vector_store]
provider = pinecone

Custom chunker

from vs_rag import VsChunker
from vs_rag.pipeline.rag_registry import RagComponentRegistry

class MyChunker(VsChunker):
    async def chunk(self, text, metadata): ...

RagComponentRegistry.register_chunker("my_chunker", lambda cfg, embedder: MyChunker())
[chunker]
strategy = my_chunker

The same pattern works for rerankers (register_reranker), faithfulness checkers (register_faithfulness_checker), and fallback handlers (register_fallback).


Multi-user / multi-collection

vs-rag is multi-tenant by design. Pass user_ref to ingest and query — the pipeline automatically routes each user to their own Qdrant collections and scopes cache keys by user.

# User A's documents stay isolated from User B's
await pipeline.ingest(doc, user_ref="user_a")
await pipeline.ingest(doc, user_ref="user_b")

response = await pipeline.query("...", user_ref="user_a")  # only searches user_a's collections

The collection prefix is configurable:

[rag]
collection_prefix = myapp

Accuracy evaluation

A built-in eval runner measures correctness, faithfulness, and retrieval quality against a QA dataset:

cd tests
python eval_runner.py --dataset datasets/company_handbook_qa.json

# Skip re-ingestion if already done
python eval_runner.py --dataset datasets/company_handbook_qa.json --skip-ingest

# Enable Graph RAG (requires re-ingest)
python eval_runner.py --dataset datasets/company_handbook_qa.json --graph

Results are saved to tests/eval_report_<dataset_name>.json with per-question breakdown and category-level aggregates.


Built-in components reference

Layer Built-in options
Embedder litellm (optional — auto-loaded when embedder.provider = litellm is set; requires litellm package)
Vector store qdrant
Chunker recursive, fixed, semantic, document_aware
Reranker cross_encoder, passthrough
Fallback abstain, retry
Query cache vs (VsCacheManager)
Embedding cache vs (VsCacheManager)
Graph store InMemoryGraphStore

Metadata

Release files for vs-rag 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vs-rag 0.1.1
File Size Uploaded Provenance
vs_rag-0.1.1.tar.gz 30.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for vs-rag 0.1.1
File Interpreter ABI Platform
vs_rag-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 69.1 kB

Release files / vs_rag-0.1.1.tar.gz

Download URL vs_rag-0.1.1.tar.gz
Size 30.1 kB
Tags Source
SHA-256 checksum
How to use checksums
badae13fe68ea4bf04ed3f1ad7e533a307412ae3ec9245db3e683206b92281cc
BLAKE2b-256 checksum
How to use checksums
f4b09e6249c31e0628ced917a13d24af8f401b675354d5d495909f5bdc723138
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / vs_rag-0.1.1-py3-none-any.whl

Download URL vs_rag-0.1.1-py3-none-any.whl
Size 39.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4ca6cea316af2b51f2f2eb72d111eafdb897ec8cf615ba8e0fe890a2e7344415
BLAKE2b-256 checksum
How to use checksums
21b0026e4f484c7b478635f2f48ddde7e65c9533d910f95cd6eca5290790c275
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Security for vs-rag 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page