How do I keep a code index fresh without re-embedding everything?

Embed only what changed. An incremental indexer skips the files whose content is the same, re-chunks the files that changed, and reuses the stored embedding of every chunk whose text is the same. cocoindex-code works this way: ccc index re-indexes changed files, and its MCP search tool refreshes the index before each query by default.

Checked Markdown version

Reuse happens per file and per chunk

Embedding a chunk takes model compute, so an incremental indexer avoids it at 2 levels: the file and the chunk. cocoindex-code is built on the CocoIndex engine, and its per-file function is memoized with @coco.fn(memo=True) (indexer.py). The engine handles each kind of change this way (Index your codebase for AI agents with CocoIndex V1):

ChangeWork done
A file did not changeThe file is skipped.
A file is addedOnly that file is chunked and embedded.
A file is editedThe file is re-chunked. Chunks whose text is the same keep their embedding. New chunks are embedded. Chunks that no longer exist are deleted.
A file is deletedIts rows are removed from the index.

Chunk boundaries decide how much an edit invalidates. cocoindex-code tries to split files at function and class boundaries with Tree-sitter. When you edit one function, chunks whose text did not change keep their embeddings, so the model runs only for the chunks that contain the edit. A neighboring chunk can change too, because chunks overlap by up to 150 characters.

What triggers a refresh in cocoindex-code

  • The MCP search tool. Its refresh_index parameter defaults to true, so the index is refreshed before the query runs.
  • The skill. It instructs the agent to run ccc index or ccc search --refresh when the index may be stale, for example at the start of a session or after significant code changes. A plain ccc search does not refresh.
  • The CLI. ccc index updates the index. ccc search --refresh <query> updates it and then searches.
  • Plugin hooks. In an initialized project, the Grok plugin (installed with --trust) and the Oh My Pi plugin run an incremental ccc index at session start and after each edit.

Install and connect

bash
pipx install 'cocoindex-code[full]'
# or with uv:
uv tool install --upgrade 'cocoindex-code[full]'

cd your-repo
ccc index
bash
# Claude Code: skill (recommended in the README)
npx skills add cocoindex-io/cocoindex-code

# Claude Code: MCP
claude mcp add cocoindex-code -- ccc mcp

# OpenAI Codex CLI
codex mcp add cocoindex-code -- ccc mcp

Cursor and other MCP clients that launch a stdio server take the generic entry. The client must start ccc mcp with the project as its working directory:

json
{
  "mcpServers": {
    "cocoindex-code": {
      "command": "ccc",
      "args": ["mcp"]
    }
  }
}

Changes that repeat more work

  • New embedding model. A different model produces different vectors, so everything is embedded again. In cocoindex-code, run ccc reset && ccc index after you switch models.
  • Language overrides and custom chunkers. A change to language_overrides or to a chunker re-chunks all files. Chunks whose text did not change keep their cached embeddings, so the model runs only for the chunks that differ. After a chunker change, run ccc daemon restart before ccc index.
  • File patterns. A change to include_patterns, exclude_patterns or max_file_size needs no rebuild. The next ccc index updates the existing index.

Build your own incremental index

In your own pipeline, the CocoIndex framework does the same work. @coco.fn(memo=True) skips a file when its content and the function’s code are both unchanged:

python
@coco.fn(memo=True)
async def process_file(
    file: FileLike,
    table: postgres.TableTarget[CodeEmbedding],
) -> None:
    text = await file.read_text()
    language = detect_code_language(filename=str(file.file_path.path.name))
    chunks = _splitter.split(
        text,
        chunk_size=1000,
        min_chunk_size=300,
        chunk_overlap=300,
        language=language,
    )
    id_gen = IdGenerator()
    await coco.map(process_chunk, chunks, file.file_path.path, id_gen, table)

cocoindex update main catches up once and exits. cocoindex update -L main keeps watching and applies each change as you save. The full walkthrough has the rest of the pipeline.

Limits, and when another tool fits better

  • Refresh per query. By default, the refresh runs before each MCP query. Pass refresh_index=false to skip it when the index is already current.
  • Other change detection. Claude Context finds changed files with Merkle trees and also re-indexes only those files.
  • Hosted indexes. The GitHub MCP Server searches code that is on GitHub. It does not see edits you have not pushed.
  • Branches. cocoindex-code indexes the working tree of one project. CocoIndex Code Plus (ccx), a separate self-hosted product, indexes a repository at several long-lived branches and processes each file once for all branches that contain it.

Sources and further reading