1use arrow::buffer::{OffsetBuffer, ScalarBuffer};
7use arrow_array::ListArray;
8use arrow_schema::Field;
9use datafusion::functions::regex::regexplike::RegexpLikeFunc;
10use datafusion::functions::string::contains::ContainsFunc;
11use datafusion::functions_nested::array_has;
12use datafusion_common::{Column, scalar::ScalarValue};
13use std::collections::HashSet;
14use std::{any::Any, ops::Bound, sync::Arc};
15
16use datafusion_expr::{
17 Expr,
18 expr::{Like, ScalarFunction},
19};
20use inverted::query::{FtsQuery, FtsQueryNode, FtsSearchParams, MatchQuery, fill_fts_query_column};
21use lance_core::Result;
22
23use lance_datafusion::udf::CONTAINS_TOKENS_UDF;
24
25use crate::IndexParams;
26pub use crate::metrics::MetricsCollector;
27pub use lance_index_core::scalar::{
28 AnyQuery, BuiltinIndexType, CreatedIndex, IndexFile, IndexReader, IndexStore, IndexWriter,
29 LANCE_SCALAR_INDEX, OldIndexDataFilter, RowIdRemapper, ScalarIndex, ScalarIndexParams,
30 SearchResult, TrainingCriteria, TrainingOrdering, UpdateCriteria,
31};
32
33pub mod bitmap;
34pub mod bloomfilter;
35pub mod btree;
36pub mod expression;
37pub mod fmindex;
38pub mod inverted;
39pub mod json;
40pub mod label_list;
41pub mod lance_format;
42pub mod ngram;
43pub mod registry;
44#[cfg(feature = "geo")]
45pub mod rtree;
46pub mod zoned;
47pub mod zonemap;
48
49pub use inverted::tokenizer::InvertedIndexParams;
50
51pub fn index_files_to_table(
57 files: Vec<lance_index_core::scalar::IndexFile>,
58) -> Vec<lance_table::format::IndexFile> {
59 files
60 .into_iter()
61 .map(|f| lance_table::format::IndexFile {
62 path: f.path,
63 size_bytes: f.size_bytes,
64 })
65 .collect()
66}
67
68pub fn table_files_to_index(
74 files: Vec<lance_table::format::IndexFile>,
75) -> Vec<lance_index_core::scalar::IndexFile> {
76 files
77 .into_iter()
78 .map(|f| lance_index_core::scalar::IndexFile {
79 path: f.path,
80 size_bytes: f.size_bytes,
81 })
82 .collect()
83}
84
85impl IndexParams for InvertedIndexParams {
86 fn as_any(&self) -> &dyn std::any::Any {
87 self
88 }
89
90 fn index_name(&self) -> &str {
91 "INVERTED"
92 }
93}
94
95#[derive(Debug, Clone, PartialEq)]
97pub struct FullTextSearchQuery {
98 pub query: FtsQuery,
99
100 pub limit: Option<i64>,
102
103 pub wand_factor: Option<f32>,
108}
109
110impl FullTextSearchQuery {
111 pub fn new(query: String) -> Self {
113 let query = MatchQuery::new(query).into();
114 Self {
115 query,
116 limit: None,
117 wand_factor: None,
118 }
119 }
120
121 pub fn new_fuzzy(term: String, max_distance: Option<u32>) -> Self {
123 let query = MatchQuery::new(term).with_fuzziness(max_distance).into();
124 Self {
125 query,
126 limit: None,
127 wand_factor: None,
128 }
129 }
130
131 pub fn new_query(query: FtsQuery) -> Self {
133 Self {
134 query,
135 limit: None,
136 wand_factor: None,
137 }
138 }
139
140 pub fn with_column(mut self, column: String) -> Result<Self> {
143 self.query = fill_fts_query_column(&self.query, &[column], true)?;
144 Ok(self)
145 }
146
147 pub fn with_columns(mut self, columns: &[String]) -> Result<Self> {
150 self.query = fill_fts_query_column(&self.query, columns, true)?;
151 Ok(self)
152 }
153
154 pub fn limit(mut self, limit: Option<i64>) -> Self {
157 self.limit = limit;
158 self
159 }
160
161 pub fn wand_factor(mut self, wand_factor: Option<f32>) -> Self {
162 self.wand_factor = wand_factor;
163 self
164 }
165
166 pub fn columns(&self) -> HashSet<String> {
167 self.query.columns()
168 }
169
170 pub fn params(&self) -> FtsSearchParams {
171 FtsSearchParams::new()
172 .with_limit(self.limit.map(|limit| limit as usize))
173 .with_wand_factor(self.wand_factor.unwrap_or(1.0))
174 }
175}
176
177#[derive(Debug, Clone, PartialEq)]
187pub enum SargableQuery {
188 Range(Bound<ScalarValue>, Bound<ScalarValue>),
190 IsIn(Vec<ScalarValue>),
192 Equals(ScalarValue),
194 FullTextSearch(FullTextSearchQuery),
196 IsNull(),
198 LikePrefix(ScalarValue),
201}
202
203fn escape_like_pattern(s: &str) -> String {
206 let mut out = String::with_capacity(s.len() + 2);
207 for c in s.chars() {
208 if matches!(c, '\\' | '%' | '_') {
209 out.push('\\');
210 }
211 out.push(c);
212 }
213 out
214}
215
216impl AnyQuery for SargableQuery {
217 fn as_any(&self) -> &dyn Any {
218 self
219 }
220
221 fn format(&self, col: &str) -> String {
222 match self {
223 Self::Range(lower, upper) => match (lower, upper) {
224 (Bound::Unbounded, Bound::Unbounded) => "true".to_string(),
225 (Bound::Unbounded, Bound::Included(rhs)) => format!("{} <= {}", col, rhs),
226 (Bound::Unbounded, Bound::Excluded(rhs)) => format!("{} < {}", col, rhs),
227 (Bound::Included(lhs), Bound::Unbounded) => format!("{} >= {}", col, lhs),
228 (Bound::Included(lhs), Bound::Included(rhs)) => {
229 format!("{} >= {} && {} <= {}", col, lhs, col, rhs)
230 }
231 (Bound::Included(lhs), Bound::Excluded(rhs)) => {
232 format!("{} >= {} && {} < {}", col, lhs, col, rhs)
233 }
234 (Bound::Excluded(lhs), Bound::Unbounded) => format!("{} > {}", col, lhs),
235 (Bound::Excluded(lhs), Bound::Included(rhs)) => {
236 format!("{} > {} && {} <= {}", col, lhs, col, rhs)
237 }
238 (Bound::Excluded(lhs), Bound::Excluded(rhs)) => {
239 format!("{} > {} && {} < {}", col, lhs, col, rhs)
240 }
241 },
242 Self::IsIn(values) => {
243 format!(
244 "{} IN [{}]",
245 col,
246 values
247 .iter()
248 .map(|val| val.to_string())
249 .collect::<Vec<_>>()
250 .join(",")
251 )
252 }
253 Self::FullTextSearch(query) => {
254 format!("fts({})", query.query)
255 }
256 Self::IsNull() => {
257 format!("{} IS NULL", col)
258 }
259 Self::Equals(val) => {
260 format!("{} = {}", col, val)
261 }
262 Self::LikePrefix(prefix) => {
263 format!("{} LIKE '{}%'", col, prefix)
264 }
265 }
266 }
267
268 fn to_expr(&self, col: String) -> Expr {
269 let col_expr = Expr::Column(Column::new_unqualified(col));
270 match self {
271 Self::Range(lower, upper) => match (lower, upper) {
272 (Bound::Unbounded, Bound::Unbounded) => {
273 Expr::Literal(ScalarValue::Boolean(Some(true)), None)
274 }
275 (Bound::Unbounded, Bound::Included(rhs)) => {
276 col_expr.lt_eq(Expr::Literal(rhs.clone(), None))
277 }
278 (Bound::Unbounded, Bound::Excluded(rhs)) => {
279 col_expr.lt(Expr::Literal(rhs.clone(), None))
280 }
281 (Bound::Included(lhs), Bound::Unbounded) => {
282 col_expr.gt_eq(Expr::Literal(lhs.clone(), None))
283 }
284 (Bound::Included(lhs), Bound::Included(rhs)) => col_expr.between(
285 Expr::Literal(lhs.clone(), None),
286 Expr::Literal(rhs.clone(), None),
287 ),
288 (Bound::Included(lhs), Bound::Excluded(rhs)) => col_expr
289 .clone()
290 .gt_eq(Expr::Literal(lhs.clone(), None))
291 .and(col_expr.lt(Expr::Literal(rhs.clone(), None))),
292 (Bound::Excluded(lhs), Bound::Unbounded) => {
293 col_expr.gt(Expr::Literal(lhs.clone(), None))
294 }
295 (Bound::Excluded(lhs), Bound::Included(rhs)) => col_expr
296 .clone()
297 .gt(Expr::Literal(lhs.clone(), None))
298 .and(col_expr.lt_eq(Expr::Literal(rhs.clone(), None))),
299 (Bound::Excluded(lhs), Bound::Excluded(rhs)) => col_expr
300 .clone()
301 .gt(Expr::Literal(lhs.clone(), None))
302 .and(col_expr.lt(Expr::Literal(rhs.clone(), None))),
303 },
304 Self::IsIn(values) => col_expr.in_list(
305 values
306 .iter()
307 .map(|val| Expr::Literal(val.clone(), None))
308 .collect::<Vec<_>>(),
309 false,
310 ),
311 Self::FullTextSearch(query) => col_expr.like(Expr::Literal(
312 ScalarValue::Utf8(Some(query.query.to_string())),
313 None,
314 )),
315 Self::IsNull() => col_expr.is_null(),
316 Self::Equals(value) => col_expr.eq(Expr::Literal(value.clone(), None)),
317 Self::LikePrefix(prefix) => match prefix {
318 ScalarValue::Utf8(Some(s))
319 | ScalarValue::LargeUtf8(Some(s))
320 | ScalarValue::Utf8View(Some(s)) => {
321 let escaped = escape_like_pattern(s);
334 let needs_escape = escaped.as_str() != s.as_str();
335 let pattern = format!("{}%", escaped);
336 let pattern_value = match prefix {
337 ScalarValue::LargeUtf8(_) => ScalarValue::LargeUtf8(Some(pattern)),
338 _ => ScalarValue::Utf8(Some(pattern)),
339 };
340 if needs_escape {
341 Expr::Like(Like {
342 negated: false,
343 expr: Box::new(col_expr),
344 pattern: Box::new(Expr::Literal(pattern_value, None)),
345 escape_char: Some('\\'),
346 case_insensitive: false,
347 })
348 } else {
349 col_expr.like(Expr::Literal(pattern_value, None))
350 }
351 }
352 other => col_expr.like(Expr::Literal(other.clone(), None)),
353 },
354 }
355 }
356
357 fn dyn_eq(&self, other: &dyn AnyQuery) -> bool {
358 match other.as_any().downcast_ref::<Self>() {
359 Some(o) => self == o,
360 None => false,
361 }
362 }
363}
364
365#[derive(Debug, Clone, PartialEq)]
367pub enum LabelListQuery {
368 HasAllLabels(Vec<ScalarValue>),
370 HasAnyLabel(Vec<ScalarValue>),
372}
373
374impl AnyQuery for LabelListQuery {
375 fn as_any(&self) -> &dyn Any {
376 self
377 }
378
379 fn format(&self, col: &str) -> String {
380 format!("{}", self.to_expr(col.to_string()))
381 }
382
383 fn to_expr(&self, col: String) -> Expr {
384 match self {
385 Self::HasAllLabels(labels) => {
386 let labels_arr = ScalarValue::iter_to_array(labels.iter().cloned()).unwrap();
387 let offsets_buffer =
388 OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0, labels_arr.len() as i32]));
389 let labels_list = ListArray::try_new(
390 Arc::new(Field::new("item", labels_arr.data_type().clone(), true)),
391 offsets_buffer,
392 labels_arr,
393 None,
394 )
395 .unwrap();
396 let labels_arr = Arc::new(labels_list);
397 Expr::ScalarFunction(ScalarFunction {
398 func: Arc::new(array_has::ArrayHasAll::new().into()),
399 args: vec![
400 Expr::Column(Column::new_unqualified(col)),
401 Expr::Literal(ScalarValue::List(labels_arr), None),
402 ],
403 })
404 }
405 Self::HasAnyLabel(labels) => {
406 let labels_arr = ScalarValue::iter_to_array(labels.iter().cloned()).unwrap();
407 let offsets_buffer =
408 OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0, labels_arr.len() as i32]));
409 let labels_list = ListArray::try_new(
410 Arc::new(Field::new("item", labels_arr.data_type().clone(), true)),
411 offsets_buffer,
412 labels_arr,
413 None,
414 )
415 .unwrap();
416 let labels_arr = Arc::new(labels_list);
417 Expr::ScalarFunction(ScalarFunction {
418 func: Arc::new(array_has::ArrayHasAny::new().into()),
419 args: vec![
420 Expr::Column(Column::new_unqualified(col)),
421 Expr::Literal(ScalarValue::List(labels_arr), None),
422 ],
423 })
424 }
425 }
426 }
427
428 fn dyn_eq(&self, other: &dyn AnyQuery) -> bool {
429 match other.as_any().downcast_ref::<Self>() {
430 Some(o) => self == o,
431 None => false,
432 }
433 }
434}
435
436#[derive(Debug, Clone, PartialEq)]
438pub enum TextQuery {
439 StringContains(String),
441 Regex(String),
448 }
451
452impl AnyQuery for TextQuery {
453 fn as_any(&self) -> &dyn Any {
454 self
455 }
456
457 fn format(&self, col: &str) -> String {
458 format!("{}", self.to_expr(col.to_string()))
459 }
460
461 fn to_expr(&self, col: String) -> Expr {
462 match self {
463 Self::StringContains(substr) => Expr::ScalarFunction(ScalarFunction {
464 func: Arc::new(ContainsFunc::new().into()),
465 args: vec![
466 Expr::Column(Column::new_unqualified(col)),
467 Expr::Literal(ScalarValue::Utf8(Some(substr.clone())), None),
468 ],
469 }),
470 Self::Regex(pattern) => Expr::ScalarFunction(ScalarFunction {
475 func: Arc::new(RegexpLikeFunc::new().into()),
476 args: vec![
477 Expr::Column(Column::new_unqualified(col)),
478 Expr::Literal(ScalarValue::Utf8(Some(pattern.clone())), None),
479 ],
480 }),
481 }
482 }
483
484 fn dyn_eq(&self, other: &dyn AnyQuery) -> bool {
485 match other.as_any().downcast_ref::<Self>() {
486 Some(o) => self == o,
487 None => false,
488 }
489 }
490}
491
492#[derive(Debug, Clone, PartialEq)]
494pub enum TokenQuery {
495 TokensContains(String),
498}
499
500#[derive(Debug, Clone, PartialEq)]
505pub enum BloomFilterQuery {
506 Equals(ScalarValue),
508 IsNull(),
510 IsIn(Vec<ScalarValue>),
512}
513
514impl AnyQuery for BloomFilterQuery {
515 fn as_any(&self) -> &dyn Any {
516 self
517 }
518
519 fn format(&self, col: &str) -> String {
520 match self {
521 Self::Equals(val) => {
522 format!("{} = {}", col, val)
523 }
524 Self::IsNull() => {
525 format!("{} IS NULL", col)
526 }
527 Self::IsIn(values) => {
528 format!(
529 "{} IN [{}]",
530 col,
531 values
532 .iter()
533 .map(|val| val.to_string())
534 .collect::<Vec<_>>()
535 .join(",")
536 )
537 }
538 }
539 }
540
541 fn to_expr(&self, col: String) -> Expr {
542 let col_expr = Expr::Column(Column::new_unqualified(col));
543 match self {
544 Self::Equals(value) => col_expr.eq(Expr::Literal(value.clone(), None)),
545 Self::IsNull() => col_expr.is_null(),
546 Self::IsIn(values) => col_expr.in_list(
547 values
548 .iter()
549 .map(|val| Expr::Literal(val.clone(), None))
550 .collect::<Vec<_>>(),
551 false,
552 ),
553 }
554 }
555
556 fn dyn_eq(&self, other: &dyn AnyQuery) -> bool {
557 match other.as_any().downcast_ref::<Self>() {
558 Some(o) => self == o,
559 None => false,
560 }
561 }
562}
563
564impl AnyQuery for TokenQuery {
565 fn as_any(&self) -> &dyn Any {
566 self
567 }
568
569 fn format(&self, col: &str) -> String {
570 format!("{}", self.to_expr(col.to_string()))
571 }
572
573 fn to_expr(&self, col: String) -> Expr {
574 match self {
575 Self::TokensContains(substr) => Expr::ScalarFunction(ScalarFunction {
576 func: Arc::new(CONTAINS_TOKENS_UDF.clone()),
577 args: vec![
578 Expr::Column(Column::new_unqualified(col)),
579 Expr::Literal(ScalarValue::Utf8(Some(substr.clone())), None),
580 ],
581 }),
582 }
583 }
584
585 fn dyn_eq(&self, other: &dyn AnyQuery) -> bool {
586 match other.as_any().downcast_ref::<Self>() {
587 Some(o) => self == o,
588 None => false,
589 }
590 }
591}
592
593#[cfg(feature = "geo")]
594#[derive(Debug, Clone, PartialEq)]
595pub struct RelationQuery {
596 pub value: ScalarValue,
597 pub field: Field,
598}
599
600#[cfg(feature = "geo")]
602#[derive(Debug, Clone, PartialEq)]
603pub enum GeoQuery {
604 IntersectQuery(RelationQuery),
605 IsNull,
606}
607
608#[cfg(feature = "geo")]
609impl AnyQuery for GeoQuery {
610 fn as_any(&self) -> &dyn Any {
611 self
612 }
613
614 fn format(&self, col: &str) -> String {
615 match self {
616 Self::IntersectQuery(query) => {
617 format!("Intersect({} {})", col, query.value)
618 }
619 Self::IsNull => {
620 format!("{} IS NULL", col)
621 }
622 }
623 }
624
625 fn to_expr(&self, _col: String) -> Expr {
626 todo!()
627 }
628
629 fn dyn_eq(&self, other: &dyn AnyQuery) -> bool {
630 match other.as_any().downcast_ref::<Self>() {
631 Some(o) => self == o,
632 None => false,
633 }
634 }
635}
636
637pub fn compute_next_prefix(prefix: &str) -> Option<String> {
657 if prefix.is_empty() {
658 return None;
659 }
660
661 let chars: Vec<char> = prefix.chars().collect();
662
663 for i in (0..chars.len()).rev() {
665 if let Some(next_char) = next_unicode_char(chars[i]) {
666 let mut result: String = chars[..i].iter().collect();
667 result.push(next_char);
668 return Some(result);
669 }
670 }
672
673 None
675}
676
677fn next_unicode_char(c: char) -> Option<char> {
680 let cp = c as u32;
681 let next_cp = cp.checked_add(1)?;
682
683 let next_cp = if (0xD800..=0xDFFF).contains(&next_cp) {
685 0xE000
686 } else {
687 next_cp
688 };
689
690 char::from_u32(next_cp)
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696
697 #[test]
698 fn test_like_prefix_to_expr_escapes_metacharacters() {
699 let query = SargableQuery::LikePrefix(ScalarValue::Utf8(Some("a_b%x".to_string())));
703 let Expr::Like(like) = query.to_expr("name".to_string()) else {
704 panic!("expected a LIKE expression");
705 };
706 assert_eq!(like.escape_char, Some('\\'));
707 assert!(!like.negated);
708 assert!(!like.case_insensitive);
709 let Expr::Literal(ScalarValue::Utf8(Some(pattern)), _) = like.pattern.as_ref() else {
710 panic!("expected a Utf8 literal pattern");
711 };
712 assert_eq!(pattern.as_str(), "a\\_b\\%x%");
713
714 let query = SargableQuery::LikePrefix(ScalarValue::Utf8(Some("app".to_string())));
717 let Expr::Like(like) = query.to_expr("name".to_string()) else {
718 panic!("expected a LIKE expression");
719 };
720 assert_eq!(like.escape_char, None);
721 let Expr::Literal(ScalarValue::Utf8(Some(pattern)), _) = like.pattern.as_ref() else {
722 panic!("expected a Utf8 literal pattern");
723 };
724 assert_eq!(pattern.as_str(), "app%");
725
726 let query = SargableQuery::LikePrefix(ScalarValue::Utf8View(Some("a_b%x".to_string())));
731 let Expr::Like(like) = query.to_expr("name".to_string()) else {
732 panic!("expected a LIKE expression");
733 };
734 assert_eq!(like.escape_char, Some('\\'));
735 let Expr::Literal(ScalarValue::Utf8(Some(pattern)), _) = like.pattern.as_ref() else {
736 panic!("expected a Utf8 literal pattern");
737 };
738 assert_eq!(pattern.as_str(), "a\\_b\\%x%");
739
740 let query = SargableQuery::LikePrefix(ScalarValue::Utf8View(Some("app".to_string())));
741 let Expr::Like(like) = query.to_expr("name".to_string()) else {
742 panic!("expected a LIKE expression");
743 };
744 assert_eq!(like.escape_char, None);
745 let Expr::Literal(ScalarValue::Utf8(Some(pattern)), _) = like.pattern.as_ref() else {
746 panic!("expected a Utf8 literal pattern");
747 };
748 assert_eq!(pattern.as_str(), "app%");
749 }
750}