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}