Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

411 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-rag

A single-binary local RAG (Retrieval-Augmented Generation) database for your documents — ingest, index, and query PDFs, Word files, Markdown, and images on your filesystem. Embeddings work out of the box via a bundled pure-Go model (spec 032); a local Ollama is optional for alternative models.

Status: alpha (v0.3.x) — feature-complete and working end-to-end. Multi-vault support, cross-encoder reranking, muninn-style MCP daemon, and Obsidian-aware ingestion. Full spec: PRD_RAG_Database.md.

Why

A local RAG database should be as frictionless as git init; git add; git commit — no Docker, no API keys, no cloud services. Install the binary, run go-rag init, and you have a working RAG system.

Requirements

  • Go 1.22+ (build from source)
  • (Optional) Ollama + an embedding model — only if you prefer Ollama embeddings over the bundled pure-Go default

Installation

One-line installer (macOS/Linux — recommended):

curl -fsSL https://madeinoz67.github.io/go-rag/install.sh | sh
go-rag version

The script resolves the latest release, verifies the download against the published SHA-256 checksums, then installs go-rag on your PATH.

Homebrew (macOS/Linux):

brew install madeinoz67/tap/go-rag

Build from source (needs Go 1.22+):

make build   # → ./bin/go-rag

Prebuilt binaries, the Docker image (ghcr.io/madeinoz67/go-rag), and Windows builds for every release: github.com/madeinoz67/go-rag/releases. Website & docs: https://madeinoz67.github.io/go-rag/.

Quickstart

ollama pull mxbai-embed-large        # OPTIONAL — only for Ollama embeddings (the default pure-Go model needs no pull)
make build                           # build the static binary into ./bin
./bin/go-rag init                    # create ./.go-rag/ (config + data)
./bin/go-rag add ./my-docs/          # ingest a folder (PDF/Word/Markdown/text)
./bin/go-rag query "how does X work?"  # hybrid search, source-cited results
./bin/go-rag                         # dashboard (daemon + DB status at a glance)

Commands

Ingest & query

Command Description
go-rag init Initialize a new RAG database
go-rag add <path> Add files or directories (idempotent; progress bar)
go-rag scan [--watch] Scan for changes (2-layer: fsnotify + polling)
go-rag query "<q>" Hybrid semantic + keyword search (optional --no-rerank)
go-rag status Daemon + database statistics and health
go-rag config [get|set] View or change configuration

Inspect & maintain

Command Description
go-rag files [--json] List ingested file paths
go-rag dirs [--json] Per-directory file + chunk counts
go-rag reprocess <path> Force re-ingest a directory (applies current reader/embedder; bypasses dedup)
go-rag migrate Re-embed all documents to the current model

Vaults

Command Description
go-rag vault create <name> Create a new isolated vault
go-rag vault list List all vaults with doc counts, model, daemon status
go-rag vault delete <name> Remove a vault (--force to skip confirm)
go-rag vault clear <name> Clear data, preserve config
go-rag vault clone <src> <dst> Clone a vault
go-rag vault export <name> Export a vault as a tar archive (--output file.tar)
go-rag vault import <name> --from <path> Import an existing database as a vault (no re-ingestion)
go-rag --vault <name> <command> Run any command against a specific vault

Daemon (MCP-over-HTTP, muninn-style)

Command Description
go-rag start Start the daemon in the background (loopback 127.0.0.1:7878; --bind-external to bind a non-loopback address)
go-rag stop Stop the running daemon
go-rag mcp stdio→HTTP proxy (bridges Claude Desktop to the daemon)

Document Vaults

Vaults are isolated document corpora — each vault is a separate Pebble database with its own config, embedding model, and indexes. No cross-vault contamination.

# Create vaults with different embedding models
go-rag vault create cyber-notes --embedding_model mxbai-embed-large
go-rag vault create personal --embedding_model nomic-embed-text

# Import an existing database without re-ingesting
go-rag vault import obsidian --from ~/Documents/ObsidianVault/.go-rag

# Add docs to each vault independently
go-rag --vault cyber-notes add ~/Documents/ObsidianVault/
go-rag --vault personal add ~/Documents/Personal/

# Query each vault (results are isolated)
go-rag --vault cyber-notes query "honeypot deception"

# Start per-vault daemons (each vault can have its own mcp_addr)
go-rag --vault cyber-notes start

# List all vaults
go-rag vault list

Vault root: ~/.go-rag/vaults/ (override via GO_RAG_VAULT_ROOT). Vault names: lowercase alphanumeric + hyphens, 1–64 chars.

Structured PDF ingestion

go-rag add extracts the full structure of a PDF (not just flattened text):

  • Metadata — title, author, subject, keywords from the PDF Info dictionary → Document.Metadata. Keywords flow into the --tags filter.
  • Document hierarchy — headings from DOCX (Word styles), PDF (bookmark outline or font-size heuristics), and text (pattern heuristics). Threaded into each chunk's section_context breadcrumb (spec 025 parity).
  • Tables — grid-detected tables rendered as searchable Markdown in the chunk content. Cross-page tables get continuation markers.
  • Image/chart captions — embedded images extracted and captioned by a local vision model (opt-in). The caption text becomes a searchable chunk so image content is retrievable.

Enabling image captioning

Captioning is opt-in (default off). Set in .go-rag/config.json:

{
  "captioning_enabled": true,
  "captioning_model": "minicpm-v4.6:latest"
}

Tested models (chart-caption quality, June 2026):

Model Speed Quality
minicpm-v4.6:latest 4.5s ✅ Recommended — correct, concise, fastest
glm-ocr:latest 137s Accurate but too slow for ingest
llava:latest 6.5s Miscounts (not recommended)

Provider abstraction (model backends)

Every model-using component (embeddings, enrichment, captioning, reranking) can use Ollama (default) or any OpenAI-compatible endpoint (OpenAI, Azure, vLLM, LM Studio). Per-capability config:

{
  "captioning_provider": "openai",
  "captioning_endpoint": "https://api.openai.com/v1",
  "captioning_api_key": "sk-...",
  "embedding_provider": "ollama"
}

The same provider/endpoint/api_key pattern applies to embedding_*, enrichment_*, captioning_*, and rerank_*. Embeddings default to native (the bundled pure-Go model, spec 032 — no service required); enrichment/captioning/rerank default to Ollama at ollama_url.

Hybrid retrieval (RRF)

Hybrid mode fuses the BM25 (keyword) and vector (semantic) ranked lists with Reciprocal Rank Fusion using a single symmetric constant k (default 60, the retrieval book's canonical value):

score(d) = Σ 1/(k + rank)        # rank is 1-based; summed over the lists d appears in

The same k applies to both lists — the prior asymmetric per-list constants have been removed so the fusion is reviewable and matches the standard formula. You can tune k per corpus (a larger k flattens rank dominance):

go-rag config set rrf_k 120              # persist a non-default constant
go-rag query "..." --rrf-k 30            # one-off override for a single query

rrf_k is honoured identically by the CLI, REST, gRPC, and MCP surfaces, and is inert in keyword/semantic mode (those use a single list, so there is nothing to fuse). An absent rrf_k (or 0) means "use the default 60".

Cross-encoder reranking

After RRF retrieval returns top-20 candidates, an optional Ollama LLM scores each query–chunk pair directly, cutting semantic noise (e.g., unrelated chunks with low vector similarity). Enabled via config:

go-rag config set rerank_model phi3:latest   # or llama3.1, mistral, etc.
go-rag query "honeypot deception"            # reranked: noise scored 0.000
go-rag query "honeypot deception" --no-rerank  # skip reranking (faster)

Embedding instruction prefixes

Instruction-tuned embedding models (the default nomic-embed-text, plus E5 and BGE families) expect asymmetric prefixes: a retrieval query and an indexed document are different roles. go-rag applies the right prefix automatically so each text reaches the model in its trained role (search_query: for queries, search_document: for documents on nomic; query:/passage: on E5; a query instruction on BGE). Models that don't use prefixes (e.g. mxbai-embed-large, all-MinLM-L6-v2) are left untouched.

This is on by default (auto) and requires no configuration for the common case. Override per vault:

go-rag config set embedding_prefix off              # disable prefixing
go-rag config set embedding_query_prefix "query: " # explicit per-role overrides
go-rag config set embedding_doc_prefix "passage: "

Query caching

The daemon caches query results and query embeddings in-process so a repeated query returns instantly — no Ollama round-trip, no retrieve/fuse/rerank work. Caching is on by default, transparent (a cached hit is byte-identical to a cold result), and never goes stale: an internal index epoch bumps on every ingest/delete/migrate (including the asynchronous vector landing), and migrate flushes both caches. It only helps the long-lived daemon (one-shot go-rag query calls each start cold).

go-rag query "..." --no-cache          # force a fresh result this once (still caches it)
go-rag config set query_cache_enabled false   # global kill-switch
go-rag config set query_cache_results 512     # result-cache capacity (0 = off)
go-rag config set query_cache_embeddings 1024 # query-embedding-cache capacity (0 = off)
go-rag status                            # shows: cache: result 2/256 (3 hits, 1 misses), …

The prefix never alters stored document content or document identity, so changing the convention is a re-embed (go-rag reprocess), not a re-ingest. go-rag status reports the active convention; a query whose convention differs from the corpus is refused with a re-embed hint rather than scored across a half-prefixed corpus.

Retrieval-quality benchmark (BEIR, manual)

The committed golden dataset (testdata/golden/) is a small regression gate — it saturates at recall 1.0, so it catches breakage but can't show a quality delta. To actually measure a retrieval change (e.g. the effect of instruction prefixes), run a real BEIR benchmark — opt-in and slow (it fully ingests the corpus with the real model), not in CI:

go-rag eval --benchmark scifact \
  --embedder ollama --embedding-model nomic-embed-text \
  --embedding-prefix off  --no-rerank --mode semantic   # baseline
go-rag eval --benchmark scifact \
  --embedder ollama --embedding-model nomic-embed-text \
  --embedding-prefix auto --no-rerank --mode semantic   # prefixes on

The dataset is fetched once (cached under ~/.go-rag/benchmarks/). For large datasets, MS MARCO is supported via subsampling — its full corpus is ~8.8M passages, so --benchmark msmarco streams it once and keeps a tractable, reproducible subset (sampled dev queries + their relevant passages + a stride sample of distractors):

go-rag eval --benchmark msmarco \
  --embedder ollama --embedding-model nomic-embed-text \
  --embedding-prefix auto --no-rerank --mode semantic \
  --benchmark-queries 200 --benchmark-distractors 8000

--benchmark-distractors sizes the distractor pool (larger = harder/more realistic but slower to ingest). Measured results (nomic-embed-text, semantic-only):

dataset asymmetry prefix effect (off → on)
SciFact (300 q) low (claims ~ abstracts) near-neutral: recall@5 0.735→0.744, recall@10 0.813→0.807
MS MARCO (200 q, 8k pool) high (short query / long passage) positive on every metric: recall@5 0.988→0.998, MRR 0.954→0.971, NDCG 0.964→0.977

The contrast is the point: instruction prefixes help where query/passage asymmetry is high (MS MARCO) and help least where it's low (SciFact). The MS MARCO magnitude here is capped by the easy ~8k subsample (recall near-saturated); a larger pool (--benchmark-distractors 50000+) would show a bigger effect but ingests slowly (~1000 passages/min — cross-document embed batching is audit H12).

Attribution: BEIR (Thakur et al., 2021, https://arxiv.org/abs/2104.08663); SciFact is CC BY-NC. Benchmark data is fetched at runtime and is not committed to this repo.

Embedding drift monitoring & version pinning

An Ollama-server or model update can silently change the vectors a given model produces (a pooling/quantization change), tanking recall with no error. go-rag pins the embedding profile the corpus was built under and checks it at boot, so this drift is caught before a query — not noticed after retrieval quality collapses.

A corpus baseline {model, dim, convention, ollama-version} is persisted per vault, written on first embed, refreshed on migrate, and backfilled on first boot for older vaults. At daemon startup (and in status) the live config + live Ollama version are compared against it:

  • model / dim / convention mismatchhard drift: the daemon starts degraded/health reports ready:false (liveness ok:true, so the process stays up and status/migrate still work) and mismatched queries are refused. Run go-rag migrate to re-embed under the current model.
  • Ollama-version change (same model) → soft drift: a warning; queries still serve, but re-indexing is recommended.
go-rag status        # shows: baseline: model=… dim=… conv=… ollama=…/live=…, drift: <verdict>
curl localhost:7879/health   # {"ok":true,"ready":false,"drift_verdict":"hard-drift",…}
go-rag migrate       # remediate: re-embed under the current model + refresh the baseline

This layers on the existing query-time mismatch guard (which refuses a mismatched query) — the boot check makes drift visible before the first query and catches Ollama-server drift that the model/dim guard cannot.

MCP daemon

start re-execs a detached daemon that owns the Pebble database and serves MCP over HTTP (:7878). 30 MCP tools spanning query/status, ingest & maintenance, document/chunk inspection, injection-poisoning triage, vaults, retrieval-quality eval, and admin auth. The core set: go_rag_query, go_rag_status, go_rag_add, go_rag_init, go_rag_scan, go_rag_config, go_rag_files, go_rag_dirs, go_rag_reprocess, go_rag_migrate — plus go_rag_get_chunk, go_rag_list_documents, go_rag_poison_list, go_rag_vault_list, go_rag_eval, go_rag_guide, and the spec 045 admin-auth tools. Run go-rag mcp (a stdio→HTTP proxy) to bridge a stdio client like Claude Desktop to the daemon.

Wire into Claude Desktop:

{"mcpServers": {
  "go-rag": {
    "command": "/abs/path/to/go-rag",
    "args": ["mcp", "--vault", "cyber-notes"]
  }
}}

MuninnDB bridge (opt-in)

go-rag can optionally promote its chunks into a local MuninnDB memory vault as content-addressed engrams — so retrieved reference material also lives in your long-term agent memory (go-rag retrieves, MuninnDB remembers). It is off by default and loopback-only (an opt-in egress exception to the local-first posture, not a cloud feature).

Enable:

# 1. set the MuninnDB target-vault key (env — never written to config or logs)
export GORAG_BRIDGE_TOKEN=mk_<your-go-rag-vault-key>

# 2. configure + enable (writes the flat bridge_* config fields; validates loopback)
go-rag bridge muninn init --endpoint 127.0.0.1:8477 --target-vault go-rag

# 3. add docs as usual — their chunks promote into MuninnDB asynchronously
go-rag add ./my-docs/
go-rag bridge muninn status        # config + effective knobs

Properties:

  • Loopback-only by default — non-loopback endpoints are refused at config-validation and at dial (defense vs DNS rebinding). For Docker/multi-container (go-rag + MuninnDB in separate containers), opt in with --allow-external:
    go-rag bridge muninn init --endpoint muninndb:8477 --target-vault go-rag --allow-external
    This mirrors the daemon's --bind-external pattern: loopback is the safe default, external is an explicit operator decision.
  • Never a core operation — promotion runs async after the write ACK; ingest/query are unaffected whether MuninnDB is up, down, or slow. A down MuninnDB trips a circuit breaker (no RPC storm).
  • Idempotent (content-addressed) — re-ingesting an unchanged document is a strict no-op at MuninnDB (no duplicate engrams, no forged learning signal); a changed chunk gets a new identity → a new engram.
  • Auto-backfill — enabling on a vault with an existing corpus backfills it automatically (storm-limited; pausable from the console).
  • The Memory & Graph console view (go-rag start → the 9th sidebar item) browses the promoted engrams + shows bridge health/backfill progress.

The target-vault key (GORAG_BRIDGE_TOKEN) is read from the environment and never written to config.json or logs. Full spec: specs/060-muninn-bridge/.

Network binding & security

go-rag is loopback-only by default. Every transport (MCP :7878, REST :7879, gRPC :7880) binds to 127.0.0.1 and is unreachable from other machines unless you explicitly opt in:

# Refused — exits before opening any listener:
go-rag start --mcp-addr 0.0.0.0:7878

# Allowed, with a prominent exposure warning printed once at boot:
go-rag start --mcp-addr 0.0.0.0:7878 --bind-external

--bind-external authorizes non-loopback binding for whichever transports you configured externally. go-rag ships no TLS — external binding is plaintext at your explicit risk, and access control is your responsibility. If you expose go-rag beyond loopback, front it with a TLS-terminating reverse proxy or tunnel. The default-close posture is deliberate: a frictionless local database should never silently expose your document vault to the network.

Deployment (Docker)

go-rag ships as a static, CGO-free binary in a minimal distroless image, with a docker-compose.yml for one-command local deployment. The bundled pure-Go embedder means zero external services by default.

Quick start

docker compose up -d                         # healthy daemon; ./docs is auto-watched → drop a file, it's indexed
# (You can also ingest/query on demand via the REST API — single-writer-safe: do
#  NOT exec a second `go-rag add`; the daemon holds the Pebble lock.)
curl -s -X POST http://127.0.0.1:7879/v1/add  -H 'Content-Type: application/json' -d '{"path":"/ingest"}'
curl -s -X POST http://127.0.0.1:7879/v1/query -H 'Content-Type: application/json' -d '{"query":"retrieval","k":5}'
  • Image: ghcr.io/madeinoz67/go-rag:latest (multi-arch linux/amd64 + linux/arm64, built by the release workflow). Set build: . in docker-compose.yml to build locally instead of pulling.
  • Ports (host loopback by default): MCP 127.0.0.1:7878, REST :7879, gRPC :7880. See docker-compose.yml for the commented LAN-exposure variant (no TLS — trusted networks only).
  • Vault: named volume go-rag-data at /data (persists across down/up).
  • Ingestion: host ./docs is bind-mounted read-only at /ingest and the daemon auto-watches it (GO_RAG_WATCH_DIRS=/ingest) — drop a file and it is indexed (initial scan at boot + fsnotify + a 15 s poll; on Docker Desktop/macOS bind mounts fsnotify is unreliable, so the poll is what catches host-side changes). You can also ingest/query on demand via the REST API (POST /v1/add / /v1/query, single-writer-safe — a second go-rag add process is blocked by the Pebble lock). REST auth is disabled when the token is empty (default, loopback); set GO_RAG_MCP_TOKEN to require it.

Configuration — GO_RAG_* environment variables

go-rag config is hybrid: the file base (.go-rag/config.json in the vault) is overridden by GO_RAG_* env vars set in docker-compose.yml (an env var wins only when set + non-empty). Container-priority vars include GO_RAG_MCP_ADDR, GO_RAG_MCP_TOKEN, GO_RAG_OLLAMA_URL, GO_RAG_EMBEDDING_PROVIDER, GO_RAG_EMBEDDING_MODEL, GO_RAG_WATCH_DIRS, GO_RAG_ENRICHMENT_ENABLED. Full mapping table: specs/033-docker-deployment/contracts/interface-contracts.md.

Rules

  • Single-writer: exactly one go-rag process may own the vault volume — never set deploy.replicas > 1 or mount go-rag-data read-write into a second container.
  • Loopback-by-default (spec 007): the container binds 0.0.0.0 and passes --bind-external (required for port-forwarding to work); host exposure stays loopback by default.
  • Optional Ollama (escape hatch): the bundled embedder is the default. To use an external Ollama, set GO_RAG_EMBEDDING_PROVIDER=ollama (the actual switch — GO_RAG_EMBEDDING_MODEL alone has no effect) + GO_RAG_EMBEDDING_MODEL + GO_RAG_OLLAMA_URL. Point the URL at host Ollama (http://host.docker.internal:11434 on Docker/Podman Desktop) or at the optional sidecar (http://ollama:11434, run with --profile ollama).

Runnable validation scenarios: specs/033-docker-deployment/quickstart.md.

Architecture

Layered: CLIingest pipeline (async-after-ACK, <10ms writes) → (readers / embedder / change-detection)(BM25 FTS + vector + RRF retrieval)embedded Pebble KV. Optional cross-encoder reranking via Ollama LLM. Full design: PRD_RAG_Database.md.

Project structure

cmd/go-rag/      # single binary entrypoint
internal/
  cli/           # commands (init/add/scan/query/status/config/files/dirs/reprocess/migrate/
                 #   start/stop/vault/mcp + dashboard)
  model/         # Source → Document → Chunk → Embedding
  reader/        # FileReader interface (text/markdown/docx/pdf/image)
  embed/         # Embedder interface (Ollama /api/embed)
  rerank/        # Cross-encoder reranker (Ollama LLM scoring)
  storage/       # embedded Pebble KV + key-space prefixes
  index/         # BM25 FTS + vector + RRF retrieval + SearchWithRerank
  pipeline/      # ingest pipeline + async workers + reprocess/migrate
  watcher/       # 2-layer change detection (fsnotify + polling)
  chunk/         # text splitter
  config/        # persisted configuration
  vault/         # vault registry (create/list/delete/clear/ensure-default)
  daemon/        # background daemon lifecycle (start/stop/status + pidfile + HTTP client)
  mcp/           # MCP server (stdio + HTTP) + 10 tool dispatch
docs/            # mkdocs documentation

Development

make test     # go test -race -cover ./...
make vet      # go vet ./...
make lint     # golangci-lint run
make vuln     # govulncheck ./...
make build    # CGO_ENABLED=0 static binary → ./bin/go-rag
make help     # list all targets

License

Apache License 2.0 (SPDX: Apache-2.0) — see LICENSE. Copyright (c) 2026 Stephen Eaton. The dependency stack is permissively licensed (PRD §9.3).

About

Minimal document RAG that integrates with muninndb: go-rag extracts, Muninn remembers

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages