Joplin 语义搜索(AI Embeddings)完全指南:本地向量索引的原理、配置与使用
【免费下载链接】joplinJoplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.项目地址: https://gitcode.com/GitHub_Trending/jo/joplin
Joplin 在 v3.7 及以上版本(桌面端)内置了基于 AI Embeddings 的语义搜索能力,可以让笔记被"按含义"而非"按关键词"检索到。本文以 readme/apps/ai_semantic_search.md 为核心,结合仓库内packages/lib/services/ai/下的真实源码实现,系统讲解它的工作原理、启用方式、进度跟踪、适用场景、平台限制与插件/MCP 调用方式,帮助你把这套"本地向量搜索"能力真正用起来。
什么是语义搜索
传统的关键词搜索要求查询词与笔记文本逐字匹配,而语义搜索把文本转换为数值向量(embedding),再通过向量距离衡量"含义"的接近程度。
文档中给出的经典例子是:搜索 "the note about pet sitters for my dog"(关于帮我遛狗的宠物保姆的笔记),可以命中标题为 "Vet contacts"(兽医联系方式)的笔记——只要它的正文提到有人帮忙遛狗,即使 "pet sitter" 这个词从未出现过。
语义搜索(semantic search,也常称为向量搜索 / vector search)是对 Joplin 常规关键词搜索的补充,而非替代。两者解决的是不同的问题:
| 搜索方式 | 匹配依据 | 擅长场景 |
|---|---|---|
| 关键词搜索(full-text) | 字面词元 | 精确 token:人名、ID、错误码、文件名 |
| 语义搜索(embeddings) | 含义向量 | 概念查询、自然语言提问、改写表达 |
工作原理:完全本地的双阶段流水线
阶段一:后台索引(Embedding Indexer)
启用 AI 功能后,Joplin 会先下载一个约140 MB 的小型语言模型到本机。此后它在后台运行,逐条读取笔记,为每条笔记生成"数值指纹"并存入本地索引。
对应到源码,这一阶段由 packages/lib/services/ai/EmbeddingIndexer.ts 中的EmbeddingIndexer类负责。它是一个后台服务,持续监听item_changes(笔记变更表),把每次变更的笔记切块(chunk)、通过激活的EmbeddingProvider计算向量、写入NoteEmbedding模型对应的本地数据库表。
其中分块逻辑在 packages/lib/services/ai/chunker.ts:
- 目标每个块约500 tokens(按
TARGET_TOKENS_PER_CHUNK = 500计算),块与块之间保留10% 重叠(OVERLAP_RATIO = 0.10),符合向量检索的常见实践; - 根据文本脚本自适应分块参数:拉丁语系按约每 token 3.5 字符估算,中日韩(CJK)文本按每 token 1.2 字符、且占比超过 30% 时才切换为 CJK 配置;
- 标题会被双写进第一个块(
${title}\n\n${title}\n\n${chunks[0]}),因为标题通常是最密集的语义信号(如"帮我遛狗的宠物保姆"配一个只有附件链接的正文),这样可以提升以标题为锚点的查询命中率。
阶段二:查询匹配(Search Service)
当你发起搜索时,查询文本经过同样处理得到向量,Joplin 返回向量距离最近的笔记。这一阶段由 packages/lib/services/ai/SearchService.ts 中的SearchService类实现:
- 向量存储使用 sqlite-vec 扩展(
NoteEmbedding.vectorSearchAvailable()会检查该扩展是否成功加载,见 EmbeddingIndexer.ts); - 数据库返回 L2 距离,而向量已做 L2 归一化,因此余弦相似度可按
1 − d²/2精确换算(见cosineFromDistance),并对浮点误差做了 0~1 的截断; relevance预设由 Joplin 内部映射为具体参数(RELEVANCE_DEFAULTS),插件只需面向预设编程,即使未来更换模型也不会破坏兼容性:strict:k=5,minScore=0.86,返回更少的高置信块;normal:k=10,minScore=0.83(默认);loose:k=20,minScore=0.74,返回更多候选;
- 当活动模型未变化时,向量只嵌入一次;若按
{ noteId }作为查询,则直接复用该笔记已索引的块向量,避免重复计算。
隐私边界:一切都在本机
文档明确强调:模型是本地的,没有任何笔记内容被发送到云端服务;索引也是本地的、不同步的——每台设备各自构建自己的索引。这意味着语义搜索结果不随同步传播,换设备需要重新索引。
如何启用语义搜索
- 打开配置界面,进入AI分区;
- 勾选Enable AI features(启用 AI 功能);
- 保持Enable the embeddings indexer(启用嵌入索引器)为勾选状态(默认开启)。
首次启用时 Joplin 会下载模型,之后开始在后台索引笔记。从源码看,ai.enabled与ai.embedding.enabled是两个独立的设置项,分别控制 AI 主开关与索引器开关;索引器的运行状态与统计均可在Settings → AI面板中查看(EmbeddingIndexer.getStatus()返回modelDownloadStatus、indexerState、notesIndexed、totalNotes四类信息,indexerState会区分ai-disabled、index-disabled、vector-search-unavailable、running、idle等状态,见 EmbeddingIndexer.ts)。
进度跟踪与索引节奏
Settings → AI 面板会显示索引器的状态和已处理的笔记数量。首次为整个笔记库建立索引需要较长时间:Joplin 每 5 分钟处理 100 条笔记(对应源码中的batchSize = 100),以便把机器负载压到极小。一个 10,000 条笔记的笔记库大约需要8 小时的后台工作。你完全可以一边让它跑一边正常使用 Joplin,不必着急。
从源码看,索引节奏是自适应的(scheduleNextTick):
- 初始扫描阶段:每 30 秒 tick 一次(
initialScanInterval = 30 * Second),因为用户刚开启该功能、期待尽快看到进展,而maintenanceRunning_标志会防止 tick 背靠背执行; - 维护阶段(初始扫描完成后):每 3 分钟 tick 一次(
maintenanceInterval = 3 * Minute),既不会在每次编辑时消耗 CPU,又能保证新保存的笔记在几分钟内可被检索。
初始扫描完成之后,新增和编辑过的笔记会在几分钟内被拾取(变更通过item_changes变更流捕获,processChangeBatch会把同一笔记在同一 tick 内的多次编辑合并为一次嵌入)。值得一提的是,索引器在启动时会检查 sqlite-vec 扩展是否可用,若平台不支持则直接跳过,避免无谓的 CPU/内存开销。
使用语义搜索
启用后,语义搜索结果会混入 Joplin 内置搜索 UI 的默认全文搜索匹配结果中一并展示,无需切换任何模式。
此外,语义搜索能力向两个外部入口开放:
插件 API:joplin.ai.search()
插件可调用joplin.ai.search()按含义检索笔记(插件描述中会注明是否使用该能力)。对应实现位于 packages/lib/services/plugins/api/JoplinAi.ts,其search(options)直接委托给SearchService.search()。SearchOptions支持:
query:纯文本查询,或{ noteId }形式(复用该笔记已索引的块向量,适合"找相似笔记"场景);scope:限定搜索范围,支持'all'(默认)、'note'(单条笔记)、'folder'(按文件夹 id)、'tag'(按标签 id);回收站与冲突笔记会被排除;relevance:'strict' | 'normal' | 'loose',默认'normal'。
同时joplin.ai还提供两个辅助方法:
getEmbeddings(options):分页获取索引块对应的原始向量(含modelId、dimension、不透明游标cursor/nextCursor),供需要自行做聚类、降维或距离计算的插件使用;若分页中途模型切换,游标会停止返回行,插件应以无游标方式配合新的modelId重新开始;getIndexStatus():返回索引器的就绪状态,适合做混合检索流水线——ready时走语义搜索,否则回退到本地方案。
仓库内的测试夹具 packages/lib/testing/ai/semanticSearch.ts 展示了在测试环境启用语义搜索的完整方式:注入TestEmbeddingProvider、打开featureFlag.enableSemanticSearch与ai.enabled,然后驱动一次EmbeddingIndexer.instance().maintenance()并同步搜索表。
MCP 服务器:semantic_search_notes工具
外部 AI 应用(如 Claude Desktop、Cursor 等)可通过 MCP 服务器 使用semantic_search_notes工具。该工具定义在 packages/lib/services/ai/tools/global/semanticSearchNotes.ts,参数与行为如下:
| 参数 | 类型 | 说明 |
|---|---|---|
query | string(必填) | 自由文本查询,表达想找什么 |
notebook_id | string(可选) | 限定在单个笔记本内搜索 |
tag_id | string(可选) | 限定在带某标签的笔记内搜索(与notebook_id不可同时传) |
relevance | string(可选) | 'strict' | 'normal' | 'loose',默认'normal' |
工具返回的是排序后的块(chunk)而非整条笔记,每块包含源笔记 id、命中的块文本与相似度得分,并建议配合read_note工具读取完整上下文。若 AI 嵌入未在 Settings → AI 中启用,工具会抛出清晰错误(No embedding provider is active. Enable AI features in Settings → AI.)而不是静默返回空结果。仓库中的 McpServer.test.ts 相关测试覆盖了该工具的参数校验与错误路径。
擅长什么、不擅长什么
语义搜索把文本转化为"含义"的数值表示,因此查询本身携带足够含义时效果最好:
- 擅长:概念性/改写式查询、自然语言提问、寻找不共享精确词汇的笔记(如"the note about pet sitters for my dog");
- 不擅长:精确 token 检索——人名、ID、错误码等。单个词携带的含义太少,难以可靠匹配,还容易返回高得分的无关结果。
这两种方式互补:关键词搜索适合精确词汇,语义搜索适合含义匹配。如果你在写插件,不要把单字词或精确匹配查询直接路由到joplin.ai.search()——应将其与关键词搜索组合使用。这一设计也体现在SearchService的底层:查询向量化后按(noteId, chunkIndex)合并最高分并按minScore过滤,单字查询几乎无法越过相似度阈值线。
切换模型 / 重新索引
如果你更换了嵌入模型(例如切换 provider),Joplin 会清空索引并重建——不同模型的向量指纹不可比,干净重建是唯一安全的选择。索引器状态面板会实时展示重建过程。
源码中的handleModelChange(见 EmbeddingIndexer.ts)正是这一行为的实现:将当前活动 provider 的modelId与设置项ai.embedding.lastIndexedModelId比对,不一致时依次执行:
NoteEmbedding.clearAll()清空全部向量数据;- 重置变更游标
ai.embedding.lastProcessedChangeId为 0; - 写入新的
ai.embedding.lastIndexedModelId; - 将
ai.embedding.initialScanDone置为false,触发新一轮全量扫描。
平台支持
语义搜索要求Joplin >= v3.7,且并非所有平台都支持:
| 平台 | 嵌入是否可用 |
|---|---|
| macOS(Apple Silicon) | 是 |
| macOS(Intel) | 否——底层运行时未随该架构发布;AI 聊天仍可用 |
| Windows(x64、ARM64) | 是 |
| Linux(x64、ARM64) | 是 |
| 移动端、CLI | 否——语义搜索仅运行在桌面应用 |
在不支持嵌入的平台,索引器会保持暂停状态(vector-search-unavailable),任何依赖它的插件或 MCP 工具都会显示明确错误,而不会静默返回空结果。从源码看,这与runInBackground中"sqlite-vec 扩展未加载则拒绝启动索引器"的检查一致(见 EmbeddingIndexer.ts)。
关闭索引器
你可以保留 AI 聊天功能、仅关闭索引器:在 Settings → AI 中取消勾选 Enable the embeddings indexer即可。此时:
- 模型文件仍然保留在本地(不会卸载);
- 不再进行任何新的索引;
- 已有索引数据仍然留在磁盘上——若想彻底删除,需手动清除 AI 配置文件数据。
对应到源码,ai.embedding.enabled为false时,statusFor会把索引器状态标记为index-disabled,后台 tick 也不会再执行嵌入与落库操作。
延伸阅读
- AI 配置与聊天面板、AI 聊天面板使用说明:了解同一套 AI 基础设施下的聊天功能;
- MCP 服务器:外部 AI 应用通过
semantic_search_notes访问语义搜索的完整指南; - 常规搜索说明:与语义搜索互补的全文关键词搜索;
- 配置界面:AI 分区的所有设置项。
【免费下载链接】joplinJoplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.项目地址: https://gitcode.com/GitHub_Trending/jo/joplin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考