---
title: "Vectra local search: fixed vectors, filters, and persistence"
description: Run a deterministic Vectra 0.15.0 exercise in a temporary directory to check cosine scores, metadata filters, reopening, and canceled updates.
image: "https://zengliangyi.com/og/blog/en/vectra-local-search.png"
---

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](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/package.json) declares `vectra: ^0.15.0`; the pinned revision’s [lockfile](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/pnpm-lock.yaml) 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](https://github.com/Stevenic/vectra) 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.



```sh
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.



```js
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](/en/blog/embedding-service-design/) for finite nonzero values, matching dimensions, and model-identity checks.

## Distinguish application scores from raw cosine

ChatCrystal’s [semanticSearch](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/embedding.ts) 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](/en/blog/semantic-search-workflow/) 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](/en/blog/vectra-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.          曾亮懿       October 6, 2026      4 min read

#ChatCrystal

#AI

#Node.js](/en/blog/embedding-service-design)    [##  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.          曾亮懿       October 6, 2026      4 min read

#ChatCrystal

#AI

#Node.js](/en/blog/structured-json-prompts)    [##  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.          曾亮懿       October 5, 2026      4 min read

#ChatCrystal

#AI

#Node.js](/en/blog/ai-sdk-structured-notes)

##  From writing to code

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

[Visit my GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Vectra local search: fixed vectors, filters, and persistence",
  "description": "Run a deterministic Vectra 0.15.0 exercise in a temporary directory to check cosine scores, metadata filters, reopening, and canceled updates.",
  "url": "https://zengliangyi.com/en/blog/vectra-local-search/",
  "image": "https://zengliangyi.com/images/blog/vectra-local-search.svg",
  "datePublished": "2026-10-06T00:00:00.000Z",
  "dateModified": "2026-10-06T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/en/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/en/blog/vectra-local-search/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://zengliangyi.com/en"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog",
      "item": "https://zengliangyi.com/en/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Vectra local search: fixed vectors, filters, and persistence",
      "item": "https://zengliangyi.com/en/blog/vectra-local-search/"
    }
  ]
}
```
