A self-hostable RAG toolkit — FAISS, sentence-transformers, and pluggable LLMs behind one CLI.
Point it at a markdown corpus, get a persistent vector store and a chat CLI. Same commands whether you're on OpenAI, Ollama, or a local HuggingFace model. Built for teams that want RAG without shipping their docs to a SaaS.
- Role
- Design + Engineering (solo, open source)
- Stack
- Pythonsentence-transformersOpenAIOllamaHugging FaceDocker
- Timeline
- 2025
- Status
- Open source
- Ownership
- Solo design + engineering
- Outcome
- 3 interchangeable LLM backends
Context
Every RAG tutorial ends the same way: sign up for a vector DB SaaS, pick one LLM vendor, wire your docs into their pipeline, done. That's fine for a demo. It's not fine when your corpus is internal documentation you can't send off-box, when your budget doesn't survive a per-token pricing model, or when the “pick one LLM” decision is exactly the thing you want to defer.
I wanted a RAG stack that runs on a laptop, in a container, or on a workstation with a local model — with the same commands, the same index format, and no vendor in the middle. The output was markdown-tree-parser: a Python toolkit that ingests a markdown corpus, builds a persistent FAISS index, and answers questions through whichever LLM you happen to have available today.
Architecture
Ingest and query are separate lifecycles. The index is a directory on disk (vector_store/), swappable between machines. The LLM adapter is the only piece that changes when you switch providers — everything else keeps its state.
Constraint
- No SaaS in the loop. Vector store, embeddings, and (optionally) inference all have to run local.
- Three providers, one interface. OpenAI, Ollama, and a local HuggingFace pipeline must be swappable through config alone — no code changes, no different CLI.
- Persistence over ceremony. Ingest once, query forever. The index has to survive a process restart without a rebuild.
- CLI-first. A human should be able to
ingest,query,stats, and drop into aninteractiveREPL without writing Python. A programmatic API is a bonus, not the main event. - Structure-aware chunking. Naive fixed-window chunking destroys markdown. Chapter → section → subsection boundaries matter for retrieval quality.
Move
- Hierarchical parser first, chunker second. Markdown is parsed into a tree (
level,title,path,content), then chunked with configurable size and overlap. Each chunk carries its full path back to the source heading — so citations read “Chapter 3 › Neural Networks › Training”, not “chunk 187”. - FAISS
IndexFlatIPon normalized embeddings. Exact cosine similarity, no approximate tricks — correct default for corpora up to hundreds of thousands of chunks.IndexIVFFlatis documented as the upgrade path when a user outgrows it. - LLM adapter with three concrete implementations. OpenAI (Chat Completions), Ollama (HTTP against
127.0.0.1:11434), and a local HuggingFace text-generation pipeline. All three expose the samegenerate(prompt, context) → answersurface. - Config-driven, not code-driven.
config.jsoncontrols provider, model, chunk size, overlap, similarity threshold, temperature, and token budget..envholds secrets. Switching providers is a one-line change. - Persistent store on disk.
vector_store/faiss.index+chunks.pkl+metadata.json. Load-on-init, save-on-ingest. No hidden global state. - CLI as the first-class surface.
ingest,query,stats,interactive— each with its own flags (--k,--json,--no-sources,--force). The Python API wraps the same primitives. - Similarity threshold as a floor, not a filter. Chunks below the configured similarity score are dropped from the LLM context entirely — the model never sees weak retrievals and hallucinates less as a result.
Result
- Ingest → query → answer in three commands.
ingest sample_document.md, thenquery "what are neural networks?"— grounded answer with cited paths and per-chunk similarity scores. - Three interchangeable LLM backends (OpenAI, Ollama, local HF), driven entirely by
config.json. - Persistent FAISS store that survives restarts and moves between machines by copying a folder.
- Interactive REPL with runtime config overrides (
set similarity_threshold 0.4,set llm_provider ollama) — useful for tuning retrieval without re-ingesting. - Docker recipe included.
python:3.9-slim→ install requirements →production_app.py interactive. Deploys as a container without further changes. - JSON output mode for machine consumers — the same query surface feeds either a human REPL or another script.
What I'd change
- An evals harness. Recall@k on a labeled question set, per provider, would replace “this answer looks good” with a number. Right now tuning
similarity_thresholdand chunk size is a spot-check exercise. - Embedding cache. Re-ingesting an unchanged corpus re-embeds unchanged chunks. A content-hash keyed cache would cut wall-clock time and (for hosted embedding models) cost.
- Streaming responses. OpenAI and Ollama both support token streaming; the CLI still waits for the full completion. First-token latency dominates perceived quality — this is the highest-value polish item.
IndexIVFFlatauto-upgrade. The README calls outIndexIVFFlatfor large datasets; the code should switch to it above a chunk-count threshold rather than making the user re-read the docs.- Structured multi-doc support. Ingest today is one-markdown-at-a-time. A
ingest --dir docs/mode with per-file provenance is the obvious next feature. - Guardrails on the LLM adapter. Rate limiting, retry-with-backoff, and a cost meter (tokens in / out) would earn their keep the moment this runs in anything other than a single-user session.