# 7-54 RAG-Anything 对照后的多模态检索设计 v1

> 创建时间：2026-06-08
>
> 当前状态：`process`
>
> Owner：07-ai / 03-rust-web / knowledge-rag / search
>
> 取代：`7-53` 已放弃，不再作为搜索方案依据。
>
> 参考代码：
> - `reference-code/RAG-Anything`
> - repo：`https://github.com/HKUDS/RAG-Anything`
> - commit：`1bbca28`
> - CodeGraph：`codegraph init && codegraph index . --force --quiet`，49 files / 922 nodes / 2308 edges
> - 关键文件：`raganything/query.py`、`raganything/processor.py`、`raganything/utils.py`、`raganything/config.py`、`README.md`、`docs/multimodal_rag_failure_modes.md`

## 1. 新结论

7-53 的问题是仍在围绕“普通搜索是否混入 OCR”打转，设计重心不对。

RAG-Anything 的检索设计说明：成熟的 PDF / DOCX / Office / 图片检索，不应该依赖 MNote 自己扫 OCR sidecar 做字符串匹配，而应该把多模态内容作为一等内容写进 RAG 索引：

```text
PDF / DOCX / Office / Image
  -> parser content_list
  -> text content -> LightRAG text insert
  -> image/table/equation -> multimodal processor
  -> templated multimodal chunks
  -> LightRAG text_chunks + chunks_vdb + entities_vdb + graph
  -> query uses vector + graph retrieval
```

所以 MNote 的目标应改为：

1. 默认普通搜索仍可保持轻量本地搜索。
2. 必须有明确的“资料库检索 / 全盘资料库检索”入口，覆盖用户显式纳入资料库索引范围的 PDF、DOCX、Office、图片 OCR。
3. 全盘资料库检索必须走 LightRAG 的 chunk/reference 检索，不再走 MNote 侧 OCR sidecar 字符串补结果。
4. 多模态资料的 ingest 要尽量变成 RAG-Anything 式的结构化 chunk / entity / relation，而不是只生成 wrapper Markdown。
5. 搜索结果必须是可打开的来源命中；问答答案是另一层能力。
6. “全盘”只表示在已配置资料库索引目录内全盘检索，不表示自动索引整个 local folder。

## 2. RAG-Anything 是怎么做检索的

### 2.1 查询入口分三种

`raganything/query.py` 提供三类 query。

第一类是纯文本查询：

```python
query_param = QueryParam(mode=mode, **kwargs)
result = await self.lightrag.aquery(query, param=query_param, system_prompt=system_prompt)
```

它直接复用 LightRAG 的 `local/global/hybrid/naive/mix/bypass` 模式。README 推荐示例里人工检索常用 `mode="hybrid"`。

第二类是 VLM enhanced query：

```python
query_param = QueryParam(mode=mode, only_need_prompt=True, **kwargs)
raw_prompt = await self.lightrag.aquery(query, param=query_param)
```

它先只拿 LightRAG 检索出来的 prompt/context，不直接生成最终答案；然后从检索上下文中提取 `Image Path:`，做安全目录校验，编码图片为 base64，再把文本 context + 图片一起交给 VLM 回答。

第三类是显式 multimodal query：

```python
enhanced_query = await self._process_multimodal_query_content(query, multimodal_content)
result = await self.aquery(enhanced_query, mode=mode, system_prompt=system_prompt, **kwargs)
```

用户把表格、公式、图片等内容作为 query 附件传入时，它先用对应 processor 生成描述，再把描述拼回查询文本，最后仍走 LightRAG 检索。

### 2.2 ingest 不是 OCR sidecar，而是一等 chunk

`raganything/utils.py` 先把 parser 的 `content_list` 分成：

```text
text_content
multimodal_items
```

文本走 LightRAG 原生 `ainsert(...)`，并传 `file_paths` 用于引用。

多模态内容走 `raganything/processor.py`：

1. 图片、表格、公式先用对应 processor 生成增强描述。
2. `_apply_chunk_template(...)` 把原始路径、caption、footnote、邻近文本、章节路径、表格 body、公式文本、增强描述拼成可检索 chunk。
3. `_convert_to_lightrag_chunks_type_aware(...)` 生成 LightRAG 标准 chunk：

```python
{
  "content": formatted_chunk_content,
  "tokens": tokens,
  "full_doc_id": doc_id,
  "chunk_order_index": chunk_order_index,
  "file_path": file_ref,
  "is_multimodal": True,
  "modal_entity_name": entity_info["entity_name"],
  "original_type": data["content_type"],
  "page_idx": data["item_info"].get("page_idx", 0),
}
```

4. chunk 同时写入：

```text
lightrag.text_chunks
lightrag.chunks_vdb
```

5. 多模态主实体写入：

```text
knowledge graph
entities_vdb
full_entities
```

6. 对多模态 chunk 调 LightRAG `extract_entities(...)`，再追加 `belongs_to` 关系，最后调用 `merge_nodes_and_edges(...)` 合并回 graph。
7. `doc_status.chunks_list/chunks_count` 追加多模态 chunk id。

这才是“全盘资料库可检索”的关键：图片、表格、公式不是旁路 OCR 文件，而是进入 LightRAG 的 chunk 向量库和图谱。

### 2.3 检索失败排查也围绕多模态索引

RAG-Anything 的 failure checklist 不是让 UI 再补一层字符串搜索，而是检查：

- parser Markdown / `content_list.json` 是否可信。
- table block 的 `table_body` 是否保留结构。
- image path / caption / page index 是否对齐。
- multimodal processing flags 是否启用。
- embedding path 是否看到 enriched text，而不是只看到 raw OCR。

这对 MNote 很重要：搜索不准时，第一诊断点应该是“资料有没有以正确 chunk 进入 LightRAG”，不是前端结果列表怎么融合。

## 3. MNote 当前差距

当前 MNote 已经有：

- `/api/knowledge-rag/ingest`
- `/api/knowledge-rag/query`
- source registry
- LightRAG `/documents/scan`
- `/query/data` + references 映射
- citationUrl / citationMarkdown / locator enrich

但普通搜索仍有一条不成熟路径：

```text
/api/search/documents
  -> local_search_index
  -> includeOcr 时追加 query_knowledge_rag_ocr_sidecar_results
```

这条 `knowledge_rag_ocr_sidecar_results` 是 MNote 遍历 registry sidecar 后做文本匹配，不是 LightRAG vector/graph retrieval。它会让搜索看起来“用了 OCR”，但排序、召回、语义、图谱关系和多模态 chunk 都不是一套成熟系统。

另一个差距是 ingest。MNote 当前对 LightRAG scan 支持文件直接 symlink；对非 scan 支持文件生成 Markdown wrapper：

```text
# file
![file](<asset>)
```

这对图片入口有帮助，但不等于 RAG-Anything 的 multimodal chunk / entity / relation 管线。后者会把图片、表格、公式的描述、上下文、页码、路径都写进 LightRAG chunk 和 graph。

另一个必须保留的边界是索引范围。MNote 是 local-folder 产品，不能默认把整个本地目录都纳入 LightRAG。用户需要人工控制哪些目录参与资料库检索，否则会带来：

- 大目录首次索引成本不可控。
- 隐私和权限边界不清。
- 搜索噪音扩大，资料库结果质量下降。
- 删除/移动/重建的 watcher 压力过高。

因此资料库检索依赖显式索引目录，而不是自动覆盖整个 workspace。

## 4. 新搜索边界

### 4.1 默认搜索

默认搜索只做快速定位：

- Markdown 页面标题
- Markdown 正文
- 路径
- 最近打开 / 最近修改
- 本地轻索引资源元信息

默认不混入 OCR sidecar，也不混入 LightRAG 语义结果。

### 4.2 全盘资料库检索

搜索弹窗必须有明确入口：

```text
范围：当前页 / 工作区 / 全盘资料库
```

选择 `全盘资料库` 后：

- 调 LightRAG retrieval，而不是 MNote sidecar grep。
- 覆盖已纳入资料库索引目录的 PDF、DOCX、Office、图片 OCR、表格、公式、Markdown。
- 返回 references/chunks 作为搜索结果。
- 不默认生成 answer。
- 每条结果有来源、snippet、类型、页码 / bbox / block / resource fallback。
- 结果能直接打开对应 resource tab。
- 对未入库目录明确显示“未纳入资料库索引”，而不是让用户误以为 LightRAG 漏检。

建议协议：

```text
POST /api/knowledge-rag/search
```

请求：

```json
{
  "workspaceId": "...",
  "rootUri": "file:///...",
  "query": "压缩率 Canterbury corpus",
  "mode": "hybrid",
  "topK": 12,
  "chunkTopK": 24,
  "includeChunkContent": true,
  "includeReferences": true,
  "answer": false
}
```

实现上可以先复用 `/api/knowledge-rag/query` 的 `/query/data` 调用，只是响应包装成 search results，不展示 answer。

### 4.2.1 资料库索引范围

MNote 需要把“全盘资料库检索”建立在显式索引范围上：

```text
Knowledge Search Scope
  includeRoots:
    - docs/research
    - resources/papers
  excludePatterns:
    - "**/draft-private/**"
    - "**/.git/**"
    - "**/node_modules/**"
  runOnChange: true|false
```

产品语义：

- `includeRoots` 是用户主动加入资料库索引的目录或文件。
- `excludePatterns` 后期必须支持，用于排除私密目录、临时目录、构建产物、大型无关资产。
- LightRAG 只保证检索这些已入库范围。
- 普通本地搜索仍可搜索 Markdown / 本地轻索引，不要求 LightRAG 入库。
- 如果用户在全盘资料库模式下搜不到某文件，UI 要能提示它是否未入库、入库中、失败、已排除或已删除。

实现上，source registry 需要从“单个 source 列表”升级为“索引范围 + source 状态”：

```text
.mnote/index/lightrag-source-registry.json
  indexedRoots[]
    rootRelativePath
    recursive
    excludes[]
    runOnChange
    updatedAt
  entries[]
    sourceRootRelativePath
    sourceHash
    lightRagDocId
    chunkIds
    status
    excludedBy
```

这保留了当前“人工控制哪些需要全盘搜索”的价值，同时给后续自动 watcher / 增量重建留出边界。

### 4.3 Knowledge Ask

Knowledge Ask / Page AI 才生成答案：

- 用同一套 LightRAG references。
- 展示 `Context used`。
- 引用使用 `citationMarkdown` / `citationUrl`。
- 可选 VLM enhanced：先拿 retrieved context，再把命中的图片资源交给 VLM 参与回答。

### 4.4 当前页搜索

当前页内查找仍不走 LightRAG：

- Markdown：editor / PageAggregate 内查找。
- PDF：viewer 文本层 / sidecar locator。
- Office：预览/解析层查找。

这是局部查找，不是全盘检索。

## 5. MNote 应参考 RAG-Anything 的实施方案

### Phase A：放弃 7-53，移除 sidecar 补结果主路径

目标：先停止把 sidecar grep 当成熟 OCR 搜索。

改动：

1. 默认搜索关闭 `includeOcr`。
2. `includeOcr` 不再调用 `query_knowledge_rag_ocr_sidecar_results` 作为普通结果补充。
3. UI 增加 `全盘资料库` scope，先复用 `/api/knowledge-rag/query` 的 references 返回。
4. 结果标注 `provider=lightrag`、`matchSource=lightrag_reference`。

验收：

- 默认搜索短词不出现 PDF/OCR 假阳性。
- 选择全盘资料库后可以搜到 PDF/DOCX/OCR 资料。
- 搜索结果能打开对应 resource tab。

### Phase B：新增 references-only retrieval facade

目标：把问答和人工检索分开。

新增：

```text
POST /api/knowledge-rag/search
```

内部：

- 调 LightRAG `/query/data`。
- `include_references=true`。
- `include_chunk_content=true`。
- `mode=hybrid` 或 `mix`，后续用 benchmark 决定默认值。
- 不把 answer 放入搜索结果。
- 对 references 做 registry mapping、locator enrich、ranking。

验收：

- API 返回 `mnote.knowledge_rag.search_results.v1`。
- 每条 result 有 `citationUrl` / `citationMarkdown` / `locatorPrecision`。
- 对无 locator 的命中明确降级到文件级打开。

### Phase C：RAG-Anything 式多模态 chunk 入库

目标：让图片、表格、公式、Office/PDF 结构进入 LightRAG retrieval，而不是只存在 sidecar。

设计：

1. MNote parser 输出统一 `content_list`：
   - `text`
   - `image`
   - `table`
   - `equation`
   - `generic`
2. text 走 LightRAG 文本 insert / scan。
3. multimodal items 生成 templated chunk：
   - source path
   - page index
   - block id / bbox
   - caption / footnote
   - table body
   - equation text / latex
   - neighbor text / section path
   - enhanced description
4. 写入 LightRAG chunk/vector/graph，或通过扩展 LightRAG/RAG-Anything adapter 实现。
5. registry 保存 `sourceHash -> lightRagDocId -> chunkIds -> locator map`。

验收：

- 查询“图 3 展示了什么”能召回图片 chunk，而不是只召回邻近正文。
- 查询表格中的指标能召回 table chunk，snippet 包含表格 body。
- query result 能通过 chunkId 映射回 MNote resource tab 的 page/bbox/block。

### Phase D：VLM enhanced Ask

目标：回答中真正看图，而不是只读图片 caption。

参考 RAG-Anything：

1. 先让 LightRAG 返回 retrieval prompt/context。
2. 从 context 中提取命中图片路径或 MNote resource id。
3. 做 allowed roots / resource permission 校验。
4. 把图片编码或转为可访问 asset URL。
5. 文本 context + image 一起交给 VLM。

这一步只用于 Ask，不用于普通搜索结果。

## 6. 关键约束

- 全盘资料库检索是必需能力，不能因为默认关闭 OCR 而消失。
- 默认搜索和全盘资料库检索必须是两个明确模式，不能暗中混合。
- 不再把 sidecar grep 当成熟资料库搜索。
- locator 不足时只能声明降级，不能伪造页码/bbox。
- `file_path` 不能作为唯一真相；MNote registry 必须维护绝对路径、root-relative path、source hash、LightRAG doc id、chunk id、locator map。
- 多模态 chunk 的 source path / image path 必须做权限和安全目录校验。

## 7. 立即建议

下一步不要继续修 7-53。

应直接做：

1. 删除/停用普通搜索里的 OCR sidecar 补结果。
2. 新增 `资料库索引范围` 设置，至少支持 include roots；excludes 先设计协议，后续实现。
3. 新增 `全盘资料库` 搜索 scope，只检索已入库范围，调用 LightRAG references。
4. 写 `7-54 Phase A/B` 的 smoke：默认搜索不混 OCR；未入库 PDF/DOCX 在资料库检索中明确提示未入库；加入索引目录后可搜到并打开。
5. 后续再做 RAG-Anything 式多模态 chunk 入库。

这样既保留 MNote 的核心价值，也把检索系统拉回成熟 RAG 项目的设计方向。

## 8. Phase A/B 实施记录

时间：2026-06-08

已完成：

1. 普通搜索不再把 `includeOcr=true` 转成 `query_knowledge_rag_ocr_sidecar_results` 补结果；`/api/search/documents` 的边界固定回 `ordinary_local_search`，`knowledgeRag=false`，`ocrSidecarFallback=false`。
2. 新增 `POST /api/knowledge-rag/search`，作为 references-only retrieval facade：
   - 调 LightRAG `/query/data`
   - `include_references=true`
   - 默认保留 `include_chunk_content=true`
   - 复用 registry mapping / locator enrich / citationUrl / citationMarkdown
   - 返回 `mnote.knowledge_rag.search_results.v1`
   - 搜索结果标记 `provider=lightrag`、`matchSource=lightrag_reference`
3. source registry 增加兼容字段 `indexedRoots[]`，ingest 显式 source 时记录 include root：
   - `rootRelativePath`
   - `recursive`
   - `excludePatterns`
   - `runOnChange`
   - `updatedAtMs`
   旧 registry JSON 缺少 `indexedRoots` 时默认空数组。
4. 搜索弹窗新增 `全盘资料库`开关；默认工作区搜索发送 `includeOcr=false`，打开开关后调用 `/api/knowledge-rag/search`。
5. `scripts/task538-knowledge-rag-source-scope-api-smoke.js` 增加 `/api/knowledge-rag/search` 验证，确认 sourcePaths post-filter 后只返回目标 source，且结果来自 LightRAG references 并可打开来源。

验证：

```text
node --check rust/crates/mnote-web/browser/sidebar-tree-runtime.js
node --check scripts/task538-knowledge-rag-source-scope-api-smoke.js
cargo check --manifest-path rust/Cargo.toml -p mnote-web
cargo test --manifest-path rust/Cargo.toml -p mnote-web search_documents_route_is_owned_by_mnote_web -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web search_documents_local_folder_does_not_promote_evidence_sqlite_body_hits -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web indexed_root -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web references_only_search_result -- --nocapture
node scripts/task125-rust-web-search-server-first-smoke.js
node scripts/task538-knowledge-rag-source-scope-api-smoke.js
Playwright 登录态验证：默认搜索 includeOcr=false；全盘资料库开关调用 /api/knowledge-rag/search
```

`scripts/task128-rust-web-wolai-search-modal-smoke.js` 未作为本次验收依据：当前默认登录态要求下它直接访问 `/` 等待 topbar，曾因未建立登录态而超时。已用登录态 Playwright 脚本覆盖本次新增搜索 scope 行为。

未完成：

- `excludePatterns` 仅进入 registry 协议，尚未实现 UI 编辑和 glob 过滤。
- RAG-Anything 式 multimodal chunks / entities / relations 入库仍是 Phase C，未标完成。

## 9. Phase B.1：chunk 上下文段落级定位

时间：2026-06-08

新增结论：

当前用户需要的是“搜索结果点击后落到对应段落”，不是必须文字级 bbox。对 DOCX / Office 这类当前缺少 page/bbox 的资料，短期不应强制改成 DOCX -> PDF viewer 管线；更低风险的路径是复用 LightRAG 已返回的 chunk 上下文做段落级定位。

参考 `rag-knowledge-system` 的可采用点：

- 检索结果必须保留稳定 chunk metadata。
- 点击定位不能只用 query 短词，应使用 chunk/snippet 上下文作为 anchor。
- 目录/文件夹 scope 是 retrieval 层的过滤条件，不是 UI 结果列表后处理。

不直接照搬的点：

- `rag-knowledge-system` 的 PDF highlight 先有 `page_num`，再在页内 `search_for(anchor)`；MNote 当前 DOCX sidecar 没有页码，所以只能先做到文档内段落级定位。
- 对 DOCX 缺 page/bbox 时，不能伪造页码或 bbox，应返回 `locatorPrecision=paragraph` / `locatorDegraded=true`。

设计：

```text
LightRAG /query/search
  -> chunk content / occurrence
  -> MNote registry mapping
  -> sidecar block match
  -> locator:
       blockId
       sourceMapPath
       evidenceText = query-centered block/chunk context
       locatorPrecision = bbox | paragraph | file
  -> office-preview:
       clean evidenceText
       generate multiple anchors
       score rendered paragraphs/tables by context overlap
       scroll/highlight best paragraph
       fallback to old short text candidates only after paragraph scoring fails
```

验收样例：

- `吡咯烷`：不应因为短词优先命中 `N-甲基吡咯烷酮`，应优先选择与 returned chunk 上下文整体重合最高的段落。
- `三乙基硅` / `三甲基硅`：存在 bbox 时仍使用 bbox；没有 bbox 时段落级定位，不伪造页码。
- 同一已打开 DOCX 点击不同搜索结果时，resource tab 不重新加载，只通过 `postMessage` 更新 locator 并重新定位。

降级语义：

- `locatorPrecision=bbox`：有 page + bbox，可页内精确框选。
- `locatorPrecision=paragraph`：有 block/context，但缺 page/bbox，只保证段落级定位。
- `locatorPrecision=file`：只能打开文件，定位信息不足。
