use std::path::Path;
use dk_core::{Error, RepoId, Symbol, SymbolId};
use tantivy::collector::TopDocs;
use tantivy::query::{BooleanQuery, Occur, QueryParser, TermQuery};
use tantivy::schema::*;
use tantivy::{Directory, Index, IndexReader, IndexWriter, ReloadPolicy, TantivyDocument};
use uuid::Uuid;
pub struct SearchIndex {
index: Index,
reader: IndexReader,
writer: IndexWriter,
f_symbol_id: Field,
f_repo_id: Field,
f_name: Field,
f_qualified_name: Field,
f_signature: Field,
f_doc_comment: Field,
f_file_path: Field,
f_kind: Field,
}
impl SearchIndex {
pub fn open(path: &Path) -> dk_core::Result<Self> {
let mut schema_builder = Schema::builder();
let f_symbol_id = schema_builder.add_text_field("symbol_id", STRING | STORED);
let f_repo_id = schema_builder.add_text_field("repo_id", STRING);
let f_name = schema_builder.add_text_field("name", TEXT);
let f_qualified_name = schema_builder.add_text_field("qualified_name", TEXT);
let f_signature = schema_builder.add_text_field("signature", TEXT);
let f_doc_comment = schema_builder.add_text_field("doc_comment", TEXT);
let f_file_path = schema_builder.add_text_field("file_path", TEXT);
let f_kind = schema_builder.add_text_field("kind", STRING);
let schema = schema_builder.build();
let dir: Box<dyn Directory> = if path.exists() && path.join("meta.json").exists() {
Box::new(
tantivy::directory::MmapDirectory::open(path)
.map_err(|e| Error::Internal(format!("Failed to open index directory: {e}")))?,
)
} else {
std::fs::create_dir_all(path)?;
Box::new(
tantivy::directory::MmapDirectory::open(path)
.map_err(|e| Error::Internal(format!("Failed to open index directory: {e}")))?,
)
};
let index = Index::open_or_create(dir, schema.clone())
.map_err(|e| Error::Internal(format!("Failed to open or create index: {e}")))?;
let reader = index
.reader_builder()
.reload_policy(ReloadPolicy::OnCommitWithDelay)
.try_into()
.map_err(|e| Error::Internal(format!("Failed to create index reader: {e}")))?;
let writer = index
.writer(50_000_000) .map_err(|e| Error::Internal(format!("Failed to create index writer: {e}")))?;
Ok(Self {
index,
reader,
writer,
f_symbol_id,
f_repo_id,
f_name,
f_qualified_name,
f_signature,
f_doc_comment,
f_file_path,
f_kind,
})
}
pub fn index_symbol(&mut self, repo_id: RepoId, sym: &Symbol) -> dk_core::Result<()> {
let mut doc = TantivyDocument::new();
doc.add_text(self.f_symbol_id, sym.id.to_string());
doc.add_text(self.f_repo_id, repo_id.to_string());
doc.add_text(self.f_name, &sym.name);
doc.add_text(self.f_qualified_name, &sym.qualified_name);
if let Some(ref sig) = sym.signature {
doc.add_text(self.f_signature, sig);
}
if let Some(ref doc_comment) = sym.doc_comment {
doc.add_text(self.f_doc_comment, doc_comment);
}
doc.add_text(self.f_file_path, sym.file_path.to_string_lossy().as_ref());
doc.add_text(self.f_kind, sym.kind.to_string());
self.writer
.add_document(doc)
.map_err(|e| Error::Internal(format!("Failed to add document: {e}")))?;
Ok(())
}
pub fn remove_symbol(&mut self, symbol_id: SymbolId) -> dk_core::Result<()> {
let term = tantivy::Term::from_field_text(self.f_symbol_id, &symbol_id.to_string());
self.writer.delete_term(term);
Ok(())
}
pub fn delete_by_repo(&mut self, repo_id: RepoId) -> dk_core::Result<()> {
let term = tantivy::Term::from_field_text(self.f_repo_id, &repo_id.to_string());
self.writer.delete_term(term);
Ok(())
}
pub fn commit(&mut self) -> dk_core::Result<()> {
self.writer
.commit()
.map_err(|e| Error::Internal(format!("Failed to commit index: {e}")))?;
self.reader
.reload()
.map_err(|e| Error::Internal(format!("Failed to reload reader: {e}")))?;
Ok(())
}
pub fn search(
&self,
repo_id: RepoId,
query: &str,
limit: usize,
) -> dk_core::Result<Vec<SymbolId>> {
let searcher = self.reader.searcher();
let repo_term =
tantivy::Term::from_field_text(self.f_repo_id, &repo_id.to_string());
let repo_query = TermQuery::new(repo_term, IndexRecordOption::Basic);
let text_fields = vec![
self.f_name,
self.f_qualified_name,
self.f_signature,
self.f_doc_comment,
self.f_file_path,
];
let query_parser = QueryParser::for_index(&self.index, text_fields);
let text_query = query_parser
.parse_query(query)
.map_err(|e| Error::Internal(format!("Failed to parse query: {e}")))?;
let combined = BooleanQuery::new(vec![
(Occur::Must, Box::new(repo_query)),
(Occur::Must, text_query),
]);
let top_docs = searcher
.search(&combined, &TopDocs::with_limit(limit))
.map_err(|e| Error::Internal(format!("Search failed: {e}")))?;
let mut results = Vec::with_capacity(top_docs.len());
for (_score, doc_address) in top_docs {
let doc: TantivyDocument = searcher
.doc(doc_address)
.map_err(|e| Error::Internal(format!("Failed to retrieve doc: {e}")))?;
if let Some(id_value) = doc.get_first(self.f_symbol_id) {
if let Some(id_str) = id_value.as_str() {
let uuid = Uuid::parse_str(id_str).map_err(|e| {
Error::Internal(format!("Invalid UUID in index: {e}"))
})?;
results.push(uuid);
}
}
}
Ok(results)
}
}