use anyhow::Result;
use sts;
use tantivy::{
IndexReader, TantivyDocument, Term,
collector::{Count, TopDocs},
query::{BooleanQuery, QueryParser, TermQuery},
schema::{IndexRecordOption, Value},
snippet::SnippetGenerator,
};
use crate::doc::{Doc, FIELD};
fn u64_from_doc(doc: &TantivyDocument, field: tantivy::schema::Field) -> u64 {
doc
.get_first(field)
.and_then(|v| v.as_u64())
.unwrap_or_default()
}
fn str_from_doc(doc: &TantivyDocument, field: tantivy::schema::Field) -> String {
doc
.get_first(field)
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string()
}
fn li_from_doc(doc: &TantivyDocument, field: tantivy::schema::Field) -> Vec<String> {
doc
.get_all(field)
.filter_map(|v| v.as_str().map(String::from))
.collect()
}
pub struct Searcher {
pub reader: IndexReader,
pub parser: QueryParser,
pub ts: u64,
}
pub type Snippet = String;
#[derive(Debug)]
pub struct SearchResult {
pub li: Vec<(Doc, Snippet)>,
pub count: usize,
}
#[derive(Debug)]
pub struct Query {
pub query: String,
pub uid: u64,
pub org_id: u64,
pub repo_id_li: Vec<u64>,
pub tag_li: Vec<String>,
pub ts_begin: Option<u64>,
pub ts_end: Option<u64>,
pub limit: usize,
pub offset: usize,
pub snippet_max_num_chars: u64,
}
impl Default for Query {
fn default() -> Self {
Self {
query: String::default(),
uid: u64::default(),
org_id: u64::default(),
repo_id_li: Vec::default(),
tag_li: Vec::default(),
ts_begin: Option::default(),
ts_end: Option::default(),
limit: 10,
offset: usize::default(),
snippet_max_num_chars: 300,
}
}
}
impl Searcher {
pub fn new(reader: IndexReader, parser: QueryParser) -> Self {
Self {
reader,
parser,
ts: sts::sec(),
}
}
pub fn search(&mut self, q: Query) -> Result<SearchResult> {
let now = sts::sec();
if now > self.ts {
self.reader.reload()?;
self.ts = now;
}
let searcher = self.reader.searcher();
let mut query_li = vec![];
let query = q.query;
if !query.is_empty() {
query_li.push(self.parser.parse_query(&query)?);
}
for (val, field) in [(q.uid, FIELD.uid), (q.org_id, FIELD.org_id)] {
if val > 0 {
query_li.push(Box::new(TermQuery::new(
Term::from_field_u64(field, val),
IndexRecordOption::Basic,
)));
}
}
{
let mut repo_id_filter = vec![];
for repo_id_val in q.repo_id_li {
repo_id_filter.push(Box::new(TermQuery::new(
Term::from_field_u64(FIELD.repo_id, repo_id_val),
IndexRecordOption::Basic,
)) as Box<dyn tantivy::query::Query>);
}
if !repo_id_filter.is_empty() {
query_li.push(Box::new(BooleanQuery::union(repo_id_filter)));
}
}
for tag_val in q.tag_li {
query_li.push(Box::new(TermQuery::new(
Term::from_field_text(FIELD.tag_li, tag_val.as_ref()),
IndexRecordOption::Basic,
)));
}
if let (Some(ts_b), Some(ts_e)) = (q.ts_begin, q.ts_end) {
query_li.push(self.parser.parse_query(&format!("ts:[{ts_b} TO {ts_e}]"))?);
} else if let Some(ts_b) = q.ts_begin {
query_li.push(self.parser.parse_query(&format!("ts:[{ts_b} TO *]"))?);
} else if let Some(ts_e) = q.ts_end {
query_li.push(self.parser.parse_query(&format!("ts:[* TO {ts_e}]"))?);
}
let boolean_query = BooleanQuery::intersection(query_li);
let limit = TopDocs::with_limit(q.limit);
let (top_li, count) = if q.offset == 0 {
searcher.search(&boolean_query, &(limit, Count))?
} else {
(
searcher.search(&boolean_query, &limit.and_offset(q.offset))?,
0,
)
};
let mut li = Vec::with_capacity(top_li.len());
let query_for_snippet = self.parser.parse_query(query.as_ref())?;
let mut snippet_generator =
SnippetGenerator::create(&searcher, &*query_for_snippet, FIELD.txt)?;
snippet_generator.set_max_num_chars(q.snippet_max_num_chars as usize);
for (_score, doc_address) in &top_li {
let retrieved_doc = searcher.doc::<TantivyDocument>(*doc_address)?;
let doc = Doc {
id: u64_from_doc(&retrieved_doc, FIELD.id),
ts: u64_from_doc(&retrieved_doc, FIELD.ts),
uid: u64_from_doc(&retrieved_doc, FIELD.uid),
org_id: u64_from_doc(&retrieved_doc, FIELD.org_id),
repo_id: u64_from_doc(&retrieved_doc, FIELD.repo_id),
tag_li: li_from_doc(&retrieved_doc, FIELD.tag_li),
title: str_from_doc(&retrieved_doc, FIELD.title),
txt: str_from_doc(&retrieved_doc, FIELD.txt),
};
let snippet = snippet_generator.snippet_from_doc(&retrieved_doc);
li.push((doc, snippet.to_html()));
}
Ok(SearchResult { li, count })
}
}