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 fn idx_match_faceted(
70 &self,
71 name: &[u8],
72 query: &[u8],
73 limit: usize,
74 opts: MatchOpts<'_>,
75 ) -> KevyResult<MatchPage> {
76 let limit = limit.clamp(1, 1000);
77 let offset = opts.offset.min(10_000);
78 let fetch = limit + offset;
81 let (scope, tests) = self.resolve_clauses(name, opts.scope, opts.filters)?;
82 let sorted = self.sort_field(name, opts.sort)?;
83 let fkeys = self.facet_keys(name, opts.facets)?;
84 let fac: Vec<kevy_text::Facet> =
85 fkeys.iter().map(|(field, k)| kevy_text::Facet { field: *field, key: k.as_ref() }).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 = grouped
89 .zip(dkey.as_ref())
90 .map(|((field, _), k)| kevy_text::Distinct { field, key: k });
91 let key = sorted.map(|(_, _, ty)| move |raw: &[u8]| kevy_index::order_key(ty, raw));
92 let sort = sorted
93 .zip(key.as_ref())
94 .map(|((field, desc, _), k)| kevy_text::Sort { field, desc, key: k });
95 let boxed = box_tests(tests);
96 let filter: Vec<kevy_text::Filter> = boxed
97 .iter()
98 .map(|(f, t)| kevy_text::Filter { field: *f, test: t.as_ref() })
99 .collect();
100 let stats = self.text_corpus_stats_in(name, query, opts.typo, &scope)?;
101 let q = kevy_text::QueryOpts {
102 stats: Some(&stats),
103 typo: opts.typo,
104 fields: &scope,
105 filter: &filter,
106 sort,
107 distinct,
108 };
109 let (mut all, facets) =
110 self.gather_hits(name, query, fetch, q, opts.highlight, &fac);
111 self.order_page(name, &mut all, sorted);
112 self.collapse_union(name, &mut all, grouped);
113 if offset > 0 {
114 all.drain(..offset.min(all.len()));
115 }
116 all.truncate(limit);
117 Ok(MatchPage { hits: all, facets })
118 }
119
120 fn resolve_clauses(
129 &self,
130 name: &[u8],
131 scope: &[Vec<u8>],
132 filters: &[ValueFilter<'_>],
133 ) -> KevyResult<ResolvedClauses> {
134 if scope.is_empty() && filters.is_empty() {
135 return Ok((Vec::new(), Vec::new()));
136 }
137 let guard = self.indexes.catalog.read().unwrap_or_else(|e| e.into_inner());
138 let Some((spec, _)) = guard.1.get(name) else {
139 return Err(KevyError::NotFound("no such text index".into()));
140 };
141 let mut positions = Vec::with_capacity(scope.len());
142 for want in scope {
143 let names = || spec.fields.iter().map(|f| f.name.as_slice()).collect::<Vec<_>>();
144 let i = spec
145 .fields
146 .iter()
147 .position(|f| f.name == *want)
148 .ok_or_else(|| unknown_field("IN", want, "index", &names()))?;
149 positions.push(i);
150 }
151 let tests =
152 filters.iter().map(|f| value_test(spec, f)).collect::<KevyResult<Vec<_>>>()?;
153 Ok((positions, tests))
154 }
155}
156
157type ResolvedClauses = (Vec<usize>, Vec<(usize, kevy_index::ValueTest)>);
160
161type ValuePred = Box<dyn Fn(&[u8]) -> bool>;
165
166fn box_tests(tests: Vec<(usize, kevy_index::ValueTest)>) -> Vec<(usize, ValuePred)> {
167 tests
168 .into_iter()
169 .map(|(f, t)| {
170 let b: ValuePred = Box::new(move |v: &[u8]| t.passes(v));
171 (f, b)
172 })
173 .collect()
174}
175
176
177impl Store {
178 fn gather_hits(
181 &self,
182 name: &[u8],
183 query: &[u8],
184 fetch: usize,
185 q: kevy_text::QueryOpts<'_>,
186 highlight: Option<&[Vec<u8>]>,
187 facets: &[kevy_text::Facet],
188 ) -> (Vec<HighlightedHit>, Vec<FacetCounts>) {
189 let mut all = Vec::new();
190 let mut buckets: Vec<Vec<RawBucket>> = vec![Vec::new(); facets.len()];
191 for shard in self.shards.iter() {
192 let mut g = lock_write(shard);
193 let inner = &mut *g;
194 sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
195 if let Some((spec, ts)) = inner.idx_segs.text.iter().find(|(s, _)| s.name == name) {
196 let r = ts.matches_query_faceted(query, fetch, q, facets);
199 for m in r.hits {
200 let hl = highlight
201 .map_or_else(Vec::new, |w| hit_highlight(ts, spec, &m.key, query, w));
202 all.push((m.key, m.score, hl));
203 }
204 for (into, from) in buckets.iter_mut().zip(r.facets) {
205 for (key, label, n) in from {
206 match into.iter_mut().find(|(k, _, _)| *k == key) {
207 Some(e) => e.2 += n,
208 None => into.push((key, label, n)),
209 }
210 }
211 }
212 }
213 }
214 (all, finish_buckets(buckets))
215 }
216
217 fn order_page(
221 &self,
222 name: &[u8],
223 all: &mut Vec<HighlightedHit>,
224 sorted: Option<(usize, bool, kevy_index::ValType)>,
225 ) {
226 let Some((field, desc, ty)) = sorted else {
227 all.sort_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
228 return;
229 };
230 let mut keyed: Vec<(Option<Vec<u8>>, HighlightedHit)> = std::mem::take(all)
231 .into_iter()
232 .map(|h| (self.stored_order_key(name, &h.0, field, ty), h))
233 .collect();
234 keyed.sort_by(|a, b| {
235 kevy_text::sorted_order((a.0.as_deref(), &a.1.0), (b.0.as_deref(), &b.1.0), desc)
236 });
237 *all = keyed.into_iter().map(|(_, h)| h).collect();
238 }
239
240 fn collapse_union(
248 &self,
249 name: &[u8],
250 all: &mut Vec<HighlightedHit>,
251 grouped: Option<(usize, kevy_index::ValType)>,
252 ) {
253 let Some((field, ty)) = grouped else { return };
254 let mut seen: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
255 all.retain(|h| match self.stored_order_key(name, &h.0, field, ty) {
256 Some(k) => seen.insert(k),
257 None => true,
258 });
259 }
260
261 fn sort_field(
264 &self,
265 name: &[u8],
266 sort: Option<(&[u8], bool)>,
267 ) -> KevyResult<Option<(usize, bool, kevy_index::ValType)>> {
268 let Some((field, desc)) = sort else { return Ok(None) };
269 let Some((pos, ty)) = self.value_field("SORT", name, Some(field))? else {
270 return Ok(None);
271 };
272 Ok(Some((pos, desc, ty)))
273 }
274
275 fn facet_keys(
280 &self,
281 name: &[u8],
282 facets: &[Vec<u8>],
283 ) -> KevyResult<Vec<FacetKey>> {
284 Ok(self
285 .facet_fields(name, facets)?
286 .into_iter()
287 .map(|(field, ty)| -> FacetKey {
288 (field, Box::new(move |raw: &[u8]| kevy_index::order_key(ty, raw)))
289 })
290 .collect())
291 }
292
293 fn facet_fields(
295 &self,
296 name: &[u8],
297 facets: &[Vec<u8>],
298 ) -> KevyResult<Vec<(usize, kevy_index::ValType)>> {
299 facets
300 .iter()
301 .map(|f| {
302 Ok(self
303 .value_field("FACET", name, Some(f))?
304 .expect("a named field always resolves or errors"))
305 })
306 .collect()
307 }
308
309 fn value_field(
312 &self,
313 clause: &str,
314 name: &[u8],
315 field: Option<&[u8]>,
316 ) -> KevyResult<Option<(usize, kevy_index::ValType)>> {
317 let Some(field) = field else { return Ok(None) };
318 let guard = self.indexes.catalog.read().unwrap_or_else(|e| e.into_inner());
319 let Some((spec, _)) = guard.1.get(name) else {
320 return Err(KevyError::NotFound("no such text index".into()));
321 };
322 let stored: Vec<&[u8]> = spec.values.iter().map(|v| v.name.as_slice()).collect();
323 let pos = spec
324 .values
325 .iter()
326 .position(|v| v.name == field)
327 .ok_or_else(|| unknown_field(clause, field, "store", &stored))?;
328 Ok(Some((pos, spec.values[pos].ty)))
329 }
330
331 fn stored_order_key(
334 &self,
335 name: &[u8],
336 key: &[u8],
337 field: usize,
338 ty: kevy_index::ValType,
339 ) -> Option<Vec<u8>> {
340 for shard in self.shards.iter() {
341 let g = lock_write(shard);
342 if let Some((_, ts)) = g.idx_segs.text.iter().find(|(s, _)| s.name == name)
343 && let Some(raw) = ts.stored_value(key, field)
344 {
345 return kevy_index::order_key(ty, raw);
346 }
347 }
348 None
349 }
350}
351
352type FacetKey = (usize, Box<dyn Fn(&[u8]) -> Option<Vec<u8>>>);
355
356pub type FacetCounts = Vec<(Vec<u8>, u64)>;
359
360type RawBucket = (Vec<u8>, Vec<u8>, u64);
362
363#[derive(Debug)]
366pub struct MatchPage {
367 pub hits: Vec<HighlightedHit>,
369 pub facets: Vec<FacetCounts>,
371}
372
373fn finish_buckets(buckets: Vec<Vec<RawBucket>>) -> Vec<FacetCounts> {
376 buckets
377 .into_iter()
378 .map(|mut field| {
379 field.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.1.cmp(&b.1)));
380 field.into_iter().map(|(_, label, n)| (label, n)).collect()
381 })
382 .collect()
383}
384
385
386fn hit_highlight(
389 ts: &kevy_text::TextSegment,
390 spec: &IndexSpec,
391 key: &[u8],
392 query: &[u8],
393 want: &[Vec<u8>],
394) -> Vec<FieldSpans> {
395 ts.highlight_spans(key, query)
396 .into_iter()
397 .filter_map(|(fi, spans)| {
398 let name = spec.fields.get(fi)?.name.clone();
399 if !want.is_empty() && !want.contains(&name) {
400 return None;
401 }
402 let ranges = spans.into_iter().map(|(s, e)| (s as u32, e as u32)).collect();
403 Some((name, ranges))
404 })
405 .collect()
406}