Skip to main content

arara-rag

Portuguese-first retrieval that runs entirely on CPU. Chunking, dense and lexical retrieval, rank fusion and reranking — numpy only. No PyTorch, no ONNX Runtime, no FAISS. The whole install is 200 MB.

pip install arara-rag
from arara_rag import Arara

arara = Arara(path="./indice")            # out-of-core and persistent
arara.add_documents(
    {"lei_1234": open("lei.txt").read()},
    metadata={"ano": 2024, "tipo": "lei", "uf": "BR"},
)
hits = arara.search("qual a alíquota?", top_k=5, where={"ano": {"$gte": 2020}})
print(hits[0].doc_id, arara.resolve(hits[0]))     # exact source span

What's in it

Stage Component Size
Chunking tinyzchunk — tokenizer-free, distilled from an LLM teacher 2.1 MB
Dense static-nomic-384-pten-v2 — Model2Vec static embeddings 62 MB
Lexical BM25 over a numpy inverted index —
Rerank CXM25 — PT-BR lexical scoring bundled
Fusion Reciprocal Rank Fusion —

An index can be out-of-core: vectors live in a memory-mapped file and documents, metadata and offsets in SQLite, so the index is about 1 KB per document and cold pages can be evicted by the OS instead of being pinned on the heap. Same API either way — Arara() keeps everything in memory.

Speed and memory

One CPU core, no GPU. Measured end to end with python -m bench.profile.

documents index build query p50 query p95 index size peak RSS to serve
1,000 3.1 s 0.45 ms 0.54 ms 1.8 MB 472 MB
10,000 8.8 s 0.86 ms 6.8 ms 18 MB 481 MB
50,000 33.8 s 7.0 ms 8.0 ms 91 MB 553 MB
  • ~1,300–1,500 documents/second to chunk, embed, tokenise and index — chunking and embedding are per-document, so they run across processes. A single process manages ~300/second.
  • ~1.8 KB per document of index: 1.5 KB of vectors plus BM25 postings.
  • Query latency scales with corpus size because both retrievers score the whole corpus per query — that is what makes the ranking exact rather than approximate.
  • Peak RSS is dominated by a ~470 MB fixed cost (Python, numpy, the embedding model and the tokenizer tables); the corpus adds ~1.7 KB per document on top. Vectors are memory-mapped, so cold pages can be evicted.

Speed and memory

Retrieval quality

MTEB-BR, the Brazilian Portuguese benchmark with a public leaderboard. nDCG@10, fixed-window chunking. Metrics are computed by bench/metrics.py, which bench/validate_metrics.py checks against pytrec_eval to 0.0e+00.

Task docs dense lexical hybrid
BRTaxQAR (capped) 478 0.2934 0.4051 0.3486
FaQuADIR 244 0.7139 0.8961 0.8304
FaqBacenRetrieval 1,673 0.3745 0.4881 0.4526
JurisTCU 16,045 0.3887 0.5378 0.4890
Quati 50,000 0.3268 0.4067 0.4046

CXM25 reranking on top of the hybrid adds +0.018 to +0.077 nDCG@10 across these tasks for 1–6 ms per query (FaQuADIR: 0.8304 → 0.9078, BRTaxQAR full documents: 0.4801 → 0.5091).

Why chunking matters most

Legal documents in BR-TaxQA-R average 32,000 characters and reach 1.17M. MTEB-BR truncates them at 32k because transformer encoders cannot fit more. arara chunks, so it indexes the whole statute.

Configuration chunks nDCG@10 R@100
capped at 32k, one vector per doc (the leaderboard's setting) 478 0.1496 0.4351
capped at 32k, fixed windows 2,552 0.2934 0.6300
capped at 32k, tinyzchunk 23,319 0.3088 0.6225
full documents, fixed windows 6,439 0.4041 0.7497
full documents, paragraph splits 6,439 0.4041 0.7497
full documents, tinyzchunk 60,927 0.4287 0.7486
full documents, tinyzchunk + BM25 60,927 0.4801 0.8496
full documents, + CXM25 rerank 60,927 0.5091 0.8102

Chunking a legal corpus beats truncating it by 3.4×

Against the leaderboard

On FaQuADIR, arara's best configuration outranks all 96 models on the board — above voyage-context-4, gemini-embedding-2 and Qwen3-Embedding-8B — on one CPU core. On BR-TaxQA-R it beats 90 of 95.

The honest caveat: the leaderboard evaluates embedding models, and there is no BM25 entry on it. arara's strongest modes are lexical, and lexical retrieval is simply very good on short, high-overlap PT-BR documents — part of that gap is a missing baseline on their side, not a transformer-killing dense model on ours.

arara against the MTEB-BR leaderboard

Reranking

MTEB-BR reranking hands you a fixed candidate list and scores only the order (MAP@1000), so identity is the baseline the benchmark ships with.

Task identity dense lexical hybrid CXM25
QuatiReranking 0.2839 0.2798 0.2939 0.3066 0.3100
JurisTCUReranking 0.4150 0.3609 0.4279 0.4129 0.4845

Out-of-core, metadata, CRUD

An index is read far more than it is written, so deletes are tombstones and freed slots are recycled on the next write.

arara = Arara(path="./indice", max_chunk_chars=2000)

arara.add_documents(docs, metadata={"ano": 2024})        # insert / replace
arara.update_metadata("lei_1234", {"revisado": True})    # no re-embedding
arara.delete_document("lei_1234")                        # tombstone + slot reuse
arara.get_document("lei_1234")                           # (text, metadata)
arara.compact()                                          # reclaim file space

arara.search(q, where={"tipo": {"$in": ["lei", "decreto"]}, "ano": {"$gte": 2020}})
arara.search(q, where={"$or": [{"uf": "SP"}, {"uf": "RJ"}]})

Supported per field: $eq (bare value), $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $contains, $startswith, $endswith; plus top-level $and / $or. Field names are validated and values are bound as SQL parameters, so a filter cannot inject SQL.

Guarantees

Enforced by 88 tests, not asserted in prose:

  • every chunk is an exact substring of the canonical document, ordered and non-overlapping, with only whitespace between chunks — nothing is dropped;
  • no chunk exceeds max_chunk_chars, including on a 24,000-character line;
  • CRLF and LF inputs chunk identically and offsets still resolve;
  • the in-memory and on-disk paths return identical rankings;
  • importing the package never imports torch or onnxruntime.

Reproduce

python -m venv .venv && .venv/bin/pip install -e ".[bench,validate,dev]"
python -m pytest tests/                 # 88 tests
python bench/validate_metrics.py        # metrics vs pytrec_eval
./bench/run_all.sh                      # every suite -> bench/results/
python -m bench.profile                 # speed and memory -> docs/scaling.png
python -m bench.charts                  # regenerate the figures
python -m bench.leaderboard             # compare against MTEB-BR

Layout

arara_rag/
  chunk.py      chunking and the losslessness contract
  dense.py      static encoder
  lexical.py    BM25 inverted index + CXM25 reranker
  store.py      memory-mapped vectors, SQLite catalog, filters
  pipeline.py   Arara: add / search / rerank / CRUD
bench/          task loaders, metrics, suites, profiling, charts
tests/          88 contract and correctness tests
space/          Gradio demo

Limitations

  • PT-BR and English. The tokenizer, stemmer and stopwords are Portuguese.
  • The dense model is small and static; lexical retrieval carries the stack on short, high-overlap documents.
  • Per-chunk bookkeeping stays resident (16 bytes/chunk); vectors, text and metadata do not.
  • CXM25 reranking is ~71 µs/document, so it runs over a candidate set.
  • Index build forks worker processes to parallelise chunking and embedding. Set workers=1 where forking is unsafe or unavailable; the result is byte-identical, only slower.

License

Apache-2.0.

Metadata

Release files for arara-rag 0.4.0

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

Source distribution (sdist)

Source distribution for arara-rag 0.4.0
File Size Uploaded
arara_rag-0.4.0.tar.gz 43.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for arara-rag 0.4.0
File Interpreter ABI Platform
arara_rag-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 77.0 kB

Release files / arara_rag-0.4.0.tar.gz

Download URL arara_rag-0.4.0.tar.gz
Size 43.1 kB
Tags Source
SHA-256 checksum
How to use checksums
62373db3730be2f26c8b72b1bc8dc5902e2680504bc536661d3beafd3277a9a9
BLAKE2b-256 checksum
How to use checksums
3772a462da23e14d075f650a5dad27f7dda71e3114fd155b553aea36035d33c8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.7

Release files / arara_rag-0.4.0-py3-none-any.whl

Download URL arara_rag-0.4.0-py3-none-any.whl
Size 33.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
df753945ced02315d69bdfefe048e4e61e404d6a8ea1cc21f83ec3791ce8670b
BLAKE2b-256 checksum
How to use checksums
f0023878c56ce4553c9775bed4ea6f9ebc162ced09c705b6fe3375d9b1199e69
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.7

Release history Release notifications | RSS feed

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.1

2 release files

This release

0.4.0 This release

2 release files

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