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(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> =
83 fkeys.iter().map(|(field, k)| kevy_text::Facet { field: *field, key: k.as_ref() }).collect();
84 let grouped = self.value_field("DISTINCT", name, opts.distinct)?;
85 let dkey = grouped.map(|(_, ty)| move |raw: &[u8]| kevy_index::order_key(ty, raw));
86 let distinct = grouped
87 .zip(dkey.as_ref())
88 .map(|((field, _), k)| kevy_text::Distinct { field, key: k });
89 let key = sorted.map(|(_, _, ty)| move |raw: &[u8]| kevy_index::order_key(ty, raw));
90 let sort = sorted
91 .zip(key.as_ref())
92 .map(|((field, desc, _), k)| kevy_text::Sort { field, desc, key: k });
93 let boxed = box_tests(tests);
94 let filter: Vec<kevy_text::Filter> = boxed
95 .iter()
96 .map(|(f, t)| kevy_text::Filter { field: *f, test: t.as_ref() })
97 .collect();
98 let stats = self.text_corpus_stats_in(name, query, opts.typo, &scope)?;
99 let q = kevy_text::QueryOpts {
100 stats: Some(&stats),
101 typo: opts.typo,
102 fields: &scope,
103 filter: &filter,
104 sort,
105 distinct,
106 };
107 let (mut all, facets, cold_vals) =
108 self.gather_hits(name, query, fetch, q, &stats, opts.highlight, &fac);
109 self.order_page(name, &mut all, sorted, &cold_vals);
110 self.collapse_union(name, &mut all, grouped, &cold_vals);
111 if offset > 0 {
112 all.drain(..offset.min(all.len()));
113 }
114 all.truncate(limit);
115 Ok(MatchPage { hits: all, facets })
116 }
117
118 fn resolve_clauses(
127 &self,
128 name: &[u8],
129 scope: &[Vec<u8>],
130 filters: &[ValueFilter<'_>],
131 ) -> KevyResult<ResolvedClauses> {
132 if scope.is_empty() && filters.is_empty() {
133 return Ok((Vec::new(), Vec::new()));
134 }
135 let guard = self.indexes.catalog.read().unwrap_or_else(|e| e.into_inner());
136 let Some((spec, _)) = guard.1.get(name) else {
137 return Err(KevyError::NotFound("no such text index".into()));
138 };
139 let mut positions = Vec::with_capacity(scope.len());
140 for want in scope {
141 let names = || spec.fields.iter().map(|f| f.name.as_slice()).collect::<Vec<_>>();
142 let i = spec
143 .fields
144 .iter()
145 .position(|f| f.name == *want)
146 .ok_or_else(|| unknown_field("IN", want, "index", &names()))?;
147 positions.push(i);
148 }
149 let tests =
150 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
174
175impl Store {
176 #[allow(clippy::too_many_arguments)]
179 fn gather_hits(
180 &self,
181 name: &[u8],
182 query: &[u8],
183 fetch: usize,
184 q: kevy_text::QueryOpts<'_>,
185 stats: &kevy_text::CorpusStats,
186 highlight: Option<&[Vec<u8>]>,
187 facets: &[kevy_text::Facet],
188 ) -> (Vec<HighlightedHit>, Vec<FacetCounts>, super::text_cold::ColdVals) {
189 let mut all = Vec::new();
190 let mut buckets: Vec<Vec<RawBucket>> = vec![Vec::new(); facets.len()];
191 #[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
192 let mut cold_vals = super::text_cold::ColdVals::new();
193 for shard in self.shards.iter() {
194 let mut g = lock_write(shard);
195 let inner = &mut *g;
196 sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
197 if let Some((spec, ts)) = inner.idx_segs.text.iter().find(|(s, _)| s.name == name) {
198 let r = ts.matches_query_faceted(query, fetch, q, facets);
201 for m in r.hits {
202 let hl = highlight
203 .map_or_else(Vec::new, |w| hit_highlight(ts, spec, &m.key, query, w));
204 all.push((m.key, m.score, hl));
205 }
206 for (into, from) in buckets.iter_mut().zip(r.facets) {
207 fold_raw(into, from);
208 }
209 }
210 #[cfg(not(target_arch = "wasm32"))]
214 gather_cold(
215 inner, name, query, fetch, stats, &q, facets, highlight, &mut all,
216 &mut buckets, &mut cold_vals,
217 );
218 #[cfg(target_arch = "wasm32")]
219 let _ = stats;
220 }
221 (all, finish_buckets(buckets), cold_vals)
222 }
223
224 fn order_page(
228 &self,
229 name: &[u8],
230 all: &mut Vec<HighlightedHit>,
231 sorted: Option<(usize, bool, kevy_index::ValType)>,
232 cold_vals: &super::text_cold::ColdVals,
233 ) {
234 let Some((field, desc, ty)) = sorted else {
235 all.sort_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
236 return;
237 };
238 let mut keyed: Vec<(Option<Vec<u8>>, HighlightedHit)> = std::mem::take(all)
239 .into_iter()
240 .map(|h| (self.stored_order_key(name, &h.0, field, ty, cold_vals), h))
241 .collect();
242 keyed.sort_by(|a, b| {
243 kevy_text::sorted_order((a.0.as_deref(), &a.1.0), (b.0.as_deref(), &b.1.0), desc)
244 });
245 *all = keyed.into_iter().map(|(_, h)| h).collect();
246 }
247
248 fn collapse_union(
256 &self,
257 name: &[u8],
258 all: &mut Vec<HighlightedHit>,
259 grouped: Option<(usize, kevy_index::ValType)>,
260 cold_vals: &super::text_cold::ColdVals,
261 ) {
262 let Some((field, ty)) = grouped else { return };
263 let mut seen: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
264 all.retain(|h| match self.stored_order_key(name, &h.0, field, ty, cold_vals) {
265 Some(k) => seen.insert(k),
266 None => true,
267 });
268 }
269
270 fn sort_field(
273 &self,
274 name: &[u8],
275 sort: Option<(&[u8], bool)>,
276 ) -> KevyResult<Option<(usize, bool, kevy_index::ValType)>> {
277 let Some((field, desc)) = sort else { return Ok(None) };
278 let Some((pos, ty)) = self.value_field("SORT", name, Some(field))? else {
279 return Ok(None);
280 };
281 Ok(Some((pos, desc, ty)))
282 }
283
284 fn facet_keys(
289 &self,
290 name: &[u8],
291 facets: &[Vec<u8>],
292 ) -> KevyResult<Vec<FacetKey>> {
293 Ok(self
294 .facet_fields(name, facets)?
295 .into_iter()
296 .map(|(field, ty)| -> FacetKey {
297 (field, Box::new(move |raw: &[u8]| kevy_index::order_key(ty, raw)))
298 })
299 .collect())
300 }
301
302 fn facet_fields(
304 &self,
305 name: &[u8],
306 facets: &[Vec<u8>],
307 ) -> KevyResult<Vec<(usize, kevy_index::ValType)>> {
308 facets
309 .iter()
310 .map(|f| {
311 Ok(self
312 .value_field("FACET", name, Some(f))?
313 .expect("a named field always resolves or errors"))
314 })
315 .collect()
316 }
317
318 fn value_field(
321 &self,
322 clause: &str,
323 name: &[u8],
324 field: Option<&[u8]>,
325 ) -> KevyResult<Option<(usize, kevy_index::ValType)>> {
326 let Some(field) = field else { return Ok(None) };
327 let guard = self.indexes.catalog.read().unwrap_or_else(|e| e.into_inner());
328 let Some((spec, _)) = guard.1.get(name) else {
329 return Err(KevyError::NotFound("no such text index".into()));
330 };
331 let stored: Vec<&[u8]> = spec.values.iter().map(|v| v.name.as_slice()).collect();
332 let pos = spec
333 .values
334 .iter()
335 .position(|v| v.name == field)
336 .ok_or_else(|| unknown_field(clause, field, "store", &stored))?;
337 Ok(Some((pos, spec.values[pos].ty)))
338 }
339
340 fn stored_order_key(
345 &self,
346 name: &[u8],
347 key: &[u8],
348 field: usize,
349 ty: kevy_index::ValType,
350 cold_vals: &super::text_cold::ColdVals,
351 ) -> Option<Vec<u8>> {
352 for shard in self.shards.iter() {
353 let g = lock_write(shard);
354 if let Some((_, ts)) = g.idx_segs.text.iter().find(|(s, _)| s.name == name)
355 && let Some(raw) = ts.stored_value(key, field)
356 {
357 return kevy_index::order_key(ty, raw);
358 }
359 }
360 let raw = cold_vals.get(key)?.get(field)?.as_deref()?;
361 kevy_index::order_key(ty, raw)
362 }
363}
364
365#[cfg(not(target_arch = "wasm32"))]
370#[allow(clippy::too_many_arguments)]
371fn gather_cold(
372 inner: &mut crate::store_inner::Inner,
373 name: &[u8],
374 query: &[u8],
375 fetch: usize,
376 stats: &kevy_text::CorpusStats,
377 q: &kevy_text::QueryOpts<'_>,
378 facets: &[kevy_text::Facet],
379 highlight: Option<&[Vec<u8>]>,
380 all: &mut Vec<HighlightedHit>,
381 buckets: &mut [Vec<RawBucket>],
382 cold_vals: &mut super::text_cold::ColdVals,
383) {
384 let Some(dir) = inner.idx_segs.cold_text_of(name).filter(|d| d.has_cold()) else {
385 return;
386 };
387 let (mut bare, phrases, _prefixes) = kevy_text::parse_clauses(query);
388 bare.sort();
389 bare.dedup();
390 let page = dir.cold_page(&kevy_window::ColdPageQuery {
391 bare,
392 phrases,
393 stats,
394 filter: q.filter,
395 sort: q.sort.as_ref(),
396 distinct: q.distinct.as_ref(),
397 facets,
398 fetch,
399 });
400 let spec = inner
401 .idx_segs
402 .text
403 .iter()
404 .find(|(s, _)| s.name == name)
405 .map(|(s, _)| s.clone());
406 for h in page.hits {
407 let hl = highlight.map_or_else(Vec::new, |w| {
408 spec.as_ref().map_or_else(Vec::new, |sp| {
409 super::text_cold::cold_hit_highlight(&mut inner.store, sp, &h.key, query, w)
410 })
411 });
412 all.push((h.key, h.score, hl));
413 }
414 for (into, from) in buckets.iter_mut().zip(page.facets) {
415 fold_raw(into, from);
416 }
417 cold_vals.extend(page.values);
418}
419
420fn fold_raw(into: &mut Vec<RawBucket>, from: Vec<RawBucket>) {
423 for (key, label, n) in from {
424 match into.iter_mut().find(|(k, _, _)| *k == key) {
425 Some(e) => e.2 += n,
426 None => into.push((key, label, n)),
427 }
428 }
429}
430
431type FacetKey = (usize, Box<dyn Fn(&[u8]) -> Option<Vec<u8>>>);
434
435pub type FacetCounts = Vec<(Vec<u8>, u64)>;
438
439type RawBucket = (Vec<u8>, Vec<u8>, u64);
441
442#[derive(Debug)]
445pub struct MatchPage {
446 pub hits: Vec<HighlightedHit>,
448 pub facets: Vec<FacetCounts>,
450}
451
452fn finish_buckets(buckets: Vec<Vec<RawBucket>>) -> Vec<FacetCounts> {
455 buckets
456 .into_iter()
457 .map(|mut field| {
458 field.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.1.cmp(&b.1)));
459 field.into_iter().map(|(_, label, n)| (label, n)).collect()
460 })
461 .collect()
462}
463
464
465fn hit_highlight(
468 ts: &kevy_text::TextSegment,
469 spec: &IndexSpec,
470 key: &[u8],
471 query: &[u8],
472 want: &[Vec<u8>],
473) -> Vec<FieldSpans> {
474 ts.highlight_spans(key, query)
475 .into_iter()
476 .filter_map(|(fi, spans)| {
477 let name = spec.fields.get(fi)?.name.clone();
478 if !want.is_empty() && !want.contains(&name) {
479 return None;
480 }
481 let ranges = spans.into_iter().map(|(s, e)| (s as u32, e as u32)).collect();
482 Some((name, ranges))
483 })
484 .collect()
485}