2026-06-17 20:50:26 -04:00
|
|
|
use crate::{error, SearchDocument};
|
2026-06-08 19:33:49 -04:00
|
|
|
use crate::schema::Schema;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use tantivy::collector::TopDocs;
|
|
|
|
|
use tantivy::directory::{ManagedDirectory, MmapDirectory};
|
2026-06-09 20:09:16 -04:00
|
|
|
use tantivy::query::{BooleanQuery, Occur, QueryParser, TermQuery};
|
2026-06-17 20:50:26 -04:00
|
|
|
use tantivy::schema::{Field, IndexRecordOption, NamedFieldDocument, Value};
|
|
|
|
|
use tantivy::tokenizer::{LowerCaser, NgramTokenizer, TextAnalyzer, TokenizerManager};
|
|
|
|
|
use tantivy::{Document, Index, IndexReader, IndexWriter, ReloadPolicy, Score, TantivyDocument, Term};
|
2026-06-29 23:10:22 -04:00
|
|
|
use tantivy::indexer::UserOperation;
|
|
|
|
|
use tracing::{info, instrument, span, Level};
|
2026-06-08 19:33:49 -04:00
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
pub struct SearchIndexBuilder {
|
|
|
|
|
path: Option<PathBuf>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SearchIndexBuilder {
|
|
|
|
|
pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
|
|
|
|
|
self.path = Some(path.into());
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn build(self) -> error::Result<SearchIndex> {
|
2026-06-29 15:20:02 -04:00
|
|
|
let ngram_32 = NgramTokenizer::new(1, 32, false)?;
|
|
|
|
|
let ngram_32_lowercase = TextAnalyzer::builder(ngram_32)
|
|
|
|
|
.filter(LowerCaser)
|
|
|
|
|
.build();
|
2026-06-17 20:50:26 -04:00
|
|
|
|
2026-06-29 15:20:02 -04:00
|
|
|
let tokenizer_manager = TokenizerManager::default();
|
|
|
|
|
tokenizer_manager.register("ngram_32", ngram_32_lowercase);
|
2026-06-08 19:33:49 -04:00
|
|
|
|
2026-06-29 15:20:02 -04:00
|
|
|
let builder = Index::builder()
|
|
|
|
|
.schema(Schema::schema().clone())
|
|
|
|
|
.tokenizers(tokenizer_manager);
|
|
|
|
|
|
|
|
|
|
let index = if let Some(path) = self.path {
|
2026-06-08 19:33:49 -04:00
|
|
|
let mmap_directory = MmapDirectory::open(path)?;
|
|
|
|
|
let managed_directory = ManagedDirectory::wrap(Box::new(mmap_directory))?;
|
2026-06-29 15:20:02 -04:00
|
|
|
builder.open_or_create(managed_directory)?
|
2026-06-08 19:33:49 -04:00
|
|
|
} else {
|
2026-06-29 15:20:02 -04:00
|
|
|
builder.create_in_ram()?
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let reader = index
|
|
|
|
|
.reader_builder()
|
|
|
|
|
.reload_policy(ReloadPolicy::OnCommitWithDelay)
|
|
|
|
|
.try_into()?;
|
|
|
|
|
|
|
|
|
|
Ok(SearchIndex {
|
|
|
|
|
index,
|
|
|
|
|
reader,
|
|
|
|
|
})
|
2026-06-08 19:33:49 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:10:22 -04:00
|
|
|
#[derive(Clone)]
|
2026-06-08 19:33:49 -04:00
|
|
|
pub struct SearchIndex {
|
|
|
|
|
index: Index,
|
|
|
|
|
reader: IndexReader,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SearchIndex {
|
|
|
|
|
pub fn builder() -> SearchIndexBuilder {
|
|
|
|
|
SearchIndexBuilder::default()
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:10:22 -04:00
|
|
|
pub fn writer(&self) -> crate::Result<IndexWriter> {
|
|
|
|
|
Ok(self.index.writer(128_000_000)?)
|
2026-06-08 19:33:49 -04:00
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:10:22 -04:00
|
|
|
pub fn remove_all_of_type(&mut self, type_: u64) -> crate::Result<()> {
|
|
|
|
|
let mut writer: IndexWriter<TantivyDocument> = self.index.writer(64_000_000)?;
|
|
|
|
|
writer.delete_term(Term::from_field_u64(Schema::type_field(), type_));
|
|
|
|
|
writer.commit()?;
|
2026-06-08 19:33:49 -04:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn query(
|
|
|
|
|
&self,
|
2026-06-09 20:09:16 -04:00
|
|
|
type_: Option<u64>,
|
2026-06-08 19:33:49 -04:00
|
|
|
user_query: &str,
|
|
|
|
|
default_fields: Vec<Field>,
|
2026-06-17 20:50:26 -04:00
|
|
|
) -> error::Result<Vec<NamedFieldDocument>> {
|
2026-06-29 23:10:22 -04:00
|
|
|
let span = span!(Level::INFO, "Search Query");
|
|
|
|
|
let _enter = span.enter();
|
|
|
|
|
|
2026-06-08 19:33:49 -04:00
|
|
|
let parser = QueryParser::for_index(&self.index, default_fields);
|
|
|
|
|
let (user_query, _) = parser.parse_query_lenient(user_query);
|
2026-06-09 20:09:16 -04:00
|
|
|
|
|
|
|
|
let mut subqueries = vec![
|
|
|
|
|
(Occur::Must, user_query)
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if let Some(type_) = type_ {
|
|
|
|
|
let doc_type_term = Term::from_field_u64(Schema::type_field(), type_);
|
|
|
|
|
let doc_type_query = Box::new(TermQuery::new(doc_type_term, IndexRecordOption::Basic));
|
|
|
|
|
subqueries.push((Occur::Must, doc_type_query));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let query = BooleanQuery::new(subqueries);
|
2026-06-08 19:33:49 -04:00
|
|
|
let searcher = self.reader.searcher();
|
2026-06-17 20:50:26 -04:00
|
|
|
let results = searcher.search(&query, &TopDocs::with_limit(10000).order_by_score())?
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(_, address)| searcher.doc(*address))
|
|
|
|
|
.filter_map(Result::ok)
|
|
|
|
|
.map(|doc: SearchDocument| doc.to_named_doc(Schema::schema()))
|
|
|
|
|
.collect();
|
|
|
|
|
Ok(results)
|
2026-06-08 19:33:49 -04:00
|
|
|
}
|
|
|
|
|
}
|