1use kevy_index::IndexSpec;
6
7use super::claused::{ValueFilter, unknown_field, value_test};
8pub(crate) use super::opts::MatchOpts;
9use super::{FieldSpans, HighlightedHit, sync_segs};
10use crate::store::{Store, lock_write};
11use crate::{KevyError, KevyResult};
12
13impl Store {
14 pub fn idx_match_with(
22 &self,
23 name: &[u8],
24 query: &[u8],
25 limit: usize,
26 opts: MatchOpts<'_>,
27 ) -> KevyResult<Vec<HighlightedHit>> {
28 self.idx_match_faceted(name, query, limit, opts).map(|p| p.hits)
29 }
30
31 pub(crate) fn match_faceted_run(
34 &self,
35 name: &[u8],
36 query: &[u8],
37 limit: usize,
38 opts: MatchOpts<'_>,
39 ) -> KevyResult<MatchPage> {
40 let (limit, offset) = (limit.clamp(1, 1000), opts.offset.min(10_000));
41 let fetch = limit + offset;
43 super::text_cold::cold_refusal(self.text_has_cold(name), query, opts.typo, opts.scope)?;
44 let (scope, tests) = self.resolve_clauses(name, opts.scope, opts.filters)?;
45 let sorted = self.sort_field(name, opts.sort)?;
46 let fkeys = self.facet_keys(name, opts.facets)?;
47 let fac: Vec<kevy_text::Facet> =
48 fkeys.iter().map(|(field, k)| kevy_text::Facet::new(*field, k.as_ref())).collect();
49 let grouped = self.value_field("DISTINCT", name, opts.distinct)?;
50 let dkey = grouped.map(|(_, ty)| move |raw: &[u8]| kevy_index::order_key(ty, raw));
51 let distinct =
52 grouped.zip(dkey.as_ref()).map(|((field, _), k)| kevy_text::Distinct::new(field, k));
53 let key = sorted.map(|(_, _, ty)| move |raw: &[u8]| kevy_index::order_key(ty, raw));
54 let sort = sorted
55 .zip(key.as_ref())
56 .map(|((field, order, _), k)| kevy_text::Sort::new(field, k).with_order(order));
57 let boxed = box_tests(tests);
58 let filter: Vec<kevy_text::Filter> =
59 boxed.iter().map(|(f, t)| kevy_text::Filter::new(*f, t.as_ref())).collect();
60 let stats = self.text_corpus_stats_in(name, query, opts.typo, &scope)?;
61 let mut q = kevy_text::QueryOpts::default()
62 .with_stats(&stats)
63 .with_typo(opts.typo)
64 .with_fields(&scope)
65 .with_filter(&filter);
66 q.sort = sort;
67 q.distinct = distinct;
68 let (mut all, facets, cold_vals) =
69 self.gather_hits(name, query, fetch, q, &stats, opts.highlight, &fac);
70 self.order_page(name, &mut all, sorted, &cold_vals);
71 self.collapse_union(name, &mut all, grouped, &cold_vals);
72 if offset > 0 {
73 all.drain(..offset.min(all.len()));
74 }
75 all.truncate(limit);
76 Ok(MatchPage { hits: all, facets })
77 }
78
79 fn resolve_clauses(
88 &self,
89 name: &[u8],
90 scope: &[Vec<u8>],
91 filters: &[ValueFilter<'_>],
92 ) -> KevyResult<ResolvedClauses> {
93 if scope.is_empty() && filters.is_empty() {
94 return Ok((Vec::new(), Vec::new()));
95 }
96 let guard = self.indexes.catalog.read().unwrap_or_else(|e| e.into_inner());
97 let Some((spec, _)) = guard.1.get(name) else {
98 return Err(KevyError::NotFound("no such text index".into()));
99 };
100 let mut positions = Vec::with_capacity(scope.len());
101 for want in scope {
102 let names = || spec.fields().iter().map(|f| f.name.as_slice()).collect::<Vec<_>>();
103 let i = spec
104 .fields()
105 .iter()
106 .position(|f| f.name == *want)
107 .ok_or_else(|| unknown_field("IN", want, "index", &names()))?;
108 positions.push(i);
109 }
110 let tests = filters.iter().map(|f| value_test(spec, f)).collect::<KevyResult<Vec<_>>>()?;
111 Ok((positions, tests))
112 }
113}
114
115type ResolvedClauses = (Vec<usize>, Vec<(usize, kevy_index::ValueTest)>);
118
119type ValuePred = Box<dyn Fn(&[u8]) -> bool>;
123
124fn box_tests(tests: Vec<(usize, kevy_index::ValueTest)>) -> Vec<(usize, ValuePred)> {
125 tests
126 .into_iter()
127 .map(|(f, t)| {
128 let b: ValuePred = Box::new(move |v: &[u8]| t.passes(v));
129 (f, b)
130 })
131 .collect()
132}
133
134impl Store {
135 #[allow(clippy::too_many_arguments)]
138 fn gather_hits(
139 &self,
140 name: &[u8],
141 query: &[u8],
142 fetch: usize,
143 q: kevy_text::QueryOpts<'_>,
144 stats: &kevy_text::CorpusStats,
145 highlight: Option<&[Vec<u8>]>,
146 facets: &[kevy_text::Facet],
147 ) -> (Vec<HighlightedHit>, Vec<FacetCounts>, super::text_cold::ColdVals) {
148 let mut all = Vec::new();
149 let mut buckets: Vec<Vec<RawBucket>> = vec![Vec::new(); facets.len()];
150 #[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
151 let mut cold_vals = super::text_cold::ColdVals::new();
152 for shard in self.shards.iter() {
153 let mut g = lock_write(shard);
154 let inner = &mut *g;
155 sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
156 if let Some((spec, ts)) = inner.idx_segs.text.iter().find(|(s, _)| s.name() == name) {
157 let r = ts.matches_query_faceted(query, fetch, q, facets);
160 absorb_live(r, ts, spec, query, highlight, &mut all, &mut buckets);
161 }
162 #[cfg(not(target_arch = "wasm32"))]
166 gather_cold(
167 inner,
168 name,
169 query,
170 fetch,
171 stats,
172 &q,
173 facets,
174 highlight,
175 &mut all,
176 &mut buckets,
177 &mut cold_vals,
178 );
179 #[cfg(target_arch = "wasm32")]
180 let _ = stats;
181 }
182 (all, finish_buckets(buckets), cold_vals)
183 }
184
185 fn order_page(
189 &self,
190 name: &[u8],
191 all: &mut Vec<HighlightedHit>,
192 sorted: Option<(usize, kevy_index::SortOrder, kevy_index::ValType)>,
193 cold_vals: &super::text_cold::ColdVals,
194 ) {
195 let Some((field, order, ty)) = sorted else {
196 all.sort_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
197 return;
198 };
199 let mut keyed: Vec<(Option<Vec<u8>>, HighlightedHit)> = std::mem::take(all)
200 .into_iter()
201 .map(|h| (self.stored_order_key(name, &h.0, field, ty, cold_vals), h))
202 .collect();
203 keyed.sort_by(|a, b| {
204 kevy_text::sorted_order((a.0.as_deref(), &a.1.0), (b.0.as_deref(), &b.1.0), order)
205 });
206 *all = keyed.into_iter().map(|(_, h)| h).collect();
207 }
208
209 fn collapse_union(
217 &self,
218 name: &[u8],
219 all: &mut Vec<HighlightedHit>,
220 grouped: Option<(usize, kevy_index::ValType)>,
221 cold_vals: &super::text_cold::ColdVals,
222 ) {
223 let Some((field, ty)) = grouped else { return };
224 let mut seen: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
225 all.retain(|h| match self.stored_order_key(name, &h.0, field, ty, cold_vals) {
226 Some(k) => seen.insert(k),
227 None => true,
228 });
229 }
230
231 fn sort_field(
234 &self,
235 name: &[u8],
236 sort: Option<(&[u8], kevy_index::SortOrder)>,
237 ) -> KevyResult<Option<(usize, kevy_index::SortOrder, kevy_index::ValType)>> {
238 let Some((field, order)) = sort else { return Ok(None) };
239 let Some((pos, ty)) = self.value_field("SORT", name, Some(field))? else {
240 return Ok(None);
241 };
242 Ok(Some((pos, order, ty)))
243 }
244
245 fn facet_keys(&self, name: &[u8], facets: &[Vec<u8>]) -> KevyResult<Vec<FacetKey>> {
250 Ok(self
251 .facet_fields(name, facets)?
252 .into_iter()
253 .map(|(field, ty)| -> FacetKey {
254 (field, Box::new(move |raw: &[u8]| kevy_index::order_key(ty, raw)))
255 })
256 .collect())
257 }
258
259 fn facet_fields(
261 &self,
262 name: &[u8],
263 facets: &[Vec<u8>],
264 ) -> KevyResult<Vec<(usize, kevy_index::ValType)>> {
265 facets
266 .iter()
267 .map(|f| {
268 Ok(self
269 .value_field("FACET", name, Some(f))?
270 .expect("a named field always resolves or errors"))
271 })
272 .collect()
273 }
274
275 fn value_field(
278 &self,
279 clause: &str,
280 name: &[u8],
281 field: Option<&[u8]>,
282 ) -> KevyResult<Option<(usize, kevy_index::ValType)>> {
283 let Some(field) = field else { return Ok(None) };
284 let guard = self.indexes.catalog.read().unwrap_or_else(|e| e.into_inner());
285 let Some((spec, _)) = guard.1.get(name) else {
286 return Err(KevyError::NotFound("no such text index".into()));
287 };
288 let stored: Vec<&[u8]> = spec.values().iter().map(|v| v.name.as_slice()).collect();
289 let pos = spec
290 .values()
291 .iter()
292 .position(|v| v.name == field)
293 .ok_or_else(|| unknown_field(clause, field, "store", &stored))?;
294 Ok(Some((pos, spec.values()[pos].ty)))
295 }
296
297 fn stored_order_key(
302 &self,
303 name: &[u8],
304 key: &[u8],
305 field: usize,
306 ty: kevy_index::ValType,
307 cold_vals: &super::text_cold::ColdVals,
308 ) -> Option<Vec<u8>> {
309 for shard in self.shards.iter() {
310 let g = lock_write(shard);
311 if let Some((_, ts)) = g.idx_segs.text.iter().find(|(s, _)| s.name() == name)
312 && let Some(raw) = ts.stored_value(key, field)
313 {
314 return kevy_index::order_key(ty, raw);
315 }
316 }
317 let raw = cold_vals.get(key)?.get(field)?.as_deref()?;
318 kevy_index::order_key(ty, raw)
319 }
320}
321
322#[cfg(not(target_arch = "wasm32"))]
327#[allow(clippy::too_many_arguments)]
328fn gather_cold(
329 inner: &mut crate::store_inner::Inner,
330 name: &[u8],
331 query: &[u8],
332 fetch: usize,
333 stats: &kevy_text::CorpusStats,
334 q: &kevy_text::QueryOpts<'_>,
335 facets: &[kevy_text::Facet],
336 highlight: Option<&[Vec<u8>]>,
337 all: &mut Vec<HighlightedHit>,
338 buckets: &mut [Vec<RawBucket>],
339 cold_vals: &mut super::text_cold::ColdVals,
340) {
341 let Some(dir) = inner.idx_segs.cold_text_of(name).filter(|d| d.has_cold()) else {
342 return;
343 };
344 let mut cq = kevy_window::ColdPageQuery::parse(query, stats, fetch)
345 .with_filter(q.filter)
346 .with_facets(facets);
347 cq.sort = q.sort.as_ref();
348 cq.distinct = q.distinct.as_ref();
349 let page = dir.cold_page(&cq);
350 let spec = inner.idx_segs.text.iter().find(|(s, _)| s.name() == name).map(|(s, _)| s.clone());
351 for h in page.hits {
352 let hl = highlight.map_or_else(Vec::new, |w| {
353 spec.as_ref().map_or_else(Vec::new, |sp| {
354 super::text_cold::cold_hit_highlight(&mut inner.store, sp, &h.key, query, w)
355 })
356 });
357 all.push((h.key, h.score, hl));
358 }
359 for (into, from) in buckets.iter_mut().zip(page.facets) {
360 fold_raw(into, from);
361 }
362 cold_vals.extend(page.values);
363}
364
365fn fold_raw(into: &mut Vec<RawBucket>, from: Vec<RawBucket>) {
368 for (key, label, n) in from {
369 match into.iter_mut().find(|(k, _, _)| *k == key) {
370 Some(e) => e.2 += n,
371 None => into.push((key, label, n)),
372 }
373 }
374}
375
376type FacetKey = (usize, Box<dyn Fn(&[u8]) -> Option<Vec<u8>>>);
379
380pub type FacetCounts = Vec<(Vec<u8>, u64)>;
397
398type RawBucket = (Vec<u8>, Vec<u8>, u64);
400
401#[derive(Debug, Clone, PartialEq)]
415#[non_exhaustive]
416pub struct MatchPage {
417 pub hits: Vec<HighlightedHit>,
430 pub facets: Vec<FacetCounts>,
444}
445
446fn finish_buckets(buckets: Vec<Vec<RawBucket>>) -> Vec<FacetCounts> {
449 buckets
450 .into_iter()
451 .map(|mut field| {
452 field.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.1.cmp(&b.1)));
453 field.into_iter().map(|(_, label, n)| (label, n)).collect()
454 })
455 .collect()
456}
457
458fn absorb_live(
463 r: kevy_text::FacetedMatches,
464 ts: &kevy_text::TextSegment,
465 spec: &IndexSpec,
466 query: &[u8],
467 highlight: Option<&[Vec<u8>]>,
468 all: &mut Vec<(Vec<u8>, f64, Vec<FieldSpans>)>,
469 buckets: &mut [Vec<RawBucket>],
470) {
471 for m in r.hits {
472 let hl = highlight.map_or_else(Vec::new, |w| hit_highlight(ts, spec, &m.key, query, w));
473 all.push((m.key, m.score, hl));
474 }
475 for (into, from) in buckets.iter_mut().zip(r.facets) {
476 fold_raw(into, from);
477 }
478}
479
480fn hit_highlight(
483 ts: &kevy_text::TextSegment,
484 spec: &IndexSpec,
485 key: &[u8],
486 query: &[u8],
487 want: &[Vec<u8>],
488) -> Vec<FieldSpans> {
489 ts.highlight_spans(key, query)
490 .into_iter()
491 .filter_map(|(fi, spans)| {
492 let name = spec.fields().get(fi)?.name.clone();
493 if !want.is_empty() && !want.contains(&name) {
494 return None;
495 }
496 let ranges = spans.into_iter().map(|(s, e)| (s as u32, e as u32)).collect();
497 Some((name, ranges))
498 })
499 .collect()
500}