---
title: Vectra 本地搜索：用固定向量验证排序、过滤与持久化
description: 在临时目录运行 Vectra 0.15.0 的确定性练习，检查余弦分数、元数据过滤、重新打开索引和取消更新。
image: "https://zengliangyi.com/og/blog/vectra-local-search.png"
---

先用几条可手算的向量理解索引，比随机生成高维数组更容易发现错误。本文在临时目录运行 Vectra 0.15.0，验证排序、过滤与持久化，再说明这些结果与 ChatCrystal 的笔记搜索有什么区别。它不是语义质量或性能测试。

## LocalIndex 实际做了什么

ChatCrystal 的 [依赖清单](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/package.json)声明 `vectra: ^0.15.0`，该提交的 [锁文件](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/pnpm-lock.yaml)解析为 0.15.0。本文的练习固定使用这个版本，不能从版本范围推导未来安装结果。

该版本的 `LocalIndex.queryItems` 加载索引，在元数据过滤后遍历候选项计算余弦相似度，用容量为 K 的最小堆保留靠前结果，再排序返回。这个查询路径没有使用 HNSW 近似索引；本文也没有可复查的百万级毫秒延迟测量。默认 JSON 存储包含 `index.json`，可按配置另存元数据，并不固定生成 `items/` 目录。库也有自己的 npm 依赖，“不需要独立数据库服务”不能写成“零依赖”。公开实现与使用说明见 [Vectra 仓库](https://github.com/Stevenic/vectra)。

余弦分数可能为负数。在有效非零向量上，同向、正交、反向分别对应 1、0、-1；它不是概率，也不是“越接近 1 就越确定答案正确”。

## 跑一个确定性的最小索引

在新的临时目录中安装依赖，使用 Node 24+。以下命令只安装教学依赖；程序自行创建临时索引，结束后仅清理这个新目录。



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



先保存下面的 `lesson.mjs` 再执行最后一行。三个向量是构造数据，`query` 字符串不启用 BM25，不能把这个结果当成自然语言检索。



```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   });  
  }
```



把分数断言改成 `[1, 0, 0]`，运行会失败；恢复 `[1, 0, -1]` 后通过。把过滤条件改成不存在的项目，期望的两条结果也会失败。这样能确认断言真的检查目标行为，而不只是打印一组看似合理的数组。

## 过滤与取消更新的边界

这个版本的元数据选择器还支持 `$and`、`$or` 和若干比较操作，例如数值 `$gte`。因此“只能做等值过滤”不准确。但不能据此承诺它与完整数据库查询语言等价；对准备使用的操作符、字段类型与缺失字段，应单独构造例子核对。

`beginUpdate` 创建待提交的更新状态，`endUpdate` 写入文件并成为当前状态，`cancelUpdate` 丢弃待更新状态。上面的练习只证明取消删除后，重新打开仍能读取原项；它没有测试断电、文件写到一半、两个进程同时写入或多存储事务。

向量维度和模型兼容性应在服务边界验证。这个版本的相似度辅助代码会使用数组的重叠部分计算点积，不能把“没有报错”理解为两个维度不同的向量可以比较。非零、有限数值、维度一致和模型身份检查见[Embedding 服务设计](/blog/embedding-service-design/)。

## 应用分数与原始余弦分开看

ChatCrystal 的 [semanticSearch](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/embedding.ts)在 Vectra 候选项之上继续处理：只接纳数据库中状态为 `done` 的笔记，将多个块按笔记 ID 合并，必要时扩大候选数量，再做应用层词面匹配和分数筛选。

当前词面匹配可把对应结果的分数抬到至少 0.82，其他低于 0.58 的结果被过滤。这是该版本的应用规则，不是 Vectra 的通用阈值，也不表示 82% 或 58% 的正确率。关系扩展还可以加入折扣后的邻居分数。实际搜索入口与范围见[笔记搜索](/blog/semantic-search-workflow/)。

备份和恢复需要同时考虑知识库数据库、向量索引与写入活动。本文不提供对正在写入的数据目录直接复制的“一致性保证”；相应状态检查见[索引恢复](/blog/vectra-index-recovery/)。完成练习后，你得到的是一份可重复的索引行为证据；真实语义质量仍需同模型、同数据、明确查询与预期答案的独立评估。

分享：

##  相关文章

[##  实现 Embedding 服务：文本、分块与提交边界

对照 ChatCrystal 的真实 Embedding 流程，区分文本选择、软分块、向量校验与提交，并用离线反例验证边界。          曾亮懿       2026年10月6日      阅读约 4 分钟

#ChatCrystal

#AI

#Node.js](/blog/embedding-service-design)    [##  结构化 JSON Prompt：把输出字段与来源证据一起设计

以 ChatCrystal 摘要和关系发现为例，设计字段契约、可见输入与反例样本，并用离线 Zod 练习区分格式验证和证据验证。          曾亮懿       2026年10月6日      阅读约 4 分钟

#ChatCrystal

#AI

#Node.js](/blog/structured-json-prompts)    [##  AI SDK 结构化笔记：统一调用、Schema 与业务校验

对照 ChatCrystal 的 AI SDK 7 调用，分清 provider 适配、输出类型与业务事实，用离线 Zod 练习验证提示描述的边界。          曾亮懿       2026年10月5日      阅读约 4 分钟

#ChatCrystal

#AI

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

##  从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录，欢迎交流实现细节。

[访问我的 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 本地搜索：用固定向量验证排序、过滤与持久化",
  "description": "在临时目录运行 Vectra 0.15.0 的确定性练习，检查余弦分数、元数据过滤、重新打开索引和取消更新。",
  "url": "https://zengliangyi.com/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/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/blog/vectra-local-search/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "首页",
      "item": "https://zengliangyi.com/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "文章",
      "item": "https://zengliangyi.com/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Vectra 本地搜索：用固定向量验证排序、过滤与持久化",
      "item": "https://zengliangyi.com/blog/vectra-local-search/"
    }
  ]
}
```
