跳到正文
曾亮懿 ZengLiangYi Blog

Vectra 本地搜索:用固定向量验证排序、过滤与持久化

在临时目录运行 Vectra 0.15.0 的确定性练习,检查余弦分数、元数据过滤、重新打开索引和取消更新。

曾亮懿

曾亮懿

阅读约 4 分钟
目录 展开 收起

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

LocalIndex 实际做了什么

ChatCrystal 的 依赖清单声明 vectra: ^0.15.0,该提交的 锁文件解析为 0.15.0。本文的练习固定使用这个版本,不能从版本范围推导未来安装结果。

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

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

跑一个确定性的最小索引

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

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

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

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 服务设计。

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

ChatCrystal 的 semanticSearch在 Vectra 候选项之上继续处理:只接纳数据库中状态为 done 的笔记,将多个块按笔记 ID 合并,必要时扩大候选数量,再做应用层词面匹配和分数筛选。

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

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

分享:

相关文章

实现 Embedding 服务:文本、分块与提交边界

对照 ChatCrystal 的真实 Embedding 流程,区分文本选择、软分块、向量校验与提交,并用离线反例验证边界。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #AI #Node.js

结构化 JSON Prompt:把输出字段与来源证据一起设计

以 ChatCrystal 摘要和关系发现为例,设计字段契约、可见输入与反例样本,并用离线 Zod 练习区分格式验证和证据验证。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #AI #Node.js

AI SDK 结构化笔记:统一调用、Schema 与业务校验

对照 ChatCrystal 的 AI SDK 7 调用,分清 provider 适配、输出类型与业务事实,用离线 Zod 练习验证提示描述的边界。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #AI #Node.js

从文章到代码

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