Skip to content
曾亮懿 ZengLiangYi Blog

Vectra local search: fixed vectors, filters, and persistence

Run a deterministic Vectra 0.15.0 exercise in a temporary directory to check cosine scores, metadata filters, reopening, and canceled updates.

曾亮懿

曾亮懿

4 min read
Table of contents Show Hide

A few vectors whose scores you can calculate by hand make index errors easier to spot than randomly generated high-dimensional arrays. This article runs Vectra 0.15.0 in a temporary directory, verifies sorting, filtering, and persistence, then explains how those results differ from ChatCrystal’s note search. It measures neither semantic quality nor performance.

What LocalIndex actually does

ChatCrystal’s dependency manifest declares vectra: ^0.15.0; the pinned revision’s lockfile resolves it to 0.15.0. The exercise installs that exact version. A version range does not establish future installation results.

This version’s LocalIndex.queryItems loads the index, applies metadata filtering, traverses candidate items to calculate cosine similarity, retains leading results in a bounded min-heap of size K, and sorts them for return. This query path does not use an HNSW approximate index. This article has no reproducible measurement of millisecond latency over millions of items. Default JSON storage includes index.json and can store metadata separately depending on configuration; it does not always produce an items/ directory. The library also has npm dependencies. Requiring no separate database service does not mean having zero dependencies. See the public Vectra repository for implementation and usage details.

Cosine scores can be negative. For valid nonzero vectors, aligned, orthogonal, and opposite directions yield 1, 0, and -1 respectively. These scores are neither probabilities nor guarantees that a result near 1 contains a correct answer.

Run a deterministic minimal index

Install the dependency in a new temporary directory and use Node 24+. These commands install only the lesson’s dependency. The program creates its own temporary index and cleans up only that new directory when finished.

npm init -y
npm install --ignore-scripts --no-audit --no-fund vectra@0.15.0
node lesson.mjs

Save the following as lesson.mjs before executing the final command. The vectors are synthetic data. The query string does not enable BM25, so these results represent no natural-language retrieval.

import assert from 'node:assert/strict';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { LocalIndex } from 'vectra';

const folder = await mkdtemp(join(tmpdir(), 'vectra-lesson-'));
try {
  const index = new LocalIndex(folder);
  await index.createIndex();
  await index.batchInsertItems([
    { id: 'same', vector: [1, 0, 0], metadata: { project: 'alpha' } },
    { id: 'orthogonal', vector: [0, 1, 0], metadata: { project: 'beta' } },
    { id: 'opposite', vector: [-1, 0, 0], metadata: { project: 'alpha' } },
  ]);

  const reopened = new LocalIndex(folder);
  const all = await reopened.queryItems([1, 0, 0], '', 3);
  assert.deepEqual(
    all.map((r) => r.item.id),
    ['same', 'orthogonal', 'opposite']
  );
  assert.deepEqual(
    all.map((r) => r.score),
    [1, 0, -1]
  );

  const filtered = await reopened.queryItems([1, 0, 0], '', 3, { project: 'alpha' });
  assert.deepEqual(
    filtered.map((r) => r.item.id),
    ['same', 'opposite']
  );

  await reopened.beginUpdate();
  await reopened.deleteItem('same');
  reopened.cancelUpdate();
  assert.equal((await new LocalIndex(folder).getItem('same')).id, 'same');
  console.log('scores, filter, reopen, and cancel pass');
} finally {
  await rm(folder, { recursive: true, force: true });
}

Change the score expectation to [1, 0, 0] and the program fails; restore [1, 0, -1] and it passes. Changing the filter to a nonexistent project also fails the expected two-result assertion. This establishes that the assertions check the intended behavior rather than merely printing plausible arrays.

Boundaries of filters and canceled updates

This version’s metadata selector also supports $and, $or, and several comparisons, such as numeric $gte. Saying it supports equality alone is inaccurate. That does not make it equivalent to a complete database query language. Construct separate cases for the operators, field types, and missing fields you intend to use.

beginUpdate creates pending update state; endUpdate writes the file and makes that state current; cancelUpdate discards pending state. The exercise proves only that reopening after canceling a deletion still returns the original item. It does not test power loss, partially written files, two concurrent writing processes, or transactions across stores.

Validate vector dimensions and model compatibility at the service boundary. This version’s similarity helpers use the overlapping array portion for dot products. Absence of an exception does not establish that mismatched dimensions are comparable. See embedding service design for finite nonzero values, matching dimensions, and model-identity checks.

Distinguish application scores from raw cosine

ChatCrystal’s semanticSearch adds processing above Vectra candidates: it accepts notes whose database status is done, merges chunks by note ID, expands the candidate count when necessary, and applies application-level lexical matching and score filtering.

The current lexical rule can raise a matching result’s score to at least 0.82; other results below 0.58 are filtered out. These are application rules for this revision, not general Vectra thresholds or 82% and 58% correctness rates. Relation expansion can add neighbors with discounted scores. See note search for the actual entry points and scope.

Backups and recovery need to account for the knowledge-base database, vector index, and active writes together. This article does not promise consistency from directly copying a directory while it is being written. See index recovery for relevant state checks. The lesson provides reproducible evidence of index behavior; real semantic quality still needs a separate evaluation with the same model and data, explicit queries, and expected answers.

Share:

Related Posts

Building an Embedding Service: Text, Chunks, and Commits

Read ChatCrystal’s actual embedding pipeline, separate text selection, soft chunking, vector validation, and commits, and verify boundaries offline.

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

Structured JSON Prompts: Field Design & Verifiable Evidence

Use ChatCrystal’s summaries and relations to define fields, visible evidence, and counterexamples, then distinguish shape and evidence checks with offline Zod.

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

AI SDK structured notes: adapters, schemas, and business checks

Read ChatCrystal’s AI SDK 7 calls, distinguish provider adaptation from output shape and factual validity, and test description limits with offline Zod.

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

From writing to code

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