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.
//
//! Physical planning for score-ordered text limits.
use uqa_operators::{OperatorTree, TextTopKPlan, TextTopKStrategy};
/// Storage and query facts needed to choose an exact text top-k algorithm.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextTopKCapabilities {
/// Number of analyzed query-term occurrences. Occurrences, rather than
/// unique terms, matter because duplicate query terms contribute twice.
pub analyzed_term_count: usize,
pub indexed_document_count: u64,
}
/// Push a score limit into a simple, field-bound text leaf.
///
/// Boolean/fusion trees deliberately remain exhaustive: cutting a child before
/// its parent changes the carrier. Single-term and effectively unbounded
/// searches also remain exhaustive because WAND cannot prune them profitably.
#[must_use]
pub fn plan_text_top_k(
tree: OperatorTree,
k: usize,
capabilities: TextTopKCapabilities,
) -> OperatorTree {
let (query, field, scoring, top_k) = match tree {
OperatorTree::Term {
query,
field,
scoring,
top_k,
} => (query, field, scoring, top_k),
other => return other,
};
let eligible = field.is_some()
&& scoring.is_some()
&& top_k.is_none()
&& capabilities.analyzed_term_count >= 2
&& (k == 0 || (k as u128) < u128::from(capabilities.indexed_document_count));
if !eligible {
return OperatorTree::Term {
query,
field,
scoring,
top_k,
};
}
OperatorTree::Term {
query,
field,
scoring,
// Execution validates scorer-versioned block bounds against the same transaction snapshot and falls back to exact WAND when unavailable.
top_k: Some(TextTopKPlan {
k,
strategy: TextTopKStrategy::BlockMaxWand,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use uqa_operators::TextScoringMode;
#[test]
fn phrase_support_is_not_cut_off_by_bag_of_terms_top_k() {
let planned = plan_text_top_k(
OperatorTree::Phrase {
query: "red fox".into(),
field: Some("body".into()),
scoring: Some(TextScoringMode::BM25),
},
1,
TextTopKCapabilities {
analyzed_term_count: 2,
indexed_document_count: 100,
},
);
assert!(matches!(planned, OperatorTree::Phrase { query, .. } if query == "red fox"));
}
fn term() -> OperatorTree {
OperatorTree::Term {
query: "rust search".into(),
field: Some("body".into()),
scoring: Some(TextScoringMode::BM25),
top_k: None,
}
}
#[test]
fn eligible_query_defers_block_validation_to_execution() {
let planned = plan_text_top_k(
term(),
10,
TextTopKCapabilities {
analyzed_term_count: 2,
indexed_document_count: 100,
},
);
assert!(matches!(
planned,
OperatorTree::Term {
top_k: Some(TextTopKPlan {
strategy: TextTopKStrategy::BlockMaxWand,
..
}),
..
}
));
}
#[test]
fn single_term_and_unbounded_inputs_stay_exhaustive() {
for (term_count, k, documents) in [(1, 10, 100), (2, 100, 100)] {
let planned = plan_text_top_k(
term(),
k,
TextTopKCapabilities {
analyzed_term_count: term_count,
indexed_document_count: documents,
},
);
assert!(matches!(planned, OperatorTree::Term { top_k: None, .. }));
}
}
}