use super::{
storage_sql_error, Engine, HybridSearchParams, RobustHybridSearchParams, SQLError, ScoredEntry,
};
impl Engine {
pub fn hybrid_search(&self, params: &HybridSearchParams) -> Result<Vec<ScoredEntry>, SQLError> {
let signals = self.build_hybrid_signals(
params.table,
params.text_field,
params.text_query,
params.vector_field,
¶ms.query_vector,
params.knn_pool,
)?;
let tree = uqa_operators::OperatorTree::BayesianEvidenceFusion {
signals,
base_rate: None,
};
let entries =
crate::operator_tree_bridge::execute_scored_tree(self, params.table, &[], &tree)?;
Ok(Self::rank_scored_entries_top_k(entries, params.top_k))
}
pub fn robust_hybrid_search(
&self,
params: &RobustHybridSearchParams,
) -> Result<Vec<ScoredEntry>, SQLError> {
let signals = self.build_hybrid_signals(
params.table,
params.text_field,
params.text_query,
params.vector_field,
¶ms.query_vector,
params.knn_pool,
)?;
let tree = uqa_operators::OperatorTree::RobustPositiveEvidencePool {
signals,
alpha: params.alpha,
gating: uqa_operators::GatingSpec::Softplus,
weights: None,
logit_min: None,
logit_max: None,
adaptive_weights: true,
};
let entries =
crate::operator_tree_bridge::execute_scored_tree(self, params.table, &[], &tree)?;
Ok(Self::rank_scored_entries_top_k(entries, params.top_k))
}
fn build_hybrid_signals(
&self,
table_name: &str,
text_field: &str,
text_query: &str,
vector_field: &str,
query_vector: &[f32],
knn_pool: usize,
) -> Result<Vec<uqa_operators::OperatorTree>, SQLError> {
let Some(table) = self
.try_query_table(table_name)
.map_err(|error| storage_sql_error("resolve hybrid-search table", error))?
else {
return Err(SQLError::UnknownTable(table_name.to_string()));
};
self.validate_text_search_field(table_name, text_field)?;
let analyzer = table.inverted_index.read().get_search_analyzer(text_field);
let analyzed_terms = analyzer
.analyze(text_query)
.map_err(|error| storage_sql_error("analyze hybrid text query", error))?;
let mut signals = Vec::new();
if !analyzed_terms.is_empty() {
signals.push(uqa_operators::OperatorTree::Term {
query: text_query.to_string(),
field: Some(text_field.to_string()),
scoring: Some(uqa_operators::TextScoringMode::BayesianBM25),
top_k: None,
});
}
signals.push(uqa_operators::OperatorTree::CalibratedVectorMatch {
query_vector: query_vector.to_vec(),
k: knn_pool,
field: vector_field.to_string(),
threshold: None,
});
Ok(signals)
}
}