archon-search
A hybrid search server for your own machine — vector + full-text + reranking + routing across collections, with no external service to trust with your data by default.
Point it at a directory, ask it a question, get back ranked chunks with source paths:
curl -X POST http://127.0.0.1:8765/search \
-H "Authorization: Bearer $ARCHON_SEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"collection": "docs", "query": "how does the router work?"}'
{
"results": [
{
"doc_id": "docs/routing.md",
"chunk_id": "docs/routing.md#3",
"text": "The router scores each collection's centroid against the query embedding...",
"score": 0.84,
"source_path": "/Users/you/project/docs/routing.md",
"collection": "docs"
}
],
"acl_filtered": false
}
No cloud call happened to produce that by default — the index, the embeddings, and the reranker all run on the box you started archon-search on. (HyDE and RAG Fusion, both opt-in, are the exception: they call out to an LLM provider — see Features.)
What it's made of
- LanceDB as the local vector store — an embedded file format, not a service you have to run alongside it.
- fastembed for dense embeddings — no GPU required to get started.
- A cross-encoder reranker — first-stage recall from vector + FTS, second-stage precision from the reranker, RRF-fused.
- A multi-collection router — one query, many collections; the router scores each collection's centroid and picks which ones actually get searched instead of fanning out to all of them.
- FastAPI for the REST control plane, contracted via OpenAPI 3.x —
GET /openapi.jsonis the source of truth, not this file. - An MCP endpoint — the same tools your HTTP client uses, exposed to MCP clients (Claude Code, etc.) over the same auth.
It's one process. It persists everything under ~/.archon-search/. There's no separate vector DB to stand up, no message queue, no second auth system for the MCP side.
Features
Search that gets smarter about the query, not just the index:
- Vague or short queries under-retrieve with plain vector search.
hyde=trueon/searchgenerates a hypothetical answer and embeds that instead of the raw query — closes the gap between "what you typed" and "what the answer looks like." (archon-search[hyde], needs an LLM provider: Anthropic — the default, included in thehydeextra — or Ollama (archon-search[ollama], plus a running Ollama server), OpenAI (archon-search[openai-provider]),claude_cli— uses Claude Code's own login, no extra install and no API key — orllama_cpp— a local llama-server instance, no extra install and no API key; use a small, direct-response instruct model, not a reasoning model, or the entire token budget goes to hidden chain-of-thought and HyDE/RAG Fusion silently stay disabled) - One query rarely covers what a broad question needs.
rag_fusion=truedecomposes the query into sub-queries, searches all of them in parallel, and fuses the results with a second RRF pass. Mutually exclusive with HyDE — pick the one that fits the query shape. (archon-search[rag_fusion], same provider options and extras as HyDE above) - Non-English corpora get silently penalized by English-tuned defaults. Per-chunk language is detected with fastText at ingest time, and
filters.languagelets you scope a query to it. - Different collections need different embedding models (code vs. prose, different languages) —
active_embedding_modelis set per collection and enforced through ingest, search, and sync; a mismatch raises loud, not silent.
Retrieval that understands what it's indexing, not just text blobs:
- Code search on line-blind chunks returns half a function. Code files get tree-sitter-aware chunking that keeps function/class boundaries intact instead of cutting on line count.
- A hit with no location context means opening the file to find your answer. Headings, section paths, and page numbers get extracted and attached to each chunk at ingest.
- "Search everything" isn't the same as "search the right thing."
filterson/searchnarrow by file type, source-path prefix or glob, andindexed_after/indexed_before— before the query ever reaches the reranker. - A ranked list without a reason is a black box you can't debug.
/explainreturns the full pipeline trace — vector score, FTS score, rerank score, routing decision — per candidate, with per-stage timings.
Operations that don't need someone awake at 3am:
- API keys that never rotate are a liability, and rotating them without downtime is normally its own project.
POST /keys/rotateswaps in a new key while the old one stays valid through a configurable grace period. - A crashed backup script means you find out about data loss when you need the backup. A background loop exports and rotates backups on a schedule — no cron job to babysit.
- Schema changes usually mean a forced re-ingest. Migrations run through a versioned
MigrationSpecregistry with documented rollback rules — existing collections migrate in place. - FTS index rebuilds that scale with corpus size make maintenance windows scale with corpus size too.
optimize_fts()is incremental — it updates what changed, not what already existed. - "Works on my machine" isn't a deployment story. CPU and NVIDIA GPU images are published to GHCR; the tiered install wizard (
minimal/balanced/max) picks embedding + reranker models that fit your disk budget instead of downloading everything.
Installation
# pip
pip install archon-search
archon-search wizard
# uv (installs the CLI into an isolated managed environment)
uv tool install archon-search
archon-search wizard
archon-search wizard does the rest: pick a profile (minimal, balanced, or max), it pulls the matching embedding and reranker models and registers the server as a background service. Full profile comparison, flags, and disk-space requirements: Documentation/UserManual/10_installation.md.
Prefer a checkout over a package? Clone and sync:
git clone https://github.com/user538295/archon-search.git
cd archon-search
uv sync --dev
Uninstall
Stop and unregister the service first, while the CLI is still on disk:
archon-search uninstall
Add --delete-db if you also want the search database gone — that's irreversible, it removes every indexed chunk:
archon-search uninstall --delete-db
Then remove the package itself:
# pip
pip uninstall archon-search
# uv tool
uv tool uninstall archon-search
# checkout / dev install — delete the cloned directory
Neither step touches your data. uninstall only stops and unregisters the OS service; removing the package only removes the CLI binary. If you want a clean wipe, these paths are still on disk and need manual deletion:
| Path | Contents |
|---|---|
~/.archon-search/archon-search.toml |
Server config |
~/.archon-search/.search.env |
API key |
~/.archon-search/search/ |
LanceDB vector store and FTS index |
~/.archon-search/logs/ |
Server logs |
~/.archon-search/models/ |
Downloaded fastText language-ID model (only if the multilingual extra is installed) |
~/.archon-search/search-logs/ |
Telemetry JSONL (only if telemetry was enabled) |
Or skip the table and remove all of it in one shot:
rm -rf ~/.archon-search/
That does not remove the embedding/reranker model weights fastembed downloads — those are cached in fastembed's own default cache (outside ~/.archon-search/), not under this directory. Clear that separately for a truly complete wipe.
Quick start
Start the server in the foreground:
archon-search serve
That runs the FastAPI app on the configured host/port — serve defaults to 0.0.0.0:8765 (all interfaces, not just loopback), unlike the library default of 127.0.0.1:8765 used when embedding SearchConfig directly. Binding all interfaces means the server is reachable from other machines on the network the moment it starts; put a reverse proxy in front of it (see Docker) before exposing it past loopback. It blocks until you stop it (Ctrl-C). For a background service managed by launchd/systemd, use archon-search wizard instead — see Installation.
Once it's up:
GET /health— unauthenticated liveness probeGET /ready— unauthenticated readiness probeGET /docs— interactive Swagger UIGET /openapi.json— machine-readable OpenAPI schema
Then hit /search as shown at the top of this file.
Running with Docker
Two images, CPU (:latest) and NVIDIA GPU (:gpu). Both run the foreground archon-search serve subcommand, bind to 0.0.0.0:8765, persist everything under /data, and write logs to stderr so docker logs actually shows something.
Kick the tires — ephemeral, the key regenerates on every start, nothing persists:
docker run --rm -p 8765:8765 ghcr.io/user538295/archon-search:latest
Run it for real — pin the key, pin the volume:
docker run -d \
--name archon-search \
-e ARCHON_SEARCH_API_KEY=$ARCHON_SEARCH_API_KEY \
-v archon-search-data:/data \
-p 8765:8765 \
ghcr.io/user538295/archon-search:latest
Skip the volume or the env var and every restart mints a new key — every token you handed out stops working, silently. Mount a volume so the key persists at /data/.search.env, or pass ARCHON_SEARCH_API_KEY explicitly. Pick one; don't rely on neither.
Want a dev/test/prod stack with isolated volumes instead of one container? docker-compose.yml and .env.example have it. Full operator guide — compose stack, image variants, env-var reference, persistence layout: Documentation/UserManual/140_running_with_docker.md.
LanceDB is single-writer. Mount the same data volume into two running containers and the on-disk state is undefined — don't do that.
The container speaks plaintext HTTP, nothing else. Put a reverse proxy (nginx, Caddy, Traefik) in front of it before you expose it past loopback.
Authentication
Every endpoint requires a Bearer token in the Authorization header, except a small unauthenticated set: GET /health, GET /ready, GET /docs, GET /openapi.json, GET /redoc. One route has a second, narrower exemption — GET /graph/{collection}/view (the HTML graph viewer) also accepts the token as a ?token= query parameter instead of the header, since it's meant to be opened directly in a browser; the query param is still validated against the same key set (archon_search/server/middleware_auth.py).
First start auto-generates a key and writes it to ~/.archon-search/.search.env at 600. Running in Docker, CI, or across multiple hosts? Set ARCHON_SEARCH_API_KEY — it wins over the file every time. Need the key read from somewhere else? Set ARCHON_SEARCH_KEY_FILE. Need the whole runtime tree — index, logs, key file, jobs file, the fastText language-ID model cache, ingest history — under one root instead of ~/.archon-search/? Set ARCHON_SEARCH_DATA_DIR (the Docker image already does this for you, pointing it at /data). This does not relocate fastembed's embedding/reranker weight cache, which fastembed manages in its own default cache outside this tree — see Uninstall.
Configuration
Everything server-side lives in one file: ~/.archon-search/archon-search.toml.
[database]—db_path,embedding_model,chunk_size,top_k_return, model paths, per-collection embedder pool sizing (embedder_cache_size, default3;eager_load_embedders, defaultfalse, pre-warmsembedding_modelplus every distinct per-collection model and the reranker cross-encoder at startup instead of lazily)[search]— multi-collection fan-out bounds (max_fanout,fanout_timeout_seconds)[routing]—routing_shortlist_size,routing_confidence_threshold, routing strategy[collections]—pinned_collections, static collection definitions, watcher settings[telemetry]— opt-in local query logging, covered below
The full annotated reference — every key, every default — is archon-search.toml.example. This section is the map, not the territory.
REST API
GET /openapi.json is the contract — endpoint shapes, request/response types, error codes, all of it. This README is not. GET /docs serves the same thing as an interactive explorer.
Breaking changes to REST or MCP land in BREAKING.md, not buried in a changelog entry.
MCP tools
Your MCP client gets the same server your HTTP client does — same auth, same data, no separate integration to build. 16 tools always register; 4 more join when a key store is configured, 20 total (archon_search/server/mcp.py):
search— hybrid vector + FTS search; returns{"results": [...], "acl_filtered": bool}search_with_context— same assearchwith adjacent-chunk contextexplain— per-stage retrieval/reranking trace plus routing decision (mirrorsPOST /explain)ingest_file— index a single file into a collectioningest_directory— recursively index a directorylist_collections— list collection namesget_collections_meta— metadata for all collectionsget_collection_meta— metadata for one collectionlist_documents— list documents in a collectiondelete_document— remove a document bydoc_idupdate_collection— change a collection's embedding model (mirrorsPATCH /collections/{name})export_collection/import_collection— archive a collection out or restore itget_graph— entity-graph summary for one collection (nodes, edges, top entities by salience)get_graph_cross_collection— merged entity graph across 2+ collectionsgraph_impact— blast-radius caller/callee analysis for a code symbolcreate_key/list_keys/revoke_key/rotate_key— API key lifecycle, mirroring the REST/keysendpoints (registered only when key management is configured)
Telemetry (opt-in)
Off by default (enabled = false), and it stays off until you flip it. Flip it, and every search, search_with_context, search_multi, POST /route, and /explain call appends one JSONL line to a daily file under ~/.archon-search/search-logs/. It never leaves the machine — no export, no phone-home, no exceptions.
Enabling
# ~/.archon-search/archon-search.toml
[telemetry]
enabled = true
retention_days = 30 # files older than this are deleted at startup and every 24h
log_dir = "~/.archon-search/search-logs"
hash_doc_ids = false # set true to HMAC-SHA256 result_doc_ids before writing to JSONL
What is logged
query_id (random UUID), timestamp (UTC), endpoint, latency_ms, status, plus whatever's specific to the call — collection/result_count/result_doc_ids for retrieval, collections/decomposer_invoked for routing. Errors add one more field, error_kind, from a closed set: empty_query | slot_out_of_range | timeout | internal_error | validation_error | other.
What is never logged
The raw query string, never. Not a policy — a structural guarantee: the factory methods that build telemetry entries have no query parameter to pass one in. Exception messages don't make it in either; only the coarse error_kind string does.
The catch: doc_ids leak paths
doc_ids may reveal filesystem paths: result_doc_ids comes straight from the file path on disk — /Users/<name>/Documents/<project>/<file>.md. Turn telemetry on and those paths, username included, sit in your log files. Set hash_doc_ids = true and every doc_id gets HMAC-SHA256'd before it's written, opaque to anyone without the salt file at ~/.archon-search/.telemetry-salt. Decide if that's worth doing before you turn telemetry on, not after.
export_enabled does nothing yet
It's reserved for a future release. Set it to true today and the config loader logs a warning and quietly coerces it back to false (archon_search/config.py). Nothing gets transmitted in v1 — there's no code path that would.
Telemetry read-back API
Both endpoints return {"enabled": false} when telemetry is disabled.
GET /telemetry/stats
Aggregated query statistics over an optional time window.
| Parameter | Type | Description |
|---|---|---|
since |
YYYY-MM-DD | Start date (inclusive, optional) |
until |
YYYY-MM-DD | End date (inclusive, optional) |
Response shape summary:
{
"schema_version": 1,
"enabled": true,
"total_queries": 42,
"success_rate": 0.95,
"latency_ms": {"p50": 120, "p95": 380},
"by_endpoint": {"search": 30, "route": 12},
"by_collection": {"docs": 25, "code": 17},
"error_breakdown": {"timeout": 2, "internal_error": 0}
}
success_rate is null when no queries exist in the window.
GET /telemetry/entries
Paginated raw log entries.
| Parameter | Type | Description |
|---|---|---|
since |
YYYY-MM-DD | Start date (optional) |
until |
YYYY-MM-DD | End date (optional) |
collection |
string | Filter by collection name (optional) |
endpoint |
string | Filter by endpoint (optional) |
status |
string | Filter by status (optional) |
error_kind |
string | Filter by error kind (optional) |
offset |
int | Pagination offset, default 0 |
limit |
int | Page size, 1–200, default 50 |
Response includes entries, next_offset, and total_in_window. Clients should continue calling with the returned next_offset until entries is empty (equivalently, until next_offset >= total_in_window).
Evaluation harness
"Retrieval quality didn't regress" is a claim, not a feeling — tests/eval/ is what backs it: a synthetic corpus, query/label fixtures, deterministic eval backends, committed thresholds, a measured baseline. It's the sanctioned gate for any change touching retrieval, reranking, routing, or latency.
uv run pytest -m eval --thresholds-path tests/eval/thresholds.toml tests/eval/test_eval_suite.py
The backends are deterministic — corpus-aware but label-blind, so metrics hold steady across runs without loading real model weights. Latency p50/p95 is a regression guard, not a production SLA — it tells you if this change got slower than the last one, not what to expect in prod.
Current baseline (recall@k, MRR, nDCG@k, reranker lift, routing accuracy, latency percentiles): tests/eval/baselines/baseline.md, machine-readable twin at tests/eval/baselines/baseline.json. Changing a threshold or a fixture? Read tests/eval/README.md first — it's the maintenance guide and the waiver policy, not optional reading.
Development
git clone https://github.com/user538295/archon-search.git
cd archon-search
uv sync --dev
uv run pytest
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file archon_search-26.8.1987.tar.gz.
File metadata
- Download URL: archon_search-26.8.1987.tar.gz
- Upload date:
- Size: 5.3 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b1ad7cc17e83f3f7e3897e17d7955287b9b80e31703b58665cdc0c2a5b9ac289
|
|
| MD5 |
5d17e6036ad044c3fa7d07bd53eeb8a8
|
|
| BLAKE2b-256 |
11bb9738c86dedc3f5bdb2512eb5d923c92849ea8ecf22eb5bc603831f0284fe
|
Provenance
The following attestation bundles were made for archon_search-26.8.1987.tar.gz:
Publisher:
archon-search-release.yml on user538295/archon-search
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
archon_search-26.8.1987.tar.gz -
Subject digest:
b1ad7cc17e83f3f7e3897e17d7955287b9b80e31703b58665cdc0c2a5b9ac289 - Sigstore transparency entry: 2502111958
- Sigstore integration time:
-
Permalink:
user538295/archon-search@22bfdea506abdbede52d992ff5dae3deddcf5f06 -
Branch / Tag:
refs/tags/26.8.1987 - Owner: https://github.com/user538295
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
archon-search-release.yml@22bfdea506abdbede52d992ff5dae3deddcf5f06 -
Trigger Event:
push
-
Statement type:
File details
Details for the file archon_search-26.8.1987-py3-none-any.whl.
File metadata
- Download URL: archon_search-26.8.1987-py3-none-any.whl
- Upload date:
- Size: 767.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
efd3272e9dd82d0c0938b0907e041704dc01be50bae87e4e87c4a0c51af31ad9
|
|
| MD5 |
62464caf51f3642a8beca8bf660aebeb
|
|
| BLAKE2b-256 |
cba5f2b99884b3dc1629d92c90711c44475e60ec6c30b203d6a9cb3a995d966e
|
Provenance
The following attestation bundles were made for archon_search-26.8.1987-py3-none-any.whl:
Publisher:
archon-search-release.yml on user538295/archon-search
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
archon_search-26.8.1987-py3-none-any.whl -
Subject digest:
efd3272e9dd82d0c0938b0907e041704dc01be50bae87e4e87c4a0c51af31ad9 - Sigstore transparency entry: 2502112085
- Sigstore integration time:
-
Permalink:
user538295/archon-search@22bfdea506abdbede52d992ff5dae3deddcf5f06 -
Branch / Tag:
refs/tags/26.8.1987 - Owner: https://github.com/user538295
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
archon-search-release.yml@22bfdea506abdbede52d992ff5dae3deddcf5f06 -
Trigger Event:
push
-
Statement type: