Gundog is a local semantic retrieval engine for your high volume corpus. It finds relevant code and documentation by matching the semantics of your query and not just matching keywords.
Point it at your docs or code or both. It embeds everything into vectors, builds a similarity graph connecting related files, and combines semantic search with keyword matching. Ask "how does auth work?" and it retrieves the login handler, session middleware, and the ADR that explains why you chose JWT even if none of them contain the word "auth".
I wanted a clean map of all related data chunks from wide spread data sources based on a natural language query. SeaGOAT provides rather a ranked but flat and accurate pointer to specific data chunks from a single git repository. Basically, I wanted a Obsidian graph view of my docs controlled based on a natural language query without having to go through the pain of using.. well.. Obsidian.
Gundog builds these connections across repositories/data sources automatically. Vector search finds semantically related content, BM25 catches exact keyword matches, and graph expansion surfaces files you didn't know to look for.
Gundog uses ONNX Runtime and HNSW indexing by default for fast queries:
| Metric | Value |
|---|---|
| Query latency | ~15ms (after model warmup) |
| First query | ~200-300ms (model loading) |
| Accuracy | 96-100% |
| Index time | ~1 min per 100 files (depends on no. of threads) |
Based on personal testing with 60-120 files and 50 queries. Not extensively validated at scale. Your mileage may vary. See benchmark/BENCHMARK.md for details.
pip install gundog[client] # full install with daemon + TUI + indexing
pip install gundog # server only (daemon + indexing)
pip install gundog-client # client only (TUI + query)git clone https://github.com/adhityaravi/gundog.git
cd gundog
uv sync
uv run gundog --help1. Index your stuff:
gundog indexFirst run downloads the embedding model (~130MB) and converts it to ONNX format (cached at ~/.cache/gundog/onnx/ for reuse across projects using the same model). Subsequent runs are incremental and only re-index changed files.
2. Start the daemon and register your index:
gundog daemon start
gundog daemon add myproject .3. Search:
gundog query "database connection pooling"
# or launch the interactive TUI
gundog tui
# stop the daemon if you will
gundog daemon stopReturns ranked results with file paths and relevance scores. The daemon keeps the model loaded for instant queries (~15ms). The TUI provides an interactive interface with real-time search, vim-style navigation, and a visual graph.
Scans your configured sources, embeds the content, and builds a searchable index.
gundog index # uses .gundog/config.yaml
gundog index -c /path/to.yaml # custom config file
gundog index --rebuild # fresh index from scratchRuns a persistent background service for fast queries. The daemon keeps the embedding model loaded in memory, making subsequent queries instant (~15ms vs ~300ms cold start).
gundog daemon start # start daemon (bootstraps config if needed)
gundog daemon start --foreground # run in foreground (for debugging)
gundog daemon stop # stop daemon
gundog daemon status # check if daemon is running
# Index management
gundog daemon add myproject /path/to/project # register an index
gundog daemon remove myproject # unregister an index
gundog daemon list # list registered indexesThe daemon also serves a web UI at the same address for interactive queries with a visual graph. File links are auto-detected from git repos - files in a git repo with a remote get clickable links to GitHub/GitLab.
Finds relevant files for a natural language query. Requires the daemon to be running.
gundog query "error handling strategy"
gundog query "authentication" --top 5 # limit results
gundog query "auth" --index myproject # use specific index
gundog query "auth" --daemon http://192.168.1.10:7676 # use specific daemonThe gundog query command requires the daemon to be running. Daemon settings are stored at ~/.config/gundog/daemon.yaml.
Launches an interactive TUI for exploring search results with vim-style navigation and a visual relation graph.
gundog tui # connect to daemon from config
gundog tui --daemon http://192.168.1.10:7676 # connect to specific daemon
gundog tui --daemon https://gundog.example.com # connect over HTTPSThe TUI connects to the daemon over websocket and provides:
- Real-time search with debounced input
- Syntax-highlighted file preview (local only for now)
- Index switching on the fly
- Daemon URL configuration
| Key | Action |
|---|---|
/ |
Focus search input |
j / ↓ |
Next result |
k / ↑ |
Previous result |
g / G |
First / Last result |
Enter |
Open file in $EDITOR |
i |
Switch index |
L |
Set local path for file preview |
D |
Set daemon URL |
R |
Force reconnect to daemon |
? |
Toggle help |
Esc |
Back to results / Cancel |
q |
Quit |
List available indexes registered with the daemon.
gundog indexes # list all indexes
gundog indexes --daemon http://192.168.1.10:7676 # list indexes on specific daemonManage client configuration for the TUI and CLI.
gundog config --show # display current config
gundog config --init # create default config fileClient config is stored at ~/.config/gundog/client.yaml and includes settings like local paths for file preview in the TUI.
-
Embedding: Files are converted to vectors using sentence-transformers. Similar concepts end up as nearby vectors.
-
Hybrid Search: Combines semantic (vector) search with keyword (BM25) search using Reciprocal Rank Fusion. Queries like "UserAuthService" find exact matches even when embeddings might miss them.
-
Storage: Vectors stored locally using a vector DB: plain numpy; or HNSW. No external services.
-
Two-Stage Ranking: Coarse retrieval via vector+BM25 fusion, then fine-grained ranking using per-line TF-IDF scores to pinpoint the best matching line within each chunk.
-
Graph: Documents above a similarity threshold get connected, enabling traversal from direct matches to related files.
-
Query: Your query gets embedded, compared against stored vectors, fused with keyword results, and ranked. Scores are rescaled so 0% = baseline, 100% = perfect match. Irrelevant queries return nothing.
Gundog uses three config files:
| File | Scope | Purpose |
|---|---|---|
.gundog/config.yaml |
Per-project | Index settings (sources, model, storage) |
~/.config/gundog/daemon.yaml |
Per-user | Daemon settings (host, port, registered indexes) |
~/.config/gundog/client.yaml |
Per-user | Client settings (local paths for TUI file preview) |
Each project has its own .gundog/config.yaml that defines what to index and how:
sources:
- path: ./docs
glob: "**/*.md"
- path: ./src
glob: "**/*.py"
type: code # optional - for filtering with --type
ignore_preset: python # optional - predefined ignores
ignore: # optional - additional patterns to skip
- "**/test_*"
use_gitignore: true # default - auto-read .gitignore
embedding:
# Any sentence-transformers model works: https://sbert.net/docs/sentence_transformer/pretrained_models.html
model: BAAI/bge-small-en-v1.5 # default (~130MB), good balance of speed/quality
enable_onnx: true # default. forces ONNX conversion
threads: 2 # CPU threads for embedding (increase for faster indexing)
storage:
use_hnsw: true # default - O(log n) search, scales to millions. Uses numpy if false.
path: .gundog/index
graph:
similarity_threshold: 0.7 # min similarity to create edge
expand_threshold: 0.5 # min edge weight for query expansion
max_expand_depth: 2 # how far to traverse during expansion
hybrid:
enabled: true # combine vector + keyword search (default: on)
bm25_weight: 0.5 # keyword search weight
vector_weight: 0.5 # semantic search weight
recency:
enabled: false # boost recently modified files (opt-in, requires git)
weight: 0.15 # how much recency affects score (0-1)
half_life_days: 30 # days until recency boost decays to 50%
chunking:
enabled: true # default - split files into chunks for better precision
max_tokens: 512 # tokens per chunk
overlap_tokens: 50 # overlap between chunksThe type field is optional. If you want to filter results by category, assign types to your sources. Any string works.
| Option | Default | Description |
|---|---|---|
model |
BAAI/bge-small-en-v1.5 |
Any sentence-transformers model |
enable_onnx |
true |
Use ONNX Runtime |
threads |
2 |
CPU threads for embedding operations |
ONNX models are automatically and forcefully converted on first use and cached at ~/.cache/gundog/onnx/. This cache is shared across all your projects that use the same model.
The threads setting controls CPU parallelism for embedding. The default of 2 is conservative to prevent system slowdown during indexing. Increase for faster indexing on multi-core machines, decrease if your system becomes unresponsive:
embedding:
threads: 4 # use 4 CPU threadsNote on GPU support: Gundog uses ONNX Runtime for inference by default, which runs on CPU. The default installation includes CPU-only PyTorch (~176MB) rather than CUDA PyTorch (~3GB+). This is intentional - ONNX provides fast CPU inference without GPU dependencies.
If you want GPU acceleration (only useful with enable_onnx: false):
uv pip install torch --index-url https://download.pytorch.org/whl/cu121 --reinstall| Option | Default | Description |
|---|---|---|
use_hnsw |
true |
Use HNSW index for O(log n) search |
path |
.gundog/index |
Where to store the index |
Control which files are excluded from indexing:
ignore: List of glob patterns to skip (e.g.,**/test_*,**/__pycache__/*)ignore_preset: Predefined patterns for common languages:python,javascript,typescript,go,rust,javause_gitignore: Auto-read.gitignorefrom source directory (default:true)
Enabled by default for better search precision. Instead of embedding whole files (which dilutes signal), chunking splits files into overlapping segments:
chunking:
enabled: true
max_tokens: 512 # ~2000 characters per chunk
overlap_tokens: 50Results are automatically deduplicated by file, showing the best-matching chunk with line numbers.
For codebases where recent changes matter more, enable recency boosting. Files modified recently get a score boost based on their git commit history:
recency:
enabled: true
weight: 0.15 # boost multiplier (0.15 = up to 15% boost)
half_life_days: 30 # file modified 30 days ago gets 50% of max boostUses exponential decay: a file modified today gets full boost, one modified half_life_days ago gets half, and older files approach zero. Requires files to be in a git repository.
The daemon config at ~/.config/gundog/daemon.yaml controls the background service:
daemon:
host: 127.0.0.1 # bind address
port: 7676 # port number
serve_ui: true # serve web UI at root path
auth: # Auth not supported by the clients yet
enabled: false # require API key
api_key: null # set via GUNDOG_API_KEY env var or here
cors:
allowed_origins: [] # CORS origins (empty = allow all)
# Registered indexes (managed via `gundog daemon add/remove`)
indexes:
myproject: /path/to/project/.gundog
default_index: myproject # used when --index not specifiedThe client config at ~/.config/gundog/client.yaml stores TUI and client preferences:
# Daemon URL (set via TUI with 'D' key or --daemon flag)
daemon_url: http://127.0.0.1:7676
# Local paths for file preview (set via TUI with 'L' key)
local_paths:
myproject: /home/user/projects/myproject
docs: /home/user/docsWhen you set a daemon URL in the TUI (press D), it's saved to this config file. Local paths map index names to your local checkout for syntax-highlighted file preview.
Gundog downloads embedding models from HuggingFace on first run. If you're behind a corporate proxy or firewall that intercepts SSL traffic (e.g., Zscaler), you may see certificate errors.
gundog index --no-verify-sslOr set permanently:
export GUNDOG_NO_VERIFY_SSL=1export GUNDOG_CA_BUNDLE=/path/to/your-network-ca.pem
gundog index- Fork the repo
- Create a PR to gundog's main
- Make sure the CI passes
- Profit
To run checks locally
uv run tox # run all checks (lint, fmt, static, unit)
uv run tox -e lint # linting only
uv run tox -e fmt # format check only
uv run tox -e static # type check only
uv run tox -e unit # tests with coverage
