use std::path::Path;
use anyhow::Result;
use crate::model::CodeNode;
use super::store::SearchStore;
pub struct TextEngine {
store: SearchStore,
}
impl TextEngine {
pub fn open(path: impl AsRef<Path>) -> Result<(Self, bool)> {
let (store, need_reindex) = SearchStore::open(path)?;
Ok((Self { store }, need_reindex))
}
pub fn index(&mut self, node: &CodeNode, source_code: &str) -> Result<()> {
self.store.insert_entities_batch(&[(node.clone(), source_code.to_string())])
}
pub fn index_batch(&mut self, items: &[(CodeNode, String)]) -> Result<()> {
self.store.insert_entities_batch(items)
}
pub fn search(&self, query: &str, limit: usize) -> Result<Vec<(CodeNode, f64)>> {
if query.is_empty() {
return Ok(Vec::new());
}
self.store.search_fts(query, limit)
}
pub fn remove_by_file(&mut self, file_path: &str) -> Result<usize> {
self.store.delete_entities_by_file(file_path)
}
pub fn clear(&mut self) -> Result<()> {
self.store.clear_entities()
}
pub fn doc_count(&self) -> usize {
self.store.entity_count().unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{NodeId, NodeKind};
fn make_node(name: &str, kind: NodeKind) -> CodeNode {
CodeNode {
id: NodeId::new(0), kind, name: name.into(),
file_path: Some("src/test.rs".into()),
line_range: Some((1, 5)),
doc_comment: None,
signature: Some(format!("fn {}()", name)), visibility: None,
module_path: vec![],
}
}
fn tmp_path(label: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut p = std::env::temp_dir();
p.push(format!("text_fts_{}_{}.db", label, COUNTER.fetch_add(1, Ordering::Relaxed)));
let _ = std::fs::remove_file(&p);
p
}
#[test]
fn test_index_and_search() -> Result<()> {
let (mut engine, _) = TextEngine::open(tmp_path("index_search"))?;
engine.index(&make_node("add_user", NodeKind::Function), "fn add_user(name: &str)")?;
engine.index(&make_node("delete_user", NodeKind::Function), "fn delete_user(id: u64)")?;
let results = engine.search("add_user", 10)?;
assert!(!results.is_empty());
assert!(results[0].0.name.contains("add_user"));
Ok(())
}
#[test]
fn test_empty_engine() -> Result<()> {
let (engine, _) = TextEngine::open(tmp_path("empty"))?;
assert!(engine.search("anything", 10)?.is_empty());
Ok(())
}
#[test]
fn test_persistence() -> Result<()> {
let path = tmp_path("persist");
{
let (mut engine, _) = TextEngine::open(&path)?;
engine.index(&make_node("persist_test", NodeKind::Function), "fn test()")?;
}
let (engine, _) = TextEngine::open(&path)?;
assert_eq!(engine.doc_count(), 1);
let results = engine.search("persist_test", 10)?;
assert!(!results.is_empty());
Ok(())
}
#[test]
fn test_clear() -> Result<()> {
let (mut engine, _) = TextEngine::open(tmp_path("clear"))?;
engine.index(&make_node("x", NodeKind::Function), "")?;
assert_eq!(engine.doc_count(), 1);
engine.clear()?;
assert_eq!(engine.doc_count(), 0);
Ok(())
}
#[test]
fn test_remove_by_file() -> Result<()> {
let (mut engine, _) = TextEngine::open(tmp_path("remove"))?;
let node_a = CodeNode {
id: NodeId::new(0), kind: NodeKind::Function,
name: "alpha_unique".into(),
file_path: Some("src/alpha.rs".into()),
line_range: Some((1, 3)), doc_comment: None,
signature: None, module_path: vec![], visibility: None,
};
let node_b = CodeNode {
id: NodeId::new(1), kind: NodeKind::Function,
name: "beta_unique".into(),
file_path: Some("src/beta.rs".into()),
line_range: Some((1, 3)), doc_comment: None,
signature: None, module_path: vec![], visibility: None,
};
engine.index_batch(&[(node_a, "alpha".into()), (node_b, "beta".into())])?;
assert_eq!(engine.doc_count(), 2);
let removed = engine.remove_by_file("src/alpha.rs")?;
assert_eq!(removed, 1);
assert_eq!(engine.doc_count(), 1);
Ok(())
}
#[test]
fn test_short_keyword_baseline() -> Result<()> {
let (mut engine, _) = TextEngine::open(tmp_path("short_keyword"))?;
engine.index(&make_node("a_helper", NodeKind::Function), "fn a_helper(x: u32)")?;
engine.index(&make_node("udp_send", NodeKind::Function), "fn udp_send(sock: u32)")?;
let short = engine.search("a", 10)?;
assert!(
short.iter().any(|(n, _)| n.name == "a_helper"),
"1 字符 token 查询应命中 a_helper"
);
let two = engine.search("udp", 10)?;
assert!(two.iter().any(|(n, _)| n.name == "udp_send"), "2 字符 token 应命中");
Ok(())
}
}