Extractive, provenance-preserving context compression for RAG pipelines.
Project description
ragcompress
Extractive, provenance-preserving context compression for RAG pipelines.
ragcompress is a drop-in reranker/compression stage between your retriever
and your generator LLM. Given a query and a list of retrieved chunks, it
selects the minimal set of original, verbatim sentences needed to answer
the query — instead of summarizing (which can hallucinate) or truncating to a
fixed top-k (which wastes budget on irrelevant sentences inside relevant
chunks).
retriever ──> [ragcompress] ──> generator LLM
keeps only the sentences the query needs,
verbatim, with per-sentence source attribution
Why
- Extractive, never abstractive. Every output sentence exists verbatim in an input chunk. Nothing is paraphrased, so the compression stage cannot hallucinate — and citations stay valid.
- Provenance built in. Each kept sentence carries its source
doc_idandchunk_id, queryable on the result object and optionally inlined as[doc_id]markers in the context string. - Adaptive token budget. Instead of a fixed top-k, an elbow-detection budget keeps sentences until relevance falls off a cliff — short context for easy questions, longer for multi-hop ones.
- Framework-native adapters. A LlamaIndex
NodePostprocessorand a LangChainBaseDocumentCompressor(drop-in forContextualCompressionRetriever). - CPU-friendly. The default scorer is a small pretrained cross-encoder
(
ms-marco-MiniLM-L-6-v2) plus a MiniLM embedder for redundancy removal — no fine-tuning, no GPU required.
Install
pip install ragcompress # core
pip install ragcompress[llamaindex] # + LlamaIndex adapter
pip install ragcompress[langchain] # + LangChain adapter
pip install ragcompress[attention] # + attention-based scorer (local HF models)
pip install ragcompress[benchmarks] # + benchmark suite deps
Quickstart
from ragcompress import Chunk, Compressor, FixedBudget
chunks = [
Chunk(doc_id="dune-wiki", chunk_id="c0", text="Dune is a novel by Frank Herbert. ..."),
# ... more retrieved chunks
]
compressor = Compressor(budget=FixedBudget(max_tokens=500))
result = compressor.compress("Who wrote Dune?", chunks)
result.text # compressed context, ready for your prompt
result.compression_ratio # e.g. 0.18
result.sentences[0].source_doc_id # provenance for every kept sentence
result.provenance() # [(doc_id, chunk_id), ...] in output order
Swap in the adaptive budget:
from ragcompress import AdaptiveBudget, Compressor
compressor = Compressor(budget=AdaptiveBudget(min_tokens=64, max_tokens=1024))
How it works
- Split — chunks are segmented into sentences with provenance tags. Code fences, markdown tables, and list items are kept atomic (never split mid-fence).
- Score — each
(query, sentence)pair is scored by a cross-encoder reranker; an MMR-style redundancy pass (cosine similarity between candidates) penalizes near-duplicates of higher-ranked sentences so diverse evidence wins. - Budget — a controller selects sentences:
FixedBudget(top-k and/or token cap) orAdaptiveBudget(elbow detection on the score distribution, bounded by amin_tokens/max_tokensrange). - Reassemble — survivors are deduplicated, restored to original reading
order (doc → chunk → sentence position, which reads more coherently to
the generator than score order), and joined into the final context with
optional
[doc_id]provenance markers.
Scoring backends
| Scorer | When to use |
|---|---|
CrossEncoderScorer (default) |
Portable, CPU-friendly; any pretrained reranker from the HF hub |
AttentionScorer (opt-in, [attention] extra) |
You run a local HF model and want AttnComp-style scores from query→sentence attention mass in one forward pass |
from ragcompress.scoring import AttentionScorer
compressor = Compressor(scorer=AttentionScorer(model="path/or/hf-id-of-local-model"))
Framework adapters
LlamaIndex (pip install ragcompress[llamaindex]):
from ragcompress.adapters.llamaindex import RagCompressPostprocessor
postprocessor = RagCompressPostprocessor.from_compressor(compressor) # mode="merge" | "filter"
query_engine = index.as_query_engine(node_postprocessors=[postprocessor])
LangChain (pip install ragcompress[langchain]):
from langchain.retrievers import ContextualCompressionRetriever
from ragcompress.adapters.langchain import RagCompressDocumentCompressor
retriever = ContextualCompressionRetriever(
base_compressor=RagCompressDocumentCompressor(compressor=compressor),
base_retriever=vectorstore.as_retriever(),
)
Runnable examples: examples/basic_usage.py,
examples/llamaindex_pipeline.py,
examples/langchain_pipeline.py.
Benchmarks
The suite in benchmarks/ compares against no-compression,
naive top-k truncation, and LLMLingua-2 on HotpotQA, TriviaQA, and Natural
Questions — measuring token reduction, latency, citation validity (does
the compressed context still contain a gold-supporting sentence), and
downstream EM/F1 when a generator LLM is configured:
pip install ragcompress[benchmarks]
python benchmarks/run_benchmark.py --dataset hotpotqa --n 500 --generator anthropic
Sample run — HotpotQA distractor (n=30), 500-token budget, CPU (Apple
Silicon), default cross-encoder scorer, retention metrics (add
--generator anthropic for downstream EM/F1; llmlingua2 skipped — run
with the [benchmarks] extra installed to include it):
| Method | Token reduction | Latency (ms) | Citation validity |
|---|---|---|---|
| no_compression | 0.0% | 0.0 | 100.0% |
| top3_truncation | 67.1% | 0.0 | 43.3% |
| ragcompress_fixed | 58.8% | 718.8 | 100.0% |
| ragcompress_adaptive | 84.2% | 406.9 | 96.7% |
Naive truncation at a similar budget silently drops a gold-supporting
sentence in 57% of questions; ragcompress keeps it while cutting more
tokens. Full report: benchmarks/results/hotpotqa_n30.md.
API sketch
@dataclass
class Chunk:
doc_id
chunk_id
text
metadata
@dataclass
class ScoredSentence:
text
score
source_chunk_id
source_doc_id
sentence_index_in_chunk
@dataclass
class CompressedContext:
text
sentences
original_token_count
compressed_token_count
compression_ratio
CompressedContext.provenance() returns (doc_id, chunk_id) per kept
sentence; .sources() returns the contributing doc IDs.
Guarantees
- Extractiveness invariant (tested): every output sentence is a verbatim substring of some input chunk — never paraphrased or altered.
- Provenance invariant (tested): every output sentence's
source_chunk_id/source_doc_idmaps back to the chunk that contains it.
Development
git clone https://github.com/ragcompress/ragcompress && cd ragcompress
pip install -e ".[dev,langchain,llamaindex]"
pytest # fast unit tests, no downloads
RAGCOMPRESS_RUN_MODEL_TESTS=1 pytest -m model # real-model smoke tests
ruff check . && mypy
See CONTRIBUTING.md. Licensed under Apache-2.0.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ragcompress-0.1.0.tar.gz.
File metadata
- Download URL: ragcompress-0.1.0.tar.gz
- Upload date:
- Size: 42.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
839e1f24ab6864bdcd34ae1f67cb191d57bdfbc0d5b4e463b7f8c4947e4e18a7
|
|
| MD5 |
0ac5936f63f42af68a012be5079ca0af
|
|
| BLAKE2b-256 |
c281ae63bed62713278b4967d717d68803650a0a5876cc788b8cf4d5a5f25a43
|
File details
Details for the file ragcompress-0.1.0-py3-none-any.whl.
File metadata
- Download URL: ragcompress-0.1.0-py3-none-any.whl
- Upload date:
- Size: 35.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0372d13b8404967da73b75f5eb029a768bb98c04e8f6f4c618402ec333dfc6c1
|
|
| MD5 |
aaea5dc930041556b4ff68843771e3d8
|
|
| BLAKE2b-256 |
2a1040304a8b54fed6c6abf2d162d76708ef396794b4157fd60125c5e250e6c3
|