1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//
//! Hybrid text/vector query construction and execution.
use super::{
storage_sql_error, Engine, HybridSearchParams, RobustHybridSearchParams, SQLError, ScoredEntry,
};
impl Engine {
/// Exact single-prior hybrid retrieval under cross-modal conditional
/// independence. Query-level Bayesian BM25 and query-pool-transformed KNN
/// emit signed prior-free evidence, the evidence logits add without
/// gating or confidence scaling, and the resolved corpus relevance prior
/// enters exactly once. Returns the top-`top_k` entries by descending
/// posterior score.
pub fn hybrid_search(&self, params: &HybridSearchParams) -> Result<Vec<ScoredEntry>, SQLError> {
self.with_direct_table_query(params.table, false, |engine, name, _| {
let signals = engine.build_hybrid_signals(
name,
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(
engine,
name,
params.table,
&[],
&tree,
)?;
Ok(uqa_scoring::rank_scored_entries_top_k(
entries,
params.top_k,
))
})
}
/// Explicit robust positive-evidence hybrid ranking. This method applies
/// Softplus gating, confidence scaling, and adaptive query-pool weights;
/// its output is a bounded ranking heuristic rather than the exact
/// single-prior posterior returned by [`Self::hybrid_search`].
pub fn robust_hybrid_search(
&self,
params: &RobustHybridSearchParams,
) -> Result<Vec<ScoredEntry>, SQLError> {
self.with_direct_table_query(params.table, false, |engine, name, _| {
let signals = engine.build_hybrid_signals(
name,
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(
engine,
name,
params.table,
&[],
&tree,
)?;
Ok(uqa_scoring::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()
.search_analyzer_revision(text_field)
.map_err(|error| storage_sql_error("resolve hybrid analyzer revision", error))?;
let analyzed_terms = analyzer
.analyze_tokens(text_query)
.map_err(|error| storage_sql_error("analyze hybrid text query", error))?;
let mut signals = Vec::new();
if !analyzed_terms.tokens().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,
});
}
// The vector field is part of the hybrid API contract even when its
// candidate pool is empty. Always lower the leaf so EngineDriver
// validates field existence, index availability, dimensions, and
// finite query values instead of silently degrading to text-only.
signals.push(uqa_operators::OperatorTree::CalibratedVectorMatch {
query_vector: query_vector.to_vec(),
k: knn_pool,
field: vector_field.to_string(),
threshold: None,
});
Ok(signals)
}
}