---
title: 语义搜索流程：请求、候选去重与关系扩展
description: 沿 ChatCrystal 实现检查查询向量、候选升级、词项加分和关系扩展，用实际 API 区分笔记列表筛选与语义检索。
image: "https://zengliangyi.com/og/blog/semantic-search-workflow.png"
---

搜索返回的五条记录，不等于五个向量候选；返回分数也不等于未经调整的余弦相似度。要解释漏召回或排序，先区分应用的检索阶段与接口字段。

本文沿 [ChatCrystal 向量服务](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/embedding.ts) 提交 [`7642768`](https://github.com/ZengLiangYi/ChatCrystal/tree/764276814e5c5a33ecbfeae63754f74b9261a3a8) 说明流程；以下常量描述该版本规则，不是通用的最佳检索参数。

## 两个入口处理不同问题

`/api/notes?search=...` 在标题与摘要上使用 SQL LIKE 条件，适合检查已有词项；`/api/search?q=...` 生成查询向量并检索笔记候选。前者也有 LIKE 通配语义，不能概括为任意输入都按字面匹配。

默认本机连接的 CLI 例子：



```bash
crystal   --json   notes   list   --search   SQLite   --limit   5  
  crystal   --json   search   "SQLite 并发写入问题"   --limit   5
```



第一个 JSON 是含 `items` 的列表对象，第二个是候选数组。JSON 分支会传递数量参数，避免 TTY 搜索界面使用自己的固定数量；符合条件的结果仍可能少于上限。

## 从笔记文本到候选

笔记使用选定字段建立向量，查询使用当前向量配置。两端需要匹配；换模型后应按 [Embedding 配置指南](/blog/embedding-model-configuration) 重建并验证。应用用段落分组生成多个 chunk，同一篇笔记可以拥有多个向量。

[Vectra v0.15.0 的 LocalIndex](https://github.com/Stevenic/vectra/blob/v0.15.0/src/LocalIndex.ts) 对候选计算相似度并保留 top K；这条路径不是 HNSW 或 LSH 的近似候选查找。检索成本和质量仍需按实际库规模与模型测试，不能从“本地索引”推导零成本或固定响应速度。

## 数据库核对、去重与排序

应用读取候选对应的数据库 chunk，并要求笔记状态为 `done`。索引中存在一个条目，不代表它能作为有效结果返回。多个 chunk 按笔记去重，保留较高分。

词项处理包括 ASCII 技术词与汉字二元组。候选包含全部提取词项时，分数至少提升到 `0.82`；否则低于 `0.58` 的候选会被过滤。这个加分发生在向量候选物化之后，不是独立扫描全库的关键词回退。

若去重、核对或筛选后候选不足，代码逐步扩大候选数，并受可取得的索引数量或停止条件限制。它可以补充候选，却不能保证始终返回请求数量，也不能证明语义相似的正确笔记一定入选。

## 用 API 对比关系扩展

查询通过 `--data-urlencode` 编码，避免手工拼接中文或特殊字符：



```bash
curl   --get   http://localhost:3721/api/search   \  
    --data-urlencode   'q=SQLite 并发写入'   \  
    --data-urlencode   'limit=5'
```



可选增加关系扩展：



```bash
curl   --get   http://localhost:3721/api/search   \  
    --data-urlencode   'q=SQLite 并发写入'   \  
    --data-urlencode   'limit=5'   \  
    --data-urlencode   'expand=true'
```



当前扩展读取直接命中的一跳关系，要求关系 confidence 至少 `0.5`，关联结果按 `直接分数 × 0.7 × confidence` 折扣。它不是多跳路径搜索；已进入结果集合的笔记会跳过，因此不能说会为每条关联选择最强路径。扩展后结果可能超过直接命中的数量上限。

## 正确解释返回字段

[HTTP 路由](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/routes/notes.ts) 用 `data` 包装数组，提供 `note_id`、`conversation_id`、标题、项目、分数、标签与 `via_relation`。`via_relation` 标记关系类型，不是完整关联路径，也不是来源内容已经正确的证明。CLI 数组已解包，不需 `.data[]`。

读取 `note_id` 对应正文，再核对原始会话和当前任务。用有答案、同义表述、精确词项和无答案查询比较两个入口与扩展开关；记录误匹配而不只展示漂亮的正例。实际复用流程见 [找回调试方案](/blog/recover-debugging-notes)，关系作为证据的边界见 [RAG 与知识图谱](/blog/rag-and-knowledge-graphs)。

分享：

##  相关文章

[##  找回调试方案：从搜索候选到验证可复用条件

用一个明确标注的调试场景，检查笔记、向量和检索返回，再核对原始对话与当前项目，避免把相关分数当作正确答案。          曾亮懿       2026年10月3日      阅读约 4 分钟

#ChatCrystal

#语义搜索

#调试](/blog/recover-debugging-notes)    [##  语义搜索与关系上下文：发现隐藏依赖但保留验证

用构造的重构场景组合直接检索和一跳关系扩展，解释分数、候选和来源字段，不把关联线索写成依赖证明。          曾亮懿       2026年10月3日      阅读约 3 分钟

#ChatCrystal

#语义搜索

#知识图谱](/blog/semantic-search-relation-context)    [##  从 RAG 到知识图谱：让个人知识检索保留关系

区分关键词检索、向量搜索、RAG 与笔记关系图，结合 ChatCrystal 的候选召回和一跳扩展实现，说明关系能补充什么、不能证明什么。          曾亮懿       2026年6月5日      阅读约 6 分钟

#ChatCrystal

#RAG

#知识图谱

#检索](/blog/rag-and-knowledge-graphs)

##  从文章到代码

到我的 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": "语义搜索流程：请求、候选去重与关系扩展",
  "description": "沿 ChatCrystal 实现检查查询向量、候选升级、词项加分和关系扩展，用实际 API 区分笔记列表筛选与语义检索。",
  "url": "https://zengliangyi.com/blog/semantic-search-workflow/",
  "image": "https://zengliangyi.com/images/blog/semantic-search-workflow.svg",
  "datePublished": "2026-10-03T00:00:00.000Z",
  "dateModified": "2026-10-03T00: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/semantic-search-workflow/"
  }
}
{
  "@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": "语义搜索流程：请求、候选去重与关系扩展",
      "item": "https://zengliangyi.com/blog/semantic-search-workflow/"
    }
  ]
}
```
