内容
# 亚历山德里亚语义检索
**适用于 [alexandria](https://github.com/kengo006/alexandria) 的本地语义检索实现 —— 参考配方在 [可选集成 §2](https://github.com/kengo006/alexandria/blob/main/optional-integrations.md) 中的可运行实现。**
如果您想在 Obsidian 保险库上进行通用语义搜索,请使用 [Smart Connections](https://smartconnections.app/) —— 它已经成熟,无需设置,专门为此而构建。本项目用于更狭窄的任务:为 **引用完整性工作流** 提供服务,允许语义搜索 *找到* 段落,但永远不会 *引用* 它们。 它返回的每个片段都带有文件路径和页码,以便下游代理(或您)可以回溯到源 PDF 并验证。仅限回忆,按设计和合同。
- **100% 本地**: 通过 [fastembed](https://github.com/qdrant/fastembed) (无 PyTorch)进行 ONNX 嵌入,磁盘上的向量存储在 [LanceDB](https://github.com/lancedb/lancedb) 中。 无需 API 调用,无需成本,可离线工作。
- **跨语言**: 默认的多语言模型允许一种语言的查询回忆另一种语言的段落 —— 这是关键词 grep 永远无法做到的。
- **硬件要求不高**: 上游生产实例在 16 GB 笔记本电脑上索引了来自 500 多个源文本的 45 万个分块,纯 CPU;增量更新大约需要 90 秒。
- **MCP,而不仅仅是 CLI**: 只读子代理通常没有 shell。 作为 MCP 服务器,这将 `semantic_search` 直接附加到它们的工具链中。
## 合同
本项目实现了 alexandria 文档中为其 Searcher 角色定义的接口:
```
search(query, k) -> [{file, page_start, page_end, score, text}, ...]
```
- 片段是 **指针**:`file` 相对于您的语料库根目录,页面映射回源 PDF。
- **仅限回忆**:片段永远不是引用源。 引语、页码和强调标记均需与 PDF 核实。
- **优雅降级**:代理将语义回忆视为奖励;grep 仍然是最后的保障(过时的索引意味着 *语义沉默从不证明不存在*)。
## 快速入门(5 分钟,包含合成示例)
需要 [uv](https://docs.astral.sh/uv/) 和 Python ≥ 3.11。
```bash
git clone https://github.com/kengo006/alexandria-semantic-recall
cd alexandria-semantic-recall
# 1) 在捆绑的合成语料库上构建索引(首次运行下载模型,约 0.22 GB)
uv run python build_index.py "examples/corpus"
# 2) 从 CLI 查询它 - 注意转述匹配:不需要关键词重叠
uv run python search_core.py "can spoken testimony be trusted as evidence" -k 3
# 3) 启动 MCP 服务器(stdio)
uv run python server.py
```
## 您自己的语料库
输入是一个 **文本层**:一个 `.txt` 文件的目录树,每个源 PDF 一个文件,页面边界以两种格式之一保留:
- 原生数字提取(例如 `pdftotext -layout`):表单馈送 `\f` 页面分隔符
- 扫描/OCR 源:显式标记 `===== page N =====`
如何生成文本层由您决定 — alexandria 的可选集成 §1 描述了约定。 然后:
```bash
set CORPUS_ROOT=C:\path\to\your\text-layer # 或在 unix 上导出;或传递路径作为参数
uv run python build_index.py # numpy 索引(index/)
uv run python migrate_to_lance.py # -> LanceDB(lance_db/),服务后端
uv run python build_lance_index.py # 用于快速查询的 ANN 索引
```
或者将这三个步骤合并为一个受保护的单元:
```bash
uv run python rebuild.py
```
`rebuild.py` 的存在是因为发生过一次真实事件:服务器更喜欢 `lance_db/`,因此仅刷新 numpy 索引会静默地提供过时数据。 这三个步骤被绑定在一起,并在最后进行验证。
### 保持新鲜
在添加、更改、移动或删除语料库文件后:
```bash
uv run python incremental_update.py --dry-run # 先查看增量
uv run python incremental_update.py # 仅嵌入更改的内容(~90 秒 vs. 数小时完整重建)
```
不是实时的 —— 在摄取后作为维护习惯运行。 引用层是第二道安全网:无论索引状态如何,引用都需与 PDF 核实。
## 将其连接到代理
在您的 MCP 配置(`.mcp.json` 或客户端等效项)中注册服务器:
```json
{
"mcpServers": {
"semantic-search": {
"command": "uv",
"args": ["run", "--project", "/path/to/alexandria-semantic-recall",
"python", "/path/to/alexandria-semantic-recall/server.py"]
}
}
}
```
然后给您的搜索代理一个指令 —— 这是使工具安全使用的纪律:
> 使用 `semantic_search` *发现* 候选段落,然后返回源 PDF(通过返回的文件 + 页面)并阅读其中的逐字文本。 片段定位;只有源代码才具有发言权。
alexandria 的 Searcher 角色已附带此连接,详见(搜索路径 D)。
## 设计笔记
- **分块大小以 token 为单位(≤120,对齐句子),使用嵌入模型的自有 tokenizer** —— 而不是字符。 字符↔token 的比率在不同语言之间变化约 3 倍(拉丁散文 ≈3.9 字符/token,CJK ≈1.4),因此 v0.1 的 500 字符窗口在 CJK 文本上会默默地溢出模型的 128 token 截断: **超过一半的 CJK 分块从未进入索引**,而西方文本仅丢失约 3% —— 除非跨语言测试,否则不可见,而这正是该工具的亮点。 测量:嵌入完整分块与仅其前 128 个 token 返回余弦 1.0000。 尺寸下限也在 token 中(字符下限过滤掉 CJK 分块)。 分块保持较小,因为消费者需要精确的指针,而不是摘要;页码是根据字符偏移量相对于页面标记计算的。 当无法使用 `tokenizers` 包时,字符分块仅作为备用。
- **默认模型** `paraphrase-multilingual-MiniLM-L12-v2`(0.22 GB,384 维,CPU):真正跨语言。 在对较大 8192 token 模型的 upstream 对比中,recall@5 持平 —— 小模型保持不变,因为它大约快 1.8 倍,矢量尺寸减半,在 16 GB RAM 上安全。 如果您的需求不同,可以通过 `--model` 切换;上下文更长的模型需要更小的嵌入批次。
- **嵌入批次默认为 32**:在 16 GB 机器上测量,更大的批次与长序列模型可以使 RAM 激增到上限。 仅在监控的情况下才调高。
- **fastembed 版本固定**:未来版本中的 silent pooling 更改将使新查询向量与现有索引不一致。 故意取消固定,并在取消固定时重建(加载时,您可能会看到 fastembed 自己的警告,即此模型“现在使用平均池化” —— 它是信息性的;固定保持查询端和索引端的池化一致,这才是重要的。)
- **双后端**:LanceDB 提供服务(磁盘上的文本 + 矢量,top-k 读取,驻留 RAM 大致恒定 —— 迁移保存的约 640 MB,包含 410k 分块);numpy 仍然是一个备用(删除 `lance_db/` 以备用)。 `search(query,k)` API 在两个后端上都相同。
- **增量 ID** 从 500,000,000 开始,因此它们永远不会与完整构建的 ID 冲突;查询端永远不会读取 ID。
## 状态
从一个生产系统(2026 年)中提取,该系统为一个五角色学术写作工作流提供服务 —— 有关它所连接到的架构,请参见 [alexandria](https://github.com/kengo006/alexandria)。 欢迎提出问题和适应。 [MIT](LICENSE)。