Skip to main content

kevy_embedded/
ops_index_match_opts.rs

1//! The text MATCH clause set, [`MatchOpts`]. Split from
2//! `ops_index_opts.rs` for the 500-LOC house rule; a `#[path]` child of
3//! `ops_index.rs`.
4
5use kevy_index::SortOrder;
6
7use super::opts::ValueFilter;
8
9/// Everything a text MATCH carries beyond its index, query text and
10/// result limit — the embedded twin of the wire's optional clauses.
11///
12/// Grouping them keeps one entry point instead of one per clause, and
13/// [`MatchOpts::default`] is the plain query, so a caller opts into
14/// exactly the clauses it names.
15///
16/// ```
17/// use kevy_embedded::MatchOpts;
18///
19/// let every_field: &[Vec<u8>] = &[];
20/// let opts = MatchOpts::default().with_highlight(every_field).with_typo(1);
21/// assert_eq!(opts.typo, 1);
22/// ```
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
24#[non_exhaustive]
25pub struct MatchOpts<'a> {
26    /// `HIGHLIGHT`: `None` = not requested, `Some(&[])` = every indexed
27    /// field, `Some(names)` = only those.
28    ///
29    /// ```
30    /// # use kevy_embedded::*;
31    /// # let s = Store::open(Config::default())?;
32    /// # let fields = [(&b"title"[..], 1.0), (b"body", 1.0)];
33    /// # let vals = [(&b"lang"[..], IndexValType::Str), (b"ts", IndexValType::I64)];
34    /// # s.idx_create_text(b"ft", b"d:", &fields, TokenPositions::Omit, &vals)?;
35    /// # for (k, title, body, lang, ts) in [
36    /// #     (&b"d:1"[..], &b"rust"[..], &b"a fast engine"[..], &b"en"[..], &b"1"[..]),
37    /// #     (b"d:2", b"engines", b"written in rust", b"en", b"2"),
38    /// #     (b"d:3", b"rust notes", b"ja docs", b"ja", b"3"),
39    /// # ] {
40    /// #     s.hset(k, &[(b"title", title), (b"body", body), (b"lang", lang), (b"ts", ts)])?;
41    /// # }
42    /// # let keys = |q: &[u8], opts| -> KevyResult<Vec<Vec<u8>>> {
43    /// #     let mut k: Vec<_> = s.idx_match_with(b"ft", q, 10, opts)?.into_iter().map(|h| h.0).collect();
44    /// #     k.sort();
45    /// #     Ok(k)
46    /// # };
47    /// // d:1 title "rust" (en, ts 1), d:2 body "written in rust" (en, 2), d:3 title "rust notes" (ja, 3)
48    /// let hl = MatchOpts::default().with_highlight(&[]);
49    /// let hits = s.idx_match_with(b"ft", b"engine", 10, hl)?;
50    /// // d:1 matched in `body`, bytes 7..13 ("engine")
51    /// assert_eq!(hits[0].2, [(b"body".to_vec(), vec![(7, 13)])]);
52    /// assert!(s.idx_match_with(b"ft", b"engine", 10, MatchOpts::default())?[0].2.is_empty());
53    /// # Ok::<(), kevy_embedded::KevyError>(())
54    /// ```
55    pub highlight: Option<&'a [Vec<u8>]>,
56    /// `TYPO n`: edit budget for each bare term; 0 = exact.
57    ///
58    /// ```
59    /// # use kevy_embedded::*;
60    /// # let s = Store::open(Config::default())?;
61    /// # let fields = [(&b"title"[..], 1.0), (b"body", 1.0)];
62    /// # let vals = [(&b"lang"[..], IndexValType::Str), (b"ts", IndexValType::I64)];
63    /// # s.idx_create_text(b"ft", b"d:", &fields, TokenPositions::Omit, &vals)?;
64    /// # for (k, title, body, lang, ts) in [
65    /// #     (&b"d:1"[..], &b"rust"[..], &b"a fast engine"[..], &b"en"[..], &b"1"[..]),
66    /// #     (b"d:2", b"engines", b"written in rust", b"en", b"2"),
67    /// #     (b"d:3", b"rust notes", b"ja docs", b"ja", b"3"),
68    /// # ] {
69    /// #     s.hset(k, &[(b"title", title), (b"body", body), (b"lang", lang), (b"ts", ts)])?;
70    /// # }
71    /// # let keys = |q: &[u8], opts| -> KevyResult<Vec<Vec<u8>>> {
72    /// #     let mut k: Vec<_> = s.idx_match_with(b"ft", q, 10, opts)?.into_iter().map(|h| h.0).collect();
73    /// #     k.sort();
74    /// #     Ok(k)
75    /// # };
76    /// // d:1 title "rust" (en, ts 1), d:2 body "written in rust" (en, 2), d:3 title "rust notes" (ja, 3)
77    /// assert!(keys(b"rusr", MatchOpts::default())?.is_empty());
78    /// let one_edit = MatchOpts::default().with_typo(1);
79    /// assert_eq!(keys(b"rusr", one_edit)?.len(), 3);
80    /// # Ok::<(), kevy_embedded::KevyError>(())
81    /// ```
82    pub typo: u32,
83    /// `OFFSET n`: hits to skip before `limit` takes effect.
84    ///
85    /// ```
86    /// # use kevy_embedded::*;
87    /// # let s = Store::open(Config::default())?;
88    /// # let fields = [(&b"title"[..], 1.0), (b"body", 1.0)];
89    /// # let vals = [(&b"lang"[..], IndexValType::Str), (b"ts", IndexValType::I64)];
90    /// # s.idx_create_text(b"ft", b"d:", &fields, TokenPositions::Omit, &vals)?;
91    /// # for (k, title, body, lang, ts) in [
92    /// #     (&b"d:1"[..], &b"rust"[..], &b"a fast engine"[..], &b"en"[..], &b"1"[..]),
93    /// #     (b"d:2", b"engines", b"written in rust", b"en", b"2"),
94    /// #     (b"d:3", b"rust notes", b"ja docs", b"ja", b"3"),
95    /// # ] {
96    /// #     s.hset(k, &[(b"title", title), (b"body", body), (b"lang", lang), (b"ts", ts)])?;
97    /// # }
98    /// # let keys = |q: &[u8], opts| -> KevyResult<Vec<Vec<u8>>> {
99    /// #     let mut k: Vec<_> = s.idx_match_with(b"ft", q, 10, opts)?.into_iter().map(|h| h.0).collect();
100    /// #     k.sort();
101    /// #     Ok(k)
102    /// # };
103    /// // d:1 title "rust" (en, ts 1), d:2 body "written in rust" (en, 2), d:3 title "rust notes" (ja, 3)
104    /// let all = s.idx_match_with(b"ft", b"rust", 10, MatchOpts::default())?;
105    /// let skip = MatchOpts::default().with_offset(1);
106    /// let rest = s.idx_match_with(b"ft", b"rust", 10, skip)?;
107    /// assert_eq!(rest[0].0, all[1].0);
108    /// # Ok::<(), kevy_embedded::KevyError>(())
109    /// ```
110    pub offset: usize,
111    /// `IN <field…>`: the declared field names to score within; empty =
112    /// the whole document.
113    ///
114    /// ```
115    /// # use kevy_embedded::*;
116    /// # let s = Store::open(Config::default())?;
117    /// # let fields = [(&b"title"[..], 1.0), (b"body", 1.0)];
118    /// # let vals = [(&b"lang"[..], IndexValType::Str), (b"ts", IndexValType::I64)];
119    /// # s.idx_create_text(b"ft", b"d:", &fields, TokenPositions::Omit, &vals)?;
120    /// # for (k, title, body, lang, ts) in [
121    /// #     (&b"d:1"[..], &b"rust"[..], &b"a fast engine"[..], &b"en"[..], &b"1"[..]),
122    /// #     (b"d:2", b"engines", b"written in rust", b"en", b"2"),
123    /// #     (b"d:3", b"rust notes", b"ja docs", b"ja", b"3"),
124    /// # ] {
125    /// #     s.hset(k, &[(b"title", title), (b"body", body), (b"lang", lang), (b"ts", ts)])?;
126    /// # }
127    /// # let keys = |q: &[u8], opts| -> KevyResult<Vec<Vec<u8>>> {
128    /// #     let mut k: Vec<_> = s.idx_match_with(b"ft", q, 10, opts)?.into_iter().map(|h| h.0).collect();
129    /// #     k.sort();
130    /// #     Ok(k)
131    /// # };
132    /// // d:1 title "rust" (en, ts 1), d:2 body "written in rust" (en, 2), d:3 title "rust notes" (ja, 3)
133    /// let title = [b"title".to_vec()];
134    /// let opts = MatchOpts::default().with_scope(&title);
135    /// assert_eq!(keys(b"rust", opts)?, [b"d:1".to_vec(), b"d:3".to_vec()], "d:2 has it only in body");
136    /// # Ok::<(), kevy_embedded::KevyError>(())
137    /// ```
138    pub scope: &'a [Vec<u8>],
139    /// `FILTER …`: non-scoring predicates over stored values, ANDed.
140    /// They decide which documents are eligible, not what a term is
141    /// worth, so the corpus statistics stay whole-corpus.
142    ///
143    /// ```
144    /// # use kevy_embedded::*;
145    /// # let s = Store::open(Config::default())?;
146    /// # let fields = [(&b"title"[..], 1.0), (b"body", 1.0)];
147    /// # let vals = [(&b"lang"[..], IndexValType::Str), (b"ts", IndexValType::I64)];
148    /// # s.idx_create_text(b"ft", b"d:", &fields, TokenPositions::Omit, &vals)?;
149    /// # for (k, title, body, lang, ts) in [
150    /// #     (&b"d:1"[..], &b"rust"[..], &b"a fast engine"[..], &b"en"[..], &b"1"[..]),
151    /// #     (b"d:2", b"engines", b"written in rust", b"en", b"2"),
152    /// #     (b"d:3", b"rust notes", b"ja docs", b"ja", b"3"),
153    /// # ] {
154    /// #     s.hset(k, &[(b"title", title), (b"body", body), (b"lang", lang), (b"ts", ts)])?;
155    /// # }
156    /// # let keys = |q: &[u8], opts| -> KevyResult<Vec<Vec<u8>>> {
157    /// #     let mut k: Vec<_> = s.idx_match_with(b"ft", q, 10, opts)?.into_iter().map(|h| h.0).collect();
158    /// #     k.sort();
159    /// #     Ok(k)
160    /// # };
161    /// // d:1 title "rust" (en, ts 1), d:2 body "written in rust" (en, 2), d:3 title "rust notes" (ja, 3)
162    /// let en = [ValueFilter::Eq { field: b"lang", value: b"en" }];
163    /// let opts = MatchOpts::default().with_filters(&en);
164    /// assert_eq!(keys(b"rust", opts)?, [b"d:1".to_vec(), b"d:2".to_vec()]);
165    /// # Ok::<(), kevy_embedded::KevyError>(())
166    /// ```
167    pub filters: &'a [ValueFilter<'a>],
168    /// `SORT <field> ASC|DESC`: select by a stored value instead of by
169    /// score. Selecting, not re-ordering — a document that wins on the
170    /// key is chosen even when its score would never have reached the
171    /// page.
172    ///
173    /// ```
174    /// # use kevy_embedded::*;
175    /// # let s = Store::open(Config::default())?;
176    /// # let fields = [(&b"title"[..], 1.0), (b"body", 1.0)];
177    /// # let vals = [(&b"lang"[..], IndexValType::Str), (b"ts", IndexValType::I64)];
178    /// # s.idx_create_text(b"ft", b"d:", &fields, TokenPositions::Omit, &vals)?;
179    /// # for (k, title, body, lang, ts) in [
180    /// #     (&b"d:1"[..], &b"rust"[..], &b"a fast engine"[..], &b"en"[..], &b"1"[..]),
181    /// #     (b"d:2", b"engines", b"written in rust", b"en", b"2"),
182    /// #     (b"d:3", b"rust notes", b"ja docs", b"ja", b"3"),
183    /// # ] {
184    /// #     s.hset(k, &[(b"title", title), (b"body", body), (b"lang", lang), (b"ts", ts)])?;
185    /// # }
186    /// # let keys = |q: &[u8], opts| -> KevyResult<Vec<Vec<u8>>> {
187    /// #     let mut k: Vec<_> = s.idx_match_with(b"ft", q, 10, opts)?.into_iter().map(|h| h.0).collect();
188    /// #     k.sort();
189    /// #     Ok(k)
190    /// # };
191    /// // d:1 title "rust" (en, ts 1), d:2 body "written in rust" (en, 2), d:3 title "rust notes" (ja, 3)
192    /// let newest = MatchOpts::default().with_sort(b"ts", SortOrder::Desc);
193    /// let hits = s.idx_match_with(b"ft", b"rust", 2, newest)?;
194    /// assert_eq!((&hits[0].0[..], &hits[1].0[..]), (&b"d:3"[..], &b"d:2"[..]));
195    /// # Ok::<(), kevy_embedded::KevyError>(())
196    /// ```
197    pub sort: Option<(&'a [u8], SortOrder)>,
198    /// `DISTINCT <field>`: at most one hit per value of a stored field,
199    /// applied during selection so the page holds `limit` distinct
200    /// documents rather than `limit` that then collapse.
201    ///
202    /// ```
203    /// # use kevy_embedded::*;
204    /// # let s = Store::open(Config::default())?;
205    /// # let fields = [(&b"title"[..], 1.0), (b"body", 1.0)];
206    /// # let vals = [(&b"lang"[..], IndexValType::Str), (b"ts", IndexValType::I64)];
207    /// # s.idx_create_text(b"ft", b"d:", &fields, TokenPositions::Omit, &vals)?;
208    /// # for (k, title, body, lang, ts) in [
209    /// #     (&b"d:1"[..], &b"rust"[..], &b"a fast engine"[..], &b"en"[..], &b"1"[..]),
210    /// #     (b"d:2", b"engines", b"written in rust", b"en", b"2"),
211    /// #     (b"d:3", b"rust notes", b"ja docs", b"ja", b"3"),
212    /// # ] {
213    /// #     s.hset(k, &[(b"title", title), (b"body", body), (b"lang", lang), (b"ts", ts)])?;
214    /// # }
215    /// # let keys = |q: &[u8], opts| -> KevyResult<Vec<Vec<u8>>> {
216    /// #     let mut k: Vec<_> = s.idx_match_with(b"ft", q, 10, opts)?.into_iter().map(|h| h.0).collect();
217    /// #     k.sort();
218    /// #     Ok(k)
219    /// # };
220    /// // d:1 title "rust" (en, ts 1), d:2 body "written in rust" (en, 2), d:3 title "rust notes" (ja, 3)
221    /// let per_lang = MatchOpts::default().with_distinct(b"lang");
222    /// assert_eq!(s.idx_match_with(b"ft", b"rust", 10, per_lang)?.len(), 2, "one en hit, one ja hit");
223    /// # Ok::<(), kevy_embedded::KevyError>(())
224    /// ```
225    pub distinct: Option<&'a [u8]>,
226    /// `FACET <field…>`: count each field's values over the whole match
227    /// set. Reported alongside the page rather than shaping it.
228    ///
229    /// ```
230    /// # use kevy_embedded::*;
231    /// # let s = Store::open(Config::default())?;
232    /// # let fields = [(&b"title"[..], 1.0), (b"body", 1.0)];
233    /// # let vals = [(&b"lang"[..], IndexValType::Str), (b"ts", IndexValType::I64)];
234    /// # s.idx_create_text(b"ft", b"d:", &fields, TokenPositions::Omit, &vals)?;
235    /// # for (k, title, body, lang, ts) in [
236    /// #     (&b"d:1"[..], &b"rust"[..], &b"a fast engine"[..], &b"en"[..], &b"1"[..]),
237    /// #     (b"d:2", b"engines", b"written in rust", b"en", b"2"),
238    /// #     (b"d:3", b"rust notes", b"ja docs", b"ja", b"3"),
239    /// # ] {
240    /// #     s.hset(k, &[(b"title", title), (b"body", body), (b"lang", lang), (b"ts", ts)])?;
241    /// # }
242    /// # let keys = |q: &[u8], opts| -> KevyResult<Vec<Vec<u8>>> {
243    /// #     let mut k: Vec<_> = s.idx_match_with(b"ft", q, 10, opts)?.into_iter().map(|h| h.0).collect();
244    /// #     k.sort();
245    /// #     Ok(k)
246    /// # };
247    /// // d:1 title "rust" (en, ts 1), d:2 body "written in rust" (en, 2), d:3 title "rust notes" (ja, 3)
248    /// let lang = [b"lang".to_vec()];
249    /// let opts = MatchOpts::default().with_facets(&lang);
250    /// let page = s.idx_match_faceted(b"ft", b"rust", 1, opts)?;
251    /// assert_eq!(page.hits.len(), 1);
252    /// assert_eq!(page.facets[0], [(b"en".to_vec(), 2), (b"ja".to_vec(), 1)]);
253    /// # Ok::<(), kevy_embedded::KevyError>(())
254    /// ```
255    pub facets: &'a [Vec<u8>],
256}
257
258impl<'a> MatchOpts<'a> {
259    /// Set [`Self::highlight`] to these field names (`&[]` = every
260    /// indexed field).
261    ///
262    /// ```
263    /// let o = kevy_embedded::MatchOpts::default().with_highlight(&[]);
264    /// assert!(o.highlight.is_some());
265    /// ```
266    #[inline]
267    #[must_use]
268    pub fn with_highlight(mut self, fields: &'a [Vec<u8>]) -> Self {
269        self.highlight = Some(fields);
270        self
271    }
272
273    /// Set [`Self::typo`].
274    ///
275    /// ```
276    /// assert_eq!(kevy_embedded::MatchOpts::default().with_typo(2).typo, 2);
277    /// ```
278    #[inline]
279    #[must_use]
280    pub fn with_typo(mut self, typo: u32) -> Self {
281        self.typo = typo;
282        self
283    }
284
285    /// Set [`Self::offset`].
286    ///
287    /// ```
288    /// assert_eq!(kevy_embedded::MatchOpts::default().with_offset(10).offset, 10);
289    /// ```
290    #[inline]
291    #[must_use]
292    pub fn with_offset(mut self, offset: usize) -> Self {
293        self.offset = offset;
294        self
295    }
296
297    /// Set [`Self::scope`].
298    ///
299    /// ```
300    /// let fields = [b"title".to_vec()];
301    /// assert_eq!(kevy_embedded::MatchOpts::default().with_scope(&fields).scope.len(), 1);
302    /// ```
303    #[inline]
304    #[must_use]
305    pub fn with_scope(mut self, scope: &'a [Vec<u8>]) -> Self {
306        self.scope = scope;
307        self
308    }
309
310    /// Set [`Self::filters`].
311    ///
312    /// ```
313    /// let f = [kevy_embedded::ValueFilter::Eq { field: b"lang", value: b"en" }];
314    /// assert_eq!(kevy_embedded::MatchOpts::default().with_filters(&f).filters.len(), 1);
315    /// ```
316    #[inline]
317    #[must_use]
318    pub fn with_filters(mut self, filters: &'a [ValueFilter<'a>]) -> Self {
319        self.filters = filters;
320        self
321    }
322
323    /// Set [`Self::sort`]: select by `field`, in `order`.
324    ///
325    /// ```
326    /// use kevy_embedded::{MatchOpts, SortOrder};
327    /// assert!(MatchOpts::default().with_sort(b"ts", SortOrder::Desc).sort.is_some());
328    /// ```
329    #[inline]
330    #[must_use]
331    pub fn with_sort(mut self, field: &'a [u8], order: SortOrder) -> Self {
332        self.sort = Some((field, order));
333        self
334    }
335
336    /// Set [`Self::distinct`].
337    ///
338    /// ```
339    /// let o = kevy_embedded::MatchOpts::default().with_distinct(b"author");
340    /// assert_eq!(o.distinct, Some(&b"author"[..]));
341    /// ```
342    #[inline]
343    #[must_use]
344    pub fn with_distinct(mut self, field: &'a [u8]) -> Self {
345        self.distinct = Some(field);
346        self
347    }
348
349    /// Set [`Self::facets`].
350    ///
351    /// ```
352    /// let fields = [b"lang".to_vec()];
353    /// assert_eq!(kevy_embedded::MatchOpts::default().with_facets(&fields).facets.len(), 1);
354    /// ```
355    #[inline]
356    #[must_use]
357    pub fn with_facets(mut self, facets: &'a [Vec<u8>]) -> Self {
358        self.facets = facets;
359        self
360    }
361}