当前位置: 首页 > article >正文

语义搜索实战:从关键词到向量检索

本文面向想深入理解语义搜索实现原理的开发者。预计阅读时间10 分钟关键词搜索已经够用了试试搜怎么解决数据库死锁——你可能漏掉所有标题写SQLite WAL mode、并发写入冲突的笔记。语义搜索能跨越措辞差异直接理解意图。这篇文章拆解 ChatCrystal 的语义搜索实现从 Embedding 文本构建到 vectra 向量检索再到关系图扩展给出可运行的代码和可调的参数。语义搜索 vs 关键词搜索一个具体例子用户查询: 如何优化大文件解析速度关键词搜索SQL LIKE在title和summary字段里匹配字面量。它能找到标题含解析速度的笔记但会漏掉“JSONL 流式读取性能调优”——同一概念不同措辞“使用 readline 替代 fs.readFile”——解决方案没有优化二字“Cursor 适配器 SQLite 查询慢”——相关场景关键词完全不重叠语义搜索把查询和笔记都转成向量浮点数组通过余弦相似度匹配。优化大文件解析速度和JSONL 流式读取性能调优在向量空间中距离很近因为 Embedding 模型理解它们的语义关联。ChatCrystal 两种搜索都支持/api/notes?searchxxx走关键词/api/search?qxxx走语义。本文聚焦后者。完整搜索流程从用户输入查询到返回结果经过五个阶段查询字符串 → embedSearchQuery() // 1. 向量化查询 → vectra.queryItems() // 2. 向量检索候选集 → materializeDirectSearchHits() // 3. 物化 去重 → expandRelations() // 4. 关系扩展可选 → enrichWithTags() // 5. 批量补充标签 → 返回结果对应server/src/services/embedding.ts中的semanticSearch函数// server/src/services/embedding.tsexportasyncfunctionsemanticSearch(query:string,requestedTopK10,expandRelationsfalse,):PromiseDirectSearchHit[]{// 1. 向量化查询constembeddingawaitembedSearchQuery(query);// 2. 向量检索带候选集升级机制letcandidateKrequestedTopK;letdirectResults:DirectSearchHit[][];while(candidateK0){constresultsawaitindex.queryItemsNoteChunkMeta(embedding,query,candidateK);directResultsawaitmaterializeDirectSearchHits(db,results);if(directResults.lengthrequestedTopK||results.lengthcandidateK)break;candidateKcandidateK*2;// 结果不够翻倍候选集}// 3. 去重同一笔记多个 chunk 取最高分directResultsdirectResults.slice(0,requestedTopK);// 4. 关系扩展if(expandRelations){// 沿 note_relations 边扩展...}returndirectResults;}Embedding 文本构建核心问题为什么不直接 Embedding 笔记原文因为 LLM 生成的笔记包含结构化字段title、summary、key_conclusions、code_snippets每个字段的信息密度不同。直接拼接原文会引入噪音——代码片段的字符占比大但语义信息少标签虽短但关键词价值高。buildNoteEmbeddingText按策略组合各字段// server/src/services/embedding.tsexportfunctionbuildNoteEmbeddingText(input:BuildNoteEmbeddingTextInput):string{constparts:string[][];appendText(parts,input.title);// 标题最高权重appendText(parts,input.summary);// 摘要核心语义// 关键结论逐条加入每条独立成段for(constconclusionofstringArrayFromJson(input.keyConclusionsJson)){appendText(parts,conclusion);}appendText(parts,input.tagsText);// 标签关键词补充// 代码片段只取 description不 Embedding 代码本身constcodeSnippetssafeParseJson(input.codeSnippetsJson);if(Array.isArray(codeSnippets)){for(constsnippetofcodeSnippets){if(isRecord(snippet)){appendText(parts,snippet.description);}}}returndedupeExact(parts).join(\n\n);}关键设计决策标签参与 Embedding。标签是人工或 LLM 提取的关键词能显著提升检索精度。标题写修复 bug但标签含SQLite、WAL、并发搜索数据库并发问题依然能命中。代码片段只取 description。代码本身字符多、语义密度低Embedding description“使用 readline 逐行读取替代 fs.readFile 整文件加载”比 Embedding 代码体更有效。去重。dedupeExact移除完全重复的文本段避免噪音。Memory 笔记的特殊处理对于agent-writeback和manual-note类型的笔记额外提取结构化字段if(isMemoryNoteSource(input.sourceType)){// 代码证据截断到 1000 字符for(constsnippetofcodeSnippets){appendCodeSnippetEvidence(parts,snippet);}// 结构化经验字段constrawPayloadsafeParseJson(input.rawPayloadJson);if(isRecord(rawPayload)){appendLabeledText(parts,Root cause,rawPayload.root_cause);appendLabeledText(parts,Resolution,rawPayload.resolution);appendLabeledArray(parts,Pitfall,rawPayload.pitfalls);appendLabeledArray(parts,Pattern,rawPayload.reusable_patterns);appendLabeledArray(parts,Decision,rawPayload.decisions);}appendLabeledArray(parts,Error signature,safeParseJson(input.errorSignaturesJson));appendLabeledArray(parts,File,safeParseJson(input.filesTouchedJson));}带标签前缀Root cause: ...、Error signature: ...让 Embedding 模型理解字段语义角色提升这个错误怎么修类查询的命中率。分块策略一条笔记的 Embedding 文本可能很长。Embedding 模型有 token 上限且长文本的向量会稀释重点信息。ChatCrystal 在 500 字符处切分// server/src/services/embedding.tsconstCHUNK_SIZE500;// characters per chunkfunctionchunkText(text:string):string[]{if(text.lengthCHUNK_SIZE)return[text];constchunks:string[][];constparagraphstext.split(/\n\n/);// 按段落边界切分letcurrent;for(constparaofparagraphs){if(current.lengthpara.length2CHUNK_SIZEcurrent.length0){chunks.push(current.trim());currentpara;}else{current(current?\n\n:)para;}}if(current.trim())chunks.push(current.trim());returnchunks;}设计要点段落边界优先。不在句子中间切断保持语义完整性。500 字符 ≈ 250 token。对大多数 Embedding 模型来说是一个 chunk 的舒适区——既不过长导致语义稀释也不过短缺乏上下文。每个 chunk 独立 Embedding。一个笔记可能产生 1-5 个向量存储在 vectra 和 SQLite 的embeddings表中。向量检索vectra 的工作原理ChatCrystal 使用 vectra 作为本地向量索引。它是一个零依赖的 Node.js 向量数据库基于 HNSWHierarchical Navigable Small World算法。索引存储在{dataDir}/vectra-index/目录下// server/src/services/vector-index.tsimport{LocalIndex}fromvectra;constINDEX_PATHresolve(appConfig.dataDir,vectra-index);exportasyncfunctiongetIndex():PromiseLocalIndex{if(_index)return_index;_indexnewLocalIndex(INDEX_PATH);if(!(await_index.isIndexCreated())){await_index.createIndex();}return_index;}每个向量条目包含向量本身和元数据// 生成 Embedding 时的存储constitemawaitindex.insertItem({vector:chunk.vector,metadata:{noteId:id,chunkIndex:chunk.chunkIndex,conversationId,title,projectName,},});查询时vectra 返回 top-K 最近邻constresultsawaitindex.queryItemsNoteChunkMeta(embedding,query,candidateK);候选集升级机制一个笔记有多个 chunk直接取 top-10 可能返回 10 个 chunk 但只来自 3 条笔记去重后只有 3 条结果。ChatCrystal 的做法是逐步翻倍候选集while(candidateK0){constresultsawaitindex.queryItemsNoteChunkMeta(embedding,query,candidateK);directResultsawaitmaterializeDirectSearchHits(db,results);if(directResults.lengthrequestedTopK||results.lengthcandidateK)break;candidateKcandidateK*2;// 10 → 20 → 40 → ...}materializeDirectSearchHits做两件事从 SQLite 读取 chunk 原文按noteId去重保留最高分exportasyncfunctionmaterializeDirectSearchHits(db:PickDatabaseLike,exec,results:SemanticSearchHit[],):PromiseDirectSearchHit[]{constmaterialized:DirectSearchHit[][];for(constresultofresults){constchunkResultdb.exec(SELECT e.chunk_text FROM embeddings e JOIN notes n ON n.id e.note_id WHERE e.note_id ? AND e.chunk_index ? AND n.embedding_status done,[result.item.metadata.noteId,result.item.metadata.chunkIndex],);if(!chunkResult.length)continue;materialized.push({noteId:result.item.metadata.noteId,score:result.score,chunkText:String(chunkResult[0].values[0][0]),// ...其他字段});}// 按 noteId 去重保留最高分constseennewMapnumber,DirectSearchHit();for(constresultofmaterialized){if(!seen.has(result.noteId)||seen.get(result.noteId)!.scoreresult.score){seen.set(result.noteId,result);}}returnArray.from(seen.values());}关系扩展搜索ChatCrystal 的笔记之间有note_relations边由 LLM 在总结时自动生成。开启expandtrue后搜索会沿关系图扩展// server/src/services/embedding.ts (简化)if(expandRelationsdirectResults.length0){constresultMapnewMap(directResults.map((r)[r.noteId,r]));for(constdrofdirectResults){constrelResultdb.exec(SELECT r.relation_type, r.confidence, CASE WHEN r.source_note_id ? THEN r.target_note_id ELSE r.source_note_id END as linked_note_id FROM note_relations r WHERE (r.source_note_id ? OR r.target_note_id ?) AND r.confidence 0.5,[dr.noteId,dr.noteId,dr.noteId],);for(constrowofresultToObjects(relResult)){constlinkedIdNumber(row.linked_note_id);if(resultMap.has(linkedId))continue;// 已在结果中跳过// 分数折扣原始分 × 0.7 × 置信度constdiscountedScoredr.score*0.7*(Number(row.confidence)||0.5);resultMap.set(linkedId,{noteId:linkedId,score:Math.round(discountedScore*1000)/1000,viaRelation:row.relation_type,// 标记来源关系类型// ...其他字段});}}returnArray.from(resultMap.values()).sort((a,b)b.score-a.score);}分数折扣公式score × 0.7 × confidence。直觉关系扩展的结果天然不如直接命中可靠0.7 的折扣让它们排在直接命中之后。confidence 0.5的门槛过滤掉弱关联。viaRelation字段标记结果来源如related、duplicate前端可以据此展示关联路径。搜索 API 详解REST API# 基础搜索curlhttp://localhost:3721/api/search?qSQLite%20性能优化# 指定返回数量最大 50curlhttp://localhost:3721/api/search?q死锁limit5# 开启关系扩展curlhttp://localhost:3721/api/search?q并发expandtrue返回格式{success:true,data:[{note_id:42,conversation_id:abc123,title:SQLite WAL 模式下的并发写入问题,project_name:my-project,score:0.891,tags:[sqlite,并发,性能],via_relation:null},{note_id:58,conversation_id:def456,title:数据库连接池配置,project_name:my-project,score:0.524,tags:[database],via_relation:related}]}CLI# 基础搜索crystal search如何优化大文件解析速度# 指定返回数量crystal search死锁--limit5# JSON 输出适合脚本处理crystal search并发--jsonMCP 工具在 Claude Code 中通过 MCP 使用语义搜索// settings.json{mcpServers:{chatcrystal:{command:crystal,args:[mcp]}}}MCP 暴露search_knowledge工具AI 助手可以直接调用搜索你的知识库。搜索质量调优1. Embedding 模型选择不同模型的向量维度和语义理解能力差异很大模型维度特点nomic-embed-text(Ollama)768本地运行中文支持好text-embedding-3-small(OpenAI)1536性价比高text-embedding-3-large(OpenAI)3072最高精度text-embedding-004(Google)768多语言优化配置方式crystal configsetembedding.provider ollama crystal configsetembedding.model nomic-embed-text2. 查询措辞语义搜索对查询的措辞不敏感但以下技巧能提升精度具体 模糊。SQLite WAL 并发写入死锁比数据库问题命中率高。包含意图。怎么解决 X和X 的原理会匹配不同类型的笔记。英文技术术语保持原样。Embedding 模型对英文术语的编码通常更精确。3. 关系扩展的使用场景expandtrue适合探索式搜索——你想找的不只是直接匹配还有相关联的知识。代价是结果中会混入间接相关的笔记通过via_relation字段可以区分开。精确查找时建议关闭减少噪音。4. 候选集大小默认requestedTopK10如果你的知识库很大500 笔记可以适当增大到 20-30。候选集升级机制会自动处理 chunk 去重不用担心返回结果太少。下一步混合检索结合关键词和语义搜索的混合策略对精确匹配场景如错误代码、函数名更友好。重排序在向量检索后用 Cross-Encoder 对 (query, chunk) 对重新打分提升精度。增量索引优化当前每次更新笔记都重建所有 chunk 的向量可以改为 diff 更新。语义搜索不是银弹但它让知识检索从猜关键词变成表达意图。ChatCrystal 的实现选择了本地优先vectra Ollama零外部依赖适合个人知识库场景。项目地址github.com/ZengLiangYi/ChatCrystal

相关文章:

语义搜索实战:从关键词到向量检索

本文面向:想深入理解语义搜索实现原理的开发者。 预计阅读时间:10 分钟 关键词搜索已经够用了?试试搜"怎么解决数据库死锁"——你可能漏掉所有标题写"SQLite WAL mode"、"并发写入冲突"的笔记。语义搜索能跨越…...

Stateflow实战:构建LKA系统状态机的模块化建模与数据管理

1. 从零理解LKA系统与Stateflow建模 第一次接触车道保持辅助系统(LKA)时,我盯着那个能在高速上自动修正方向的方向盘看了半天。这玩意儿到底怎么判断什么时候该介入?后来才知道,核心就是藏在控制器里的状态机逻辑。Sta…...

【Perplexity环境新闻搜索实战指南】:20年老炮亲授3大避坑法则与实时情报提纯术

更多请点击: https://intelliparadigm.com 第一章:Perplexity环境新闻搜索实战指南导论 Perplexity 是一款以实时、可信与上下文感知为设计核心的 AI 搜索工具,其底层融合了多源新闻 API、语义检索模型及动态引用验证机制,特别适…...

用Midas Civil搞定箱梁桥抗倾覆验算:从规范解读到多支座工况的实操避坑

用Midas Civil实现箱梁桥抗倾覆验算的工程实践指南 箱梁桥作为现代交通基础设施的重要组成部分,其抗倾覆稳定性直接关系到桥梁运营安全。2018版《公路钢混及预混桥涵设计规范》(JTG 3362-2018)首次系统性地提出了抗倾覆验算要求,…...

Excel MCP Server终极指南:5步实现无Excel环境下的Excel文件操作

Excel MCP Server终极指南:5步实现无Excel环境下的Excel文件操作 【免费下载链接】excel-mcp-server A Model Context Protocol server for Excel file manipulation 项目地址: https://gitcode.com/gh_mirrors/ex/excel-mcp-server Excel MCP Server是一个基…...

时间序列预测损失函数全解析:从MSE到分位数损失的选择指南

1. 项目概述:为什么时间序列预测的损失函数值得深究?做时间序列预测,无论是金融市场的股价波动、电商平台的销量起伏,还是工业设备的传感器读数,我们最终都要面对一个核心问题:如何衡量模型预测得好不好&am…...

编写同城公益捐书物资登记流转程序,统计闲置书籍物资,对接公益捐赠渠道。

一个完全去营销化、偏工程与社会创新视角的 Python 示例项目,定位为创新与创业实验课程原型,不绑定任何公益平台、不引导捐赠渠道、不涉及任何机构背书,仅作为物资登记与流转建模工具。 同城公益捐书物资登记流转程序 ——基于物资生命周期管…...

别再只怪外力了!手把手教你用砂纸“解剖”MLCC,排查电容失效真凶(附打磨实操图)

低成本破解MLCC失效之谜:砂纸打磨法的实战指南 当产线上突然出现大批量MLCC失效时,硬件工程师们常常陷入两难——既没有价值百万的金相显微镜,也无法承受将样品送往专业实验室的高昂成本和时间延误。这时,一套简单粗暴却行之有效的…...

开发雨天居家室内活动推荐程序,根据人数年龄自动生成雨天居家休闲创意活动。

一个完全去营销化、偏工程与创业实验视角的 Python 示例项目,定位为课程级 MVP 原型,不绑定任何平台、不推荐商品、不引导消费。雨天居家室内活动推荐程序——基于人数与年龄的规则推荐系统实验一、实际应用场景描述在下雨天气,家庭或室友常面…...

如何在Windows上轻松安装安卓应用:APK-Installer完整指南

如何在Windows上轻松安装安卓应用:APK-Installer完整指南 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 你是否曾经想在Windows电脑上直接运行安卓应用&am…...

EMD vs NEMD:分子动力学算热导率,新手到底该选哪个?

EMD与NEMD方法实战指南:如何为你的热导率计算选择最佳方案 在纳米材料和新型功能材料的研究中,热导率的精确计算是理解材料热输运性能的关键。面对平衡态分子动力学(EMD)和非平衡态分子动力学(NEMD)两种主流方法,许多研究者常常陷入选择困境。…...

当A*算法遇上真实山地DEM:一份给无人机/机器人路径规划者的Python避坑指南

当A*算法遇上真实山地DEM:无人机路径规划的Python实战与优化 山地路径规划的独特挑战 在无人机和机器人导航领域,山地地形带来了传统路径规划算法难以应对的复杂性。与平坦城市环境不同,山地DEM(数字高程模型)数据包含…...

推荐几款实测有效的降重工具,要求同时对付查重系统和AIGC检测

毕业季论文两大 “生死关”—— 知网 / 维普 / 格子达等查重标红、AIGC 疑似率超标,已成为无数学生的噩梦。普通降重工具仅能降重复率,改写后仍难逃 AI 检测;AI 写作工具生成内容流畅度高,却自带明显 AI 痕迹,双检极易…...

WSL2下CUDA版本切换实战:从CUDA 12.0降级到11.1,成功安装diff-gaussian-rasterization

WSL2环境下CUDA版本切换与diff-gaussian-rasterization安装全指南 在AI和图形学项目的复现过程中,CUDA版本与依赖库的兼容性问题常常成为开发者的"拦路虎"。最近在复现一篇论文时,我遇到了diff-gaussian-rasterization库因CUDA版本不匹配而无…...

掌握Simscape Electrical电机控制器设计:减少硬件测试成本60%的专业解决方案

掌握Simscape Electrical电机控制器设计:减少硬件测试成本60%的专业解决方案 【免费下载链接】Design-motor-controllers-with-Simscape-Electrical This repository contains MATLAB and Simulink files used in the "How to design motor controllers using …...

别再被CAPL路径搞懵了!getAbsFilePath、setFilePath这几个函数到底怎么用?

CAPL文件路径操作全解析:从函数原理到实战避坑指南 在CANoe自动化测试开发中,文件路径操作堪称最基础却又最容易出错的环节之一。许多工程师都经历过这样的场景:精心编写的CAPL脚本在本地测试一切正常,换到同事电脑上却频频报错&a…...

从点灯到AI:用高云Tang Nano 4K玩转FPGA+MCU混合开发(附避坑指南)

从点灯到AI:高云Tang Nano 4K混合架构开发实战与避坑指南 在嵌入式AI和边缘计算领域,FPGA凭借其并行计算能力和低功耗特性,正成为越来越多开发者的选择。而高云Tang Nano 4K这款搭载Cortex-M3硬核的FPGA开发板,以其独特的"FP…...

手把手教你用示波器抓取Intel CPU的SVID时序(附读写判定与Intel送测指南)

实战指南:利用示波器精准解析Intel CPU的SVID通信时序 当一块新设计的服务器主板首次上电时,电源管理系统的稳定性往往决定了整个平台的可靠性。作为硬件工程师,我们常常需要直面这样的场景:主板虽然能点亮,但CPU与电压…...

告别VirtualBox的‘不是Host-Only适配器’错误:一个网络配置的深度修复指南

VirtualBox Host-Only网络故障全解析:从原理到实战修复 当你正准备启动VirtualBox中的开发环境虚拟机时,突然弹出的红色错误提示框让所有工作戛然而止——"Interface is not a Host-Only Adapter"。这个看似简单的网络适配器错误背后&#xf…...

双向脑机接口:从神经信号解码到感觉编码的核心原理与挑战

1. 从科幻到现实:双向脑机接口的演进与核心挑战十几年前,当我第一次在学术会议上看到猴子用意念控制机械臂抓取食物的视频时,那种震撼至今记忆犹新。那时,脑机接口(BCI)还只是顶级实验室里昂贵的“魔术”。…...

3大突破:AEUX如何重塑设计到动画的无缝工作流

3大突破:AEUX如何重塑设计到动画的无缝工作流 【免费下载链接】AEUX Editable After Effects layers from Sketch artboards 项目地址: https://gitcode.com/gh_mirrors/ae/AEUX 在数字创意领域,设计师们长期面临着一个令人沮丧的现实&#xff1a…...

不只是格式化:深入理解Mac磁盘工具里的‘分区方案’(GUID/MBR/APM),选对才能跨平台读写

不只是格式化:深入理解Mac磁盘工具里的‘分区方案’(GUID/MBR/APM),选对才能跨平台读写 当你将一块移动硬盘从APFS格式化为ExFAT后,满心欢喜地插到Windows电脑上,却依然收到"需要格式化"的提示—…...

从无人机飞控到机械臂轨迹规划:Slerp(球面线性插值)在机器人领域的硬核应用

从无人机飞控到机械臂轨迹规划:Slerp在机器人领域的硬核应用 当无人机在强风中需要稳定转向,或是机械臂执行精密装配任务时,传统欧拉角插值常因万向节锁问题导致姿态突变。而**Slerp(球面线性插值)**通过在四元数空间…...

鸣潮模组全面指南:解锁15项游戏增强功能

鸣潮模组全面指南:解锁15项游戏增强功能 【免费下载链接】wuwa-mod Wuthering Waves pak mods 项目地址: https://gitcode.com/GitHub_Trending/wu/wuwa-mod 还在为《鸣潮》中的技能冷却时间烦恼吗?或者觉得游戏中的资源收集过于繁琐?…...

麒麟系统离线部署OnlyOffice,我踩过的那些坑(附Docker镜像包和完整配置)

麒麟系统离线部署OnlyOffice实战避坑指南 在国产化替代浪潮中,麒麟系统作为主流国产操作系统,正逐步应用于各类关键信息基础设施领域。而办公软件作为日常刚需,如何在麒麟系统上实现高效、安全的文档协作成为许多技术团队面临的挑战。OnlyOff…...

别再只用Telnet了!手把手教你给思科路由器配置SSH远程登录(附Packet Tracer验证)

从Telnet到SSH:思科路由器安全远程管理实战指南 每次看到运维同事用Telnet登录路由器时,我都忍不住想提醒——这就像在咖啡馆用明信片写密码。作为从业十年的网络工程师,我见过太多因Telnet导致的安全事故。本文将用Packet Tracer带您完成从T…...

如何为 OpenClaw 配置 Taotoken 以实现高效的 Agent 工作流

🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 如何为 OpenClaw 配置 Taotoken 以实现高效的 Agent 工作流 基础教程类,面向使用 OpenClaw 框架构建 AI Agent 的开发者…...

Arm Cortex-A715向量计算优化指南:ASIMD/SVE指令深度解析

1. Cortex-A715向量计算引擎深度解析在移动计算和嵌入式领域,Arm Cortex-A715作为最新一代高性能CPU核心,其向量计算能力直接决定了AI推理、图像处理等关键场景的性能表现。本文将深入剖析A715的ASIMD/SVE指令集架构设计,从底层硬件机制到实际…...

ETT数据集实战:如何用油温预测优化电网负载与设备维护策略

ETT数据集实战:如何用油温预测优化电网负载与设备维护策略 当一座城市的电网在盛夏午后突然崩溃,背后往往隐藏着变压器油温失控的连锁反应。去年某沿海城市电网的故障分析报告显示,超过60%的突发停电事件与变压器过热直接相关——这个数据让行…...

Cadence 5141实战:手把手教你搞定Bandgap基准电压源电路(附完整仿真流程)

Cadence 5141实战:手把手教你搞定Bandgap基准电压源电路(附完整仿真流程) 在模拟集成电路设计中,基准电压源如同心脏般重要,而Bandgap电路则是这颗心脏的核心技术。无论你是微电子专业的学生,还是刚踏入模拟…...