返回 DeepSeek-Reasonix
recall_index.go
根目录 / internal / memory / recall_index.go
1 // The session recall index: the recall pool read and tokenized once per
2 // memory snapshot, so each user turn's automatic recall costs zero disk IO.
3 // Markdown files stay the source of truth — every write path reloads the
4 // snapshot through memory.Load, which rebuilds this index with it.
5 package memory
6
7 import (
8 "strings"
9
10 "reasonix/internal/retrieval"
11 )
12
13 // RecallIndex is the prebuilt retrieval state for one immutable Set snapshot.
14 type RecallIndex struct {
15 docs []autoRecallDoc
16 fielded []retrieval.FieldedDoc // V2 shadow pool, prebuilt field split
17 }
18
19 // BuildRecallIndex reads and tokenizes the recall pool once. A zero store
20 // yields nil, which recall reports as an empty memory store.
21 func BuildRecallIndex(store Store) *RecallIndex {
22 memories := recallMemories(store.ListAll())
23 if len(memories) == 0 {
24 return nil
25 }
26 index := &RecallIndex{}
27 for _, memory := range memories {
28 index.fielded = append(index.fielded, retrieval.FieldedDoc{ID: memory.ID, Fields: map[string]string{
29 "name": memory.Name, "title": memory.Title, "keywords": memory.Keywords,
30 "subject": memory.SubjectKey, "description": memory.Description, "body": memory.Body,
31 }})
32 text := autoRecallSearchText(memory)
33 terms := retrieval.Tokens(text)
34 if len(terms) == 0 {
35 continue
36 }
37 index.docs = append(index.docs, autoRecallDoc{
38 memory: memory,
39 text: text,
40 counts: retrieval.Counts(terms),
41 length: len(terms),
42 })
43 }
44 return index
45 }
46
47 // AutoRecall runs automatic recall against this snapshot's prebuilt index —
48 // the per-turn path. Semantics are identical to the package-level AutoRecall.
49 func (s *Set) AutoRecall(query string, opts RecallOptions) RecallResult {
50 result := RecallResult{Query: strings.TrimSpace(query), CharBudget: recallCharBudget(opts.MaxChars)}
51 if genericRecallQuery(result.Query) {
52 result.Suppressed = "generic user turn"
53 return result
54 }
55 if s == nil {
56 result.Suppressed = "memory store is empty"
57 return result
58 }
59 index := s.recall
60 if index == nil {
61 // Hand-built sets (tests, embedders) carry no prebuilt index; the
62 // Load path always does, so per-turn recall stays disk-free there.
63 index = BuildRecallIndex(s.Store)
64 }
65 return autoRecallIndexed(index, result, opts)
66 }
67
67 lines GO