ReMe/docs/zh/memory_search.md
xyf2020 f04eedb3ab
Some checks failed
Security / CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Documentation / Test and build documentation (push) Has been cancelled
CI / Python packages / Build and verify distributions (push) Has been cancelled
CI / Python quality / Pre-commit (push) Has been cancelled
CI / Python tests / Unit Tests - py3.11 (push) Has been cancelled
CI / Python tests / Unit Tests - py3.13 (push) Has been cancelled
CI / ReMe Studio / Studio checks (push) Has been cancelled
CI / Windows / CLI smoke - py3.11 (push) Has been cancelled
Deploy / Documentation / Build documentation (push) Has been cancelled
Security / CodeQL / Analyze python (push) Has been cancelled
CI / Python tests / Unit Tests - py3.12 (push) Has been cancelled
Deploy / Documentation / deploy (push) Has been cancelled
feat(Tag filter): Add an optional rebuildable tag index for file frontmatter (#517)
* Add optional tag generation and normalization to auto memory

* Add tag index components and clean up temporary JSONL files

* Preserve tag index state when reconciliation fails

* Refactor and streamline application implementation

* Fix pylint C1803 warnings in tag normalization tests

* Document optional tag index configuration

* Make tag index failures non-blocking and disable auto-memory tags

* fix(tag-index): fail closed and support reindexing

* fix(tag-index): preserve complete query expressions

---------

Co-authored-by: jinli.yl <jinli.yl@alibaba-inc.com>
2026-09-07 15:52:17 +08:00

231 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Memory Search
Memory Search 是 ReMe 的记忆检索入口。默认后台持续把 `daily/`、`digest/` 里的 Markdown 构建成可搜索的 chunk 索引和
wikilink 图谱;查询时先召回最相关的片段,再沿着片段所在文件的双向链接展开上下文。`reme reindex` 以权威的内存态
`file_chunks` 为输入重建派生的 BM25 和 Embedding 索引;它不会重新扫描工作区、重新分块或改写 wikilink 图谱。
<p align="center">
<img src="../figure/auto-index-and-memory-search.svg" alt="ReMe Auto Index and Memory Search 索引、召回、融合与链接展开流程" width="92%">
</p>
文件分层、frontmatter、wikilink 和 chunking 的通用语义见 [Memory as File](./memory_as_file.md)。这里重点说明索引维护和查询执行。
```text
workspace files
├─ index_update_loop: 发现 added / modified / deleted
├─ update_index_step: 文件 -> FileNode + FileChunk[]
├─ file_store: 保存 chunk、BM25、可选 embedding、wikilink graph
└─ search_step: BM25 / vector 召回 -> RRF 融合 -> link expansion
```
## 它搜索什么
默认配置里的 `index_update_loop` 监听两类记忆目录:
- `daily_dir`:Auto Memory 生成的每日工作记忆和 session 记忆卡片。
- `digest_dir`:长期沉淀后的 digest 节点。
默认实时后缀只有 `md`。`resource_dir` 由独立的 `resource_watch_loop` 监听,并经 Auto Resource 转换成 daily 卡片后进入实时索引。
手动 `reindex` 只处理这些摄取路径已经接受的 chunk,因此不会扩大搜索文件范围。
## 索引怎么构建
### 索引更新
索引由后台 Job `index_update_loop` 维护,配置来自 `reme/config/default.yaml`:
```yaml
index_update_loop:
backend: background
watch_dirs: [daily_dir, digest_dir]
watch_suffixes: [md]
steps:
- backend: init_changes_step
monitor_type: file_store
monitor_name: default
dispatch_steps: [ update_index_step ]
- backend: watch_changes_step
dispatch_steps: [ update_index_step ]
```
启动时先跑 `init_changes_step`。它扫描 watch 目录,把磁盘上的文件 mtime 和 `file_store` 里已有的 `FileNode.st_mtime`
对比,算出新增、修改、删除三类变化,然后把 `context["changes"]` 交给 `update_index_step`。
服务运行期间由 `watch_changes_step` 接手。它用 `watchfiles.awatch()` 监听同一批目录,按 quiet window 聚合文件事件,再用
`coalesce_changes()` 把同一路径上的重复事件压成一批稳定变化。
`update_index_step` 真正写索引:
1. 按后缀选择 file chunker。
2. 把文件解析成一个 `FileNode` 和多个 `FileChunk`。
3. 对新增或修改的文件,先删除旧 chunk,再 upsert 新 chunk。
4. 对删除的文件,从 `file_store`、`keyword_index` 和 `file_graph` 清掉对应记录。
5. 有变化时 dump 到 `metadata/`,让下次启动可以恢复。
Markdown chunker 会解析 YAML frontmatter、标题结构和 wikilink,产出 `FileNode`、`FileChunk` 和 `FileLink`。更细的分块规则见
[Memory as File](./memory_as_file.md#memory-chunking)。
### 索引优化
BM25 和 FAISS HNSW 向量索引在删除节点时都采用墓碑(tombstone)标记而非物理移除,积累过多会拖慢搜索。为此内置了闲暇时间索引优化机制——
`optimize_index_cron` 定时任务在低峰期压缩墓碑并重建索引:
```yaml
optimize_index_cron:
backend: cron
cron: "0 2 * * *"
steps:
- backend: optimize_index_step
```
默认每天凌晨 2 点执行,调整 cron 表达式即可自定义调度时间。
## file_store 里有什么
默认 `file_store.default` 是 `local`:
```yaml
file_store:
default:
backend: local
embedding_store: ""
keyword_index: default
file_graph: default
```
它组合三类能力:
| 部件 | 默认状态 | 作用 |
|-------------------------|----------|---------------------------------------------------|
| `file_chunks` | 启用 | 保存 `FileChunk` 文本、行号、分数、可选 embedding |
| `keyword_index.default` | 启用 | BM25 倒排索引,chunk id 是 doc id |
| `file_graph.default` | 启用 | 保存 `FileNode` 和 wikilink 边 |
| `embedding_store` | 默认关闭 | 开启后为 chunk 生成 embedding,并支持向量召回 |
所以开箱搜索主要是 BM25 + 链接展开。把 `embedding_store: default` 打开后,`SearchStep` 会同时跑向量召回和关键词召回。此时若将
`file_store` 的 `backend` 从 `local` 改为 `faiss`,向量检索会从线性扫描升级为 FAISS HNSW 索引,在大规模 chunk 场景下召回效率更高。
Embedding store 可通过 `health_check_timeout` 配置启动探测。临时失败只会跳过本次向量回填,BM25 仍可使用;
后续真实请求成功后会自动恢复缺失向量的回填。
已经完成真实服务验证的嵌入式集成可以调用 `resume_embedding(verified=True)`,修复同一向量空间内缺失的向量。
切换 Embedding 向量空间必须显式运行 `reindex` Job,并传入 `scope: embedding`;该 Job 成功完成前向量搜索保持不可用。
`scope: bm25` 只重建关键词索引;`scope: tag` 从当前文件图重建可选的标签索引;`scope: all` 依次重建
BM25、Embedding 和标签索引。BM25 和 Embedding 使用当前的 `file_chunks` 快照,标签索引使用文件图中
`FileNode` 的 frontmatter。
## 怎么搜索
`search` Job 也是在 `default.yaml` 中配置:
```yaml
search:
backend: base
description: "Hybrid workspace search (vector + BM25, RRF-fused)."
parameters:
query: string
limit: integer
min_score: number
start_date: string
end_date: string
steps:
- backend: search_step
vector_weight: 0.7
candidate_multiplier: 5.0
expand_links: true
max_links_per_direction: 10
```
调用时:
```bash
reme search query="最近关于索引的讨论" limit=5
```
`start_date` 和 `end_date` 可以按 `YYYY-MM-DD` 做包含边界的日期过滤:
```bash
reme search query="索引回归" start_date=2026-06-01 end_date=2026-06-20 limit=10
```
`search_step` 的执行顺序是:
```mermaid
flowchart LR
A["query + limit"] --> B["candidates = min(200, limit * candidate_multiplier)"]
B --> C["file_store.vector_search(...)"]
B --> D["file_store.keyword_search(...)"]
C --> E["RRF 融合"]
D --> E
E --> F["min_score 过滤"]
F --> G["截断到 limit"]
G --> H["expand_links(...)"]
H --> I["Response.answer + metadata"]
```
如果只有 BM25 有结果,就直接返回 BM25 排名;如果只有向量有结果,就直接返回向量排名;两边都有结果时,用 RRF 融合。RRF 不直接比较
BM25 分数和 cosine 分数,而是比较两个列表里的名次:
```text
fused_score = vector_weight / (60 + vector_rank)
+ keyword_weight / (60 + keyword_rank)
```
默认 `vector_weight=0.7`,所以启用 embedding 后语义召回权重更高;关键词仍能把精确词命中的 chunk 拉上来。
## BM25 怎么工作
`keyword_search()` 调用 `keyword_index.retrieve(query, limit)`。BM25 索引里每个 chunk 是一篇文档:
- `doc_id` 是 `FileChunk.id`。
- `content` 是 `FileChunk.text`。
- tokenizer 把文本切成 token。
- 倒排表记录 token 出现在哪些 chunk 里、每个 chunk 的词频是多少。
- 查询时只对 query token 命中的 posting list 打分,再返回分数最高的 chunk id。
当文件被修改时,`LocalFileStore.upsert()` 会先删除该文件旧 `chunk_ids` 对应的 BM25 doc,再添加新 chunk 文本。删除采用 lazy
delete,后续可通过 optimize 压缩索引。
## 渐进式展开怎么看
Memory Search 的“渐进式”不是一次把全库内容塞进结果,而是分三层展开:
1. 第一层是 chunk 召回:只返回最相关的 `limit` 个文本片段。
2. 第二层是文件定位:每个结果带 `path:start_line-end_line`。调用 `read` 时需要把它们分别作为 `path`、
`start_line` 和 `end_line` 传入,行号范围不是 `path` 的一部分。
3. 第三层是链接邻居:对命中文件调用 `expand_links()`,展开最多 `max_links_per_direction` 个 outlinks 和 inlinks。
展开的数据来自 `file_graph`,不是重新扫文件:
```text
命中 chunk
-> chunk.path
-> file_store.get_outlinks(path)
-> file_store.get_inlinks(path)
-> file_store.get_nodes(neighbor_paths)
-> 渲染邻居的 path、name、description、anchor
```
这让搜索结果既保持短,又能看到“这条记忆连接到哪些长期节点、资源或其他 daily note”。如果某条结果值得继续追,可以用
`read path=...` 打开原文,或用 `traverse path=... depth=2` 沿 wikilink 图谱继续扩展。
## 返回结果长什么样
`SearchStep` 会把结果写到两个地方:
- `response.answer`:给人看的文本,每个命中块包含路径、行号、分数和 chunk 内容,后面跟 outlinks / inlinks。
- `response.metadata`:给程序看的结构化结果,包括 `results`、`link_expansion`、`counts`。
典型文本结构:
```text
========== daily/2026-06-20/retrieval-regression.md:12-28 [score=0.0317 keyword=4.8120] ==========
...命中的记忆片段...
outlinks (2):
-> digest/indexing.md name="Indexing" description="..."
inlinks (1):
<- daily/2026-06-19.md name="..."
```
`counts` 会告诉你本次向量、关键词各召回了多少候选,以及最终返回多少条。默认 embedding 关闭时,`vector` 通常是 `0`,`hybrid` 是
`false`。