mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
Some checks failed
CI / Python tests / Unit Tests - py3.12 (push) Has been cancelled
CI / Python tests / Unit Tests - py3.13 (push) Has been cancelled
CI / TypeScript integrations / Type-check, test, and pack (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 javascript-typescript (push) Has been cancelled
Security / CodeQL / Analyze python (push) Has been cancelled
CI / Documentation / Test and build documentation (push) Has been cancelled
CI / Python quality / Pre-commit (push) Has been cancelled
CI / Python tests / Unit Tests - py3.11 (push) Has been cancelled
Deploy / Documentation / deploy (push) Has been cancelled
* fix: recover embedding after transient health failure * refactor(embedding_store): remove provider_success_count and simplify health recovery logic - Deleted provider_success_count attribute and related methods across embedding and file stores - Updated _recover_after_real_request to rely solely on is_healthy flag for recovery decisions - Removed redundant counting logic for provider successes during embedding operations - Cleaned up health status management to streamline provider recovery detection - Adjusted unit tests to align with removal of provider_success_count and maintain health checks consistency * refactor(embedding_store): use default health check timeout * fix(embedding_store): ensure is_healthy remains unchanged on cache hits - Updated get_embeddings docstring to clarify cache hits must not alter is_healthy state - Improved code comment for embedding dimension matching method * fix(file_store): make embedding recovery race-safe * ci: use default CodeQL query suite * fix(file_store): preserve queued embedding rebuilds * fix(file_store): preserve verified recovery without chunks
249 lines
10 KiB
Markdown
249 lines
10 KiB
Markdown
# Memory Search
|
|
|
|
Memory Search is ReMe's memory retrieval entry point. The default background loop continuously builds Markdown under
|
|
`daily/` and `digest/` into a searchable chunk index and wikilink graph. At query time, it first recalls the most
|
|
relevant fragments and then expands context along the bidirectional links of the files containing those fragments.
|
|
`reme reindex` has a broader rebuild scope that also scans `resource/` and JSONL; it is intentionally different from the
|
|
live watcher.
|
|
|
|
<p align="center">
|
|
<img src="../figure/auto-index-and-memory-search.svg" alt="ReMe Auto Index and Memory Search indexing, recall, fusion, and link expansion" width="92%">
|
|
</p>
|
|
|
|
For the general semantics of file layers, frontmatter, wikilinks, and chunking, see
|
|
[Memory as File](./memory_as_file.md). This page focuses on index maintenance and query execution.
|
|
|
|
```text
|
|
workspace files
|
|
├─ index_update_loop: detect added / modified / deleted
|
|
├─ update_index_step: file -> FileNode + FileChunk[]
|
|
├─ file_store: store chunks, BM25, optional embeddings, and the wikilink graph
|
|
└─ search_step: BM25 / vector recall -> RRF fusion -> link expansion
|
|
```
|
|
|
|
## What It Searches
|
|
|
|
The default `index_update_loop` watches two memory directories:
|
|
|
|
- `daily_dir`: daily working memory and session memory cards generated by Auto Memory.
|
|
- `digest_dir`: long-term distilled digest nodes.
|
|
|
|
The live watcher handles only the `md` suffix. A separate `resource_watch_loop` watches `resource_dir`, and Auto
|
|
Resource turns those inputs into daily cards that enter the live index. When `reme reindex` is run manually, its
|
|
configuration scans
|
|
`daily_dir`, `digest_dir`, and `resource_dir` for `md` and `jsonl`; Markdown uses the `markdown` chunker and JSONL uses
|
|
the
|
|
`jsonl` chunker.
|
|
|
|
## How the Index Is Built
|
|
|
|
### Index Update
|
|
|
|
The background Job `index_update_loop` maintains the index using configuration from `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` runs at startup. It scans the watched directories, compares file mtimes on disk with
|
|
`FileNode.st_mtime` values already stored in `file_store`, calculates added, modified, and deleted changes, and passes
|
|
`context["changes"]` to `update_index_step`.
|
|
|
|
While the service is running, `watch_changes_step` takes over. It uses `watchfiles.awatch()` to watch the same
|
|
directories, groups file events within a quiet window, and uses `coalesce_changes()` to collapse repeated events for the
|
|
same path into one stable batch of changes.
|
|
|
|
`update_index_step` performs the actual index writes:
|
|
|
|
1. Select a file chunker by suffix.
|
|
2. Parse the file into one `FileNode` and multiple `FileChunk` objects.
|
|
3. For an added or modified file, delete its old chunks before upserting the new chunks.
|
|
4. For a deleted file, remove its records from `file_store`, `keyword_index`, and `file_graph`.
|
|
5. When changes exist, dump state to `metadata/` so it can be restored on the next startup.
|
|
|
|
The Markdown chunker parses YAML frontmatter, heading structure, and wikilinks into `FileNode`, `FileChunk`, and
|
|
`FileLink`
|
|
objects. For detailed chunking rules, see [Memory as File](./memory_as_file.md#memory-chunking).
|
|
|
|
### Index Optimization
|
|
|
|
Both BM25 and the FAISS HNSW vector index use tombstone markers instead of physical removal when deleting nodes; too
|
|
many tombstones degrade search performance. An idle-time optimization mechanism is built in—the `optimize_index_cron`
|
|
scheduled job compacts tombstones and rebuilds indexes during off-peak hours:
|
|
|
|
```yaml
|
|
optimize_index_cron:
|
|
backend: cron
|
|
cron: "0 2 * * *"
|
|
steps:
|
|
- backend: optimize_index_step
|
|
```
|
|
|
|
By default it runs at 2:00 AM daily; adjust the cron expression to customize the schedule.
|
|
|
|
## What file_store Contains
|
|
|
|
The default `file_store.default` backend is `local`:
|
|
|
|
```yaml
|
|
file_store:
|
|
default:
|
|
backend: local
|
|
embedding_store: ""
|
|
keyword_index: default
|
|
file_graph: default
|
|
```
|
|
|
|
It combines three kinds of capability:
|
|
|
|
| Part | Default state | Purpose |
|
|
|-------------------------|---------------|-------------------------------------------------------------------------|
|
|
| `file_chunks` | Enabled | Store `FileChunk` text, line numbers, scores, and optional embeddings. |
|
|
| `keyword_index.default` | Enabled | BM25 inverted index where chunk ID is the document ID. |
|
|
| `file_graph.default` | Enabled | Store `FileNode` objects and wikilink edges. |
|
|
| `embedding_store` | Disabled | When enabled, generate embeddings for chunks and support vector recall. |
|
|
|
|
Out of the box, search therefore uses primarily BM25 plus link expansion. After setting `embedding_store: default`,
|
|
`SearchStep` runs vector and keyword recall together. Additionally, switching the `file_store` `backend` from `local` to
|
|
`faiss` upgrades vector retrieval from a linear scan to a FAISS HNSW index, offering faster recall at scale.
|
|
|
|
The embedding store accepts `health_check_timeout` for its startup probe. A temporary failure skips the current vector
|
|
backfill while keeping BM25 available; a later successful provider request resumes the missing-vector backfill
|
|
automatically.
|
|
|
|
Embedded integrations that have already verified a provider can call `resume_embedding(verified=True)`. When changing
|
|
the embedding vector space, pass `rebuild=True`; persisted vectors are invalidated before a serial background rebuild,
|
|
and vector search remains unavailable until the rebuilt vectors are safely persisted.
|
|
|
|
## How to Search
|
|
|
|
The `search` Job is also configured in `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
|
|
```
|
|
|
|
Call it with:
|
|
|
|
```bash
|
|
reme search query="recent discussions about indexing" limit=5
|
|
```
|
|
|
|
Use `start_date` and `end_date` for inclusive `YYYY-MM-DD` filtering:
|
|
|
|
```bash
|
|
reme search query="index regression" start_date=2026-06-01 end_date=2026-06-20 limit=10
|
|
```
|
|
|
|
`search_step` executes in this order:
|
|
|
|
```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 fusion"]
|
|
D --> E
|
|
E --> F["min_score filter"]
|
|
F --> G["truncate to limit"]
|
|
G --> H["expand_links(...)"]
|
|
H --> I["Response.answer + metadata"]
|
|
```
|
|
|
|
If only BM25 has results, the BM25 ranking is returned directly. If only vector search has results, the vector ranking
|
|
is returned directly. When both have results, they are fused with RRF. RRF does not compare BM25 and cosine scores
|
|
directly; it compares ranks in the two result lists:
|
|
|
|
```text
|
|
fused_score = vector_weight / (60 + vector_rank)
|
|
+ keyword_weight / (60 + keyword_rank)
|
|
```
|
|
|
|
The default `vector_weight=0.7` gives semantic recall more weight when embeddings are enabled, while keyword search can
|
|
still promote chunks with exact term matches.
|
|
|
|
## How BM25 Works
|
|
|
|
`keyword_search()` calls `keyword_index.retrieve(query, limit)`. Each chunk is a document in the BM25 index:
|
|
|
|
- `doc_id` is `FileChunk.id`.
|
|
- `content` is `FileChunk.text`.
|
|
- The tokenizer splits text into tokens.
|
|
- The inverted index records which chunks contain each token and its term frequency within each chunk.
|
|
- A query scores only the posting lists matching its tokens and returns the highest-scoring chunk IDs.
|
|
|
|
When a file changes, `LocalFileStore.upsert()` first removes the BM25 documents corresponding to the file's old
|
|
`chunk_ids`
|
|
and then adds the new chunk text. Deletion is lazy; the index can later be compacted with optimize.
|
|
|
|
## Progressive Expansion
|
|
|
|
"Progressive" in Memory Search does not mean putting the entire repository into one result. Retrieval expands in three
|
|
layers:
|
|
|
|
1. Chunk recall: return only the `limit` most relevant text fragments.
|
|
2. File location: each result includes `path:start_line-end_line`. Pass the path and line bounds separately as `path`,
|
|
`start_line`, and `end_line` when calling `read`; the range is not part of the `path` value.
|
|
3. Link neighbors: call `expand_links()` for each matched file and expand at most `max_links_per_direction` outlinks and
|
|
inlinks.
|
|
|
|
Expansion data comes from `file_graph` rather than rescanning files:
|
|
|
|
```text
|
|
matched chunk
|
|
-> chunk.path
|
|
-> file_store.get_outlinks(path)
|
|
-> file_store.get_inlinks(path)
|
|
-> file_store.get_nodes(neighbor_paths)
|
|
-> render neighbor path, name, description, and anchor
|
|
```
|
|
|
|
This keeps search results short while still showing which long-term nodes, resources, or other daily notes a memory
|
|
connects to. If a result is worth pursuing, use `read path=...` to open the source or
|
|
`traverse path=... depth=2` to continue along the wikilink graph.
|
|
|
|
## Return Format
|
|
|
|
`SearchStep` writes results in two places:
|
|
|
|
- `response.answer`: human-readable text. Each matched block contains its path, line numbers, score, and chunk content,
|
|
followed by outlinks and inlinks.
|
|
- `response.metadata`: structured programmatic results containing `results`, `link_expansion`, and `counts`.
|
|
|
|
Typical text structure:
|
|
|
|
```text
|
|
========== daily/2026-06-20/retrieval-regression.md:12-28 [score=0.0317 keyword=4.8120] ==========
|
|
...matched memory fragment...
|
|
outlinks (2):
|
|
-> digest/indexing.md name="Indexing" description="..."
|
|
inlinks (1):
|
|
<- daily/2026-06-19.md name="..."
|
|
```
|
|
|
|
`counts` reports how many vector and keyword candidates were recalled and how many results were ultimately returned.
|
|
With embeddings disabled by default, `vector` is usually `0` and `hybrid` is `false`.
|