// MemoryStore::recall。仍是同一个聚合根上的方法。
// ---------------------------------------------------------------------------
// recall:向量与关键词融合
// ---------------------------------------------------------------------------
///|
/// 混合检索:向量 + BM25 双路 → RRF 融合 → 只返回 Active/Unstructured。
/// 嵌入失败时仅 BM25 单路。无结果返回空列表(不编造)。
pub fn MemoryStore::recall(
self : MemoryStore,
user_id : String,
query : String,
top_k? : Int32,
) -> Result[Array[MemoryEntry], MoomemError] {
if self.closed {
return Err(MoomemError::InvalidOperation("store is closed"))
}
match validate_user_id(user_id) {
Ok(_) => ()
Err(e) => return Err(e)
}
let k = match top_k {
Some(v) =>
if v < 1 {
DEFAULT_RECALL_TOP_K
} else if v > MAX_RECALL_TOP_K {
MAX_RECALL_TOP_K
} else {
v
}
None => DEFAULT_RECALL_TOP_K
}
let candidates_n = if self.recall_candidates > 0 {
self.recall_candidates
} else {
let auto = k * 2
if auto > MAX_RECALL_TOP_K {
MAX_RECALL_TOP_K
} else {
auto
}
}
let candidates_n = if candidates_n < k { k } else { candidates_n }
// 嵌入查询(失败 → 单路 BM25)
let query_vector : Array[Double] = match self.embedder.embed([query]) {
Ok(vs) => if vs.length() > 0 { vs[0] } else { [] }
Err(_) => []
}
let vector_hits = self.vector_index.search(
user_id, query_vector, candidates_n,
)
let keyword_hits = self.keyword_index.search(user_id, query, candidates_n)
let vector_ids : Array[String] = vector_hits.map(p => p.0)
let keyword_ids : Array[String] = keyword_hits.map(p => p.0)
let fused = match self.fusion_policy {
EqualRrf => rrf([vector_ids, keyword_ids], self.rrf_k, k)
AdaptiveLexical => {
let bm25_scores : Array[Double] = keyword_hits.map(p => p.1)
let strong = lexical_strong(
bm25_scores,
self.lexical_floor,
self.lexical_gap,
)
let soft = if strong && self.vector_weight_when_lexical <= 0.05 {
let out : Array[String] = []
let limit = if k < keyword_ids.length() {
k
} else {
keyword_ids.length()
}
for i = 0; i < limit; i = i + 1 {
out.push(keyword_ids[i])
}
out
} else {
let vector_weight = if strong {
self.vector_weight_when_lexical
} else {
self.vector_weight_when_semantic
}
rrf_weighted(
vector_ids,
keyword_ids,
self.rrf_k,
k,
vector_weight,
1.0 - vector_weight,
)
}
protect_bm25_topk(soft, keyword_ids, k)
}
}
let results : Array[MemoryEntry] = []
for id in fused {
match self.entries.get(id) {
Some(e) =>
if e.user_id == user_id && e.is_recallable() {
results.push(e)
}
None => ()
}
}
// 补齐到 top_k(仅当已有命中;零命中返回空列表,不编造)
match self.backfill {
BackfillRecency =>
if results.length() > 0 && results.length() < k {
let in_results : Map[String, Bool] = Map([])
for e in results {
in_results.set(e.id, true)
}
match self.by_user.get(user_id) {
Some(ids) => {
// 最新优先(插入序倒序)
let mut i = ids.length() - 1
while i >= 0 && results.length() < k {
match self.entries.get(ids[i]) {
Some(e) =>
if e.is_recallable() && !in_results.contains(e.id) {
in_results.set(e.id, true)
results.push(e)
}
None => ()
}
i = i - 1
}
}
None => ()
}
}
NoBackfill => ()
}
Ok(results)
}