Skip to content
曾亮懿 ZengLiangYi Blog

Vectra index recovery: separate state, cleanup, and rebuilding

Diagnose semantic-search failures across note status, Vectra files, and cleanup tasks, then choose a single-note or full-index recovery path.

曾亮懿

曾亮懿

4 min read
Table of contents Show Hide

An empty or failing search is not automatically an index-corruption incident. ChatCrystal keeps note status, SQLite embedding rows, Vectra files, and deletion cleanup tasks as separate pieces. Identify the inconsistent layer before choosing a repair scope.

This article follows the vector index, embedding flow, and database initialization at 7642768. The full-recovery example moves the index aside. Stop every writer, including desktop applications sharing the directory, and back up the whole data directory first. Replace <note-id> in commands with an actual ID.

Record the symptom and status

An empty result can mean there are no notes, notes are still pending, vector generation failed, the index directory is missing, or the query uses a model incompatible with stored vectors. crystal status only helps establish aggregate counts. Inspect a note’s embedding_status through the API:

curl 'http://localhost:3721/api/notes?limit=100'
curl 'http://localhost:3721/api/notes/<note-id>'

The list is time-ordered and capped at 100 items; note objects include the status field. done is database state and does not establish index completeness: initialization marks notes with existing embedding rows as done without checking their Vectra items. pending, failed, and syncing need further inspection. Do not treat GET /api/notes?embedding_status=pending as a filter: this route does not implement that query parameter.

Understand the database and index boundary

On first access, getIndex() creates a LocalIndex under vectra-index; it does not scan SQLite’s embedding table and reconstruct every vector. The database and file index can therefore be out of sync.

A note rebuild first requests vectors from the model, inserts new items in a Vectra update, swaps the SQLite embedding rows and marks the note syncing, deletes old items, commits the update, and finally marks the note done. On an error it tries cancelUpdate(); if the database has already been swapped, it saves failed for a later retry. A forced process termination can still leave an intermediate state that needs inspection.

Deleting a note creates a pending row in vector_cleanup_tasks. Each semantic search preflight tries at most 25 pending cleanup tasks; a failed cleanup remains pending for a later retry, while search continues. That is a retry path for deletion work, not a general repair for arbitrary corrupted files.

Try a low-risk single-note rebuild first

When one note is affected, keep the index in place and call the synchronous entry point, then inspect the result and status:

curl --request POST \
  'http://localhost:3721/api/notes/<note-id>/embed'
curl 'http://localhost:3721/api/notes/<note-id>'

This usually generates vectors, but there is a fast confirmation path: when a note is syncing and all its database vector IDs are already committed in Vectra, it is marked done without requesting the model again. The endpoint is therefore not an unconditional forced rebuild, and it does not rewrite the summary. When generation runs, the model, endpoint, and vector dimensions must be usable. If the provider or embedding model changed, first follow the embedding configuration guide and confirm the clearing and rebuild scope.

Decide whether to clear the whole index

Only after confirming that the file index is unreadable and the database notes are still usable sources should you perform a full rebuild. This example uses the default CLI directory and creates a unique backup location with mktemp to avoid overwriting or nesting inside an existing backup:

crystal serve stop &&
  recovery_backup=$(mktemp -d "$HOME/.chatcrystal/vectra-recovery.XXXXXX") &&
  mv "$HOME/.chatcrystal/data/vectra-index" "$recovery_backup/vectra-index" &&
  crystal serve --daemon &&
  curl --request POST 'http://localhost:3721/api/notes/<note-id>/embed'

Packaged applications resolve their data directory through runtime paths; confirm the actual directory from configuration and logs before applying this example. List affected notes using GET /api/notes?limit=100&offset=0 and subsequent pages. Repeat the single-note rebuild for every affected ID, checking each response until all targets are covered.

clearEmbeddingIndex() deletes the index and clears the in-process cache without marking existing done notes as pending. POST /api/embeddings/batch selects only pending, failed, and syncing, while a restart can mark notes with embedding rows as done. Moving only the index aside and calling the batch endpoint can therefore return queued: 0; explicit per-note rebuilding avoids omitting those notes.

Accept the recovery with repeatable checks

Record three kinds of evidence:

  1. The target notes reach done, and the queue’s failure count no longer grows.
  2. Run a known-answer query, a paraphrase, and a no-answer query; record inclusion, order, and false matches.
  3. Restart the server and query again to verify that the rebuilt index can be reopened.

Readable files only establish that the index can be accessed, not that retrieval is useful. If results remain empty, inspect model settings, fields submitted for embedding, and query conditions; see the semantic search workflow.

Share:

Related Posts

AI Conversation Formats: JSONL Events vs SQLite Snapshots

Compare 5 AI adapter formats: separate JSONL events from SQLite keys, and inspect indexed, orphan, and unknown-version chat transcripts in memory stores.

曾亮懿 曾亮懿
4 min read
#ChatCrystal #Node.js #SQLite

How Claude Code, Cursor, Copilot, and Trae Store Chats on Disk

Compare Claude Code, Cursor, Copilot, and Trae storage paths. Distinguish raw copies from exports, and verify migration retention with test manifests.

曾亮懿 曾亮懿
4 min read
#ChatCrystal #Node.js #SQLite

Five AI conversation parsers: retained fields and loss boundaries

Build an acceptance matrix from Claude Code, Codex, Cursor, Trae, and Copilot parsers, checking tool flags, thinking, order, and snapshot coverage.

曾亮懿 曾亮懿
4 min read
#ChatCrystal #TypeScript #Node.js

From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.