use std::sync::Arc;
use nodedb_physical::physical_plan::TextScoreSpec;
use nodedb_types::result::QueryResult;
use nodedb_types::text_search::TextSearchParams;
use nodedb_types::value::Value;
use crate::engine::fts::{TextSearchRequest, run_text_search};
use crate::error::LiteError;
use crate::nodedb::lock_ext::LockExt;
use crate::query::engine::LiteQueryEngine;
use crate::storage::engine::StorageEngine;
use super::super::adapter::LitePhysicalFut;
use super::rows::{TextRowGate, admitted_ids, is_eligible};
use super::scores::ScoreColumns;
pub(super) struct SearchArgs<'o> {
pub collection: &'o str,
pub field: &'o str,
pub query: &'o str,
pub top_k: usize,
pub params: TextSearchParams,
pub filters: &'o [u8],
pub rls_filters: &'o [u8],
pub scores: &'o [TextScoreSpec],
}
pub(super) struct PhraseArgs<'o> {
pub collection: &'o str,
pub field: &'o str,
pub terms: &'o [String],
pub top_k: usize,
pub filters: &'o [u8],
pub rls_filters: &'o [u8],
pub scores: &'o [TextScoreSpec],
}
fn hit_result(hits: Vec<(String, f64)>, scores: &ScoreColumns) -> QueryResult {
let mut columns = vec!["id".to_string(), "score".to_string()];
columns.extend(scores.aliases());
let rows = hits
.into_iter()
.map(|(id, score)| {
let score_values = scores.values(&id);
let mut row = Vec::with_capacity(2 + score_values.len());
row.push(Value::String(id));
row.push(Value::Float(score));
row.extend(score_values);
row
})
.collect();
QueryResult {
columns,
rows,
rows_affected: 0,
command: None,
}
}
pub(super) fn text_search<'a, S: StorageEngine + 'a>(
engine: &'a LiteQueryEngine<S>,
args: SearchArgs<'_>,
) -> Result<LitePhysicalFut<'a>, LiteError> {
let gate = TextRowGate::new(args.filters, args.rls_filters)?;
let collection = args.collection.to_owned();
let field = args.field.to_owned();
let query = args.query.to_owned();
let top_k = args.top_k;
let params = args.params;
let specs = args.scores.to_vec();
let fts_state = Arc::clone(&engine.fts_state);
let crdt = Arc::clone(&engine.crdt);
Ok(Box::pin(async move {
let admitted = admitted_ids(&crdt, &collection, &gate)?;
let results = run_text_search(
&fts_state,
&crdt,
TextSearchRequest {
collection: &collection,
field: &field,
query: &query,
top_k,
params: ¶ms,
allowed_ids: admitted.as_ref(),
},
)?;
let scores = ScoreColumns::compute(&fts_state, &collection, &specs, admitted.as_ref())?;
Ok(hit_result(
results
.into_iter()
.map(|r| (r.id, f64::from(1.0 - r.distance)))
.collect(),
&scores,
))
}))
}
pub(super) fn phrase_search<'a, S: StorageEngine + 'a>(
engine: &'a LiteQueryEngine<S>,
args: PhraseArgs<'_>,
) -> Result<LitePhysicalFut<'a>, LiteError> {
let gate = TextRowGate::new(args.filters, args.rls_filters)?;
let collection = args.collection.to_owned();
let field = args.field.to_owned();
let terms = args.terms.to_vec();
let top_k = args.top_k;
let specs = args.scores.to_vec();
let fts_state = Arc::clone(&engine.fts_state);
let crdt = Arc::clone(&engine.crdt);
Ok(Box::pin(async move {
let admitted = admitted_ids(&crdt, &collection, &gate)?;
let params = TextSearchParams::default();
let mut hits: Vec<(String, f64)> = fts_state
.manager
.lock_or_recover()
.phrase_search(&collection, &field, &terms, usize::MAX, ¶ms)?
.into_iter()
.filter(|r| is_eligible(admitted.as_ref(), &r.doc_id))
.map(|r| (r.doc_id, f64::from(r.score)))
.collect();
hits.truncate(top_k);
let scores = ScoreColumns::compute(&fts_state, &collection, &specs, admitted.as_ref())?;
Ok(hit_result(hits, &scores))
}))
}