1use kevy_index::IndexSpec;
6
7use super::claused::{ValueFilter, unknown_field, value_test};
8use super::{FieldSpans, HighlightedHit, sync_segs};
9use crate::store::{Store, lock_write};
10use crate::{KevyError, KevyResult};
11
12#[derive(Debug, Clone, Copy, Default)]
19pub struct MatchOpts<'a> {
20 pub highlight: Option<&'a [Vec<u8>]>,
23 pub typo: u32,
25 pub offset: usize,
27 pub scope: &'a [Vec<u8>],
30 pub filters: &'a [ValueFilter<'a>],
34 pub sort: Option<(&'a [u8], bool)>,
39 pub distinct: Option<&'a [u8]>,
43 pub facets: &'a [Vec<u8>],
46}
47
48impl Store {
49 pub fn idx_match_with(
57 &self,
58 name: &[u8],
59 query: &[u8],
60 limit: usize,
61 opts: MatchOpts<'_>,
62 ) -> KevyResult<Vec<HighlightedHit>> {
63 self.idx_match_faceted(name, query, limit, opts).map(|p| p.hits)
64 }
65
66 pub(crate) fn match_faceted_run(
69 &self,
70 name: &[u8],
71 query: &[u8],
72 limit: usize,
73 opts: MatchOpts<'_>,
74 ) -> KevyResult<MatchPage> {
75 let (limit, offset) = (limit.clamp(1, 1000), opts.offset.min(10_000));
76 let fetch = limit + offset;
78 super::text_cold::cold_refusal(self.text_has_cold(name), query, opts.typo, opts.scope)?;
79 let (scope, tests) = self.resolve_clauses(name, opts.scope, opts.filters)?;
80 let sorted = self.sort_field(name, opts.sort)?;
81 let fkeys = self.facet_keys(name, opts.facets)?;
82 let fac: Vec<kevy_text::Facet> = fkeys
83 .iter()
84 .map(|(field, k)| kevy_text::Facet { field: *field, key: k.as_ref() })
85 .collect();
86 let grouped = self.value_field("DISTINCT", name, opts.distinct)?;
87 let dkey = grouped.map(|(_, ty)| move |raw: &[u8]| kevy_index::order_key(ty, raw));
88 let distinct =
89 grouped.zip(dkey.as_ref()).map(|((field, _), k)| kevy_text::Distinct { field, key: k });
90 let key = sorted.map(|(_, _, ty)| move |raw: &[u8]| kevy_index::order_key(ty, raw));
91 let sort = sorted.zip(key.as_ref()).map(|((field, desc, _), k)| kevy_text::Sort {
92 field,
93 desc,
94 key: k,
95 });
96 let boxed = box_tests(tests);
97 let filter: Vec<kevy_text::Filter> =
98 boxed.iter().map(|(f, t)| kevy_text::Filter { field: *f, test: t.as_ref() }).collect();
99 let stats = self.text_corpus_stats_in(name, query, opts.typo, &scope)?;
100 let q = kevy_text::QueryOpts {
101 stats: Some(&stats),
102 typo: opts.typo,
103 fields: &scope,
104 filter: &filter,
105 sort,
106 distinct,
107 };
108 let (mut all, facets, cold_vals) =
109 self.gather_hits(name, query, fetch, q, &stats, opts.highlight, &fac);
110 self.order_page(name, &mut all, sorted, &cold_vals);
111 self.collapse_union(name, &mut all, grouped, &cold_vals);
112 if offset > 0 {
113 all.drain(..offset.min(all.len()));
114 }
115 all.truncate(limit);
116 Ok(MatchPage { hits: all, facets })
117 }
118
119 fn resolve_clauses(
128 &self,
129 name: &[u8],
130 scope: &[Vec<u8>],
131 filters: &[ValueFilter<'_>],
132 ) -> KevyResult<ResolvedClauses> {
133 if scope.is_empty() && filters.is_empty() {
134 return Ok((Vec::new(), Vec::new()));
135 }
136 let guard = self.indexes.catalog.read().unwrap_or_else(|e| e.into_inner());
137 let Some((spec, _)) = guard.1.get(name) else {
138 return Err(KevyError::NotFound("no such text index".into()));
139 };
140 let mut positions = Vec::with_capacity(scope.len());
141 for want in scope {
142 let names = || spec.fields.iter().map(|f| f.name.as_slice()).collect::<Vec<_>>();
143 let i = spec
144 .fields
145 .iter()
146 .position(|f| f.name == *want)
147 .ok_or_else(|| unknown_field("IN", want, "index", &names()))?;
148 positions.push(i);
149 }
150 let tests = filters.iter().map(|f| value_test(spec, f)).collect::<KevyResult<Vec<_>>>()?;
151 Ok((positions, tests))
152 }
153}
154
155type ResolvedClauses = (Vec<usize>, Vec<(usize, kevy_index::ValueTest)>);
158
159type ValuePred = Box<dyn Fn(&[u8]) -> bool>;
163
164fn box_tests(tests: Vec<(usize, kevy_index::ValueTest)>) -> Vec<(usize, ValuePred)> {
165 tests
166 .into_iter()
167 .map(|(f, t)| {
168 let b: ValuePred = Box::new(move |v: &[u8]| t.passes(v));
169 (f, b)
170 })
171 .collect()
172}
173
174impl Store {
175 #[allow(clippy::too_many_arguments)]
178 fn gather_hits(
179 &self,
180 name: &[u8],
181 query: &[u8],
182 fetch: usize,
183 q: kevy_text::QueryOpts<'_>,
184 stats: &kevy_text::CorpusStats,
185 highlight: Option<&[Vec<u8>]>,
186 facets: &[kevy_text::Facet],
187 ) -> (Vec<HighlightedHit>, Vec<FacetCounts>, super::text_cold::ColdVals) {
188 let mut all = Vec::new();
189 let mut buckets: Vec<Vec<RawBucket>> = vec![Vec::new(); facets.len()];
190 #[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
191 let mut cold_vals = super::text_cold::ColdVals::new();
192 for shard in self.shards.iter() {
193 let mut g = lock_write(shard);
194 let inner = &mut *g;
195 sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
196 if let Some((spec, ts)) = inner.idx_segs.text.iter().find(|(s, _)| s.name == name) {
197 let r = ts.matches_query_faceted(query, fetch, q, facets);
200 absorb_live(r, ts, spec, query, highlight, &mut all, &mut buckets);
201 }
202 #[cfg(not(target_arch = "wasm32"))]
206 gather_cold(
207 inner,
208 name,
209 query,
210 fetch,
211 stats,
212 &q,
213 facets,
214 highlight,
215 &mut all,
216 &mut buckets,
217 &mut cold_vals,
218 );
219 #[cfg(target_arch = "wasm32")]
220 let _ = stats;
221 }
222 (all, finish_buckets(buckets), cold_vals)
223 }
224
225 fn order_page(
229 &self,
230 name: &[u8],
231 all: &mut Vec<HighlightedHit>,
232 sorted: Option<(usize, bool, kevy_index::ValType)>,
233 cold_vals: &super::text_cold::ColdVals,
234 ) {
235 let Some((field, desc, ty)) = sorted else {
236 all.sort_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
237 return;
238 };
239 let mut keyed: Vec<(Option<Vec<u8>>, HighlightedHit)> = std::mem::take(all)
240 .into_iter()
241 .map(|h| (self.stored_order_key(name, &h.0, field, ty, cold_vals), h))
242 .collect();
243 keyed.sort_by(|a, b| {
244 kevy_text::sorted_order((a.0.as_deref(), &a.1.0), (b.0.as_deref(), &b.1.0), desc)
245 });
246 *all = keyed.into_iter().map(|(_, h)| h).collect();
247 }
248
249 fn collapse_union(
257 &self,
258 name: &[u8],
259 all: &mut Vec<HighlightedHit>,
260 grouped: Option<(usize, kevy_index::ValType)>,
261 cold_vals: &super::text_cold::ColdVals,
262 ) {
263 let Some((field, ty)) = grouped else { return };
264 let mut seen: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
265 all.retain(|h| match self.stored_order_key(name, &h.0, field, ty, cold_vals) {
266 Some(k) => seen.insert(k),
267 None => true,
268 });
269 }
270
271 fn sort_field(
274 &self,
275 name: &[u8],
276 sort: Option<(&[u8], bool)>,
277 ) -> KevyResult<Option<(usize, bool, kevy_index::ValType)>> {
278 let Some((field, desc)) = sort else { return Ok(None) };
279 let Some((pos, ty)) = self.value_field("SORT", name, Some(field))? else {
280 return Ok(None);
281 };
282 Ok(Some((pos, desc, ty)))
283 }
284
285 fn facet_keys(&self, name: &[u8], facets: &[Vec<u8>]) -> KevyResult<Vec<FacetKey>> {
290 Ok(self
291 .facet_fields(name, facets)?
292 .into_iter()
293 .map(|(field, ty)| -> FacetKey {
294 (field, Box::new(move |raw: &[u8]| kevy_index::order_key(ty, raw)))
295 })
296 .collect())
297 }
298
299 fn facet_fields(
301 &self,
302 name: &[u8],
303 facets: &[Vec<u8>],
304 ) -> KevyResult<Vec<(usize, kevy_index::ValType)>> {
305 facets
306 .iter()
307 .map(|f| {
308 Ok(self
309 .value_field("FACET", name, Some(f))?
310 .expect("a named field always resolves or errors"))
311 })
312 .collect()
313 }
314
315 fn value_field(
318 &self,
319 clause: &str,
320 name: &[u8],
321 field: Option<&[u8]>,
322 ) -> KevyResult<Option<(usize, kevy_index::ValType)>> {
323 let Some(field) = field else { return Ok(None) };
324 let guard = self.indexes.catalog.read().unwrap_or_else(|e| e.into_inner());
325 let Some((spec, _)) = guard.1.get(name) else {
326 return Err(KevyError::NotFound("no such text index".into()));
327 };
328 let stored: Vec<&[u8]> = spec.values.iter().map(|v| v.name.as_slice()).collect();
329 let pos = spec
330 .values
331 .iter()
332 .position(|v| v.name == field)
333 .ok_or_else(|| unknown_field(clause, field, "store", &stored))?;
334 Ok(Some((pos, spec.values[pos].ty)))
335 }
336
337 fn stored_order_key(
342 &self,
343 name: &[u8],
344 key: &[u8],
345 field: usize,
346 ty: kevy_index::ValType,
347 cold_vals: &super::text_cold::ColdVals,
348 ) -> Option<Vec<u8>> {
349 for shard in self.shards.iter() {
350 let g = lock_write(shard);
351 if let Some((_, ts)) = g.idx_segs.text.iter().find(|(s, _)| s.name == name)
352 && let Some(raw) = ts.stored_value(key, field)
353 {
354 return kevy_index::order_key(ty, raw);
355 }
356 }
357 let raw = cold_vals.get(key)?.get(field)?.as_deref()?;
358 kevy_index::order_key(ty, raw)
359 }
360}
361
362#[cfg(not(target_arch = "wasm32"))]
367#[allow(clippy::too_many_arguments)]
368fn gather_cold(
369 inner: &mut crate::store_inner::Inner,
370 name: &[u8],
371 query: &[u8],
372 fetch: usize,
373 stats: &kevy_text::CorpusStats,
374 q: &kevy_text::QueryOpts<'_>,
375 facets: &[kevy_text::Facet],
376 highlight: Option<&[Vec<u8>]>,
377 all: &mut Vec<HighlightedHit>,
378 buckets: &mut [Vec<RawBucket>],
379 cold_vals: &mut super::text_cold::ColdVals,
380) {
381 let Some(dir) = inner.idx_segs.cold_text_of(name).filter(|d| d.has_cold()) else {
382 return;
383 };
384 let (mut bare, phrases, _prefixes) = kevy_text::parse_clauses(query);
385 bare.sort();
386 bare.dedup();
387 let page = dir.cold_page(&kevy_window::ColdPageQuery {
388 bare,
389 phrases,
390 stats,
391 filter: q.filter,
392 sort: q.sort.as_ref(),
393 distinct: q.distinct.as_ref(),
394 facets,
395 fetch,
396 });
397 let spec = inner.idx_segs.text.iter().find(|(s, _)| s.name == name).map(|(s, _)| s.clone());
398 for h in page.hits {
399 let hl = highlight.map_or_else(Vec::new, |w| {
400 spec.as_ref().map_or_else(Vec::new, |sp| {
401 super::text_cold::cold_hit_highlight(&mut inner.store, sp, &h.key, query, w)
402 })
403 });
404 all.push((h.key, h.score, hl));
405 }
406 for (into, from) in buckets.iter_mut().zip(page.facets) {
407 fold_raw(into, from);
408 }
409 cold_vals.extend(page.values);
410}
411
412fn fold_raw(into: &mut Vec<RawBucket>, from: Vec<RawBucket>) {
415 for (key, label, n) in from {
416 match into.iter_mut().find(|(k, _, _)| *k == key) {
417 Some(e) => e.2 += n,
418 None => into.push((key, label, n)),
419 }
420 }
421}
422
423type FacetKey = (usize, Box<dyn Fn(&[u8]) -> Option<Vec<u8>>>);
426
427pub type FacetCounts = Vec<(Vec<u8>, u64)>;
430
431type RawBucket = (Vec<u8>, Vec<u8>, u64);
433
434#[derive(Debug)]
437pub struct MatchPage {
438 pub hits: Vec<HighlightedHit>,
440 pub facets: Vec<FacetCounts>,
442}
443
444fn finish_buckets(buckets: Vec<Vec<RawBucket>>) -> Vec<FacetCounts> {
447 buckets
448 .into_iter()
449 .map(|mut field| {
450 field.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.1.cmp(&b.1)));
451 field.into_iter().map(|(_, label, n)| (label, n)).collect()
452 })
453 .collect()
454}
455
456fn absorb_live(
461 r: kevy_text::FacetedMatches,
462 ts: &kevy_text::TextSegment,
463 spec: &IndexSpec,
464 query: &[u8],
465 highlight: Option<&[Vec<u8>]>,
466 all: &mut Vec<(Vec<u8>, f64, Vec<FieldSpans>)>,
467 buckets: &mut [Vec<RawBucket>],
468) {
469 for m in r.hits {
470 let hl = highlight.map_or_else(Vec::new, |w| hit_highlight(ts, spec, &m.key, query, w));
471 all.push((m.key, m.score, hl));
472 }
473 for (into, from) in buckets.iter_mut().zip(r.facets) {
474 fold_raw(into, from);
475 }
476}
477
478fn hit_highlight(
481 ts: &kevy_text::TextSegment,
482 spec: &IndexSpec,
483 key: &[u8],
484 query: &[u8],
485 want: &[Vec<u8>],
486) -> Vec<FieldSpans> {
487 ts.highlight_spans(key, query)
488 .into_iter()
489 .filter_map(|(fi, spans)| {
490 let name = spec.fields.get(fi)?.name.clone();
491 if !want.is_empty() && !want.contains(&name) {
492 return None;
493 }
494 let ranges = spans.into_iter().map(|(s, e)| (s as u32, e as u32)).collect();
495 Some((name, ranges))
496 })
497 .collect()
498}