use indexmap::IndexMap;
use std::collections::BTreeMap;
use crate::lexer::TokenLexer;
use crate::query::{QueryMatchScore, QuerySearchID, QuerySearchLimit, QuerySearchOffset};
use crate::store::StoreItem;
use crate::store::fst::{StoreFSTActionBuilder, typo_factor};
use crate::store::identifiers::{StoreObjectIID, StoreTermHash, StoreTermHashed};
use crate::store::kv::{StoreKVAcquireMode, StoreKVAction, StoreKVActionBuilder};
const MISSING_MATCH_SCORE: u16 = 100;
impl super::Executor {
pub fn search(
&self,
item: StoreItem,
_event_id: QuerySearchID,
lexer: TokenLexer,
limit: QuerySearchLimit,
offset: QuerySearchOffset,
) -> Result<Vec<String>, ()> {
if let StoreItem(collection, Some(bucket), None) = item {
let _kv_read_guard = self.kv_pool.lock_read_access();
let _fst_read_guard = self.fst_pool.lock_read_access();
let (Ok(kv_store), Ok(fst_store)) = (
self.kv_pool
.acquire(StoreKVAcquireMode::OpenOnly, collection),
self.fst_pool.acquire(collection, bucket),
) else {
return Err(());
};
let (higher_limit, mut alternates_try) = (
self.app_conf.store.kv.retain_word_objects,
self.app_conf.search.query_alternates_try,
);
let (prefix_matching_enabled, fuzzy_matching_enabled) = (
self.fst_pool.fst_action_config.prefix_matching_enabled,
self.fst_pool.fst_action_config.fuzzy_matching_enabled,
);
executor_kv_lock_read!(kv_store);
let (kv_action, fst_action) = (
StoreKVActionBuilder::access(bucket, kv_store),
StoreFSTActionBuilder::access(fst_store),
);
let terms: Vec<(String, StoreTermHashed, usize)> = lexer.collect();
let term_count = terms.len();
let mut scoring_matrix: IndexMap<StoreObjectIID, Vec<QueryMatchScore>> =
IndexMap::with_capacity(24usize.min(usize::from(limit)));
'matches: for (idx, (term, term_hash, _)) in terms.iter().enumerate() {
let iids = kv_action
.get_term_to_iids(*term_hash)
.unwrap_or(None)
.unwrap_or_default();
tracing::debug!("got exact search executor iids: {iids:?} for term: {term:?}");
for iid in iids.into_iter() {
let inserted = update_score(&mut scoring_matrix, iid, 0, idx, term_count);
if inserted {
if scoring_matrix.len() >= higher_limit {
tracing::trace!(?term, "got enough completed results for term");
break 'matches;
}
}
}
}
#[cfg(debug_assertions)]
tracing::debug!(?scoring_matrix);
if scoring_matrix.len() < higher_limit && alternates_try > 0 && prefix_matching_enabled
{
tracing::debug!(
"not enough iids were found ({}/{higher_limit}), looking for prefixes",
scoring_matrix.len(),
);
'terms: for (idx, (term, _, original_len)) in terms.iter().enumerate() {
let Some(suggestions) = fst_action.lookup_begins(term, *original_len) else {
tracing::trace!("did not get any completed word for term {term:?}");
continue 'terms;
};
merge_suggestions(
suggestions,
&mut scoring_matrix,
term,
idx,
term_count,
&kv_action,
&mut alternates_try,
higher_limit,
);
}
}
#[cfg(debug_assertions)]
tracing::debug!(?scoring_matrix);
if scoring_matrix.len() < higher_limit && alternates_try > 0 && fuzzy_matching_enabled {
tracing::debug!(
"not enough iids were found ({}/{higher_limit}), looking for fuzzy matches",
scoring_matrix.len(),
);
'terms: for (idx, (term, _, original_word_len)) in terms.iter().enumerate() {
let max_typo_factor = typo_factor(*original_word_len);
let mut typo_factor = 1u32;
while alternates_try > 0 && typo_factor <= max_typo_factor {
let Some(suggestions) = fst_action.lookup_typos(term, typo_factor) else {
tracing::trace!("did not get any completed word for term {term:?}");
continue 'terms;
};
merge_suggestions(
suggestions,
&mut scoring_matrix,
term,
idx,
term_count,
&kv_action,
&mut alternates_try,
higher_limit,
);
typo_factor += 1;
}
}
}
#[cfg(debug_assertions)]
tracing::debug!(?scoring_matrix);
let found_iids = scoring_matrix
.into_iter()
.map(|(iid, scores)| (iid, scores.into_iter().sum()));
let all_iids = sorted_groups(found_iids).flat_map(|(_, v)| v);
let (limit_usize, offset_usize) = (limit as usize, offset as usize);
let mut result_oids = Vec::with_capacity(limit_usize);
'paging: for (index, found_iid) in all_iids.skip(offset_usize).enumerate() {
if index >= limit_usize {
break 'paging;
}
if let Ok(Some(oid)) = kv_action.get_iid_to_oid(found_iid) {
result_oids.push(oid);
} else {
tracing::error!("failed getting search executor iid-to-oid");
}
}
tracing::info!("got search executor final oids: {:?}", result_oids);
return Ok(result_oids);
}
Err(())
}
}
#[allow(clippy::too_many_arguments)] fn merge_suggestions(
suggestions: impl Iterator<Item = (String, QueryMatchScore)>,
scoring_matrix: &mut IndexMap<StoreObjectIID, Vec<QueryMatchScore>>,
term: &String,
term_idx: usize,
term_count: usize,
kv_action: &StoreKVAction<'_>,
alternates_try: &mut usize,
higher_limit: usize,
) {
'suggestions: for (suggested_word, suggestion_score) in suggestions {
if suggested_word.eq(term) {
continue;
}
tracing::trace!(?term, ?suggested_word, "got completed word for term");
let suggested_term_hash = StoreTermHash::from(&suggested_word);
let suggested_iids = match kv_action.get_term_to_iids(suggested_term_hash) {
Ok(Some(suggested_iids)) => suggested_iids,
Ok(None) => continue,
Err(_) => continue,
};
for suggested_iid in suggested_iids.into_iter().take(*alternates_try) {
*alternates_try = unsafe { alternates_try.unchecked_sub(1) };
let inserted = update_score(
scoring_matrix,
suggested_iid,
suggestion_score,
term_idx,
term_count,
);
if inserted {
if scoring_matrix.len() >= higher_limit {
tracing::trace!(?term, "got enough completed results for term");
break 'suggestions;
}
}
}
}
tracing::trace!(
?term,
"done completing results for term, now {} total results",
scoring_matrix.len()
);
}
fn update_score(
scoring_matrix: &mut IndexMap<StoreObjectIID, Vec<QueryMatchScore>>,
iid: StoreObjectIID,
score: QueryMatchScore,
term_idx: usize,
term_count: usize,
) -> bool {
match scoring_matrix.entry(iid) {
indexmap::map::Entry::Occupied(mut occupied_entry) => {
let entry_score = unsafe { occupied_entry.get_mut().get_unchecked_mut(term_idx) };
let new_score = score.min(*entry_score);
tracing::trace!(entry_score, new_score, "Updating to min score");
*entry_score = new_score;
false
}
indexmap::map::Entry::Vacant(vacant_entry) => {
let mut scores = vec![MISSING_MATCH_SCORE; term_count];
tracing::trace!(new_score = score, "Inserting new score");
unsafe { *scores.get_unchecked_mut(term_idx) = score };
vacant_entry.insert(scores);
true
}
}
}
fn sorted_groups(
map: impl ExactSizeIterator<Item = (StoreObjectIID, QueryMatchScore)>,
) -> impl ExactSizeIterator<Item = (QueryMatchScore, Vec<StoreObjectIID>)> {
let mut btree: BTreeMap<QueryMatchScore, Vec<StoreObjectIID>> = BTreeMap::new();
for (k, v) in map.into_iter() {
btree.entry(v).or_default().push(k);
}
btree.into_iter()
}