use std::collections::{HashMap, HashSet};
use anyhow::Result;
use rusqlite::Connection;
use crate::memory::{self, Memory};
pub fn search_multi_hop(
conn: &Connection,
query: &str,
project: Option<&str>,
limit: i64,
) -> Result<MultiHopResult> {
let fetch = limit * 3;
let first_hop = crate::search::search(conn, Some(query), project, None, fetch, 0, true)?;
let first_hop_ids: Vec<i64> = first_hop.iter().map(|m| m.id).collect();
if first_hop.is_empty() {
return Ok(MultiHopResult {
memories: vec![],
hops: 1,
entities_discovered: vec![],
});
}
let mut discovered_entities: Vec<String> = Vec::new();
let mut seen_entities: HashSet<String> = HashSet::new();
let query_entities = crate::entity::extract_entities(query, "");
for e in &query_entities {
seen_entities.insert(e.to_lowercase());
}
for mem in &first_hop {
let entities = crate::entity::extract_entities(&mem.title, &mem.text);
for e in entities {
let lower = e.to_lowercase();
if !seen_entities.contains(&lower) {
seen_entities.insert(lower);
discovered_entities.push(e);
}
}
}
if discovered_entities.is_empty() {
let memories = first_hop.into_iter().take(limit as usize).collect();
return Ok(MultiHopResult {
memories,
hops: 1,
entities_discovered: vec![],
});
}
let first_hop_set: HashSet<i64> = first_hop_ids.iter().copied().collect();
let mut second_hop_ids: Vec<i64> = Vec::new();
for entity_name in &discovered_entities {
let entity_results = crate::entity::search_by_entity(conn, entity_name, fetch)?;
for id in entity_results {
if !first_hop_set.contains(&id) && !second_hop_ids.contains(&id) {
second_hop_ids.push(id);
}
}
}
for entity_name in &discovered_entities {
let safe_query = format!("\"{}\"", entity_name.replace('"', "\"\""));
if let Ok(fts_results) =
memory::search_memories_fts(conn, &safe_query, project, None, fetch, 0)
{
for mem in fts_results {
if !first_hop_set.contains(&mem.id) && !second_hop_ids.contains(&mem.id) {
second_hop_ids.push(mem.id);
}
}
}
}
if second_hop_ids.is_empty() {
let memories = first_hop.into_iter().take(limit as usize).collect();
return Ok(MultiHopResult {
memories,
hops: 1,
entities_discovered: discovered_entities,
});
}
let mut scores: HashMap<i64, f64> = HashMap::new();
let k = 60.0;
for (rank, id) in first_hop_ids.iter().enumerate() {
*scores.entry(*id).or_default() += 1.0 / (k + rank as f64 + 1.0);
}
for (rank, id) in second_hop_ids.iter().enumerate() {
*scores.entry(*id).or_default() += 0.5 / (k + rank as f64 + 1.0);
}
let mut ranked: Vec<(i64, f64)> = scores.into_iter().collect();
ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let top_ids: Vec<i64> = ranked.iter().take(limit as usize).map(|(id, _)| *id).collect();
let loaded = memory::get_memories_by_ids(conn, &top_ids, None)?;
let id_to_mem: HashMap<i64, Memory> = loaded.into_iter().map(|m| (m.id, m)).collect();
let memories = top_ids
.iter()
.filter_map(|id| id_to_mem.get(id).cloned())
.collect();
Ok(MultiHopResult {
memories,
hops: 2,
entities_discovered: discovered_entities,
})
}
pub struct MultiHopResult {
pub memories: Vec<Memory>,
pub hops: u8,
pub entities_discovered: Vec<String>,
}