kevy_window/
text_query.rs1use std::collections::HashMap;
14
15use kevy_text::cold::{decode_fwd, posting_df, score_cold, score_cold_phrase};
16use kevy_text::{CorpusStats, sorted_order};
17
18use super::TextColdDir;
19
20#[derive(Debug)]
22pub struct ColdPageQuery<'a> {
23 pub bare: Vec<Vec<u8>>,
25 pub phrases: Vec<Vec<Vec<u8>>>,
27 pub stats: &'a CorpusStats,
29 pub filter: &'a [kevy_text::Filter<'a>],
31 pub sort: Option<&'a kevy_text::Sort<'a>>,
33 pub distinct: Option<&'a kevy_text::Distinct<'a>>,
35 pub facets: &'a [kevy_text::Facet<'a>],
37 pub fetch: usize,
39}
40
41#[derive(Debug)]
43pub struct ColdHit {
44 pub key: Vec<u8>,
46 pub score: f64,
50 pub okey: Option<Vec<u8>>,
52}
53
54#[derive(Debug)]
56pub struct ColdPage {
57 pub hits: Vec<ColdHit>,
59 pub values: HashMap<Vec<u8>, Vec<Option<Vec<u8>>>>,
62 pub facets: Vec<Vec<kevy_text::Bucket>>,
65}
66
67impl TextColdDir {
68 pub fn cold_stats(&self, tokens: &[Vec<u8>]) -> (u64, u64, Vec<(Vec<u8>, u32)>) {
73 let n_docs: u64 = self.segs.iter().map(|c| c.n_docs).sum();
74 let total_len: u64 = self.segs.iter().map(|c| c.total_len).sum();
75 let df = tokens
76 .iter()
77 .map(|t| {
78 let frozen: u32 = self
79 .segs
80 .iter()
81 .filter_map(|c| c.seg.get(t).ok().flatten())
82 .filter_map(|p| posting_df(&p))
83 .sum();
84 let dead = self.df_dead.get(t).copied().unwrap_or(0);
85 (t.clone(), frozen.saturating_sub(dead))
86 })
87 .collect();
88 (n_docs, total_len, df)
89 }
90
91 pub fn cold_page(&self, q: &ColdPageQuery) -> ColdPage {
94 let acc = self.accumulate(q);
95 let need_values = !q.filter.is_empty()
96 || q.sort.is_some()
97 || q.distinct.is_some()
98 || !q.facets.is_empty();
99 let mut values: HashMap<Vec<u8>, Vec<Option<Vec<u8>>>> = HashMap::new();
100 let mut cands: Vec<ColdHit> = Vec::new();
101 for (key, score) in acc {
102 let vals = if need_values {
103 let Some(v) = self.frozen_values(&key) else { continue };
104 if !passes(&v, q.filter) {
105 continue;
106 }
107 Some(v)
108 } else {
109 None
110 };
111 let okey = q.sort.and_then(|s| vals.as_ref()?.get(s.field)?.as_deref().and_then(s.key));
112 if let Some(v) = vals {
113 values.insert(key.clone(), v);
114 }
115 cands.push(ColdHit { key, score, okey });
116 }
117 let facets = self.count_facets(q, &cands, &values);
118 order_page(&mut cands, q.sort.is_some(), q.sort.is_some_and(|s| s.desc));
119 if let Some(d) = q.distinct {
120 collapse(&mut cands, d, &values);
121 }
122 cands.truncate(q.fetch);
123 values.retain(|k, _| cands.iter().any(|c| &c.key == k));
124 ColdPage { hits: cands, values, facets }
125 }
126
127 fn accumulate(&self, q: &ColdPageQuery) -> HashMap<Vec<u8>, f64> {
130 let mut acc = HashMap::new();
131 for cs in &self.segs {
132 let dead = |k: &[u8]| self.tombs.get(k).is_some_and(|s| s.contains(&cs.seq));
133 for t in &q.bare {
134 if let Ok(Some(payload)) = cs.seg.get(t) {
135 let _ = score_cold(&payload, t, q.stats, &dead, &mut acc);
136 }
137 }
138 for phrase in &q.phrases {
139 let payloads: Option<Vec<Vec<u8>>> =
140 phrase.iter().map(|t| cs.seg.get(t).ok().flatten()).collect();
141 if let Some(payloads) = payloads {
144 let _ = score_cold_phrase(&payloads, phrase, q.stats, &dead, &mut acc);
145 }
146 }
147 }
148 acc
149 }
150
151 fn frozen_values(&self, key: &[u8]) -> Option<Vec<Option<Vec<u8>>>> {
154 let mut fwd_key = vec![0u8];
155 fwd_key.extend_from_slice(key);
156 for cs in &self.segs {
157 if self.tombs.get(key).is_some_and(|s| s.contains(&cs.seq)) {
158 continue;
159 }
160 if let Ok(Some(payload)) = cs.seg.get(&fwd_key) {
161 return decode_fwd(&payload).map(|r| r.values);
162 }
163 }
164 None
165 }
166
167 fn count_facets(
171 &self,
172 q: &ColdPageQuery,
173 cands: &[ColdHit],
174 values: &HashMap<Vec<u8>, Vec<Option<Vec<u8>>>>,
175 ) -> Vec<Vec<kevy_text::Bucket>> {
176 q.facets
177 .iter()
178 .map(|f| {
179 let mut counts: HashMap<Vec<u8>, (Vec<u8>, u64)> = HashMap::new();
180 for c in cands {
181 let Some(raw) =
182 values.get(&c.key).and_then(|v| v.get(f.field)).and_then(Option::as_deref)
183 else {
184 continue;
185 };
186 let Some(k) = (f.key)(raw) else { continue };
187 counts.entry(k).or_insert_with(|| (raw.to_vec(), 0)).1 += 1;
188 }
189 let mut out: Vec<kevy_text::Bucket> =
190 counts.into_iter().map(|(k, (label, n))| (k, label, n)).collect();
191 out.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.1.cmp(&b.1)));
192 out
193 })
194 .collect()
195 }
196}
197
198fn order_page(cands: &mut [ColdHit], sorted: bool, desc: bool) {
202 if sorted {
203 cands.sort_by(|a, b| {
204 sorted_order((a.okey.as_deref(), &a.key), (b.okey.as_deref(), &b.key), desc)
205 });
206 } else {
207 cands.sort_by(|a, b| {
208 b.score
209 .partial_cmp(&a.score)
210 .unwrap_or(std::cmp::Ordering::Equal)
211 .then_with(|| a.key.cmp(&b.key))
212 });
213 }
214}
215
216fn collapse(
220 cands: &mut Vec<ColdHit>,
221 d: &kevy_text::Distinct,
222 values: &HashMap<Vec<u8>, Vec<Option<Vec<u8>>>>,
223) {
224 let mut seen: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
225 cands.retain(|c| {
226 let identity = values
227 .get(&c.key)
228 .and_then(|v| v.get(d.field))
229 .and_then(Option::as_deref)
230 .and_then(d.key);
231 match identity {
232 None => true,
233 Some(id) => seen.insert(id),
234 }
235 });
236}
237
238fn passes(values: &[Option<Vec<u8>>], filter: &[kevy_text::Filter]) -> bool {
241 filter
242 .iter()
243 .all(|f| values.get(f.field).and_then(Option::as_deref).is_some_and(|v| (f.test)(v)))
244}