semantic-search-mcp
semantic-search-mcp
一个轻量、自包含的 RAG-lite 检索引擎:它索引磁盘上的文件,并回答“什么内容与这个查询语义相关”——仅此而已。它不调用 LLM,也不生成答案。它返回最相关的文本块(文件、行号、分数),以便任何消费方——人类、脚本或通过 MCP 的 LLM——自行决定如何处理这些内容。
首次运行后,一切都在本地离线运行:
嵌入模型:
@huggingface/transformers运行Xenova/all-MiniLM-L6-v2,使用 int8 量化权重在 CPU 上执行。无需 GPU、无需 API 密钥、查询时无需网络调用。向量存储:
@lancedb/lancedb——一个嵌入式、基于文件的向量数据库。无需服务器进程,无需 Docker。接口:一个 CLI 和一个 stdio MCP 服务器,因此任何支持 MCP 的代理(Claude Code、Cursor、Zed 等)都可以直接搜索你的语料库。
快速开始
npm install -g @adborroto/semantic-search-mcp
semantic-search add ~/code/my-project # add a folder to the corpus
semantic-search index # embed it (incremental on later runs)
semantic-search search "how does the retry logic work"这就是全部设置。无需手动编写配置文件——add 命令会为你创建并管理它。要无需安装即可试用:
npx @adborroto/semantic-search-mcp add ~/code/my-project安装大小提醒:约 950MB 的依赖项,加上首次使用时下载的约 25MB 嵌入模型。其中几乎都是原生二进制文件,在这一层无法避免——
@lancedb/lancedb(约 430MB,包含其平台二进制文件)和 ONNX 运行时(约 300MB,在一个包中为每个平台提供构建版本)。两者都只缓存一次;首次运行后所有操作都在离线进行。
Related MCP server: rag-retriever-mcp
系统要求
Node.js >= 22(
node:sqlite从 22 版本开始才稳定,用于回退后端)。约 950MB 磁盘空间用于依赖项,约 25MB 用于嵌入模型,加上每个索引块约 1–3 KB。
无需 GPU、无需外部服务、无需数据库服务器。
为什么是“RAG-lite”
完整的 RAG 流水线是:检索块 → 将其输入 LLM → LLM 编写答案。本项目止步于第一步。这保持了简单、快速、运行成本低且易于推理——并且它能与你正在使用的任何 LLM 或代理框架干净地组合,而不是捆绑自己带有偏见的生成层。
管理语料库
semantic-search add ~/code/api ~/notes # add one or more folders
semantic-search list # show what's configured
semantic-search remove api # by folder name...
semantic-search remove ~/notes # ...or by path
semantic-search config # where config + index actually liveadd 验证每个路径是否为真实目录,将其解析为绝对路径,并跳过重复项(包括通过符号链接到达的同一目录)。remove 还会从索引中清除该文件夹的块,使其内容不再出现在结果中——如果希望将其从语料库中移除但保留可搜索性,请传递 --keep-index。
存储位置
配置和索引遵循 XDG 基础目录规范,因此它们能在升级后保留,并被所有安装方法共享:
内容 | 位置 |
配置 |
|
索引 + 模型缓存 |
|
可以通过 SS_CONFIG_PATH、SS_INDEX_DIR、SS_MODEL_CACHE_DIR 或标准的 XDG_CONFIG_HOME / XDG_DATA_HOME 覆盖任何一项。SS_STORE_BACKEND=sqlite 强制使用回退后端。
索引包含你索引的所有内容的逐字文本。 如果你将其指向私有代码,
~/.local/share/semantic-search/会以纯文本形式保存这些内容。切勿提交它,也不要将其附加到错误报告中。
每个选项都在 src/config.js 中有文档说明——块大小、忽略模式、模型名称、top-k、并发数。直接编辑 config.json 仍然适用于这些选项;add/remove 会保留它们不拥有的任何键。
用法
索引
semantic-search index # all configured folders
semantic-search index ~/code/one-project # just this folder, ignoring config
semantic-search index --force # reprocess everything索引是增量式的:未更改的文件根据修改时间跳过,内容实际未更改(仅被触碰)的文件跳过重新嵌入,从磁盘删除的文件会从索引中清除。只有实际更改的内容才会被重新处理。
配置了多个文件夹后,index 会按顺序遍历它们,并显示每个文件夹的标题和总计:
[1/3] my-api /home/me/code/my-api ─────────────────────────────
↺ indexed src/auth/middleware.js (8 chunks)
2 indexed 1,203 skipped 16 chunks 4.1s
[2/3] my-app /home/me/code/my-app ─────────────────────────────
...
──────────────────────────────────────────────────────────────
total 5 indexed 3,891 skipped 0 deleted 41 chunks 12.3s每次 index <path> 调用仅清除该路径下文件的过期条目,因此索引文件夹 B 永远不会触及文件夹 A 的条目。
有用的标志:--max-files <n> 在 N 个新文件后停止(限制大型语料库的内存),--concurrency <n> 设置并行度,--verbose 将每个文件记录到 stderr。
搜索
semantic-search search "how does the retry logic work" -k 5打印一个包含文件路径、行号、分数和文本预览的表格。
检索是混合式的:查询被发送到两个独立的臂——一个是对嵌入的向量搜索,另一个是对相同块的 BM25 全文搜索——两个排名通过倒数排名融合进行融合。这两个臂的失败方式不同:向量臂会遗漏它没有语义句柄的精确标识符、错误字符串和配置键;词汇臂会遗漏同义改写。同时运行两者是一种召回率修复,而基于排名而非分数进行融合可以防止无界的 BM25 分数淹没余弦相似度。
在 config.json 中设置 "hybridSearch": false 以仅使用向量检索,设置 "rrfK" 以调整 RRF 的排名平滑常数(默认 60,来自论文)。
索引哪些内容
将其指向一个文件夹,其中的所有内容都会被递归索引。没有“支持”的文件扩展名白名单——.dart、.kt、.java、.tsx、.sql、.erb 以及任何其他文本文件都会按原样索引,.pdf 和 .docx 会先通过解析器处理。
有四类内容被排除:
git 忽略的任何内容,如果该文件夹是一个 git 仓库。
.gitignore在任何深度都会被遵循,以及.git/info/exclude、你的全局排除文件和否定模式(!keep.this)。这委托给git ls-files而不是重新实现,因此它与 git 完全匹配——这意味着你的项目已经忽略的生成和供应商输出不会进入索引,而无需你维护第二个列表。你的
.indexignore规则(见下文),用于已提交但不应可搜索的内容——测试夹具、快照、已检入的密钥模板。二进制文件,按扩展名(图像、归档文件、字体、编译对象、模型权重)和内容判断——前 4KB 中的 NUL 字节表示二进制,与
grep -I使用的启发式相同。这是一种安全措施,用于防止非文本字节进入分词器,而不是对什么值得索引的判断。超过 500,000 字节的文件(
maxFileSizeBytes),这是防止生成的单行兆字节文件耗尽内存的主要防护。
符号链接会被跳过而不是跟随,因此文件夹内的链接无法将外部内容拉入索引。
对于不是 git 仓库的文件夹,没有 .gitignore 可以依赖,因此一个小的内置列表(node_modules/、.git/、dist/、build/、coverage/、vendor/ 等)仍然适用。
要排除更多内容,请在以下任一位置放置一个 gitignore 风格的 .indexignore:
在你索引的文件夹内——模式相对于该文件夹;
在你的配置旁边(
~/.config/semantic-search/.indexignore)——适用于所有地方。
参见 .indexignore.example 获取涵盖 iOS、Android、Flutter、Ruby 和 JVM 构建工件的起点。
MCP 服务器
semantic-search mcp启动一个 stdio MCP 服务器,暴露六个工具。
search(query, k?) — 语义搜索,返回原始 JSON:
[{ filePath, text, score, offset, startLine }, ...]gather(query, k?, contextLines?) — 相同的搜索,返回为单个格式化的 Markdown 块,可直接放入上下文窗口:
### [1/5] my-api · src/auth/session.js · line 42 · score 0.923
```
...chunk text...
```contextLines(默认 0)从源文件中读取每个块周围的 N 行额外内容——当块边界切断了你需要的上下文时很有用。
list_folders() — 每个配置的文件夹及其名称和绝对路径。这是一个很好的首次调用,以便代理知道存在哪些语料库。
cat_file(filePath, startLine?, endLine?) — 通过绝对路径读取文件,如 search/gather 返回的那样。限制在配置的文件夹内(参见安全性)。
grep(pattern, folder?, fileGlob?, caseSensitive?, maxResults?) — 在语料库中进行字面或正则表达式搜索,适用于需要精确匹配而非相似性的情况。根据索引器会索引的完全相同文件列表进行过滤,因此 git 忽略和 .indexignore 的文件无法通过精确匹配搜索泄露。
my-api · src/auth/session.js:42 export function createSession(user) {index(root?, force?, maxFiles?, concurrency?) — 触发增量重新索引,以便代理无需通过 shell 命令即可刷新语料库。
所有搜索工具共享与 CLI 相同的排名和文件解析代码;两者都没有重新实现。
注册到 MCP 客户端
Claude Code:
claude mcp add --scope user semantic-search -- semantic-search mcp
claude mcp list # should show "✔ Connected"任何接受 JSON 服务器定义的客户端:
{
"mcpServers": {
"semantic-search": {
"command": "semantic-search",
"args": ["mcp"]
}
}
}这里优先使用全局安装而不是 npx:裸 npx 每次服务器启动时都会重新解析包,增加启动延迟并静默获取升级。如果使用 npx,请固定版本——npx -y @adborroto/semantic-search-mcp@0.1.0 mcp。
新的 MCP 服务器通常只在会话启动时被拾取,因此注册后请启动一个新会话。
安全性
这是一个本地、单用户工具,具有简单的信任模型:配置文件夹内的任何内容都可以被任何能够访问服务器的 MCP 客户端读取。
cat_file拒绝配置文件夹之外的路径,首先解析符号链接,因此文件夹内的链接无法用于逃逸。grep根据索引器构建的相同文件列表进行过滤——git 的忽略规则加上你的.indexignore——因此故意排除在索引之外的文件不会通过精确匹配搜索泄露。子进程使用 argv 数组生成(从不使用 shell),因此模式无法注入命令。
鉴于此,不要将其指向你不会交给 LLM 提供商的语料库——块会返回给任何请求它们的客户端。参见 SECURITY.md。
工作原理
文件发现
规则是“索引文件夹下的所有内容”,唯一有趣的部分是不索引什么。与其重新实现 git 的忽略语义——嵌套的 .gitignore 文件、否定模式、info/exclude、全局排除文件——git 根目录通过以下方式枚举:
git ls-files -z --cached --others --exclude-standard跟踪文件加上未跟踪但未被忽略的文件,限定在其运行的目录内。git 忽略的任何内容自然不存在。非 git 文件夹回退到带有内置模式列表的简单递归遍历。
相同的函数支持索引器和 MCP grep 工具(src/ignoreRules.js)。这是有意为之:grep 通过 shell 调用真正的 grep -r,它会愉快地报告 git 忽略的构建输出中的命中,因此它根据索引器自己的文件列表过滤结果。如果两者分别推导规则,它们会漂移,忽略列表将不再是边界。
混合检索
查询通过两个臂并行运行:
向量——嵌入查询,按余弦距离取最近邻,然后通过一个小的词汇提升重新排序该列表,针对包含字面查询词的块。
词汇——对相同块文本进行 BM25 搜索,通过 LanceDB 全文索引(
sqlite回退在 JS 中计算 BM25,因为node:sqlite不能保证附带 FTS5)。
两个排名通过 RRF(倒数排名融合)融合:每个列表对其返回的每个块贡献 1 / (60 + rank),然后将贡献求和。基于排名而非分数进行融合是关键——余弦相似度处于 [-1, 1] 区间,而 BM25 上界无限制,因此直接相加或平均原始分数会让某一分支因语料库规模而悄悄压倒另一分支。
为什么需要两个分支:对向量分支输出施加的词汇增强只能重新排序向量查询已经返回的结果。如果一个块仅有的信号是精确的术语匹配——例如错误码、符号名、没有语义邻域的配置键——那么它落在向量池之外时是无法被检索到的。词汇分支独立地检索它。这是召回修复,而非重排序修复,也正因如此,搜索分数现在看起来像 0.03 而不是 0.9:它们是 RRF 求和值,而非余弦相似度。只有它们的排序才有意义。
全文索引在每个索引运行结束时重建,因为 FTS 索引不覆盖构建后添加的行——否则,刚刚写入的块对词汇分支来说将是不可见的。
分块
文本按段落分割,然后贪婪地打包成约 200 个 token、~35 个 token 重叠的块,使用嵌入模型的真实分词器计数,而非字符数近似值。这并非随意决定:all-MiniLM-L6-v2 有 256 个 token 的窗口,超出的部分会静默截断,因此块的大小被设计为能容纳于此窗口内,并为 [CLS]/[SEP] token 留出边距。重叠还额外受限于:重叠加上下一个段落绝不能超过该限制——否则,块的尾部会在嵌入时被丢弃,但 search 仍会返回它。
单个段落超过硬限制(例如压缩后的代码包、一条巨大的日志行)则会回退到按词打包,采用相同的重叠逻辑,并且任何超过 500 字符的单个“词”会先被切片,这样没有任何大块会一次性送入分词器。
Token 计数每个段落/词只计算一次并缓存,以便在重叠计算中重复使用。早期版本在每次重叠查找时都会重新分词,这在小型输入上没问题,但在大型仓库上会导致 CPU 飙升和数 GB 的内存增长。如果你扩展分块器,请保留这一特性。
增量重新索引
没有单独的清单——向量存储本身就是清单。每个存储的块都携带其源文件的 mtimeMs 和 sha256 内容哈希。每次运行时:
如果文件在磁盘上的
mtime与存储的匹配,则跳过该文件,不读取内容。如果
mtime发生变化但内容哈希相同(例如touch操作),则跳过重新嵌入。否则,删除该文件的旧块并插入新嵌入的块。
列表完成后,任何已索引但不再存在于磁盘上的路径(且位于正在索引的根目录下)都会被清理。
存储后端
默认是 LanceDB:嵌入式、文件支持、真正的向量搜索。一个 node:sqlite + 暴力余弦回退方案(src/store/sqliteFallbackStore.js)实现了相同的接口(src/store/vectorStore.js),适用于 LanceDB 原生绑定无法加载的环境——例如沙箱容器、不常见的架构。通过 SS_STORE_BACKEND=sqlite 切换。
回退方案每次搜索都会进行全表扫描:对于数万个块来说尚可,但无法应对更多。LanceDB 的默认度量是 L2 而非余弦,因此本项目在每次查询时都会显式设置 .distanceType('cosine'),因为嵌入向量是按归一化向量进行比较的。
项目结构
src/
config.js Defaults + config file resolution (XDG) — the only source of tunables
configFile.js Read/modify/write the config file (backs add/remove/list)
embeddings.js transformers.js pipeline + tokenizer (lazy singletons)
chunker.js Token-aware paragraph packing with overlap
ignoreRules.js What is indexable: git ignore rules + .indexignore + binary filter,
shared by the indexer and grep so they can't drift apart
safePath.js Path confinement for the MCP file-reading tools
version.js Version read from package.json
extractors/ text (anything not binary), pdf (pdf-parse), docx (mammoth)
store/
vectorStore.js Storage interface + backend selector
lancedbStore.js LanceDB implementation (default)
sqliteFallbackStore.js node:sqlite + manual cosine fallback
indexer.js List + extract + chunk + embed + incremental upsert/prune
search.js Hybrid retrieval: vector + BM25 arms fused with RRF — shared by CLI and MCP
mcp-server.js MCP stdio server: the six tools above
index.js CLI entrypoint (commander)
scripts/index-all.sh Batched indexing for very large corpora on constrained hosts (Linux)开发
git clone https://github.com/adborroto/semantic-search-mcp.git
cd semantic-search-mcp
npm install
npm test # unit + end-to-end (node:test, no framework)
npm run test:unit # skip the slow end-to-end test
npm run lint在检出根目录下的 config.json 优先级高于 XDG 位置,因此你可以针对一个测试语料库进行开发,而无需触碰你的真实配置。测试总是写入临时目录。参见 CONTRIBUTING.md。
不在范围内(设计上如此)
答案生成。 本工具返回块,而非答案。请自行将块输入给 LLM。
使用第二个模型进行重排序。 混合检索加 RRF 无需额外依赖,且能达到大部分效果——但它不是交叉编码器重排序器。
Web 界面。 仅限 CLI 和 MCP。
大规模语料库。 为个人或团队规模的文档与代码语料库而构建——数万个块,而非数百万。两个后端均假设这个规模。
许可证
This server cannot be deployed
Maintenance
Related MCP Connectors
Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.
Agentic search over your Dewey document collections from any MCP-compatible client.
Hosted MCP memory: save sessions/decisions once, search from Claude, Cursor, ChatGPT. EU-hosted FTS.
Related MCP Servers
- AlicenseAqualityDmaintenanceLocal-first RAG indexing and semantic search MCP server. Enables document retrieval and context-aware queries using local embedding models.313 npmMIT
- FlicenseAqualityDmaintenanceA local-first document retrieval engine that mounts as an MCP tool for agents to index files, search for relevant passages, and let the agent's own LLM answer.4-

devitway-rag-starterofficial
FlicenseNot gradedqualityDmaintenanceMinimal local RAG stack with an MCP server that provides document search for any agent.1-- AlicenseAqualityBmaintenanceIndexes local Markdown/text files into a SQLite database with vector embeddings and provides MCP tools for semantic search without cloud dependencies.3AGPL 3.0