1use std::path::Path;
7use anyhow::Result;
8
9use crate::model::CodeNode;
10use super::store::SearchStore;
11
12pub struct TextEngine {
17 store: SearchStore,
18}
19
20impl TextEngine {
21 pub fn open(path: impl AsRef<Path>) -> Result<(Self, bool)> {
28 let (store, need_reindex) = SearchStore::open(path)?;
29 Ok((Self { store }, need_reindex))
30 }
31
32 pub fn index(&mut self, node: &CodeNode, source_code: &str) -> Result<()> {
34 self.store.insert_entities_batch(&[(node.clone(), source_code.to_string())])
35 }
36
37 pub fn index_batch(&mut self, items: &[(CodeNode, String)]) -> Result<()> {
39 self.store.insert_entities_batch(items)
40 }
41
42 pub fn search(&self, query: &str, limit: usize) -> Result<Vec<(CodeNode, f64)>> {
44 if query.is_empty() {
45 return Ok(Vec::new());
46 }
47 self.store.search_fts(query, limit)
48 }
49
50 pub fn remove_by_file(&mut self, file_path: &str) -> Result<usize> {
52 self.store.delete_entities_by_file(file_path)
53 }
54
55 pub fn clear(&mut self) -> Result<()> {
57 self.store.clear_entities()
58 }
59
60 pub fn doc_count(&self) -> usize {
62 self.store.entity_count().unwrap_or(0)
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use crate::model::{NodeId, NodeKind};
70
71 fn make_node(name: &str, kind: NodeKind) -> CodeNode {
72 CodeNode {
73 id: NodeId::new(0), kind, name: name.into(),
74 file_path: Some("src/test.rs".into()),
75 line_range: Some((1, 5)),
76 doc_comment: None,
77 signature: Some(format!("fn {}()", name)), visibility: None,
78 module_path: vec![],
79 }
80 }
81
82 fn tmp_path(label: &str) -> std::path::PathBuf {
83 use std::sync::atomic::{AtomicU64, Ordering};
84 static COUNTER: AtomicU64 = AtomicU64::new(0);
85 let mut p = std::env::temp_dir();
86 p.push(format!("text_fts_{}_{}.db", label, COUNTER.fetch_add(1, Ordering::Relaxed)));
87 let _ = std::fs::remove_file(&p);
88 p
89 }
90
91 #[test]
92 fn test_index_and_search() -> Result<()> {
93 let (mut engine, _) = TextEngine::open(tmp_path("index_search"))?;
94 engine.index(&make_node("add_user", NodeKind::Function), "fn add_user(name: &str)")?;
95 engine.index(&make_node("delete_user", NodeKind::Function), "fn delete_user(id: u64)")?;
96 let results = engine.search("add_user", 10)?;
97 assert!(!results.is_empty());
98 assert!(results[0].0.name.contains("add_user"));
99 Ok(())
100 }
101
102 #[test]
103 fn test_empty_engine() -> Result<()> {
104 let (engine, _) = TextEngine::open(tmp_path("empty"))?;
105 assert!(engine.search("anything", 10)?.is_empty());
106 Ok(())
107 }
108
109 #[test]
110 fn test_persistence() -> Result<()> {
111 let path = tmp_path("persist");
112 {
113 let (mut engine, _) = TextEngine::open(&path)?;
114 engine.index(&make_node("persist_test", NodeKind::Function), "fn test()")?;
115 }
116 let (engine, _) = TextEngine::open(&path)?;
117 assert_eq!(engine.doc_count(), 1);
118 let results = engine.search("persist_test", 10)?;
119 assert!(!results.is_empty());
120 Ok(())
121 }
122
123 #[test]
124 fn test_clear() -> Result<()> {
125 let (mut engine, _) = TextEngine::open(tmp_path("clear"))?;
126 engine.index(&make_node("x", NodeKind::Function), "")?;
127 assert_eq!(engine.doc_count(), 1);
128 engine.clear()?;
129 assert_eq!(engine.doc_count(), 0);
130 Ok(())
131 }
132
133 #[test]
134 fn test_remove_by_file() -> Result<()> {
135 let (mut engine, _) = TextEngine::open(tmp_path("remove"))?;
136 let node_a = CodeNode {
137 id: NodeId::new(0), kind: NodeKind::Function,
138 name: "alpha_unique".into(),
139 file_path: Some("src/alpha.rs".into()),
140 line_range: Some((1, 3)), doc_comment: None,
141 signature: None, module_path: vec![], visibility: None,
142 };
143 let node_b = CodeNode {
144 id: NodeId::new(1), kind: NodeKind::Function,
145 name: "beta_unique".into(),
146 file_path: Some("src/beta.rs".into()),
147 line_range: Some((1, 3)), doc_comment: None,
148 signature: None, module_path: vec![], visibility: None,
149 };
150 engine.index_batch(&[(node_a, "alpha".into()), (node_b, "beta".into())])?;
151 assert_eq!(engine.doc_count(), 2);
152
153 let removed = engine.remove_by_file("src/alpha.rs")?;
154 assert_eq!(removed, 1);
155 assert_eq!(engine.doc_count(), 1);
156 Ok(())
157 }
158
159 #[test]
163 fn test_short_keyword_baseline() -> Result<()> {
164 let (mut engine, _) = TextEngine::open(tmp_path("short_keyword"))?;
165 engine.index(&make_node("a_helper", NodeKind::Function), "fn a_helper(x: u32)")?;
166 engine.index(&make_node("udp_send", NodeKind::Function), "fn udp_send(sock: u32)")?;
167 let short = engine.search("a", 10)?;
169 assert!(
170 short.iter().any(|(n, _)| n.name == "a_helper"),
171 "1 字符 token 查询应命中 a_helper"
172 );
173 let two = engine.search("udp", 10)?;
175 assert!(two.iter().any(|(n, _)| n.name == "udp_send"), "2 字符 token 应命中");
176 Ok(())
177 }
178}