1use crate::signals::is_positive_signal;
6use crate::{Engine, EngineResult, RetrieveInput, RetrieveOutput};
7use hippmem_core::hash::stable_hash64;
8use hippmem_core::ids::MemoryId;
9use hippmem_core::model::links::{ActivationStep, RecallChannel, RetrievalResult};
10use hippmem_core::model::unit::{GeneratedBy, MemoryLifecycle, MemoryUnit};
11use hippmem_core::time::Clock;
12use hippmem_model::deterministic::extract::DeterministicExtractor;
13use hippmem_model::lang::active_locales;
14use hippmem_retrieval::explain::deduce_dimensions;
15use hippmem_retrieval::seeds::{multi_channel_seeds, rrf_fuse};
16use hippmem_retrieval::spreading::spread_multi_hop_fused;
17use hippmem_retrieval::warnings::check_warnings;
18use hippmem_store::activation_log::ActivationLogger;
19use hippmem_store::kv::InvertedIndex;
20use hippmem_store::semantic::vector_index::BinaryIndex;
21use hippmem_store::semantic::vector_index::VectorIndex;
22use std::collections::HashMap;
23
24impl Engine {
25 pub fn retrieve(&self, input: RetrieveInput) -> EngineResult<RetrieveOutput> {
27 let start = std::time::Instant::now();
28 let params = self.params.read();
29
30 let extractor = DeterministicExtractor;
32 let query_content = hippmem_core::model::unit::MemoryContent {
33 raw: input.query.clone(),
34 summary: None,
35 normalized: None,
36 language: hippmem_core::model::unit::Language::Zh,
37 content_type: hippmem_core::model::enums::ContentType::UserStatement,
38 };
39 let understanding = extractor
40 .extract_sync_immediate(&query_content)
41 .unwrap_or_else(|_| hippmem_model::traits::ImmediateExtraction {
42 entities: vec![],
43 topics: vec![],
44 explicit_causals: vec![],
45 language: hippmem_core::model::unit::Language::Zh,
46 content_type: None,
47 importance: hippmem_core::score::UnitScore::new(0.0),
48 });
49
50 let inverted = InvertedIndex::new(self.store.db_arc());
52
53 let entity_hits: Vec<(MemoryId, f32)> = understanding
55 .entities
56 .iter()
57 .filter_map(|em| {
58 let key = hippmem_core::hash::stable_hash64(&em.canonical);
59 inverted.get_entity(&key).ok().map(|ids| {
60 ids.into_iter()
61 .map(|id| (MemoryId(id), 0.2f32))
62 .collect::<Vec<_>>()
63 })
64 })
65 .flatten()
66 .collect();
67
68 let topic_hits: Vec<(MemoryId, f32)> = understanding
70 .topics
71 .iter()
72 .filter_map(|t| {
73 let key = hippmem_core::hash::stable_hash64(&t.label);
74 inverted.get_topic(&key).ok().map(|ids| {
75 ids.into_iter()
76 .map(|id| (MemoryId(id), 0.15f32))
77 .collect::<Vec<_>>()
78 })
79 })
80 .flatten()
81 .collect();
82
83 let now = hippmem_core::time::SystemClock.now();
85 let temporal_keys = temporal_bucket_keys(now);
86 let mut temporal_hit_ids = std::collections::HashSet::new();
87 for tk in &temporal_keys {
88 if let Ok(ids) = inverted.get_temporal(tk) {
89 for id in ids {
90 temporal_hit_ids.insert(MemoryId(id));
91 }
92 }
93 }
94 let mut temporal_hit_vec: Vec<MemoryId> = temporal_hit_ids.into_iter().collect();
97 temporal_hit_vec.sort();
98 let temporal_hits: Vec<(MemoryId, bool)> =
99 temporal_hit_vec.into_iter().map(|id| (id, true)).collect();
100
101 let mut bm25_hits: Vec<(MemoryId, f32)> = self
103 .fulltext_index
104 .lock()
105 .search(&input.query, params.seed_per_channel as usize)
106 .unwrap_or_default()
107 .into_iter()
108 .map(|(id, score)| {
109 let norm = (score / params.bm25_norm_factor).tanh();
110 (MemoryId(id), norm)
111 })
112 .collect();
113 bm25_hits.sort_by(|(a_id, a_s), (b_id, b_s)| {
116 b_s.partial_cmp(a_s)
117 .unwrap_or(std::cmp::Ordering::Equal)
118 .then_with(|| a_id.cmp(b_id))
119 });
120
121 let semantic_hits: Vec<(MemoryId, f32)> = {
123 let query_texts = vec![input.query.clone()];
124 self.embedder
125 .embed_sync(&query_texts)
126 .ok()
127 .and_then(|vectors| vectors.first().cloned())
128 .map(|query_vec| {
129 let idx = self.dense_vector_index.lock();
130 idx.search(&query_vec, params.seed_per_channel as usize)
131 .unwrap_or_default()
132 .into_iter()
133 .map(|(id, l2_dist)| {
134 let cos_sim = 1.0 / (1.0 + l2_dist);
136 (MemoryId(id), cos_sim)
137 })
138 .filter(|(_, sim)| *sim > 0.0)
139 .collect()
140 })
141 .unwrap_or_default()
142 };
143
144 let binary_hits: Vec<(MemoryId, f32)> = {
146 let query_bc = query_binary_code(&input.query);
147 let idx = self.binary_code_index.lock();
148 idx.search(&query_bc, params.seed_per_channel as usize)
149 .unwrap_or_default()
150 .into_iter()
151 .map(|(id, hamming)| {
152 let sim = 1.0 - (hamming as f32 / 128.0);
153 (MemoryId(id), sim.max(0.0))
154 })
155 .filter(|(_, sim)| *sim > 0.0)
156 .collect()
157 };
158
159 let query_goals = extract_query_goals(&input.query);
161 let goal_hits: Vec<(MemoryId, usize)> = query_goals
162 .iter()
163 .filter_map(|goal| {
164 let key = stable_hash64(goal);
165 inverted.get_goal(&key).ok().map(|ids| {
166 ids.into_iter()
167 .map(|id| (MemoryId(id), 1))
168 .collect::<Vec<_>>()
169 })
170 })
171 .flatten()
172 .collect();
173
174 let query_events = extract_query_events(&input.query);
176 let event_hits: Vec<(MemoryId, usize)> = query_events
177 .iter()
178 .filter_map(|event| {
179 let key = stable_hash64(event);
180 inverted.get_event(&key).ok().map(|ids| {
181 ids.into_iter()
182 .map(|id| (MemoryId(id), 1))
183 .collect::<Vec<_>>()
184 })
185 })
186 .flatten()
187 .collect();
188
189 let causal_hits: Vec<(MemoryId, usize)> = understanding
191 .explicit_causals
192 .iter()
193 .filter_map(|c| {
194 let causal_str = format!("{} -> {}", c.cause, c.effect);
195 let key = stable_hash64(&causal_str);
196 inverted.get_causal(&key).ok().map(|ids| {
197 ids.into_iter()
198 .map(|id| (MemoryId(id), 1))
199 .collect::<Vec<_>>()
200 })
201 })
202 .flatten()
203 .collect();
204
205 let recent_hits: Vec<(MemoryId, f32)> = {
207 let mut recent_map: HashMap<MemoryId, f32> = HashMap::new();
208
209 for mid in &input.context.recent_memory_ids {
211 recent_map
212 .entry(*mid)
213 .and_modify(|s| *s = (*s + 0.3).min(1.0))
214 .or_insert(0.3);
215 }
216
217 let graph = hippmem_store::graph::GraphStore::new(self.store.db_arc());
219 for mid in &input.context.recent_memory_ids {
220 if let Ok(links) = graph.get_outgoing(mid) {
221 for link in links.iter().take(8) {
222 recent_map
223 .entry(link.target_id)
224 .and_modify(|s| *s = (*s + 0.15).min(1.0))
225 .or_insert(0.15);
226 }
227 }
228 }
229
230 let act_log = ActivationLogger::new(self.store.db_arc());
232 if let Ok(records) = act_log.read_all() {
233 let mut freq: HashMap<MemoryId, u32> = HashMap::new();
234 for rec in records.iter() {
235 if !is_positive_signal(&rec.signal) {
239 continue;
240 }
241 for &mid in &rec.used_memory_ids {
242 *freq.entry(MemoryId(mid)).or_default() += 1;
244 }
245 }
246 let max_freq = freq.values().max().copied().unwrap_or(1) as f32;
247 for (mid, count) in freq {
248 let score = (count as f32 / max_freq) * 0.25;
249 recent_map
250 .entry(mid)
251 .and_modify(|s| *s = (*s + score).min(1.0))
252 .or_insert(score);
253 }
254
255 let result_set_rejected: std::collections::HashSet<MemoryId> = records
260 .iter()
261 .filter(|r| r.signal == "UserRejected" && r.used_memory_ids.is_empty())
262 .filter_map(|r| {
263 records
264 .iter()
265 .find(|p| p.retrieval_id == r.retrieval_id && p.signal == "retrieve")
266 })
267 .flat_map(|p| p.used_memory_ids.iter().map(|id| MemoryId(*id)))
268 .collect();
269 if !result_set_rejected.is_empty() {
270 recent_map.retain(|id, _| !result_set_rejected.contains(id));
271 }
272 }
273
274 let mut hits: Vec<(MemoryId, f32)> = recent_map.into_iter().collect();
275 hits.sort_by_key(|(id, _)| *id);
278 hits.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
279 hits.truncate(params.seed_per_channel as usize);
280 hits
281 };
282
283 let seed_result = multi_channel_seeds(
284 &input.query,
285 &entity_hits,
286 &temporal_hits,
287 &semantic_hits,
288 &topic_hits,
289 &bm25_hits,
290 &binary_hits,
291 &goal_hits,
292 &event_hits,
293 &causal_hits,
294 &recent_hits,
295 params.seed_per_channel as usize,
296 );
297
298 let mut fused_scores: HashMap<MemoryId, (f32, RecallChannel)> =
300 if seed_result.seeds.is_empty() {
301 let fallback = load_limited_units(self.store.db_arc(), 50)
304 .into_iter()
305 .filter(|u| !is_retrieval_seed_excluded(u))
306 .collect::<Vec<_>>();
307 fallback
308 .into_iter()
309 .map(|u| (u.id, (0.3_f32, RecallChannel::RecentActivation)))
310 .collect()
311 } else {
312 rrf_fuse(&seed_result.seeds, ¶ms)
313 };
314
315 let mut unit_map: HashMap<MemoryId, MemoryUnit> = HashMap::new();
317 for unit in load_units_by_ids(
318 self.store.db_arc(),
319 &fused_scores.keys().cloned().collect::<Vec<_>>(),
320 ) {
321 unit_map.insert(unit.id, unit);
322 }
323 fused_scores.retain(|id, _| !unit_map.get(id).is_some_and(is_retrieval_seed_excluded));
329 let seed_ids: Vec<MemoryId> = fused_scores.keys().cloned().collect();
330
331 let importance_map: HashMap<MemoryId, f32> = unit_map
333 .iter()
334 .map(|(id, unit)| (*id, unit.understanding.importance.value()))
335 .collect();
336 let usage_map: HashMap<MemoryId, f32> = unit_map
338 .iter()
339 .map(|(id, unit)| (*id, unit.activation.usage_score.value()))
340 .collect();
341
342 let graph = hippmem_store::graph::GraphStore::new(self.store.db_arc());
343 let mut links_map: HashMap<MemoryId, Vec<hippmem_core::model::links::AssociationLink>> =
344 HashMap::new();
345
346 for sid in &seed_ids {
348 if let Ok(links) = graph.get_outgoing(sid) {
349 links_map.insert(*sid, links);
350 }
351 }
352
353 let neighbor_ids: Vec<MemoryId> = links_map
355 .values()
356 .flatten()
357 .map(|l| l.target_id)
358 .filter(|tid| !links_map.contains_key(tid))
359 .collect();
360 for unit in load_units_by_ids(self.store.db_arc(), &neighbor_ids) {
362 unit_map.entry(unit.id).or_insert(unit);
363 }
364 for nid in &neighbor_ids {
368 if unit_map.get(nid).is_some_and(is_retrieval_seed_excluded) {
371 continue;
372 }
373 if let Ok(links) = graph.get_outgoing(nid) {
374 links_map.insert(*nid, links);
375 }
376 }
377
378 let (activated, merged_count) = spread_multi_hop_fused(
380 &fused_scores,
381 &links_map,
382 ¶ms,
383 &importance_map,
384 &usage_map,
385 input.max_hops.map(|h| h as u32),
386 );
387 let max_k = input.top_k.min(activated.len());
388
389 let extra_ids: Vec<MemoryId> = activated
391 .iter()
392 .map(|(id, _, _)| *id)
393 .filter(|id| !unit_map.contains_key(id))
394 .collect();
395 for unit in load_units_by_ids(self.store.db_arc(), &extra_ids) {
396 unit_map.insert(unit.id, unit);
397 }
398
399 let loaded_units: Vec<MemoryUnit> = activated
401 .iter()
402 .filter_map(|(id, _, _)| unit_map.get(id).cloned())
403 .collect();
404 let mut reranked = hippmem_retrieval::rerank::rerank_by_energy(&activated, &loaded_units);
405
406 apply_question_aware_boost(&input.query, &mut reranked, ¶ms);
409
410 reranked.retain(|(_, _, _, unit)| {
412 !matches!(unit.lifecycle, MemoryLifecycle::Compressed { .. })
413 });
414
415 let results: Vec<RetrievalResult> = reranked
417 .iter()
418 .take(max_k)
419 .map(|(_id, energy, trace, unit)| {
420 let matched = deduce_dimensions(trace);
421 let warns = check_warnings(unit, *energy);
422 RetrievalResult {
423 memory: unit.clone(),
424 final_score: *energy,
425 activation_trace: trace.clone(),
426 matched_dimensions: matched,
427 warnings: warns,
428 }
429 })
430 .collect();
431
432 let channel_contributions: Vec<(RecallChannel, u32)> = {
437 let mut map: HashMap<RecallChannel, u32> = HashMap::new();
438 for seed in seed_result
439 .seeds
440 .iter()
441 .filter(|s| !unit_map.get(&s.id).is_some_and(is_retrieval_seed_excluded))
442 {
443 *map.entry(seed.channel).or_default() += 1;
444 }
445 map.into_iter().collect()
446 };
447
448 let retrieval_id = {
451 let act_log = ActivationLogger::new(self.store.db_arc());
452 let used_ids: Vec<u128> = results.iter().map(|r| r.memory.id.0).collect();
454 let now_ms =
455 if let Ok(t) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
456 t.as_millis() as i64
457 } else {
458 0
459 };
460 let _ = act_log.record(&hippmem_store::activation_log::ActivationRecord {
461 retrieval_id: now_ms as u64,
462 used_memory_ids: used_ids,
463 signal: "retrieve".into(),
464 recorded_at_ms: now_ms,
465 });
466 now_ms as u64
467 };
468
469 {
474 let kv = hippmem_store::kv::KvStore::new(self.store.db_arc());
475 let now_ts = hippmem_core::time::Timestamp::from_millis(retrieval_id as i64);
476 let max_co = params.co_activation_keep as usize;
477 for r in &results {
478 let raw = kv
479 .get(&r.memory.id.0)
480 .map_err(|e| crate::EngineError::Store(e.to_string()))?;
481 let Some(raw) = raw else { continue };
482 let (mut unit, _): (MemoryUnit, _) =
483 bincode::serde::decode_from_slice(&raw, bincode::config::standard())
484 .map_err(|e| crate::EngineError::Internal(e.to_string()))?;
485 unit.activation.retrieval_count = unit.activation.retrieval_count.saturating_add(1);
486 unit.activation.last_retrieved_at = Some(now_ts);
487 let mut co: Vec<hippmem_core::model::links::CoActivationCount> =
489 unit.activation.co_activations.clone();
490 for other in &results {
491 if other.memory.id == r.memory.id {
492 continue;
493 }
494 if let Some(existing) = co.iter_mut().find(|c| c.with == other.memory.id) {
495 existing.count = existing.count.saturating_add(1);
496 existing.last_at = now_ts;
497 } else {
498 co.push(hippmem_core::model::links::CoActivationCount {
499 with: other.memory.id,
500 count: 1,
501 last_at: now_ts,
502 });
503 }
504 }
505 if co.len() > max_co {
507 co.sort_by_key(|c| std::cmp::Reverse(c.last_at));
508 co.truncate(max_co);
509 }
510 unit.activation.co_activations = co;
511 let encoded = bincode::serde::encode_to_vec(&unit, bincode::config::standard())
512 .map_err(|e| crate::EngineError::Internal(e.to_string()))?;
513 kv.put(r.memory.id.0, &encoded)
514 .map_err(|e| crate::EngineError::Store(e.to_string()))?;
515 }
516 }
517
518 Ok(RetrieveOutput {
519 retrieval_id,
520 results,
521 trace: crate::RetrievalTrace {
522 seeds: seed_result
523 .seeds
524 .iter()
525 .filter(|s| !unit_map.get(&s.id).is_some_and(is_retrieval_seed_excluded))
527 .map(|s| crate::SeedRecord {
528 id: s.id,
529 channel: s.channel,
530 initial_energy: s.score,
531 rank_in_channel: s.rank_in_channel,
532 })
533 .collect(),
534 steps: activated
535 .iter()
536 .flat_map(|(_, _, trace)| trace.clone())
537 .collect(),
538 hops_used: activated
539 .iter()
540 .flat_map(|(_, _, trace)| trace.iter())
541 .map(|s| s.hop)
542 .max()
543 .unwrap_or(0),
544 merged_count,
545 },
546 diagnostics: crate::RetrievalDiagnostics {
547 channel_contributions,
548 reranked: true,
549 pruned_branches: 0,
550 backend_used: crate::BackendUsage {
551 embedder: self.embedder.backend_id().to_string(),
552 reranker: Some("rule".into()),
553 },
554 latency_ms: start.elapsed().as_millis() as u32,
555 },
556 })
557 }
558}
559
560#[derive(Debug, Clone, Copy, PartialEq)]
566enum QuestionType {
567 Why,
569 How,
571 What,
573 Correction,
575 Preference,
577 None,
579}
580
581fn detect_question_type(query: &str) -> QuestionType {
587 let q = query.to_lowercase();
588
589 for lang in active_locales() {
591 if let Some((before, after)) = lang.change_pair {
592 if q.contains(before) && q.contains(after) {
593 return QuestionType::Correction;
594 }
595 }
596 }
597
598 for lang in active_locales() {
602 for keyword in lang.q_correction {
603 if q.contains(keyword) {
604 return QuestionType::Correction;
605 }
606 }
607 }
608 for lang in active_locales() {
609 for keyword in lang.q_preference {
610 if q.contains(keyword) {
611 return QuestionType::Preference;
612 }
613 }
614 }
615 for lang in active_locales() {
616 for keyword in lang.q_why {
617 if q.contains(keyword) {
618 return QuestionType::Why;
619 }
620 }
621 }
622 for lang in active_locales() {
623 for keyword in lang.q_how {
624 if q.contains(keyword) {
625 return QuestionType::How;
626 }
627 }
628 }
629 for lang in active_locales() {
630 for keyword in lang.q_what {
631 if q.contains(keyword) {
632 return QuestionType::What;
633 }
634 }
635 }
636 QuestionType::None
637}
638
639fn explanatory_pattern_score(text: &str) -> f32 {
641 let mut score = 0.0f32;
642 for lang in active_locales() {
643 for (pattern, boost) in lang.explanatory {
644 if text.contains(pattern) {
645 score += boost;
646 }
647 }
648 }
649 score.min(0.20) }
651
652fn content_type_boost(query: &str) -> Vec<(hippmem_core::model::unit::ContentType, f32)> {
662 let qt = detect_question_type(query);
663 let mut boosts = Vec::new();
664
665 match qt {
666 QuestionType::Correction => {
667 boosts.push((hippmem_core::model::unit::ContentType::Correction, 0.12));
669 }
670 QuestionType::Preference => {
671 boosts.push((hippmem_core::model::unit::ContentType::Preference, 0.08));
673 boosts.push((hippmem_core::model::unit::ContentType::Decision, 0.04));
675 }
676 QuestionType::Why => {
677 boosts.push((hippmem_core::model::unit::ContentType::Decision, 0.08));
679 boosts.push((hippmem_core::model::unit::ContentType::TaskState, 0.08));
680 }
681 QuestionType::How => {
682 boosts.push((hippmem_core::model::unit::ContentType::TaskState, 0.08));
684 }
685 QuestionType::What => {
686 boosts.push((
692 hippmem_core::model::unit::ContentType::ProjectKnowledge,
693 0.15,
694 ));
695 }
696 QuestionType::None => {
697 }
699 }
700
701 if qt != QuestionType::Correction {
703 let q = query.to_lowercase();
704 let has_correction_signal = active_locales().iter().any(|lang| {
705 lang.q_correction.iter().any(|kw| q.contains(kw))
706 || lang
707 .change_pair
708 .is_some_and(|(b, a)| q.contains(b) && q.contains(a))
709 });
710 if has_correction_signal {
711 boosts.push((hippmem_core::model::unit::ContentType::Correction, 0.10));
712 }
713 }
714
715 boosts
716}
717
718fn apply_question_aware_boost(
728 query: &str,
729 reranked: &mut [(MemoryId, f32, Vec<ActivationStep>, MemoryUnit)],
730 params: &hippmem_core::config::AlgoParams,
731) {
732 let qt = detect_question_type(query);
733 let ct_boosts = content_type_boost(query);
734 let cap = params.seed_energy_cap;
735 let what_subject: Option<String> = if qt == QuestionType::What {
737 extract_subject_for_what_query(query)
738 } else {
739 None
740 };
741
742 match qt {
744 QuestionType::Why => {
745 for (_, energy, _, unit) in reranked.iter_mut() {
746 let boost = explanatory_pattern_score(&unit.content.raw);
747 if boost > 0.0 {
748 *energy = (*energy + boost).min(cap);
749 }
750 }
751 }
752 QuestionType::Correction
753 | QuestionType::Preference
754 | QuestionType::How
755 | QuestionType::What
756 | QuestionType::None => {
757 }
759 }
760
761 if !ct_boosts.is_empty() {
765 for (_, energy, _, unit) in reranked.iter_mut() {
766 for (ct, boost) in &ct_boosts {
767 if unit.content.content_type != *ct {
768 continue;
769 }
770 if qt == QuestionType::What
772 && *ct == hippmem_core::model::unit::ContentType::ProjectKnowledge
773 {
774 if let Some(ref subject) = what_subject {
775 let content_lower = unit.content.raw.to_lowercase();
776 if !content_lower.contains(&subject.to_lowercase()) {
777 break; }
779 }
780 }
781 *energy = (*energy + boost).min(cap);
782 break; }
784 }
785 }
786
787 let keywords = extract_discriminative_keywords(query);
793 if !keywords.is_empty() {
794 for (_, energy, _, unit) in reranked.iter_mut() {
795 let mut kw_bonus = 0.0f32;
796 let content_lower = unit.content.raw.to_lowercase();
797 for kw in &keywords {
798 if content_lower.contains(&kw.to_lowercase()) {
799 kw_bonus += 0.04;
800 }
801 }
802 if kw_bonus > 0.0 {
803 *energy = (*energy + kw_bonus.min(0.08)).min(cap);
804 }
805 }
806 }
807
808 if qt == QuestionType::What {
813 if let Some(ref subject) = extract_subject_for_what_query(query) {
814 let subject_lower = subject.to_lowercase();
815 for (_, energy, _, unit) in reranked.iter_mut() {
816 let content_lower = unit.content.raw.to_lowercase();
817 let has_definition = active_locales().iter().any(|lang| {
818 lang.definition_patterns
819 .iter()
820 .any(|pat| content_lower.contains(&format!("{} {pat}", subject_lower)))
821 });
822 if has_definition {
823 *energy = (*energy + 0.05).min(cap);
824 }
825 }
826 }
827 }
828
829 reranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
831}
832
833fn extract_subject_for_what_query(query: &str) -> Option<String> {
842 let q = query.to_lowercase();
843 for lang in active_locales() {
844 for delimiter in lang.what_delimiters {
845 if let Some(pos) = q.find(delimiter) {
846 let prefix = &q[..pos];
847 let subject = if let Some(particle) = lang.possessive_particle {
848 prefix
851 .rsplit(particle)
852 .next()
853 .unwrap_or("")
854 .rsplit(|c: char| c.is_whitespace() || c == '?' || c == '?')
855 .next()
856 .unwrap_or("")
857 .trim()
858 .to_string()
859 } else {
860 prefix
861 .rsplit(|c: char| c.is_whitespace() || c == '?' || c == '?')
862 .next()
863 .unwrap_or("")
864 .trim()
865 .to_string()
866 };
867 if subject.len() >= 2 {
868 return Some(subject);
869 }
870 return None;
871 }
872 }
873 }
874 None
875}
876
877fn extract_discriminative_keywords(query: &str) -> Vec<String> {
885 let stop_words: Vec<&str> = active_locales()
887 .iter()
888 .flat_map(|lang| lang.stop_words.iter().copied())
889 .collect();
890
891 let mut keywords: Vec<String> = Vec::new();
892 let mut seen = std::collections::HashSet::new();
893
894 for word in query.split(|c: char| !c.is_alphanumeric()) {
896 let is_keyword = (word.len() >= 2 && word.chars().any(|c| c.is_uppercase()))
897 || (word.chars().all(|c| c.is_ascii_alphabetic()) && word.len() >= 3);
898 if is_keyword
899 && !stop_words.contains(&word.to_lowercase().as_str())
900 && seen.insert(word.to_string())
901 {
902 keywords.push(word.to_string());
903 }
904 }
905
906 for word in query
908 .split(|c: char| c.is_whitespace() || c.is_ascii_punctuation() || c == '?' || c == '?')
909 {
910 let trimmed = word.trim();
911 if trimmed.chars().count() >= 2
912 && trimmed.chars().all(|c| c as u32 > 0x2E80) && !stop_words.contains(&trimmed)
914 && seen.insert(trimmed.to_string())
915 {
916 keywords.push(trimmed.to_string());
917 }
918 }
919
920 keywords.truncate(5); keywords
922}
923
924fn query_binary_code(text: &str) -> [u8; 16] {
926 let bc0 = stable_hash64(&format!("bc_0_{}", text));
927 let bc1 = stable_hash64(&format!("bc_1_{}", text));
928 let mut bytes = [0u8; 16];
929 bytes[..8].copy_from_slice(&bc0.to_le_bytes());
930 bytes[8..].copy_from_slice(&bc1.to_le_bytes());
931 bytes
932}
933
934fn temporal_bucket_keys(ts: hippmem_core::time::Timestamp) -> Vec<u32> {
936 let ms = ts.0;
937 vec![
938 (ms / 3_600_000) as u32, (ms / 86_400_000) as u32, (ms / 604_800_000) as u32, ]
942}
943
944pub(crate) fn load_all_units(db: std::sync::Arc<redb::Database>) -> Vec<MemoryUnit> {
945 use redb::ReadableDatabase;
946 use redb::ReadableTable;
947 let mut units = Vec::new();
948 let read_txn = db.begin_read().expect("read transaction should succeed");
949 let table = read_txn
950 .open_table(hippmem_store::store::MEMORY_KV)
951 .expect("memory_kv table should exist");
952 let iter = table.iter().expect("iter should succeed");
953 for entry in iter.flatten() {
954 let (_key, value) = entry;
955 if let Ok((unit, _)) = bincode::serde::decode_from_slice::<MemoryUnit, _>(
956 value.value(),
957 bincode::config::standard(),
958 ) {
959 units.push(unit);
960 }
961 }
962 units
963}
964
965fn load_units_by_ids(db: std::sync::Arc<redb::Database>, ids: &[MemoryId]) -> Vec<MemoryUnit> {
967 if ids.is_empty() {
968 return vec![];
969 }
970 use redb::ReadableDatabase;
971 let mut units = Vec::new();
972 let read_txn = db.begin_read().expect("read transaction should succeed");
973 let table = read_txn
974 .open_table(hippmem_store::store::MEMORY_KV)
975 .expect("memory_kv table should exist");
976 for id in ids {
977 if let Some(value) = table.get(id.0).expect("get should succeed") {
978 if let Ok((unit, _)) = bincode::serde::decode_from_slice::<MemoryUnit, _>(
979 value.value(),
980 bincode::config::standard(),
981 ) {
982 units.push(unit);
983 }
984 }
985 }
986 units
987}
988
989fn extract_query_goals(text: &str) -> Vec<String> {
991 let mut goals = Vec::new();
992 for lang in active_locales() {
993 for m in lang.goal_markers {
994 if text.contains(m) {
995 goals.push(format!("goal_marker:{m}"));
996 }
997 }
998 }
999 goals
1000}
1001
1002fn extract_query_events(text: &str) -> Vec<String> {
1004 let mut events = Vec::new();
1005 for lang in active_locales() {
1006 for m in lang.event_markers {
1007 if text.contains(m) {
1008 events.push(format!("event_marker:{m}"));
1009 }
1010 }
1011 }
1012 events
1013}
1014
1015fn is_retrieval_seed_excluded(unit: &MemoryUnit) -> bool {
1026 matches!(unit.lifecycle, MemoryLifecycle::Compressed { .. })
1027 || unit.provenance.generated_by == GeneratedBy::Consolidation
1028}
1029
1030fn load_limited_units(db: std::sync::Arc<redb::Database>, limit: usize) -> Vec<MemoryUnit> {
1031 use redb::ReadableDatabase;
1032 use redb::ReadableTable;
1033 let mut units = Vec::new();
1034 let read_txn = db.begin_read().expect("read transaction should succeed");
1035 let table = read_txn
1036 .open_table(hippmem_store::store::MEMORY_KV)
1037 .expect("memory_kv table should exist");
1038 let iter = table.iter().expect("iter should succeed");
1039 for entry in iter.flatten().take(limit) {
1040 let (_key, value) = entry;
1041 if let Ok((unit, _)) = bincode::serde::decode_from_slice::<MemoryUnit, _>(
1042 value.value(),
1043 bincode::config::standard(),
1044 ) {
1045 units.push(unit);
1046 }
1047 }
1048 units
1049}