sg / cocoindex-code lance·hnsw
fork of cocoindex-io/cocoindex-code ↗

Code search that stays resident.

This fork turns the one-shot CLI into a long-running service. A background daemon keeps the embedding model loaded and the index open, serving AST-aware semantic search to your coding agents over MCP — backed by an embedded LanceDB + HNSW store, searchable on any git branch, and sized to fit the memory limit of the container it runs in.

hnsw · greedy descent

searching
layer 2 · sparse, long hops layer 1 layer 0 · every chunk
Query latency stays sublinear in codebase size: the search descends a layered graph instead of scanning every vector. Below 256 chunks the ANN index is skipped and LanceDB’s exact flat scan is used instead.
Model loads
1 · per daemon
Per-branch indexes
0
Vector tables
1
Languages
31

the delta

Everything this fork adds on top of the semantic-search core

Upstream gives you AST-based chunking, embeddings and semantic search over a checked-out tree. This fork keeps all of that and rebuilds the parts around it that a shared, always-on deployment needs.

daemonprocess model

A warm, concurrent service — not a cold command

The background daemon keeps the embedding model resident and the index open, so the model loads once instead of once per invocation. Every connection is handled as its own task, so searches from several agents run in parallel rather than queueing behind one another.

mcpstdio + http

MCP over stdio, or one HTTP endpoint shared by every client

ccc mcp serves a stdio MCP server for a single agent. Set COCOINDEX_CODE_MCP_PORT and the daemon also exposes a streamable-HTTP MCP endpoint against the same in-process registry — so many clients share one warm process and one copy of the model.

branchgit refs

Search any branch, index none of them

Query an arbitrary ref — not just the checked-out one — by searching the base index with the branch’s changed files hidden, then scanning the branch’s own version of those files with ripgrep. Nothing per-branch is embedded, stored, or checked out. How it works ↓

ripgrepexact search

Exact text and regex, alongside search by meaning

A ripgrep MCP tool and ccc grep for when you know the literal string — a symbol, an error message, a config key. It reads the working tree directly, so it needs no index, never triggers one, and finds matches in files the index skips. Branch-aware through the same resolution path as search.

lancedbvector store

Embedded LanceDB with an HNSW index

Chunk vectors live in an in-process LanceDB table indexed with an HNSW graph over cosine distance — no server to run, sublinear query latency, and multi-version reads that never block on a writer. Rows are queryable while the first index pass is still running. Details ↓

memorycgroup aware

Sizes itself to the container’s memory limit

Indexing is memory-hungry enough to trip the OOM killer in a capped container. The daemon reads the cgroup limit at startup, caps how many files it indexes concurrently, and throttles further in real time as usage climbs. You just set --memory the normal Docker way. Details ↓

scheduledaily upkeep

A daily pull → index → push workflow

One maintenance run per day, each step best-effort so a failure in one doesn’t stop the next: fetch and hard-reset the mirror (opt-in), incrementally re-index the refreshed tree, then write an index-stats snapshot to MySQL for DevLake. HTTPS credentials are injected through an inline git credential helper — never written to disk, never in the remote URL, never in argv.

doctordiagnostics

Operational visibility for the parts that can go wrong

ccc doctor checks settings, daemon health, the model, the memory budget, file matching, index health, and — when git pull is enabled — runs a read-only git ls-remote probe so a bad remote or expired token surfaces before the next scheduled pull. ccc memory-stats prints just the memory and scan-queue section without loading the model.

side by side

What changed

ConcernBefore this forkIn this fork
Process modelOne-shot CLI; a stdio MCP server per clientA warm daemon serving CLI, stdio MCP and HTTP MCP concurrently
Vector storesqlite-vec, exact brute-force scanLanceDB + HNSW ANN, cosine distance, multi-version reads
Search scopeThe checked-out working treeAny git ref, with no per-branch index or embedding
Exact search—ccc grep and a ripgrep MCP tool, index-free and branch-aware
Memory under a capEngine defaults, no runtime throttlingConcurrency sized from the cgroup limit and throttled live
Disk growth—Automatic compaction and pruning, plus ccc compact and recovery scripts
UpkeepRun index yourselfA daily scheduled pull → index → metrics-push workflow
DistributionPublished packageSource checkout or a locally built Docker image — nothing published

This is an independent fork maintained for internal use. It is not on PyPI, is not the upstream package, and is not affiliated with or endorsed by the upstream maintainers.

branch search

One index, every branch

The index is built from the checked-out tree — normally the branch a deployment keeps reset to, the base. A branch query decomposes into two halves that can’t disagree about what the branch contains, and reassembles them into a single ranked result set.

base side

base index · all chunksvector search over the warm HNSW table
files the branch touchedexcluded, so a stale base version never surfaces

branch side

added + modified files, branch versionread from the git object database with git show
ripgrep scan, term-count rankedmemory-budgeted batches — a 5,000-file diff costs more passes, not more RAM
One result set, each hit labelled by where it came from: source: semantic source: lexical
search a ref
# semantic + lexical, one result set
ccc search --branch feature/login "session handling"

# exact text on the same ref view
ccc grep --branch feature/login 'session_token'

Nothing is ever checked out. No code path runs checkout, switch or reset for a search. HEAD, the git index, the working tree and the local branch list come out byte-identical — verified in the test suite for both tools. Parallel searches of different branches can’t conflict with each other or with an in-flight index pass.

A branch pushed minutes ago is searchable. A ref is resolved as given, then through each remote’s refs/remotes/, then by an on-demand fetch of that one branch — reusing the scheduled pull’s credentials. Every branch search refreshes the clone first, throttled to one round-trip per minute.

Environment variableDefaultEffect
COCOINDEX_CODE_BASE_REFauto (HEAD)The ref the base index represents, and the diff base. Auto-detected; override when it differs.
COCOINDEX_CODE_BRANCH_FETCH_ENABLEDonSet falsy to forbid the on-demand fetch, restricting search to refs already in the clone.
COCOINDEX_CODE_BRANCH_REFRESH_SECONDS60Minimum seconds between pre-search clone refreshes; 0 refreshes on every search.

The full design, including deferred work on a hardened read-only git guarantee layer, is documented in docs/branch-search.md.

vector backend

LanceDB + HNSW, in process

Chunk embeddings live in one embedded LanceDB table under your index directory. It runs in-process, so the tool stays zero-config while still serving concurrent readers — and branch search adds no second table.

Reads never block on writes

LanceDB is multi-version: a search opens a consistent snapshot and reads it independently of any writer. Rows become queryable as they commit, so the index is searchable during the very first pass — a query only waits when the table is genuinely empty.

Tuned for recall at top-k

The graph is built with m=20 and ef_construction=300, queried at ef=256 — defaults that favour recall for typical top-k code search. HNSW is approximate; raise ef for more recall, lower it for less latency.

Disk stays bounded

Append-only storage would balloon to tens of GB unmanaged. Each pass compacts fragments and prunes versions older than a 10-minute window — the margin that lets in-flight readers finish. ccc compact does a one-time aggressive reclaim and reports what it freed.

PropertyValue
Tablelancedb/code_chunks.lance
IndexHNSW, cosine distance
ANN threshold256 chunks
Score1 − cosine_distance · 0..1, higher is better
Language filterexact match
Path filterGLOB * / ? → SQL LIKE
Auto upkeepcompact + prune > 10 min

One behavioural delta from the old backend. GLOB character classes such as [abc] are not supported by the LanceDB path filter and are treated as literal text. Everything else about --lang and --path filtering is unchanged.

Recovery, if a write is killed partway. An OOM or a docker stop mid-merge can leave the latest table version referencing a truncated file. scripts/lance_recover.py rolls back to the newest version that reads cleanly and prunes the corrupt files; re-run ccc index to re-apply anything dropped.

Switching embedding models needs a rebuild. Different vector dimensions mean ccc reset && ccc index — vectors are re-exported from source rather than migrated in place.

memory governor

Fits the box it’s given

Set a memory limit the normal Docker way and the daemon does the rest: it reads the cgroup limit at startup, sizes its indexing fan-out and branch-scan batches to fit, and eases off in real time as usage approaches the cap. There is no app-specific configuration to get right.

Text scans are governed too

ccc grep, the ripgrep tool and the diff side of every branch search spawn rg subprocesses charged to the same cgroup. All three pass through one gate: at most 4 scans at once, files over 16 MB skipped, match lines cut at 2,000 characters.

Excess requests queue — never rejected

A scan past the pool size waits for a permit, then gets served; nothing is dropped or killed. Waiting is cheap — the permit is taken before any rg process, worker thread or file handle is allocated — but it does add latency, and any wait over 2 s is logged with the queue state.

Under pressure, interactive work is protected. Hard pressure halves scan concurrency alongside the indexing gate; soft pressure eases off indexing only, so a search or grep still gets served.

ccc memory-stats
Text scan queue: 4 running, 7 queued (peak queued: 12)
Scans delayed over 2s: 9 (longest wait: 6.4s).
Raise COCOINDEX_CODE_MAX_CONCURRENT_SCANS if
there's memory headroom.
optional fine-tuning
# plan for less than the hard cap, or set a limit
# explicitly when no cgroup limit is present
-e COCOINDEX_CODE_MEMORY_LIMIT_MB=1500

# pin the in-flight file count, bypassing the heuristic
-e COCOINDEX_CODE_MAX_INFLIGHT_FILES=32

# pin concurrent rg scans (default 4)
-e COCOINDEX_CODE_MAX_CONCURRENT_SCANS=2

Sized from the detected limit: indexing fan-out and branch-scan batch size. Fixed policy, deliberately: scan concurrency and the 16 MB cut — a made-up per-scan byte estimate would look more precise than it is. Both are overridable, and ccc memory-stats prints what’s in effect.

quickstart

Two ways to run it

Nothing is published to PyPI and no images are pushed — install from a source checkout for development, or build the Docker image yourself for the deployment path.

from source · uv
git clone https://github.com/systemgroupnet/cocoindex-hnsw-lancedb.git
cd cocoindex-hnsw-lancedb
uv sync

uv run ccc init      # create settings
uv run ccc index     # build the index
uv run ccc search "authentication logic"

# put `ccc` on PATH from this checkout
uv tool install '.[full]'   # [full] = local embeddings
docker · build locally
# slim (~450 MB) — LiteLLM cloud embeddings
docker build -t cocoindex-code:local -f docker/Dockerfile .

# full — adds sentence-transformers, no API key needed
docker build -t cocoindex-code:full \
  --build-arg CCC_VARIANT=full -f docker/Dockerfile .

# run it persistent, with a memory cap
COCOINDEX_MEM_LIMIT=4g \
COCOINDEX_CODE_IMAGE=cocoindex-code:local docker compose up -d
connect a coding agent
# native install
claude mcp add cocoindex-code -- ccc mcp
codex mcp add cocoindex-code -- ccc mcp

# against a persistent container (-i, never -it)
claude mcp add cocoindex-code -- docker exec -i \
  -e COCOINDEX_CODE_HOST_CWD="$PWD" cocoindex-code ccc mcp

The daemon starts itself. It comes up on first use and stays warm across sessions, so the embedding model loads once. ccc index auto-initializes too — you can skip straight to indexing.

refresh_index defaults to off. Searches read the current table directly, on the assumption the index is refreshed out of band by the scheduled pass. Pass it explicitly to force an incremental update first — and even then, a refresh that finds a pass already in flight reads concurrently instead of blocking.

Embeddings: local SentenceTransformers via the [full] extra (free, no API key, defaulting to snowflake-arctic-embed-xs), or 100+ cloud providers through LiteLLM. See EMBEDDINGS.md.

surface area

The CLI, and the two MCP tools

CommandWhat it does
ccc initInitialize a project — writes settings, adds .cocoindex_code/ to .gitignore
ccc indexBuild or update the index, with streaming progress. Auto-inits if needed.
ccc search <query>Semantic search. Filter by --lang / --path, paginate, or target a --branch.
ccc grep <pattern>Exact text or regex via ripgrep — no index needed. Globs, context lines, --branch.
ccc pullFetch and hard-reset to the git upstream. Discards local changes to tracked files.
ccc statusIndex stats — chunk count, file count, language breakdown
ccc doctorDiagnostics across settings, daemon, model, memory, file matching and index health
ccc memory-statsJust the memory budget, concurrency gates and scan queue — no model load
ccc compactReclaim disk: compact fragments and prune every superseded version
ccc resetDelete the index databases. --all also removes settings.
ccc mcpRun as an MCP server on stdio
ccc daemon status | restart | stopInspect or control the background daemon

search — find code by meaning

Returns chunks with path, language, code, line numbers, a cosine score and a source field. Takes limit, offset, refresh_index, languages, paths and branch.

Use it when you can describe what the code does but not what it’s called.

ripgrep — find code by text

Returns matches with file_path, line_number, content, span, and a truncated flag. Takes pattern, limit, globs, case_sensitive, fixed_strings, context_lines and branch.

Honours .gitignore, skips .git and .cocoindex_code. Needs the rg binary — bundled in the Docker image.

31 languages

AST-aware chunking, per language

Chunks follow syntax rather than line windows, so a result is a function or a class instead of an arbitrary slice. Extensions can be extended or overridden in project settings.

c .c cpp .cpp .cc .cxx .h .hpp csharp .cs css .css .scss dtd .dtd fortran .f .f90 .f95 .f03 go .go html .html .htm java .java javascript .js json .json kotlin .kt .kts lua .lua markdown .md .mdx pascal .pas .dpr php .php python .py r .r ruby .rb rust .rs scala .scala solidity .sol sql .sql svelte .svelte swift .swift toml .toml tsx .tsx typescript .ts vue .vue xml .xml yaml .yaml .yml