Hybrid RAG (DuckDB vector + BM25 + RRF + recency/keyword priors + optional cross-encoder rerank) as an installable library + CLI.
redevops-rag
Hybrid RAG as a small, installable library + CLI โ **DuckDB vector + BM25 fused via Reciprocal Rank Fusion, recency & keyword priors, and an optional cross-encoder rerank.**
It's the retrieval pipeline from redevops-io/rag-saas-platform carved out of its multi-tenant SaaS shell (no Auth0/Stripe/Kubernetes/workspace coupling) so you can drop the same retrieval over any folder โ a docs tree, a repo, an Obsidian vault โ in three lines.
Pipeline
query
โโ dense : sentence-transformers embedding โ DuckDB arraycosinesimilarity (threshold 0.4)
โโ sparse : DuckDB FTS BM25
โโ Reciprocal Rank Fusion score = ฮฃ 1 / (k + rank), k = 60
โโ recency prior 0.5 ** (age_days / 90)
โโ keyword prior ร(1 + 0.05ยทterm_hits), capped 1.5
โโ (optional) cross-encoder rerank BAAI/bge-reranker-v2-m3 โ top-k
Install
Not on PyPI yet โ install from git:
pip install "git+https://github.com/redevops-io/redevops-rag.git" # core
pip install "redevops-rag[rerank] @ git+https://github.com/redevops-io/redevops-rag.git" # + cross-encoder rerank
pip install "redevops-rag[llm] @ git+https://github.com/redevops-io/redevops-rag.git" # + answer synthesis
CLI
redevops-rag index ~/obsidian-vault # chunk + embed + index into ./redevops_rag.duckdb
redevops-rag search "how do we rotate API keys" # hybrid search, top chunks
redevops-rag --rerank search "..." # add the cross-encoder rerank stage
redevops-rag ask "what's our incident process?" # search + synthesized answer (needs an LLM, below)
Answer synthesis (ask) talks to any OpenAI-compatible endpoint โ a local MLX/llama.cpp server, OpenAI, or Anthropic behind a gateway:
export REDEVOPSRAGLLMBASEURL=http://localhost:8080/v1 # e.g. a Mac running mlx_lm.server
export REDEVOPSRAGLLM_MODEL=DeepSeek-V4-Flash
export REDEVOPSRAGLLMAPIKEY=EMPTY # or sk-... for a cloud endpoint
redevops-rag ask "summarize our on-call runbook"
Library (the 3 lines)
from redevops_rag import RAG
rag = RAG(dbpath="vault.duckdb") # add usereranker=True for the cross-encoder stage rag.index("~/obsidian-vault") # chunk + embed + index (incremental: re-run anytime) hits = rag.search("zero-downtime deploys", k=8)
answer = rag.ask("zero-downtime deploys")["answer"] # if an LLM env is set
Why a folder, not a sync
Point it at one central copy of the knowledge base and query it โ you don't copy 200k files to every machine, you index once and retrieve. For a team, run it on one box (or behind a thin service) so everyone hits a single index. Configuration / skills / CLAUDE.md are small โ those belong in git, not in a RAG.
Configuration
| env | default | meaning | |-----|---------|---------| | REDEVOPSRAGEMBED_MODEL | BAAI/bge-small-en-v1.5 | sentence-transformers embedding model | | REDEVOPSRAGRERANK_MODEL | BAAI/bge-reranker-v2-m3 | cross-encoder for --rerank | | REDEVOPSRAGLLMBASEURL / MODEL / API_KEY | โ | OpenAI-compatible endpoint for ask |
Status: v0.1, not yet large-corpus benchmarked. The retrieval logic is a faithful port
of the production pipeline; the packaging/CLI are new. Validate on your own data.
AGPL-3.0-or-later ยท redevops.io