1use std::collections::HashSet;
5
6use datafusion::config::ConfigOptions;
7use std::ops::Range;
8use std::pin::Pin;
9use std::sync::{Arc, LazyLock};
10use std::task::{Context, Poll};
11
12use crate::index::DatasetIndexExt;
13use arrow::array::AsArray;
14use arrow_array::{Array, Float32Array, Int64Array, RecordBatch};
15use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema, SchemaRef, SortOptions};
16use arrow_select::concat::concat_batches;
17use async_recursion::async_recursion;
18use chrono::Utc;
19use datafusion::common::{DFSchema, JoinType, NullEquality, SchemaExt, exec_datafusion_err};
20use datafusion::functions_aggregate;
21use datafusion::logical_expr::{Expr, ScalarUDF, col, lit};
22use datafusion::physical_expr::PhysicalSortExpr;
23#[allow(deprecated)]
24use datafusion::physical_plan::coalesce_batches::CoalesceBatchesExec;
25use datafusion::physical_plan::expressions;
26use datafusion::physical_plan::projection::ProjectionExec as DFProjectionExec;
27use datafusion::physical_plan::sorts::sort::SortExec;
28use datafusion::physical_plan::{
29 ExecutionPlan, SendableRecordBatchStream,
30 aggregates::{AggregateExec, AggregateMode, PhysicalGroupBy},
31 display::DisplayableExecutionPlan,
32 limit::GlobalLimitExec,
33 repartition::RepartitionExec,
34 union::UnionExec,
35};
36use datafusion::scalar::ScalarValue;
37use datafusion_expr::ExprSchemable;
38use datafusion_expr::execution_props::ExecutionProps;
39use datafusion_functions::core::getfield::GetFieldFunc;
40use datafusion_physical_expr::expressions::Column;
41use datafusion_physical_expr::{LexOrdering, Partitioning, PhysicalExpr, create_physical_expr};
42use datafusion_physical_plan::joins::PartitionMode;
43use datafusion_physical_plan::projection::ProjectionExec;
44use datafusion_physical_plan::stream::RecordBatchStreamAdapter;
45use datafusion_physical_plan::{empty::EmptyExec, joins::HashJoinExec};
46use futures::future::BoxFuture;
47use futures::stream::{Stream, StreamExt};
48use futures::{FutureExt, TryStreamExt};
49use lance_arrow::floats::{FloatType, coerce_float_vector};
50use lance_arrow::{DataTypeExt, SchemaExt as ArrowSchemaExt};
51use lance_core::datatypes::{
52 BlobHandling, Field, OnMissing, Projection, escape_field_path_for_project, format_field_path,
53};
54use lance_core::error::LanceOptionExt;
55use lance_core::utils::address::RowAddress;
56use lance_core::utils::mask::{RowAddrMask, RowAddrTreeMap};
57use lance_core::utils::tokio::get_num_compute_intensive_cpus;
58use lance_core::{ROW_ADDR, ROW_ID, ROW_OFFSET};
59use lance_datafusion::aggregate::Aggregate;
60use lance_datafusion::exec::{
61 LanceExecutionOptions, OneShotExec, StrictBatchSizeExec, analyze_plan, execute_plan,
62};
63use lance_datafusion::expr::safe_coerce_scalar;
64use lance_datafusion::projection::ProjectionPlan;
65use lance_file::reader::FileReaderOptions;
66use lance_index::IndexCriteria;
67use lance_index::scalar::FullTextSearchQuery;
68use lance_index::scalar::expression::ScalarIndexExpr;
69use lance_index::scalar::expression::{INDEX_EXPR_RESULT_SCHEMA, IndexExprResult, PlannerIndexExt};
70use lance_index::scalar::inverted::query::{
71 FtsQuery, FtsQueryNode, FtsSearchParams, MatchQuery, PhraseQuery, fill_fts_query_column,
72};
73use lance_index::scalar::inverted::{SCORE_COL, SCORE_FIELD};
74use lance_index::vector::{DEFAULT_QUERY_PARALLELISM, DIST_COL, Query};
75use lance_index::{metrics::NoOpMetricsCollector, scalar::inverted::FTS_SCHEMA};
76use lance_io::stream::RecordBatchStream;
77use lance_linalg::distance::MetricType;
78use lance_table::format::{Fragment, IndexMetadata};
79use roaring::RoaringBitmap;
80use tracing::{Span, info_span, instrument};
81use uuid::Uuid;
82
83use super::Dataset;
84use crate::dataset::row_offsets_to_row_addresses;
85use crate::dataset::utils::SchemaAdapter;
86use crate::index::DatasetIndexInternalExt;
87use crate::index::scalar::inverted::{load_segment_details, load_segments};
88use crate::index::scalar_logical::scalar_index_fragment_bitmap;
89use crate::index::vector::utils::{
90 default_distance_type_for, get_vector_dim, get_vector_type, validate_distance_type_for,
91};
92use crate::io::exec::filtered_read::{FilteredReadExec, FilteredReadOptions};
93use crate::io::exec::fts::{
94 BoostQueryExec, FlatMatchFilterExec, FlatMatchQueryExec, MatchQueryExec, PhraseQueryExec,
95};
96use crate::io::exec::knn::MultivectorScoringExec;
97use crate::io::exec::scalar_index::{MaterializeIndexExec, ScalarIndexExec};
98use crate::io::exec::{
99 AddRowAddrExec, FilterPlan as ExprFilterPlan, KNNVectorDistanceExec, LancePushdownScanExec,
100 LanceScanExec, Planner, PreFilterSource, ScanConfig, TakeExec,
101 knn::{KNN_INDEX_SCHEMA, new_knn_exec},
102 project,
103};
104use crate::io::exec::{AddRowOffsetExec, LanceFilterExec, LanceScanConfig, get_physical_optimizer};
105use crate::{Error, Result};
106use crate::{datatypes::Schema, io::exec::fts::BooleanQueryExec};
107
108pub use lance_datafusion::exec::{ExecutionStatsCallback, ExecutionSummaryCounts};
109#[cfg(feature = "substrait")]
110use lance_datafusion::substrait::parse_substrait;
111
112pub(crate) const BATCH_SIZE_FALLBACK: usize = 8192;
113
114fn parse_env_var<T: std::str::FromStr>(env_var_name: &str, default_val: &str) -> Option<T>
116where
117 T::Err: std::fmt::Display,
118{
119 std::env::var(env_var_name)
120 .ok()
121 .and_then(|val| match val.parse() {
122 Ok(value) => Some(value),
123 Err(e) => {
124 log::warn!(
125 "Failed to parse the environment variable {}='{}': {}, the default value is: {}.",
126 env_var_name,
127 val,
128 e,
129 default_val
130 );
131 None
132 }
133 })
134}
135
136pub fn get_default_batch_size() -> Option<usize> {
139 parse_env_var("LANCE_DEFAULT_BATCH_SIZE", &BATCH_SIZE_FALLBACK.to_string())
140}
141
142pub const LEGACY_DEFAULT_FRAGMENT_READAHEAD: usize = 4;
143
144pub static DEFAULT_FRAGMENT_READAHEAD: LazyLock<Option<usize>> = LazyLock::new(|| {
145 parse_env_var(
146 "LANCE_DEFAULT_FRAGMENT_READAHEAD",
147 &LEGACY_DEFAULT_FRAGMENT_READAHEAD.to_string(),
148 )
149});
150
151const DEFAULT_XTR_OVERFETCH_VALUE: u32 = 10;
152
153pub static DEFAULT_XTR_OVERFETCH: LazyLock<u32> = LazyLock::new(|| {
154 parse_env_var(
155 "LANCE_XTR_OVERFETCH",
156 &DEFAULT_XTR_OVERFETCH_VALUE.to_string(),
157 )
158 .unwrap_or(DEFAULT_XTR_OVERFETCH_VALUE)
159});
160
161const DEFAULT_IO_BUFFER_SIZE_VALUE: u64 = 2 * 1024 * 1024 * 1024;
165
166pub static DEFAULT_IO_BUFFER_SIZE: LazyLock<u64> = LazyLock::new(|| {
167 parse_env_var(
168 "LANCE_DEFAULT_IO_BUFFER_SIZE",
169 &DEFAULT_IO_BUFFER_SIZE_VALUE.to_string(),
170 )
171 .unwrap_or(DEFAULT_IO_BUFFER_SIZE_VALUE)
172});
173
174pub fn get_default_io_buffer_size_override() -> Option<u64> {
180 parse_env_var(
181 "LANCE_DEFAULT_IO_BUFFER_SIZE",
182 &DEFAULT_IO_BUFFER_SIZE_VALUE.to_string(),
183 )
184}
185
186#[derive(Debug, Clone)]
191pub struct ColumnOrdering {
192 pub ascending: bool,
193 pub nulls_first: bool,
194 pub column_name: String,
195}
196
197impl ColumnOrdering {
198 pub fn asc_nulls_first(column_name: String) -> Self {
199 Self {
200 ascending: true,
201 nulls_first: true,
202 column_name,
203 }
204 }
205
206 pub fn asc_nulls_last(column_name: String) -> Self {
207 Self {
208 ascending: true,
209 nulls_first: false,
210 column_name,
211 }
212 }
213
214 pub fn desc_nulls_first(column_name: String) -> Self {
215 Self {
216 ascending: false,
217 nulls_first: true,
218 column_name,
219 }
220 }
221
222 pub fn desc_nulls_last(column_name: String) -> Self {
223 Self {
224 ascending: false,
225 nulls_first: false,
226 column_name,
227 }
228 }
229}
230
231#[derive(Clone)]
245pub enum MaterializationStyle {
246 Heuristic,
258 AllLate,
260 AllEarly,
262 AllEarlyExcept(Vec<u32>),
264}
265
266impl MaterializationStyle {
267 pub fn all_early_except(columns: &[impl AsRef<str>], schema: &Schema) -> Result<Self> {
268 let field_ids = schema
269 .project(columns)?
270 .field_ids()
271 .into_iter()
272 .map(|id| id as u32)
273 .collect();
274 Ok(Self::AllEarlyExcept(field_ids))
275 }
276}
277
278#[derive(Debug)]
279struct PlannedFilteredScan {
280 plan: Arc<dyn ExecutionPlan>,
281 limit_pushed_down: bool,
282 filter_pushed_down: bool,
283}
284
285pub struct FilterPlan {
286 query_filter: Option<QueryFilter>,
288 refine_query_filter: bool,
289 expr_filter_plan: ExprFilterPlan,
291}
292
293impl FilterPlan {
294 pub fn new(query_filter: Option<QueryFilter>, expr_filter_plan: ExprFilterPlan) -> Self {
295 Self {
296 query_filter,
297 refine_query_filter: false,
298 expr_filter_plan,
299 }
300 }
301
302 pub fn disable_refine(&mut self) {
303 self.expr_filter_plan = ExprFilterPlan::default();
304 self.refine_query_filter = false;
305 }
306
307 pub fn make_refine_only(&mut self) {
308 self.expr_filter_plan.make_refine_only();
309 self.refine_query_filter = true;
310 }
311
312 pub fn fts_filter(&self) -> Option<FullTextSearchQuery> {
313 match &self.query_filter {
314 Some(QueryFilter::Fts(query)) => Some(query.clone()),
315 _ => None,
316 }
317 }
318
319 pub fn vector_filter(&self) -> Option<Query> {
320 match &self.query_filter {
321 Some(QueryFilter::Vector(query)) => Some(query.clone()),
322 _ => None,
323 }
324 }
325
326 pub fn has_refine(&self) -> bool {
327 self.expr_filter_plan.has_refine() || self.refine_query_filter
328 }
329
330 pub async fn refine_columns(&self, dataset: &Arc<Dataset>) -> Result<Vec<String>> {
331 let mut columns = vec![];
332
333 if self.expr_filter_plan.has_refine() {
334 columns.extend(self.expr_filter_plan.refine_columns());
335 }
336
337 if self.refine_query_filter {
338 match &self.query_filter {
339 Some(QueryFilter::Fts(fts_query)) => {
340 let cols = if fts_query.columns().is_empty() {
341 let indexed_columns = fts_indexed_columns(dataset.clone()).await?;
342 let q = fill_fts_query_column(&fts_query.query, &indexed_columns, false)?;
343 q.columns()
344 } else {
345 fts_query.columns()
346 };
347
348 if let FtsQuery::Match(_) = &fts_query.query {
351 columns.extend(cols.iter().cloned().collect::<Vec<_>>());
352 }
353 }
354 Some(QueryFilter::Vector(vector_query)) => {
355 columns.push(vector_query.column.clone());
356 }
357 None => {}
358 }
359 }
360
361 Ok(columns)
362 }
363
364 pub async fn refine_filter(
365 &self,
366 input: Arc<dyn ExecutionPlan>,
367 scanner: &Scanner,
368 ) -> Result<Arc<dyn ExecutionPlan>> {
369 let mut plan = input;
370
371 if self.refine_query_filter {
372 match &self.query_filter {
373 Some(QueryFilter::Fts(fts_query)) => {
374 plan = scanner.flat_fts_filter(plan, fts_query).await?;
375 }
376 Some(QueryFilter::Vector(vector_query)) => {
377 plan = scanner.flat_knn(plan, vector_query)?;
378 }
379 None => {}
380 }
381 }
382
383 if let Some(refine_expr) = &self.expr_filter_plan.refine_expr {
384 plan = Arc::new(LanceFilterExec::try_new(refine_expr.clone(), plan)?);
387 }
388
389 Ok(plan)
390 }
391}
392
393#[derive(Debug, Clone, Default)]
394pub struct LanceFilter {
395 query_filter: Option<QueryFilter>,
396 expr_filter: Option<ExprFilter>,
397}
398
399impl LanceFilter {
400 pub fn is_none(&self) -> bool {
401 self.query_filter.is_none() && self.expr_filter.is_none()
402 }
403}
404
405#[derive(Debug, Clone)]
407pub enum QueryFilter {
408 Fts(FullTextSearchQuery),
409 Vector(Query),
410}
411
412#[derive(Debug, Clone)]
414pub enum ExprFilter {
415 Sql(String),
417 Substrait(Vec<u8>),
419 Datafusion(Expr),
421}
422
423impl ExprFilter {
424 #[allow(unused)]
433 #[instrument(level = "trace", name = "filter_to_df", skip_all)]
434 pub fn to_datafusion(&self, dataset_schema: &Schema, full_schema: &Schema) -> Result<Expr> {
435 match self {
436 Self::Sql(sql) => {
437 let schema = Arc::new(ArrowSchema::from(full_schema));
438 let planner = Planner::new(schema.clone());
439 let filter = planner.parse_filter(sql)?;
440
441 let df_schema = DFSchema::try_from(schema)?;
442 let ret_field = filter.to_field(&df_schema)?.1;
443 let ret_type = ret_field.data_type();
444 if ret_type != &DataType::Boolean {
445 return Err(Error::invalid_input_source(
446 format!("The filter {} does not return a boolean", filter).into(),
447 ));
448 }
449
450 let optimized = planner.optimize_expr(filter).map_err(|e| {
451 Error::invalid_input(format!("Error optimizing sql filter: {sql} ({e})"))
452 })?;
453 Ok(optimized)
454 }
455 #[cfg(feature = "substrait")]
456 Self::Substrait(expr) => {
457 use lance_datafusion::exec::{LanceExecutionOptions, get_session_context};
458
459 let ctx = get_session_context(&LanceExecutionOptions::default());
460 let state = ctx.state();
461 let schema = Arc::new(ArrowSchema::from(dataset_schema));
462 let expr = parse_substrait(expr, schema.clone(), &ctx.state())
463 .now_or_never()
464 .expect("could not parse the Substrait filter in a synchronous fashion")?;
465 let planner = Planner::new(schema);
466 planner.optimize_expr(expr.clone()).map_err(|e| {
467 Error::invalid_input(format!(
468 "Error optimizing substrait filter: {expr:?} ({e})"
469 ))
470 })
471 }
472 #[cfg(not(feature = "substrait"))]
473 Self::Substrait(_) => Err(Error::not_supported_source(
474 "Substrait filter is not supported in this build".into(),
475 )),
476 Self::Datafusion(expr) => Ok(expr.clone()),
477 }
478 }
479}
480
481#[derive(Debug, Clone)]
483pub enum AggregateExpr {
484 #[cfg(feature = "substrait")]
485 Substrait(Vec<u8>),
486 Datafusion {
487 group_by: Vec<Expr>,
488 aggregates: Vec<Expr>,
489 },
490}
491
492impl AggregateExpr {
493 pub fn builder() -> AggregateExprBuilder<false> {
506 AggregateExprBuilder::new()
507 }
508
509 #[cfg(feature = "substrait")]
511 pub fn substrait(bytes: impl Into<Vec<u8>>) -> Self {
512 Self::Substrait(bytes.into())
513 }
514
515 pub fn datafusion(group_by: Vec<Expr>, aggregates: Vec<Expr>) -> Self {
518 Self::Datafusion {
519 group_by,
520 aggregates,
521 }
522 }
523
524 fn parse(self, #[allow(unused_variables)] schema: Arc<ArrowSchema>) -> Result<Aggregate> {
531 match self {
532 #[cfg(feature = "substrait")]
533 Self::Substrait(bytes) => {
534 use lance_datafusion::exec::{LanceExecutionOptions, get_session_context};
535 use lance_datafusion::substrait::parse_substrait_aggregate;
536
537 let ctx = get_session_context(&LanceExecutionOptions::default());
538 parse_substrait_aggregate(&bytes, schema, &ctx.state())
539 .now_or_never()
540 .expect("could not parse the Substrait aggregate in a synchronous fashion")
541 }
542 Self::Datafusion {
543 group_by,
544 aggregates,
545 } => Ok(Aggregate::new(group_by, aggregates)),
546 }
547 }
548}
549
550#[derive(Debug, Clone)]
555pub struct AggregateExprBuilder<const HAS_PENDING: bool> {
556 group_by: Vec<Expr>,
557 aggregates: Vec<Expr>,
558}
559
560impl Default for AggregateExprBuilder<false> {
561 fn default() -> Self {
562 Self {
563 group_by: Vec::new(),
564 aggregates: Vec::new(),
565 }
566 }
567}
568
569impl AggregateExprBuilder<false> {
570 pub fn new() -> Self {
572 Self::default()
573 }
574
575 pub fn build(self) -> AggregateExpr {
577 AggregateExpr::Datafusion {
578 group_by: self.group_by,
579 aggregates: self.aggregates,
580 }
581 }
582}
583
584impl<const HAS_PENDING: bool> AggregateExprBuilder<HAS_PENDING> {
585 pub fn group_by(mut self, column: impl Into<String>) -> AggregateExprBuilder<false> {
590 self.group_by.push(col(column.into()));
591 AggregateExprBuilder {
592 group_by: self.group_by,
593 aggregates: self.aggregates,
594 }
595 }
596
597 pub fn group_by_columns(
602 mut self,
603 columns: impl IntoIterator<Item = impl Into<String>>,
604 ) -> AggregateExprBuilder<false> {
605 for column in columns {
606 self.group_by.push(col(column.into()));
607 }
608 AggregateExprBuilder {
609 group_by: self.group_by,
610 aggregates: self.aggregates,
611 }
612 }
613
614 pub fn count_star(mut self) -> AggregateExprBuilder<true> {
616 self.aggregates
617 .push(functions_aggregate::count::count(lit(1)));
618 AggregateExprBuilder {
619 group_by: self.group_by,
620 aggregates: self.aggregates,
621 }
622 }
623
624 pub fn count(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
629 self.aggregates
630 .push(functions_aggregate::count::count(col(column.into())));
631 AggregateExprBuilder {
632 group_by: self.group_by,
633 aggregates: self.aggregates,
634 }
635 }
636
637 pub fn sum(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
639 self.aggregates
640 .push(functions_aggregate::sum::sum(col(column.into())));
641 AggregateExprBuilder {
642 group_by: self.group_by,
643 aggregates: self.aggregates,
644 }
645 }
646
647 pub fn avg(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
649 self.aggregates
650 .push(functions_aggregate::average::avg(col(column.into())));
651 AggregateExprBuilder {
652 group_by: self.group_by,
653 aggregates: self.aggregates,
654 }
655 }
656
657 pub fn min(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
659 self.aggregates
660 .push(functions_aggregate::min_max::min(col(column.into())));
661 AggregateExprBuilder {
662 group_by: self.group_by,
663 aggregates: self.aggregates,
664 }
665 }
666
667 pub fn max(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
669 self.aggregates
670 .push(functions_aggregate::min_max::max(col(column.into())));
671 AggregateExprBuilder {
672 group_by: self.group_by,
673 aggregates: self.aggregates,
674 }
675 }
676}
677
678impl AggregateExprBuilder<true> {
679 pub fn alias(mut self, name: impl Into<String>) -> AggregateExprBuilder<false> {
681 let pending = self.aggregates.pop().expect("pending aggregate must exist");
682 self.aggregates.push(pending.alias(name.into()));
683 AggregateExprBuilder {
684 group_by: self.group_by,
685 aggregates: self.aggregates,
686 }
687 }
688
689 pub fn build(self) -> AggregateExpr {
691 AggregateExpr::Datafusion {
692 group_by: self.group_by,
693 aggregates: self.aggregates,
694 }
695 }
696}
697
698#[derive(Clone)]
712pub struct Scanner {
713 dataset: Arc<Dataset>,
714
715 projection_plan: ProjectionPlan,
722 blob_handling: BlobHandling,
723
724 prefilter: bool,
726
727 materialization_style: MaterializationStyle,
729
730 filter: LanceFilter,
732
733 full_text_query: Option<FullTextSearchQuery>,
735
736 batch_size: Option<usize>,
738
739 batch_size_bytes: Option<u64>,
742
743 batch_readahead: usize,
745
746 fragment_readahead: Option<usize>,
748
749 io_buffer_size: Option<u64>,
751
752 limit: Option<i64>,
753 offset: Option<i64>,
754
755 ordering: Option<Vec<ColumnOrdering>>,
764
765 nearest: Option<Query>,
766
767 use_scalar_index: bool,
773
774 use_stats: bool,
778
779 ordered: bool,
783
784 fragments: Option<Vec<Fragment>>,
786
787 index_segments: Option<Vec<Uuid>>,
789
790 fast_search: bool,
797
798 include_deleted_rows: bool,
800
801 scan_stats_callback: Option<ExecutionStatsCallback>,
803
804 strict_batch_size: bool,
809
810 file_reader_options: Option<FileReaderOptions>,
812
813 aggregate: Option<Aggregate>,
814
815 legacy_with_row_id: bool,
836 legacy_with_row_addr: bool,
838 explicit_projection: bool,
841 autoproject_scoring_columns: bool,
843}
844
845#[derive(Debug, Clone)]
847pub enum TakeOperation {
848 RowIds(Vec<u64>),
850 RowAddrs(Vec<u64>),
852 RowOffsets(Vec<u64>),
857}
858
859impl TakeOperation {
860 fn extract_u64_list(list: &[Expr]) -> Option<Vec<u64>> {
861 let mut u64s = Vec::with_capacity(list.len());
862 for expr in list {
863 if let Expr::Literal(lit, _) = expr {
864 if let Some(ScalarValue::UInt64(Some(val))) =
865 safe_coerce_scalar(lit, &DataType::UInt64)
866 {
867 u64s.push(val);
868 } else {
869 return None;
870 }
871 } else {
872 return None;
873 }
874 }
875 Some(u64s)
876 }
877
878 fn merge(self, other: Self) -> Option<Self> {
879 match (self, other) {
880 (Self::RowIds(mut left), Self::RowIds(right)) => {
881 left.extend(right);
882 Some(Self::RowIds(left))
883 }
884 (Self::RowAddrs(mut left), Self::RowAddrs(right)) => {
885 left.extend(right);
886 Some(Self::RowAddrs(left))
887 }
888 (Self::RowOffsets(mut left), Self::RowOffsets(right)) => {
889 left.extend(right);
890 Some(Self::RowOffsets(left))
891 }
892 _ => None,
893 }
894 }
895
896 fn try_from_expr(expr: &Expr) -> Option<(Self, Option<Expr>)> {
914 if let Expr::BinaryExpr(binary) = expr {
915 match binary.op {
916 datafusion_expr::Operator::And => {
917 let left_take = Self::try_from_expr(&binary.left);
918 let right_take = Self::try_from_expr(&binary.right);
919 match (left_take, right_take) {
920 (Some(_), Some(_)) => {
921 return None;
927 }
928 (Some((left_op, left_rem)), None) => {
929 let remainder = match left_rem {
930 Some(expr) => Expr::and(expr, binary.right.as_ref().clone()),
934 None => binary.right.as_ref().clone(),
935 };
936 return Some((left_op, Some(remainder)));
937 }
938 (None, Some((right_op, right_rem))) => {
939 let remainder = match right_rem {
940 Some(expr) => Expr::and(expr, binary.left.as_ref().clone()),
941 None => binary.left.as_ref().clone(),
942 };
943 return Some((right_op, Some(remainder)));
944 }
945 (None, None) => {
946 return None;
947 }
948 }
949 }
950 datafusion_expr::Operator::Eq => {
951 if let (Expr::Column(col), Expr::Literal(lit, _)) =
953 (binary.left.as_ref(), binary.right.as_ref())
954 && let Some(ScalarValue::UInt64(Some(val))) =
955 safe_coerce_scalar(lit, &DataType::UInt64)
956 {
957 if col.name == ROW_ID {
958 return Some((Self::RowIds(vec![val]), None));
959 } else if col.name == ROW_ADDR {
960 return Some((Self::RowAddrs(vec![val]), None));
961 } else if col.name == ROW_OFFSET {
962 return Some((Self::RowOffsets(vec![val]), None));
963 }
964 }
965 }
966 datafusion_expr::Operator::Or => {
967 let left_take = Self::try_from_expr(&binary.left);
968 let right_take = Self::try_from_expr(&binary.right);
969 if let (Some(left), Some(right)) = (left_take, right_take) {
970 if left.1.is_some() || right.1.is_some() {
971 return None;
978 }
979 return left.0.merge(right.0).map(|op| (op, None));
980 }
981 }
982 _ => {}
983 }
984 } else if let Expr::InList(in_expr) = expr
985 && let Expr::Column(col) = in_expr.expr.as_ref()
986 && let Some(u64s) = Self::extract_u64_list(&in_expr.list)
987 {
988 if col.name == ROW_ID {
989 return Some((Self::RowIds(u64s), None));
990 } else if col.name == ROW_ADDR {
991 return Some((Self::RowAddrs(u64s), None));
992 } else if col.name == ROW_OFFSET {
993 return Some((Self::RowOffsets(u64s), None));
994 }
995 }
996 None
997 }
998}
999
1000impl Scanner {
1001 pub fn new(dataset: Arc<Dataset>) -> Self {
1002 let projection_plan = ProjectionPlan::full(dataset.clone()).unwrap();
1003 let file_reader_options = dataset.file_reader_options.clone();
1004 let mut scanner = Self {
1005 dataset,
1006 projection_plan,
1007 blob_handling: BlobHandling::default(),
1008 prefilter: false,
1009 materialization_style: MaterializationStyle::Heuristic,
1010 filter: LanceFilter::default(),
1011 full_text_query: None,
1012 batch_size: None,
1013 batch_size_bytes: None,
1014 batch_readahead: get_num_compute_intensive_cpus(),
1015 fragment_readahead: None,
1016 io_buffer_size: None,
1017 limit: None,
1018 offset: None,
1019 ordering: None,
1020 nearest: None,
1021 use_stats: true,
1022 ordered: true,
1023 fragments: None,
1024 index_segments: None,
1025 fast_search: false,
1026 use_scalar_index: true,
1027 include_deleted_rows: false,
1028 scan_stats_callback: None,
1029 strict_batch_size: false,
1030 file_reader_options,
1031 aggregate: None,
1032 legacy_with_row_addr: false,
1033 legacy_with_row_id: false,
1034 explicit_projection: false,
1035 autoproject_scoring_columns: true,
1036 };
1037 scanner.apply_blob_handling();
1038 scanner
1039 }
1040
1041 fn apply_blob_handling(&mut self) {
1042 let projection = self
1043 .projection_plan
1044 .physical_projection
1045 .clone()
1046 .with_blob_handling(self.blob_handling.clone());
1047 self.projection_plan.physical_projection = projection;
1048 }
1049
1050 pub fn blob_handling(&mut self, blob_handling: BlobHandling) -> &mut Self {
1051 self.blob_handling = blob_handling;
1052 self.apply_blob_handling();
1053 self
1054 }
1055
1056 pub fn from_fragment(dataset: Arc<Dataset>, fragment: Fragment) -> Self {
1057 Self {
1058 fragments: Some(vec![fragment]),
1059 ..Self::new(dataset)
1060 }
1061 }
1062
1063 pub fn with_fragments(&mut self, fragments: Vec<Fragment>) -> &mut Self {
1067 self.fragments = Some(fragments);
1068 self
1069 }
1070
1071 pub fn with_index_segments(&mut self, segments: Vec<Uuid>) -> Result<&mut Self> {
1079 if segments.is_empty() {
1080 return Err(Error::invalid_input(
1081 "with_index_segments does not accept an empty segment list".to_string(),
1082 ));
1083 }
1084 self.index_segments = Some(segments);
1085 Ok(self)
1086 }
1087
1088 fn get_batch_size(&self) -> usize {
1089 get_default_batch_size().unwrap_or_else(|| {
1095 self.batch_size.unwrap_or_else(|| {
1096 std::cmp::max(
1097 self.dataset.object_store().block_size() / 4,
1098 BATCH_SIZE_FALLBACK,
1099 )
1100 })
1101 })
1102 }
1103
1104 fn ensure_not_fragment_scan(&self) -> Result<()> {
1105 if self.is_fragment_scan() {
1106 Err(Error::not_supported(
1107 "This operation is not supported for fragment scan".to_string(),
1108 ))
1109 } else {
1110 Ok(())
1111 }
1112 }
1113
1114 fn is_fragment_scan(&self) -> bool {
1115 self.fragments.is_some()
1116 }
1117
1118 pub fn empty_project(&mut self) -> Result<&mut Self> {
1122 self.project(&[] as &[&str])
1123 }
1124
1125 pub fn project<T: AsRef<str>>(&mut self, columns: &[T]) -> Result<&mut Self> {
1129 let transformed_columns: Vec<(&str, String)> = columns
1130 .iter()
1131 .map(|c| (c.as_ref(), escape_field_path_for_project(c.as_ref())))
1132 .collect();
1133
1134 self.project_with_transform(&transformed_columns)
1135 }
1136
1137 pub fn project_with_transform(
1141 &mut self,
1142 columns: &[(impl AsRef<str>, impl AsRef<str>)],
1143 ) -> Result<&mut Self> {
1144 self.explicit_projection = true;
1145 self.projection_plan = ProjectionPlan::from_expressions(self.dataset.clone(), columns)?;
1146 if self.legacy_with_row_id {
1147 self.projection_plan.include_row_id();
1148 }
1149 if self.legacy_with_row_addr {
1150 self.projection_plan.include_row_addr();
1151 }
1152 self.apply_blob_handling();
1153 Ok(self)
1154 }
1155
1156 pub fn prefilter(&mut self, should_prefilter: bool) -> &mut Self {
1165 self.prefilter = should_prefilter;
1166 self
1167 }
1168
1169 pub fn scan_stats_callback(&mut self, callback: ExecutionStatsCallback) -> &mut Self {
1171 self.scan_stats_callback = Some(callback);
1172 self
1173 }
1174
1175 pub fn materialization_style(&mut self, style: MaterializationStyle) -> &mut Self {
1187 self.materialization_style = style;
1188 self
1189 }
1190
1191 pub fn filter(&mut self, filter: &str) -> Result<&mut Self> {
1207 self.filter.expr_filter = Some(ExprFilter::Sql(filter.to_string()));
1208 Ok(self)
1209 }
1210
1211 pub fn filter_query(&mut self, filter: QueryFilter) -> Result<&mut Self> {
1230 self.filter.query_filter = Some(filter);
1231 Ok(self)
1232 }
1233
1234 pub fn full_text_search(&mut self, query: FullTextSearchQuery) -> Result<&mut Self> {
1248 let fields = query.columns();
1249 if !fields.is_empty() {
1250 for field in fields.iter() {
1251 if self.dataset.schema().field(field).is_none() {
1252 return Err(Error::invalid_input(format!("Column {} not found", field)));
1253 }
1254 }
1255 }
1256
1257 self.full_text_query = Some(query);
1258 Ok(self)
1259 }
1260
1261 pub fn filter_substrait(&mut self, filter: &[u8]) -> Result<&mut Self> {
1266 self.filter.expr_filter = Some(ExprFilter::Substrait(filter.to_vec()));
1267 Ok(self)
1268 }
1269
1270 pub fn filter_expr(&mut self, filter: Expr) -> &mut Self {
1271 self.filter.expr_filter = Some(ExprFilter::Datafusion(filter));
1272 self
1273 }
1274
1275 pub fn aggregate(&mut self, aggregate: AggregateExpr) -> Result<&mut Self> {
1280 let schema: Arc<ArrowSchema> = Arc::new(self.dataset.schema().into());
1281 let parsed = aggregate.parse(schema)?;
1282 self.aggregate = Some(parsed);
1283 Ok(self)
1284 }
1285
1286 pub fn batch_size(&mut self, batch_size: usize) -> &mut Self {
1292 self.batch_size = Some(batch_size);
1293 self
1294 }
1295
1296 pub fn batch_size_bytes(&mut self, batch_size_bytes: u64) -> &mut Self {
1305 self.batch_size_bytes = Some(batch_size_bytes);
1306 self
1307 }
1308
1309 pub fn include_deleted_rows(&mut self) -> &mut Self {
1320 self.include_deleted_rows = true;
1321 self
1322 }
1323
1324 pub fn io_buffer_size(&mut self, size: u64) -> &mut Self {
1343 self.io_buffer_size = Some(size);
1344 self
1345 }
1346
1347 pub fn batch_readahead(&mut self, nbatches: usize) -> &mut Self {
1350 self.batch_readahead = nbatches;
1351 self
1352 }
1353
1354 pub fn fragment_readahead(&mut self, nfragments: usize) -> &mut Self {
1358 self.fragment_readahead = Some(nfragments);
1359 self
1360 }
1361
1362 pub fn scan_in_order(&mut self, ordered: bool) -> &mut Self {
1376 self.ordered = ordered;
1377 self
1378 }
1379
1380 pub fn use_scalar_index(&mut self, use_scalar_index: bool) -> &mut Self {
1386 self.use_scalar_index = use_scalar_index;
1387 self
1388 }
1389
1390 pub fn strict_batch_size(&mut self, strict_batch_size: bool) -> &mut Self {
1397 self.strict_batch_size = strict_batch_size;
1398 self
1399 }
1400
1401 pub fn limit(&mut self, limit: Option<i64>, offset: Option<i64>) -> Result<&mut Self> {
1408 if limit.unwrap_or_default() < 0 {
1409 return Err(Error::invalid_input(
1410 "Limit must be non-negative".to_string(),
1411 ));
1412 }
1413 if let Some(off) = offset
1414 && off < 0
1415 {
1416 return Err(Error::invalid_input(
1417 "Offset must be non-negative".to_string(),
1418 ));
1419 }
1420 self.limit = limit;
1421 self.offset = offset;
1422 Ok(self)
1423 }
1424
1425 pub fn nearest(&mut self, column: &str, q: &dyn Array, k: usize) -> Result<&mut Self> {
1429 if !self.prefilter {
1430 self.ensure_not_fragment_scan()?;
1433 }
1434
1435 if k == 0 {
1436 return Err(Error::invalid_input("k must be positive".to_string()));
1437 }
1438 if q.is_empty() {
1439 return Err(Error::invalid_input(
1440 "Query vector must have non-zero length".to_string(),
1441 ));
1442 }
1443 let (vector_type, element_type) = get_vector_type(self.dataset.schema(), column)?;
1445 let dim = get_vector_dim(self.dataset.schema(), column)?;
1446
1447 let q = match q.data_type() {
1448 DataType::List(_) | DataType::FixedSizeList(_, _) => {
1449 if !matches!(vector_type, DataType::List(_)) {
1450 return Err(Error::invalid_input(format!(
1451 "Query is multivector but column {}({})is not multivector",
1452 column, vector_type,
1453 )));
1454 }
1455
1456 if let Some(list_array) = q.as_list_opt::<i32>() {
1457 for i in 0..list_array.len() {
1458 let vec = list_array.value(i);
1459 if vec.len() != dim {
1460 return Err(Error::invalid_input(format!(
1461 "query dim({}) doesn't match the column {} vector dim({})",
1462 vec.len(),
1463 column,
1464 dim,
1465 )));
1466 }
1467 }
1468 list_array.values().clone()
1469 } else {
1470 let fsl = q.as_fixed_size_list();
1471 if fsl.value_length() as usize != dim {
1472 return Err(Error::invalid_input(format!(
1473 "query dim({}) doesn't match the column {} vector dim({})",
1474 fsl.value_length(),
1475 column,
1476 dim,
1477 )));
1478 }
1479 fsl.values().clone()
1480 }
1481 }
1482 _ => {
1483 if q.len() != dim {
1484 return Err(Error::invalid_input(format!(
1485 "query dim({}) doesn't match the column {} vector dim({})",
1486 q.len(),
1487 column,
1488 dim,
1489 )));
1490 }
1491 q.slice(0, q.len())
1492 }
1493 };
1494
1495 let key = match &element_type {
1496 dt if dt == q.data_type() => q,
1497 dt if dt.is_floating() => coerce_float_vector(
1498 q.as_any().downcast_ref::<Float32Array>().unwrap(),
1499 FloatType::try_from(dt)?,
1500 )?,
1501 _ => {
1502 return Err(Error::invalid_input(format!(
1503 "Column {} has element type {} and the query vector is {}",
1504 column,
1505 element_type,
1506 q.data_type(),
1507 )));
1508 }
1509 };
1510
1511 self.nearest = Some(Query {
1512 column: column.to_string(),
1513 key,
1514 k,
1515 lower_bound: None,
1516 upper_bound: None,
1517 minimum_nprobes: 1,
1518 maximum_nprobes: None,
1519 ef: None,
1520 refine_factor: None,
1521 metric_type: None,
1522 use_index: true,
1523 query_parallelism: DEFAULT_QUERY_PARALLELISM,
1524 dist_q_c: 0.0,
1525 });
1526 Ok(self)
1527 }
1528
1529 #[cfg(test)]
1530 fn nearest_mut(&mut self) -> Option<&mut Query> {
1531 self.nearest.as_mut()
1532 }
1533
1534 pub fn distance_range(
1536 &mut self,
1537 lower_bound: Option<f32>,
1538 upper_bound: Option<f32>,
1539 ) -> &mut Self {
1540 if let Some(q) = self.nearest.as_mut() {
1541 q.lower_bound = lower_bound;
1542 q.upper_bound = upper_bound;
1543 }
1544 self
1545 }
1546
1547 pub fn nprobes(&mut self, n: usize) -> &mut Self {
1552 if let Some(q) = self.nearest.as_mut() {
1553 q.minimum_nprobes = n;
1554 q.maximum_nprobes = Some(n);
1555 } else {
1556 log::warn!("nprobes is not set because nearest has not been called yet");
1557 }
1558 self
1559 }
1560
1561 #[deprecated(note = "Use nprobes instead")]
1566 pub fn nprobs(&mut self, n: usize) -> &mut Self {
1567 if let Some(q) = self.nearest.as_mut() {
1568 q.minimum_nprobes = n;
1569 q.maximum_nprobes = Some(n);
1570 } else {
1571 log::warn!("nprobes is not set because nearest has not been called yet");
1572 }
1573 self
1574 }
1575
1576 pub fn minimum_nprobes(&mut self, n: usize) -> &mut Self {
1584 if let Some(q) = self.nearest.as_mut() {
1585 q.minimum_nprobes = n;
1586 } else {
1587 log::warn!("minimum_nprobes is not set because nearest has not been called yet");
1588 }
1589 self
1590 }
1591
1592 pub fn maximum_nprobes(&mut self, n: usize) -> &mut Self {
1604 if let Some(q) = self.nearest.as_mut() {
1605 q.maximum_nprobes = Some(n);
1606 } else {
1607 log::warn!("maximum_nprobes is not set because nearest has not been called yet");
1608 }
1609 self
1610 }
1611
1612 pub fn ef(&mut self, ef: usize) -> &mut Self {
1613 if let Some(q) = self.nearest.as_mut() {
1614 q.ef = Some(ef);
1615 }
1616 self
1617 }
1618
1619 pub fn fast_search(&mut self) -> &mut Self {
1625 if let Some(q) = self.nearest.as_mut() {
1626 q.use_index = true;
1627 }
1628 self.fast_search = true;
1629 self.projection_plan.include_row_id(); self
1631 }
1632
1633 pub fn refine(&mut self, factor: u32) -> &mut Self {
1643 if let Some(q) = self.nearest.as_mut() {
1644 q.refine_factor = Some(factor)
1645 };
1646 self
1647 }
1648
1649 pub fn distance_metric(&mut self, metric_type: MetricType) -> &mut Self {
1651 if let Some(q) = self.nearest.as_mut() {
1652 q.metric_type = Some(metric_type)
1653 }
1654 self
1655 }
1656
1657 pub fn order_by(&mut self, ordering: Option<Vec<ColumnOrdering>>) -> Result<&mut Self> {
1663 if let Some(ordering) = &ordering {
1664 if ordering.is_empty() {
1665 self.ordering = None;
1666 return Ok(self);
1667 }
1668 for column in ordering {
1670 self.dataset
1671 .schema()
1672 .field(&column.column_name)
1673 .ok_or(Error::invalid_input(format!(
1674 "Column {} not found",
1675 &column.column_name
1676 )))?;
1677 }
1678 }
1679 self.ordering = ordering;
1680 Ok(self)
1681 }
1682
1683 pub fn use_index(&mut self, use_index: bool) -> &mut Self {
1685 if let Some(q) = self.nearest.as_mut() {
1686 q.use_index = use_index
1687 }
1688 self
1689 }
1690
1691 pub fn query_parallelism(&mut self, query_parallelism: i32) -> &mut Self {
1701 if let Some(q) = self.nearest.as_mut() {
1702 q.query_parallelism = query_parallelism;
1703 } else {
1704 log::warn!("query_parallelism is not set because nearest has not been called yet");
1705 }
1706 self
1707 }
1708
1709 pub fn with_row_id(&mut self) -> &mut Self {
1711 self.legacy_with_row_id = true;
1712 self.projection_plan.include_row_id();
1713 self
1714 }
1715
1716 pub fn with_row_address(&mut self) -> &mut Self {
1718 self.legacy_with_row_addr = true;
1719 self.projection_plan.include_row_addr();
1720 self
1721 }
1722
1723 pub fn disable_scoring_autoprojection(&mut self) -> &mut Self {
1739 self.autoproject_scoring_columns = false;
1740 self
1741 }
1742
1743 pub fn with_file_reader_options(&mut self, options: FileReaderOptions) -> &mut Self {
1745 self.file_reader_options = Some(options);
1746 self
1747 }
1748
1749 fn resolved_file_reader_options(&self) -> Option<FileReaderOptions> {
1753 let base = self
1754 .file_reader_options
1755 .clone()
1756 .or_else(|| self.dataset.file_reader_options.clone());
1757 match (base, self.batch_size_bytes) {
1758 (Some(mut opts), Some(bsb)) => {
1759 if opts.batch_size_bytes.is_none() {
1760 opts.batch_size_bytes = Some(bsb);
1761 }
1762 Some(opts)
1763 }
1764 (Some(opts), None) => Some(opts),
1765 (None, Some(bsb)) => Some(FileReaderOptions {
1766 batch_size_bytes: Some(bsb),
1767 ..Default::default()
1768 }),
1769 (None, None) => None,
1770 }
1771 }
1772
1773 fn create_column_expr(
1775 column_name: &str,
1776 dataset: &Dataset,
1777 arrow_schema: &ArrowSchema,
1778 ) -> Result<Arc<dyn PhysicalExpr>> {
1779 let lance_schema = dataset.schema();
1780 let field_path = lance_schema
1781 .resolve_case_insensitive(column_name)
1782 .ok_or_else(|| {
1783 Error::invalid_input(format!("Field '{}' not found in schema", column_name))
1784 })?;
1785
1786 if field_path.len() == 1 {
1787 expressions::col(&field_path[0].name, arrow_schema).map_err(|e| {
1789 Error::internal(format!(
1790 "Failed to create column expression for '{}': {}",
1791 column_name, e
1792 ))
1793 })
1794 } else {
1795 let get_field_func = ScalarUDF::from(GetFieldFunc::default());
1797
1798 let mut expr = Expr::Column(datafusion::common::Column::new_unqualified(
1801 &field_path[0].name,
1802 ));
1803 for nested_field in &field_path[1..] {
1804 expr = get_field_func.call(vec![expr, lit(&nested_field.name)]);
1805 }
1806
1807 let df_schema = Arc::new(DFSchema::try_from(arrow_schema.clone())?);
1809 let execution_props = ExecutionProps::new().with_query_execution_start_time(Utc::now());
1810 create_physical_expr(&expr, &df_schema, &execution_props).map_err(|e| {
1811 Error::internal(format!(
1812 "Failed to create physical expression for nested field '{}': {}",
1813 column_name, e
1814 ))
1815 })
1816 }
1817 }
1818
1819 pub fn use_stats(&mut self, use_stats: bool) -> &mut Self {
1823 self.use_stats = use_stats;
1824 self
1825 }
1826
1827 pub async fn schema(&self) -> Result<SchemaRef> {
1829 let plan = self.create_plan().await?;
1830 Ok(plan.schema())
1831 }
1832
1833 pub fn get_expr_filter(&self) -> Result<Option<Expr>> {
1840 if let Some(filter) = &self.filter.expr_filter {
1841 let filter_schema = self.filterable_schema()?;
1842 Ok(Some(filter.to_datafusion(
1843 self.dataset.schema(),
1844 filter_schema.as_ref(),
1845 )?))
1846 } else {
1847 Ok(None)
1848 }
1849 }
1850
1851 fn add_extra_columns(&self, schema: Schema) -> Result<Schema> {
1852 let mut extra_columns = vec![ArrowField::new(ROW_OFFSET, DataType::UInt64, true)];
1853
1854 if self.nearest.as_ref().is_some() {
1855 extra_columns.push(ArrowField::new(DIST_COL, DataType::Float32, true));
1856 };
1857
1858 if self.full_text_query.is_some() {
1859 extra_columns.push(ArrowField::new(SCORE_COL, DataType::Float32, true));
1860 }
1861
1862 schema.merge(&ArrowSchema::new(extra_columns))
1863 }
1864
1865 fn filterable_schema(&self) -> Result<Arc<Schema>> {
1870 let base_schema = Projection::full(self.dataset.clone())
1871 .with_row_id()
1872 .with_row_addr()
1873 .with_row_last_updated_at_version()
1874 .with_row_created_at_version()
1875 .to_schema();
1876
1877 Ok(Arc::new(self.add_extra_columns(base_schema)?))
1878 }
1879
1880 pub(crate) fn calculate_final_projection(
1885 &self,
1886 current_schema: &ArrowSchema,
1887 ) -> Result<Vec<(Arc<dyn PhysicalExpr>, String)>> {
1888 let mut output_expr = self.projection_plan.to_physical_exprs(current_schema)?;
1891
1892 if self.autoproject_scoring_columns {
1895 if self.nearest.is_some() && output_expr.iter().all(|(_, name)| name != DIST_COL) {
1896 if self.explicit_projection {
1897 log::warn!(
1898 "Deprecation warning, this behavior will change in the future. This search specified output columns but did not include `_distance`. Currently the `_distance` column will be included. In the future it will not. Call `disable_scoring_autoprojection` to adopt the future behavior and avoid this warning"
1899 );
1900 }
1901 let vector_expr = expressions::col(DIST_COL, current_schema)?;
1902 output_expr.push((vector_expr, DIST_COL.to_string()));
1903 }
1904 if self.full_text_query.is_some()
1905 && output_expr.iter().all(|(_, name)| name != SCORE_COL)
1906 {
1907 if self.explicit_projection {
1908 log::warn!(
1909 "Deprecation warning, this behavior will change in the future. This search specified output columns but did not include `_score`. Currently the `_score` column will be included. In the future it will not. Call `disable_scoring_autoprojection` to adopt the future behavior and avoid this warning"
1910 );
1911 }
1912 let score_expr = expressions::col(SCORE_COL, current_schema)?;
1913 output_expr.push((score_expr, SCORE_COL.to_string()));
1914 }
1915 }
1916
1917 if self.legacy_with_row_id {
1918 let row_id_pos = output_expr
1919 .iter()
1920 .position(|(_, name)| name == ROW_ID)
1921 .ok_or_else(|| {
1922 Error::internal(
1923 "user specified with_row_id but the _rowid column was not in the output"
1924 .to_string(),
1925 )
1926 })?;
1927 if row_id_pos != output_expr.len() - 1 {
1928 let row_id_expr = output_expr.remove(row_id_pos);
1930 output_expr.push(row_id_expr);
1931 }
1932 }
1933
1934 if self.legacy_with_row_addr {
1935 let row_addr_pos = output_expr.iter().position(|(_, name)| name == ROW_ADDR).ok_or_else(|| {
1936 Error::internal("user specified with_row_address but the _rowaddr column was not in the output".to_string())
1937 })?;
1938 if row_addr_pos != output_expr.len() - 1 {
1939 let row_addr_expr = output_expr.remove(row_addr_pos);
1941 output_expr.push(row_addr_expr);
1942 }
1943 }
1944
1945 Ok(output_expr)
1946 }
1947
1948 #[instrument(skip_all)]
1950 pub fn try_into_stream(&self) -> BoxFuture<'_, Result<DatasetRecordBatchStream>> {
1951 async move {
1953 let plan = self.create_plan().await?;
1954
1955 Ok(DatasetRecordBatchStream::new(execute_plan(
1956 plan,
1957 LanceExecutionOptions {
1958 batch_size: self.batch_size,
1959 execution_stats_callback: self.scan_stats_callback.clone(),
1960 ..Default::default()
1961 },
1962 )?))
1963 }
1964 .boxed()
1965 }
1966
1967 pub(crate) async fn try_into_dfstream(
1968 &self,
1969 mut options: LanceExecutionOptions,
1970 ) -> Result<SendableRecordBatchStream> {
1971 let plan = self.create_plan().await?;
1972
1973 if options.execution_stats_callback.is_none() {
1975 options.execution_stats_callback = self.scan_stats_callback.clone();
1976 }
1977
1978 execute_plan(plan, options)
1979 }
1980
1981 pub async fn try_into_batch(&self) -> Result<RecordBatch> {
1982 let stream = self.try_into_stream().await?;
1983 let schema = stream.schema();
1984 let batches = stream.try_collect::<Vec<_>>().await?;
1985 Ok(concat_batches(&schema, &batches)?)
1986 }
1987
1988 #[instrument(skip_all)]
1993 pub fn count_rows(&self) -> BoxFuture<'_, Result<u64>> {
1994 async move {
1996 let mut scanner = self.clone();
1997 scanner.aggregate(AggregateExpr::builder().count_star().build())?;
1998
1999 let plan = scanner.create_plan().await?;
2000 let mut stream = execute_plan(plan, LanceExecutionOptions::default())?;
2001
2002 if let Some(first_batch) = stream.next().await {
2004 let batch = first_batch?;
2005 let array = batch
2006 .column(0)
2007 .as_any()
2008 .downcast_ref::<Int64Array>()
2009 .ok_or(Error::invalid_input(
2010 "Count plan did not return an Int64Array".to_string(),
2011 ))?;
2012 Ok(array.value(0) as u64)
2013 } else {
2014 Ok(0)
2015 }
2016 }
2017 .boxed()
2018 }
2019
2020 #[deprecated(note = "Use create_plan() instead, which now applies aggregate automatically")]
2024 pub fn create_aggregate_plan(&self) -> BoxFuture<'_, Result<Arc<dyn ExecutionPlan>>> {
2025 async move {
2026 if self.aggregate.is_none() {
2027 return Err(Error::invalid_input(
2028 "create_aggregate_plan called but no aggregate was set",
2029 ));
2030 }
2031 self.create_plan().await
2033 }
2034 .boxed()
2035 }
2036
2037 async fn apply_aggregate(
2038 &self,
2039 plan: Arc<dyn ExecutionPlan>,
2040 agg: &Aggregate,
2041 ) -> Result<Arc<dyn ExecutionPlan>> {
2042 use datafusion_physical_expr::aggregate::AggregateFunctionExpr;
2043
2044 let schema = plan.schema();
2045 let df_schema = DFSchema::try_from(schema.as_ref().clone())?;
2046
2047 let group_exprs: Vec<(Arc<dyn PhysicalExpr>, String)> = agg
2048 .group_by
2049 .iter()
2050 .map(|expr| {
2051 let name = expr.schema_name().to_string();
2052 let physical_expr =
2053 create_physical_expr(expr, &df_schema, &ExecutionProps::default())?;
2054 Ok((physical_expr, name))
2055 })
2056 .collect::<Result<_>>()?;
2057
2058 #[allow(clippy::type_complexity)]
2059 let aggr_results: Vec<(Arc<AggregateFunctionExpr>, Option<Arc<dyn PhysicalExpr>>)> = agg
2060 .aggregates
2061 .iter()
2062 .map(|expr| self.build_physical_aggregate_expr(expr, &df_schema, &schema))
2063 .collect::<Result<_>>()?;
2064
2065 let (aggr_exprs, filters): (Vec<_>, Vec<_>) = aggr_results.into_iter().unzip();
2066
2067 Ok(Arc::new(AggregateExec::try_new(
2068 AggregateMode::Single,
2069 PhysicalGroupBy::new_single(group_exprs),
2070 aggr_exprs,
2071 filters,
2072 plan,
2073 schema,
2074 )?) as Arc<dyn ExecutionPlan>)
2075 }
2076
2077 #[allow(clippy::type_complexity)]
2078 fn build_physical_aggregate_expr(
2079 &self,
2080 expr: &Expr,
2081 df_schema: &DFSchema,
2082 input_schema: &SchemaRef,
2083 ) -> Result<(
2084 Arc<datafusion_physical_expr::aggregate::AggregateFunctionExpr>,
2085 Option<Arc<dyn PhysicalExpr>>,
2086 )> {
2087 use datafusion::physical_planner::create_aggregate_expr_and_maybe_filter;
2088
2089 let coerced_expr = self.coerce_aggregate_expr(expr, df_schema)?;
2090
2091 let (agg_expr, filter, _order_by) = create_aggregate_expr_and_maybe_filter(
2093 &coerced_expr,
2094 df_schema,
2095 input_schema.as_ref(),
2096 &ExecutionProps::default(),
2097 )?;
2098
2099 Ok((agg_expr, filter))
2100 }
2101
2102 fn coerce_aggregate_expr(&self, expr: &Expr, schema: &DFSchema) -> Result<Expr> {
2110 Self::coerce_aggregate_expr_impl(expr, schema)
2111 }
2112
2113 fn coerce_aggregate_expr_impl(expr: &Expr, schema: &DFSchema) -> Result<Expr> {
2114 use datafusion::logical_expr::Expr;
2115 use datafusion::logical_expr::expr::AggregateFunction;
2116 use datafusion::logical_expr::type_coercion::functions::fields_with_udf;
2117
2118 match expr {
2119 Expr::AggregateFunction(agg_func) => {
2120 let func = &agg_func.func;
2121 let args = &agg_func.params.args;
2122
2123 if args.is_empty() {
2124 return Ok(expr.clone());
2125 }
2126
2127 let current_fields: Vec<arrow_schema::FieldRef> = args
2128 .iter()
2129 .enumerate()
2130 .map(|(i, e)| {
2131 let dt = e.get_type(schema)?;
2132 Ok(Arc::new(arrow_schema::Field::new(
2133 format!("arg_{i}"),
2134 dt,
2135 true,
2136 )))
2137 })
2138 .collect::<std::result::Result<_, datafusion::common::DataFusionError>>()?;
2139
2140 let coerced_fields = fields_with_udf(¤t_fields, func.as_ref())?;
2141 let coerced_args: Vec<Expr> = args
2142 .iter()
2143 .zip(coerced_fields.iter())
2144 .map(|(arg, target_field)| {
2145 let arg_type = arg.get_type(schema)?;
2146 let target_type = target_field.data_type();
2147 if arg_type == *target_type {
2148 Ok(arg.clone())
2149 } else {
2150 arg.clone().cast_to(target_type, schema)
2151 }
2152 })
2153 .collect::<std::result::Result<_, _>>()?;
2154
2155 Ok(Expr::AggregateFunction(AggregateFunction::new_udf(
2156 func.clone(),
2157 coerced_args,
2158 agg_func.params.distinct,
2159 agg_func.params.filter.clone(),
2160 agg_func.params.order_by.clone(),
2161 agg_func.params.null_treatment,
2162 )))
2163 }
2164 Expr::Alias(alias) => {
2165 let coerced_inner = Self::coerce_aggregate_expr_impl(&alias.expr, schema)?;
2167 Ok(coerced_inner.alias(&alias.name))
2168 }
2169 other => Err(Error::invalid_input(format!(
2170 "Expected aggregate function expression, got {:?}",
2171 other.variant_name()
2172 ))),
2173 }
2174 }
2175
2176 fn is_early_field(&self, field: &Field) -> bool {
2197 match self.materialization_style {
2198 MaterializationStyle::AllEarly => true,
2199 MaterializationStyle::AllLate => false,
2200 MaterializationStyle::AllEarlyExcept(ref cols) => !cols.contains(&(field.id as u32)),
2201 MaterializationStyle::Heuristic => {
2202 if field.is_blob() {
2203 return true;
2208 }
2209
2210 let byte_width = field.data_type().byte_width_opt();
2211 let is_cloud = self.dataset.object_store().is_cloud();
2212 if is_cloud {
2213 byte_width.is_some_and(|bw| bw < 1000)
2214 } else {
2215 byte_width.is_some_and(|bw| bw < 10)
2216 }
2217 }
2218 }
2219 }
2220
2221 fn calc_eager_projection(
2226 &self,
2227 filter_plan: &ExprFilterPlan,
2228 desired_projection: &Projection,
2229 ) -> Result<Projection> {
2230 let filter_columns = filter_plan.all_columns();
2237
2238 let filter_schema = self
2239 .dataset
2240 .empty_projection()
2241 .union_columns(filter_columns, OnMissing::Error)?
2242 .into_schema();
2243
2244 Ok(desired_projection
2246 .clone()
2247 .subtract_predicate(|f| !self.is_early_field(f))
2249 .union_schema(&filter_schema))
2251 }
2252
2253 fn validate_options(&self) -> Result<()> {
2254 if self.include_deleted_rows && !self.projection_plan.physical_projection.with_row_id {
2255 return Err(Error::invalid_input_source(
2256 "include_deleted_rows is set but with_row_id is false".into(),
2257 ));
2258 }
2259
2260 if self.aggregate.is_some() {
2261 if self.limit.is_some() || self.offset.is_some() {
2262 return Err(Error::invalid_input_source(
2263 "Cannot use limit/offset with aggregate. Apply limit to the result instead."
2264 .into(),
2265 ));
2266 }
2267 if self.ordering.is_some() {
2268 return Err(Error::invalid_input_source(
2269 "Cannot use order_by with aggregate. Apply ordering to the result instead."
2270 .into(),
2271 ));
2272 }
2273 }
2274
2275 if self.index_segments.is_some() && self.nearest.is_none() {
2276 return Err(Error::not_supported(
2277 "with_index_segments is only supported for vector search".to_string(),
2278 ));
2279 }
2280
2281 Ok(())
2282 }
2283
2284 async fn create_filter_plan(&self, use_scalar_index: bool) -> Result<FilterPlan> {
2285 let filter_schema = self.filterable_schema()?;
2286 let planner = Planner::new(Arc::new(filter_schema.as_ref().into()));
2287
2288 let filter_plan = if let Some(filter) = self.filter.expr_filter.as_ref() {
2290 let expr = filter.to_datafusion(self.dataset.schema(), filter_schema.as_ref())?;
2291 let index_info = self.dataset.scalar_index_info().await?;
2292 let filter_plan =
2293 planner.create_filter_plan(expr.clone(), &index_info, use_scalar_index)?;
2294
2295 if filter_plan.index_query.is_some() {
2298 let fragments = if let Some(fragments) = self.fragments.as_ref() {
2299 fragments
2300 } else {
2301 self.dataset.fragments()
2302 };
2303 let mut has_missing_row_count = false;
2304 for frag in fragments {
2305 if frag.physical_rows.is_none() {
2306 has_missing_row_count = true;
2307 break;
2308 }
2309 }
2310 if has_missing_row_count {
2311 let filter_plan =
2314 planner.create_filter_plan(expr.clone(), &index_info, false)?;
2315 FilterPlan::new(self.filter.query_filter.clone(), filter_plan)
2316 } else {
2317 FilterPlan::new(self.filter.query_filter.clone(), filter_plan)
2318 }
2319 } else {
2320 FilterPlan::new(self.filter.query_filter.clone(), filter_plan)
2321 }
2322 } else {
2323 FilterPlan::new(self.filter.query_filter.clone(), ExprFilterPlan::default())
2324 };
2325
2326 if filter_plan.query_filter.is_some()
2328 && self.nearest.is_none()
2329 && self.full_text_query.is_none()
2330 {
2331 return Err(Error::invalid_input_source(
2332 "Query filter can only be used with full text search or vector search".into(),
2333 ));
2334 }
2335 if self.nearest.is_some() && filter_plan.vector_filter().is_some() {
2336 return Err(Error::invalid_input_source(
2337 "Query filter can't be used with vector search".into(),
2338 ));
2339 }
2340 if self.full_text_query.is_some() && filter_plan.fts_filter().is_some() {
2341 return Err(Error::invalid_input_source(
2342 "Fts filter can't be used with fts search".into(),
2343 ));
2344 }
2345
2346 Ok(filter_plan)
2347 }
2348
2349 async fn get_scan_range(&self, filter_plan: &ExprFilterPlan) -> Result<Option<Range<u64>>> {
2350 if filter_plan.has_any_filter() {
2351 Ok(None)
2353 } else if self.ordering.is_some() {
2354 Ok(None)
2357 } else {
2358 match (self.limit, self.offset) {
2359 (None, None) => Ok(None),
2360 (Some(limit), None) => {
2361 let num_rows = self.dataset.count_all_rows().await? as i64;
2362 Ok(Some(0..limit.min(num_rows) as u64))
2363 }
2364 (None, Some(offset)) => {
2365 let num_rows = self.dataset.count_all_rows().await? as i64;
2366 Ok(Some(offset.min(num_rows) as u64..num_rows as u64))
2367 }
2368 (Some(limit), Some(offset)) => {
2369 let num_rows = self.dataset.count_all_rows().await? as i64;
2370 Ok(Some(
2371 offset.min(num_rows) as u64..(offset + limit).min(num_rows) as u64,
2372 ))
2373 }
2374 }
2375 }
2376 }
2377
2378 #[instrument(level = "debug", skip_all)]
2424 pub async fn create_plan(&self) -> Result<Arc<dyn ExecutionPlan>> {
2425 log::trace!("creating scanner plan");
2426 self.validate_options()?;
2427
2428 let use_scalar_index = self.use_scalar_index && (self.prefilter || self.nearest.is_none());
2430 let mut filter_plan = self.create_filter_plan(use_scalar_index).await?;
2431
2432 let mut use_limit_node = true;
2433 let mut plan: Arc<dyn ExecutionPlan> = match (&self.nearest, &self.full_text_query) {
2435 (Some(_), None) => self.vector_search_source(&mut filter_plan).await?,
2436 (None, Some(query)) => self.fts_search_source(&mut filter_plan, query).await?,
2437 (None, None) => {
2438 if self.projection_plan.has_output_cols()
2439 && self.projection_plan.physical_projection.is_empty()
2440 {
2441 let output_expr = self.calculate_final_projection(&ArrowSchema::empty())?;
2452 return Err(Error::not_supported_source(format!("Scans must request at least one column. Received only dynamic expressions: {:?}", output_expr).into()));
2453 }
2454
2455 let take_op = filter_plan
2456 .expr_filter_plan
2457 .full_expr
2458 .as_ref()
2459 .and_then(TakeOperation::try_from_expr);
2460 if let Some((take_op, remainder)) = take_op {
2461 filter_plan.expr_filter_plan = remainder
2464 .map(ExprFilterPlan::new_refine_only)
2465 .unwrap_or(ExprFilterPlan::default());
2466 self.take_source(take_op).await?
2467 } else {
2468 let planned_read = self
2469 .filtered_read_source(&mut filter_plan.expr_filter_plan)
2470 .await?;
2471 if planned_read.limit_pushed_down {
2472 use_limit_node = false;
2473 }
2474 if planned_read.filter_pushed_down {
2475 filter_plan.disable_refine();
2476 }
2477 planned_read.plan
2478 }
2479 }
2480 _ => {
2481 return Err(Error::invalid_input_source(
2482 "Cannot have both nearest and full text search".into(),
2483 ));
2484 }
2485 };
2486
2487 let mut pre_filter_projection = self.dataset.empty_projection();
2489
2490 if filter_plan.has_refine() {
2493 pre_filter_projection = pre_filter_projection.union_columns(
2495 filter_plan.refine_columns(&self.dataset).await?,
2496 OnMissing::Ignore,
2497 )?;
2498 }
2499
2500 if let Some(ordering) = &self.ordering {
2505 pre_filter_projection = pre_filter_projection.union_columns(
2506 ordering.iter().map(|col| &col.column_name),
2507 OnMissing::Error,
2508 )?;
2509 }
2510
2511 plan = self.take(plan, pre_filter_projection)?;
2512
2513 plan = filter_plan.refine_filter(plan, self).await?;
2515
2516 if let Some(agg) = &self.aggregate {
2518 let required_columns = agg.required_columns();
2521 let agg_projection = if required_columns.is_empty() {
2522 self.dataset.empty_projection()
2523 } else {
2524 self.dataset
2525 .empty_projection()
2526 .union_columns(&required_columns, OnMissing::Error)?
2527 };
2528 plan = self.take(plan, agg_projection)?;
2529 plan = self.apply_aggregate(plan, agg).await?;
2530
2531 let optimizer = get_physical_optimizer();
2532 let options = Default::default();
2533 for rule in optimizer.rules {
2534 plan = rule.optimize(plan, &options)?;
2535 }
2536
2537 return Ok(plan);
2538 }
2539
2540 if let Some(ordering) = &self.ordering {
2542 let ordering_columns = ordering.iter().map(|col| &col.column_name);
2543 let projection_with_ordering = self
2544 .dataset
2545 .empty_projection()
2546 .union_columns(ordering_columns, OnMissing::Error)?;
2547 plan = self.take(plan, projection_with_ordering)?;
2549 let col_exprs = ordering
2550 .iter()
2551 .map(|col| {
2552 Ok(PhysicalSortExpr {
2553 expr: Self::create_column_expr(
2554 &col.column_name,
2555 &self.dataset,
2556 plan.schema().as_ref(),
2557 )?,
2558 options: SortOptions {
2559 descending: !col.ascending,
2560 nulls_first: col.nulls_first,
2561 },
2562 })
2563 })
2564 .collect::<Result<Vec<_>>>()?;
2565 plan = Arc::new(SortExec::new(
2566 LexOrdering::new(col_exprs)
2567 .ok_or(exec_datafusion_err!("Unexpected empty sort expressions"))?,
2568 plan,
2569 ));
2570 }
2571
2572 if use_limit_node && (self.limit.unwrap_or(0) > 0 || self.offset.is_some()) {
2574 plan = self.limit_node(plan);
2575 }
2576
2577 plan = self.take(plan, self.projection_plan.physical_projection.clone())?;
2579
2580 if self.projection_plan.must_add_row_offset {
2582 plan = Arc::new(AddRowOffsetExec::try_new(plan, self.dataset.clone()).await?);
2583 }
2584
2585 let final_projection = self.calculate_final_projection(plan.schema().as_ref())?;
2587
2588 plan = Arc::new(DFProjectionExec::try_new(final_projection, plan)?);
2589
2590 if self.strict_batch_size {
2592 plan = Arc::new(StrictBatchSizeExec::new(plan, self.get_batch_size()));
2593 }
2594
2595 let optimizer = get_physical_optimizer();
2596 let options: ConfigOptions = Default::default();
2597 for rule in optimizer.rules {
2598 plan = rule.optimize(plan, &options)?;
2599 }
2600
2601 Ok(plan)
2602 }
2603
2604 fn filter_references_version_columns(&self, filter_plan: &ExprFilterPlan) -> bool {
2606 use lance_core::{ROW_CREATED_AT_VERSION, ROW_LAST_UPDATED_AT_VERSION};
2607
2608 if let Some(refine_expr) = &filter_plan.refine_expr {
2609 let column_names = Planner::column_names_in_expr(refine_expr);
2610 for col_name in column_names {
2611 if col_name == ROW_CREATED_AT_VERSION || col_name == ROW_LAST_UPDATED_AT_VERSION {
2612 return true;
2613 }
2614 }
2615 }
2616 false
2617 }
2618
2619 async fn legacy_filtered_read(
2625 &self,
2626 filter_plan: &ExprFilterPlan,
2627 projection: Projection,
2628 make_deletions_null: bool,
2629 fragments: Option<Arc<Vec<Fragment>>>,
2630 scan_range: Option<Range<u64>>,
2631 is_prefilter: bool,
2632 ) -> Result<PlannedFilteredScan> {
2633 let fragments = fragments.unwrap_or(self.dataset.fragments().clone());
2634 let mut filter_pushed_down = false;
2635
2636 let plan: Arc<dyn ExecutionPlan> = if filter_plan.has_index_query() {
2637 if self.include_deleted_rows {
2638 return Err(Error::invalid_input_source(
2639 "Cannot include deleted rows in a scalar indexed scan".into(),
2640 ));
2641 }
2642 self.scalar_indexed_scan(projection, filter_plan, fragments)
2643 .await
2644 } else if !is_prefilter
2645 && filter_plan.has_refine()
2646 && self.batch_size.is_none()
2647 && self.use_stats
2648 && !self.filter_references_version_columns(filter_plan)
2649 {
2650 filter_pushed_down = true;
2651 self.pushdown_scan(false, filter_plan)
2652 } else {
2653 let ordered = if self.ordering.is_some() || self.nearest.is_some() {
2654 false
2656 } else if projection.with_row_last_updated_at_version
2657 || projection.with_row_created_at_version
2658 {
2659 true
2662 } else {
2663 self.ordered
2664 };
2665
2666 let projection = if let Some(refine_expr) = filter_plan.refine_expr.as_ref() {
2667 if is_prefilter {
2668 let refine_cols = Planner::column_names_in_expr(refine_expr);
2669 projection.union_columns(refine_cols, OnMissing::Error)?
2670 } else {
2671 projection
2672 }
2673 } else {
2674 projection
2675 };
2676
2677 let scan_range = if filter_plan.has_refine() {
2679 None
2680 } else {
2681 scan_range
2682 };
2683
2684 let scan = self.scan_fragments(
2685 projection.with_row_id,
2686 self.projection_plan.physical_projection.with_row_addr,
2687 self.projection_plan
2688 .physical_projection
2689 .with_row_last_updated_at_version,
2690 self.projection_plan
2691 .physical_projection
2692 .with_row_created_at_version,
2693 make_deletions_null,
2694 Arc::new(projection.to_bare_schema()),
2695 fragments,
2696 scan_range,
2697 ordered,
2698 );
2699
2700 if filter_plan.has_refine() && is_prefilter {
2701 Ok(Arc::new(LanceFilterExec::try_new(
2702 filter_plan.refine_expr.clone().unwrap(),
2703 scan,
2704 )?) as Arc<dyn ExecutionPlan>)
2705 } else {
2706 Ok(scan)
2707 }
2708 }?;
2709 Ok(PlannedFilteredScan {
2710 plan,
2711 limit_pushed_down: false,
2712 filter_pushed_down,
2713 })
2714 }
2715
2716 async fn new_filtered_read(
2720 &self,
2721 filter_plan: &ExprFilterPlan,
2722 projection: Projection,
2723 make_deletions_null: bool,
2724 fragments: Option<Arc<Vec<Fragment>>>,
2725 scan_range: Option<Range<u64>>,
2726 ) -> Result<Arc<dyn ExecutionPlan>> {
2727 let mut read_options = FilteredReadOptions::basic_full_read(&self.dataset)
2728 .with_filter_plan(filter_plan.clone())
2729 .with_projection(projection);
2730
2731 if let Some(fragments) = fragments {
2732 read_options = read_options.with_fragments(fragments);
2733 }
2734
2735 if let Some(scan_range) = scan_range {
2736 read_options = read_options.with_scan_range_before_filter(scan_range)?;
2737 }
2738
2739 if let Some(batch_size) = self.batch_size {
2740 read_options = read_options.with_batch_size(batch_size as u32);
2741 }
2742
2743 if let Some(file_reader_options) = self.resolved_file_reader_options() {
2744 read_options = read_options.with_file_reader_options(file_reader_options);
2745 }
2746
2747 if let Some(fragment_readahead) = self.fragment_readahead {
2748 read_options = read_options.with_fragment_readahead(fragment_readahead);
2749 }
2750
2751 if make_deletions_null {
2752 read_options = read_options.with_deleted_rows()?;
2753 }
2754
2755 if let Some(io_buffer_size_bytes) = self.io_buffer_size {
2756 read_options = read_options.with_io_buffer_size(io_buffer_size_bytes);
2757 }
2758
2759 let index_input = filter_plan.index_query.clone().map(|index_query| {
2760 Arc::new(ScalarIndexExec::new(self.dataset.clone(), index_query))
2761 as Arc<dyn ExecutionPlan>
2762 });
2763
2764 Ok(Arc::new(FilteredReadExec::try_new(
2765 self.dataset.clone(),
2766 read_options,
2767 index_input,
2768 )?))
2769 }
2770
2771 async fn filtered_read(
2775 &self,
2776 filter_plan: &ExprFilterPlan,
2777 projection: Projection,
2778 make_deletions_null: bool,
2779 fragments: Option<Arc<Vec<Fragment>>>,
2780 scan_range: Option<Range<u64>>,
2781 is_prefilter: bool,
2782 ) -> Result<PlannedFilteredScan> {
2783 if self.dataset.is_legacy_storage() {
2785 self.legacy_filtered_read(
2786 filter_plan,
2787 projection,
2788 make_deletions_null,
2789 fragments,
2790 scan_range,
2791 is_prefilter,
2792 )
2793 .await
2794 } else {
2795 let limit_pushed_down = scan_range.is_some();
2796 let plan = self
2797 .new_filtered_read(
2798 filter_plan,
2799 projection,
2800 make_deletions_null,
2801 fragments,
2802 scan_range,
2803 )
2804 .await?;
2805 Ok(PlannedFilteredScan {
2806 filter_pushed_down: true,
2807 limit_pushed_down,
2808 plan,
2809 })
2810 }
2811 }
2812
2813 fn u64s_as_take_input(&self, u64s: Vec<u64>) -> Result<Arc<dyn ExecutionPlan>> {
2814 let row_addrs = RowAddrTreeMap::from_iter(u64s);
2815 let row_addr_mask = RowAddrMask::from_allowed(row_addrs);
2816 let index_result = IndexExprResult::Exact(row_addr_mask);
2817 let fragments_covered = self.dataset.fragment_bitmap.as_ref().clone();
2818 let batch = index_result.serialize_to_arrow(&fragments_covered)?;
2819 let stream = futures::stream::once(async move { Ok(batch) });
2820 let stream = Box::pin(RecordBatchStreamAdapter::new(
2821 INDEX_EXPR_RESULT_SCHEMA.clone(),
2822 stream,
2823 ));
2824 Ok(Arc::new(OneShotExec::new(stream)))
2825 }
2826
2827 async fn take_source(&self, take_op: TakeOperation) -> Result<Arc<dyn ExecutionPlan>> {
2828 let projection = self.projection_plan.physical_projection.clone();
2831
2832 let input = match take_op {
2833 TakeOperation::RowIds(ids) => self.u64s_as_take_input(ids),
2834 TakeOperation::RowAddrs(addrs) => self.u64s_as_take_input(addrs),
2835 TakeOperation::RowOffsets(offsets) => {
2836 let mut addrs =
2837 row_offsets_to_row_addresses(&self.dataset.get_fragments(), &offsets).await?;
2838 addrs.retain(|addr| *addr != RowAddress::TOMBSTONE_ROW);
2839 self.u64s_as_take_input(addrs)
2840 }
2841 }?;
2842
2843 let mut filtered_read_options = FilteredReadOptions::new(projection);
2844 if let Some(fragment) = self.fragments.as_ref() {
2845 filtered_read_options =
2846 filtered_read_options.with_fragments(Arc::new(fragment.clone()));
2847 }
2848
2849 Ok(Arc::new(FilteredReadExec::try_new(
2850 self.dataset.clone(),
2851 filtered_read_options,
2852 Some(input),
2853 )?))
2854 }
2855
2856 async fn filtered_read_source(
2857 &self,
2858 filter_plan: &mut ExprFilterPlan,
2859 ) -> Result<PlannedFilteredScan> {
2860 log::trace!("source is a filtered read");
2861
2862 let effective_projection = if let Some(agg) = &self.aggregate {
2866 let required_columns = agg.required_columns();
2867 if required_columns.is_empty() {
2868 self.dataset.empty_projection()
2870 } else {
2871 self.dataset
2873 .empty_projection()
2874 .union_columns(&required_columns, OnMissing::Error)?
2875 }
2876 } else {
2877 self.projection_plan.physical_projection.clone()
2878 };
2879
2880 let mut projection = if filter_plan.has_refine() {
2881 self.calc_eager_projection(filter_plan, &effective_projection)?
2885 .with_row_id()
2886 } else {
2887 effective_projection
2890 };
2891
2892 if projection.is_empty() {
2893 projection.with_row_addr = true;
2896 }
2897
2898 let scan_range = if filter_plan.is_empty() {
2899 log::trace!("pushing scan_range into filtered_read");
2900 self.get_scan_range(filter_plan).await?
2901 } else {
2902 None
2903 };
2904
2905 self.filtered_read(
2906 filter_plan,
2907 projection,
2908 self.include_deleted_rows,
2909 self.fragments.clone().map(Arc::new),
2910 scan_range,
2911 false,
2912 )
2913 .await
2914 }
2915
2916 async fn fts_search_source(
2917 &self,
2918 filter_plan: &mut FilterPlan,
2919 query: &FullTextSearchQuery,
2920 ) -> Result<Arc<dyn ExecutionPlan>> {
2921 log::trace!("source is an fts search");
2922 if self.include_deleted_rows {
2923 return Err(Error::invalid_input_source(
2924 "Cannot include deleted rows in an FTS search".into(),
2925 ));
2926 }
2927
2928 if self.prefilter {
2930 let source: Arc<dyn ExecutionPlan> = match &filter_plan.vector_filter() {
2931 Some(vector_query) => {
2932 let vector_plan = self
2934 .vector_search(&filter_plan.expr_filter_plan, vector_query)
2935 .await?;
2936 self.fts_rerank(vector_plan, query).await?
2937 }
2938 None => self.fts(&filter_plan.expr_filter_plan, query).await?,
2939 };
2940 filter_plan.disable_refine();
2942 Ok(source)
2943 } else {
2944 filter_plan.make_refine_only();
2947 self.fts(&ExprFilterPlan::default(), query).await
2948 }
2949 }
2950
2951 async fn vector_search_source(
2952 &self,
2953 filter_plan: &mut FilterPlan,
2954 ) -> Result<Arc<dyn ExecutionPlan>> {
2955 if self.include_deleted_rows {
2956 return Err(Error::invalid_input_source(
2957 "Cannot include deleted rows in a nearest neighbor search".into(),
2958 ));
2959 }
2960 let Some(query) = self.nearest.as_ref() else {
2961 return Err(Error::invalid_input("No nearest query".to_string()));
2962 };
2963
2964 if self.prefilter {
2965 log::trace!("source is a vector search (prefilter)");
2966 let source: Arc<dyn ExecutionPlan> = match &filter_plan.fts_filter() {
2968 Some(fts_query) => {
2969 let fts_plan = self.fts(&filter_plan.expr_filter_plan, fts_query).await?;
2970 let projection = self
2971 .dataset
2972 .empty_projection()
2973 .union_column(&query.column, OnMissing::Error)?;
2974 let plan = self.take(fts_plan, projection)?;
2975
2976 self.flat_knn(plan, query)?
2977 }
2978 None => {
2979 self.vector_search(&filter_plan.expr_filter_plan, query)
2980 .await?
2981 }
2982 };
2983
2984 filter_plan.disable_refine();
2985 Ok(source)
2986 } else {
2987 log::trace!("source is a vector search (postfilter)");
2988 filter_plan.make_refine_only();
2991 self.vector_search(&ExprFilterPlan::default(), query).await
2992 }
2993 }
2994
2995 async fn fragments_covered_by_fts_leaf(
2996 &self,
2997 column: &str,
2998 accum: &mut RoaringBitmap,
2999 ) -> Result<bool> {
3000 let index = self
3001 .dataset
3002 .load_scalar_index(IndexCriteria::default().for_column(column).supports_fts())
3003 .await?;
3004 match index {
3005 Some(index) => match &index.fragment_bitmap {
3006 Some(fragmap) => {
3007 *accum |= fragmap;
3008 Ok(true)
3009 }
3010 None => Ok(false),
3011 },
3012 None => Ok(false),
3013 }
3014 }
3015
3016 #[async_recursion]
3017 async fn fragments_covered_by_fts_query_helper(
3018 &self,
3019 query: &FtsQuery,
3020 accum: &mut RoaringBitmap,
3021 ) -> Result<bool> {
3022 match query {
3023 FtsQuery::Match(match_query) => {
3024 self.fragments_covered_by_fts_leaf(
3025 match_query.column.as_ref().ok_or(Error::invalid_input(
3026 "the column must be specified in the query".to_string(),
3027 ))?,
3028 accum,
3029 )
3030 .await
3031 }
3032 FtsQuery::Boost(boost) => Ok(self
3033 .fragments_covered_by_fts_query_helper(&boost.negative, accum)
3034 .await?
3035 & self
3036 .fragments_covered_by_fts_query_helper(&boost.positive, accum)
3037 .await?),
3038 FtsQuery::MultiMatch(multi_match) => {
3039 for mq in &multi_match.match_queries {
3040 if !self
3041 .fragments_covered_by_fts_leaf(
3042 mq.column.as_ref().ok_or(Error::invalid_input(
3043 "the column must be specified in the query".to_string(),
3044 ))?,
3045 accum,
3046 )
3047 .await?
3048 {
3049 return Ok(false);
3050 }
3051 }
3052 Ok(true)
3053 }
3054 FtsQuery::Phrase(phrase_query) => {
3055 self.fragments_covered_by_fts_leaf(
3056 phrase_query.column.as_ref().ok_or(Error::invalid_input(
3057 "the column must be specified in the query".to_string(),
3058 ))?,
3059 accum,
3060 )
3061 .await
3062 }
3063 FtsQuery::Boolean(bool_query) => {
3064 for query in bool_query.must.iter() {
3065 if !self
3066 .fragments_covered_by_fts_query_helper(query, accum)
3067 .await?
3068 {
3069 return Ok(false);
3070 }
3071 }
3072 for query in &bool_query.should {
3073 if !self
3074 .fragments_covered_by_fts_query_helper(query, accum)
3075 .await?
3076 {
3077 return Ok(false);
3078 }
3079 }
3080 Ok(true)
3081 }
3082 }
3083 }
3084
3085 async fn fragments_covered_by_fts_query(&self, query: &FtsQuery) -> Result<RoaringBitmap> {
3086 let all_fragments = self.get_fragments_as_bitmap();
3087
3088 let mut referenced_fragments = RoaringBitmap::new();
3089 if !self
3090 .fragments_covered_by_fts_query_helper(query, &mut referenced_fragments)
3091 .await?
3092 {
3093 Ok(all_fragments)
3095 } else {
3096 Ok(all_fragments & referenced_fragments)
3098 }
3099 }
3100
3101 async fn fts(
3103 &self,
3104 filter_plan: &ExprFilterPlan,
3105 query: &FullTextSearchQuery,
3106 ) -> Result<Arc<dyn ExecutionPlan>> {
3107 let columns = query.columns();
3108 let mut params = query.params();
3109 if params.limit.is_none() {
3110 let search_limit = match (self.limit, self.offset) {
3111 (Some(limit), Some(offset)) => Some((limit + offset) as usize),
3112 (Some(limit), None) => Some(limit as usize),
3113 (None, Some(_)) => None, (None, None) => None,
3115 };
3116 params = params.with_limit(search_limit);
3117 }
3118 let query = if columns.is_empty() {
3119 let indexed_columns = fts_indexed_columns(self.dataset.clone()).await?;
3122 fill_fts_query_column(&query.query, &indexed_columns, false)?
3123 } else {
3124 query.query.clone()
3125 };
3126
3127 let prefilter_source = self
3131 .prefilter_source(
3132 filter_plan,
3133 self.fragments_covered_by_fts_query(&query).await?,
3134 )
3135 .await?;
3136 let fts_exec = self
3137 .plan_fts(&query, ¶ms, filter_plan, &prefilter_source)
3138 .await?;
3139 Ok(fts_exec)
3140 }
3141
3142 async fn plan_fts(
3143 &self,
3144 query: &FtsQuery,
3145 params: &FtsSearchParams,
3146 filter_plan: &ExprFilterPlan,
3147 prefilter_source: &PreFilterSource,
3148 ) -> Result<Arc<dyn ExecutionPlan>> {
3149 let plan: Arc<dyn ExecutionPlan> = match query {
3150 FtsQuery::Match(query) => {
3151 self.plan_match_query(query, params, filter_plan, prefilter_source)
3152 .await?
3153 }
3154 FtsQuery::Phrase(query) => {
3155 self.plan_phrase_query(query, params, prefilter_source)
3156 .await?
3157 }
3158
3159 FtsQuery::Boost(query) => {
3160 let unlimited_params = params.clone().with_limit(None);
3164 let positive_exec = Box::pin(self.plan_fts(
3165 &query.positive,
3166 &unlimited_params,
3167 filter_plan,
3168 prefilter_source,
3169 ));
3170 let negative_exec = Box::pin(self.plan_fts(
3171 &query.negative,
3172 &unlimited_params,
3173 filter_plan,
3174 prefilter_source,
3175 ));
3176 let (positive_exec, negative_exec) =
3177 futures::future::try_join(positive_exec, negative_exec).await?;
3178 Arc::new(BoostQueryExec::new(
3179 query.clone(),
3180 params.clone(),
3181 positive_exec,
3182 negative_exec,
3183 ))
3184 }
3185
3186 FtsQuery::MultiMatch(query) => {
3187 let mut children = Vec::with_capacity(query.match_queries.len());
3188 for match_query in &query.match_queries {
3189 let child =
3190 self.plan_match_query(match_query, params, filter_plan, prefilter_source);
3191 children.push(child);
3192 }
3193 let children = futures::future::try_join_all(children).await?;
3194
3195 let schema = children[0].schema();
3196 let group_expr = vec![(
3197 expressions::col(ROW_ID, schema.as_ref())?,
3198 ROW_ID.to_string(),
3199 )];
3200
3201 let fts_node = UnionExec::try_new(children)?;
3202 let fts_node = Arc::new(RepartitionExec::try_new(
3203 fts_node,
3204 Partitioning::RoundRobinBatch(1),
3205 )?);
3206 let fts_node = Arc::new(AggregateExec::try_new(
3208 AggregateMode::Single,
3209 PhysicalGroupBy::new_single(group_expr),
3210 vec![Arc::new(
3211 datafusion_physical_expr::aggregate::AggregateExprBuilder::new(
3212 functions_aggregate::min_max::max_udaf(),
3213 vec![expressions::col(SCORE_COL, &schema)?],
3214 )
3215 .schema(schema.clone())
3216 .alias(SCORE_COL)
3217 .build()?,
3218 )],
3219 vec![None],
3220 fts_node,
3221 schema,
3222 )?);
3223 let sort_expr = PhysicalSortExpr {
3224 expr: expressions::col(SCORE_COL, fts_node.schema().as_ref())?,
3225 options: SortOptions {
3226 descending: true,
3227 nulls_first: false,
3228 },
3229 };
3230
3231 Arc::new(
3232 SortExec::new([sort_expr].into(), fts_node)
3233 .with_fetch(self.limit.map(|l| l as usize)),
3234 )
3235 }
3236 FtsQuery::Boolean(query) => {
3237 let unlimited_params = params.clone().with_limit(None);
3242
3243 let mut should = Vec::with_capacity(query.should.len());
3245 for subquery in &query.should {
3246 let plan = Box::pin(self.plan_fts(
3247 subquery,
3248 &unlimited_params,
3249 filter_plan,
3250 prefilter_source,
3251 ))
3252 .await?;
3253 should.push(plan);
3254 }
3255 let should = if should.is_empty() {
3256 Arc::new(EmptyExec::new(FTS_SCHEMA.clone()))
3257 } else if should.len() == 1 {
3258 should.pop().unwrap()
3259 } else {
3260 let unioned = UnionExec::try_new(should)?;
3261 Arc::new(RepartitionExec::try_new(
3262 unioned,
3263 Partitioning::RoundRobinBatch(1),
3264 )?)
3265 };
3266
3267 let mut must = None;
3269 for query in &query.must {
3270 let plan = Box::pin(self.plan_fts(
3271 query,
3272 &unlimited_params,
3273 filter_plan,
3274 prefilter_source,
3275 ))
3276 .await?;
3277 if let Some(joined_plan) = must {
3278 must = Some(Arc::new(HashJoinExec::try_new(
3279 joined_plan,
3280 plan,
3281 vec![(
3282 Arc::new(Column::new_with_schema(ROW_ID, &FTS_SCHEMA)?),
3283 Arc::new(Column::new_with_schema(ROW_ID, &FTS_SCHEMA)?),
3284 )],
3285 None,
3286 &datafusion_expr::JoinType::Inner,
3287 None,
3288 datafusion_physical_plan::joins::PartitionMode::CollectLeft,
3289 NullEquality::NullEqualsNothing,
3290 false,
3291 )?) as _);
3292 } else {
3293 must = Some(plan);
3294 }
3295 }
3296
3297 let mut must_not = Vec::with_capacity(query.must_not.len());
3299 for query in &query.must_not {
3300 let plan = Box::pin(self.plan_fts(
3301 query,
3302 &unlimited_params,
3303 filter_plan,
3304 prefilter_source,
3305 ))
3306 .await?;
3307 must_not.push(plan);
3308 }
3309 let must_not = if must_not.is_empty() {
3310 Arc::new(EmptyExec::new(FTS_SCHEMA.clone()))
3311 } else if must_not.len() == 1 {
3312 must_not.pop().unwrap()
3313 } else {
3314 let unioned = UnionExec::try_new(must_not)?;
3315 Arc::new(RepartitionExec::try_new(
3316 unioned,
3317 Partitioning::RoundRobinBatch(1),
3318 )?)
3319 };
3320
3321 if query.should.is_empty() && must.is_none() {
3322 return Err(Error::invalid_input(
3323 "boolean query must have at least one should/must query".to_string(),
3324 ));
3325 }
3326
3327 Arc::new(BooleanQueryExec::new(
3328 query.clone(),
3329 params.clone(),
3330 should,
3331 must,
3332 must_not,
3333 ))
3334 }
3335 };
3336
3337 Ok(plan)
3338 }
3339
3340 async fn plan_phrase_query(
3341 &self,
3342 query: &PhraseQuery,
3343 params: &FtsSearchParams,
3344 prefilter_source: &PreFilterSource,
3345 ) -> Result<Arc<dyn ExecutionPlan>> {
3346 let column = query.column.clone().ok_or(Error::invalid_input(
3347 "the column must be specified in the query".to_string(),
3348 ))?;
3349
3350 let segments = load_segments(&self.dataset, &column)
3351 .await?
3352 .ok_or(Error::invalid_input(format!(
3353 "No Inverted index found for column {}",
3354 column
3355 )))?;
3356 let details = load_segment_details(&self.dataset, &column, &segments).await?;
3357
3358 if !details.with_position {
3359 return Err(Error::invalid_input("position is not found but required for phrase queries, try recreating the index with position"
3360 .to_string()));
3361 }
3362
3363 Ok(Arc::new(PhraseQueryExec::new(
3364 self.dataset.clone(),
3365 query.clone(),
3366 params.clone(),
3367 prefilter_source.clone(),
3368 )))
3369 }
3370
3371 async fn plan_match_query(
3372 &self,
3373 query: &MatchQuery,
3374 params: &FtsSearchParams,
3375 filter_plan: &ExprFilterPlan,
3376 prefilter_source: &PreFilterSource,
3377 ) -> Result<Arc<dyn ExecutionPlan>> {
3378 let column = query
3379 .column
3380 .as_ref()
3381 .ok_or(Error::invalid_input(
3382 "the column must be specified in the query".to_string(),
3383 ))?
3384 .clone();
3385
3386 let index = self
3387 .dataset
3388 .load_scalar_index(IndexCriteria::default().for_column(&column).supports_fts())
3389 .await?;
3390
3391 let target_fragments = self
3393 .fragments
3394 .clone()
3395 .unwrap_or_else(|| self.dataset.fragments().to_vec());
3396
3397 let (match_plan, flat_match_plan) = match &index {
3398 Some(index) => {
3399 let unindexed_fragments = self
3401 .retain_target_fragments(self.dataset.unindexed_fragments(&index.name).await?);
3402
3403 if unindexed_fragments.len() == target_fragments.len() {
3405 if self.fast_search {
3406 return Ok(Arc::new(EmptyExec::new(FTS_SCHEMA.clone())));
3407 }
3408 let flat_match_plan = self
3409 .plan_flat_match_query(unindexed_fragments, query, params, filter_plan)
3410 .await?;
3411 return Ok(flat_match_plan);
3412 }
3413
3414 let match_plan: Arc<dyn ExecutionPlan> = Arc::new(MatchQueryExec::new(
3416 self.dataset.clone(),
3417 query.clone(),
3418 params.clone(),
3419 prefilter_source.clone(),
3420 ));
3421
3422 if self.fast_search || unindexed_fragments.is_empty() {
3423 (Some(match_plan), None)
3424 } else {
3425 let flat_match_plan = self
3426 .plan_flat_match_query(unindexed_fragments, query, params, filter_plan)
3427 .await?;
3428 (Some(match_plan), Some(flat_match_plan))
3429 }
3430 }
3431 None => {
3432 if self.fast_search {
3433 return Ok(Arc::new(EmptyExec::new(FTS_SCHEMA.clone())));
3434 }
3435 let flat_match_plan = self
3437 .plan_flat_match_query(target_fragments.clone(), query, params, filter_plan)
3438 .await?;
3439 (None, Some(flat_match_plan))
3440 }
3441 };
3442
3443 let plan = match (match_plan, flat_match_plan) {
3445 (Some(match_plan), Some(flat_match_plan)) => {
3446 let match_plan = UnionExec::try_new(vec![match_plan, flat_match_plan])?;
3447 let match_plan = Arc::new(RepartitionExec::try_new(
3448 match_plan,
3449 Partitioning::RoundRobinBatch(1),
3450 )?);
3451 let sort_expr = PhysicalSortExpr {
3452 expr: expressions::col(SCORE_COL, match_plan.schema().as_ref())?,
3453 options: SortOptions {
3454 descending: true,
3455 nulls_first: false,
3456 },
3457 };
3458 Arc::new(SortExec::new([sort_expr].into(), match_plan).with_fetch(params.limit))
3459 }
3460 (Some(match_plan), None) => match_plan,
3461 (None, Some(flat_match_plan)) => flat_match_plan,
3462 (None, None) => unreachable!(),
3463 };
3464
3465 Ok(plan)
3466 }
3467
3468 async fn plan_flat_match_query(
3470 &self,
3471 fragments: Vec<Fragment>,
3472 query: &MatchQuery,
3473 params: &FtsSearchParams,
3474 filter_plan: &ExprFilterPlan,
3475 ) -> Result<Arc<dyn ExecutionPlan>> {
3476 let column = query
3477 .column
3478 .as_ref()
3479 .ok_or(Error::invalid_input(
3480 "the column must be specified in the query".to_string(),
3481 ))?
3482 .clone();
3483
3484 let mut columns = vec![column];
3485 if let Some(expr) = filter_plan.full_expr.as_ref() {
3486 let filter_columns = Planner::column_names_in_expr(expr);
3487 columns.extend(filter_columns);
3488 }
3489 let flat_fts_scan_schema = Arc::new(self.dataset.schema().project(&columns).unwrap());
3490 let mut scan_node = self.scan_fragments(
3491 true,
3492 false,
3493 false,
3494 false,
3495 false,
3496 flat_fts_scan_schema,
3497 Arc::new(fragments),
3498 None,
3499 false,
3500 );
3501
3502 if let Some(expr) = filter_plan.full_expr.as_ref() {
3503 scan_node = Arc::new(LanceFilterExec::try_new(expr.clone(), scan_node)?);
3505 }
3506
3507 let flat_match_plan = Arc::new(FlatMatchQueryExec::new(
3508 self.dataset.clone(),
3509 query.clone(),
3510 params.clone(),
3511 scan_node,
3512 ));
3513 Ok(flat_match_plan)
3514 }
3515
3516 async fn vector_search(
3518 &self,
3519 filter_plan: &ExprFilterPlan,
3520 q: &Query,
3521 ) -> Result<Arc<dyn ExecutionPlan>> {
3522 let mut q = q.clone();
3523
3524 let (vector_type, element_type) = get_vector_type(self.dataset.schema(), &q.column)?;
3526
3527 let column_id = self.dataset.schema().field_id(q.column.as_str())?;
3528 let use_index = q.use_index;
3529 let indices = if use_index {
3530 self.dataset.load_indices().await?
3531 } else {
3532 Arc::new(vec![])
3533 };
3534 let index_and_segments = if use_index {
3535 if let Some(requested_segments) = self.index_segments.as_ref() {
3536 let requested_segment_set =
3537 requested_segments.iter().copied().collect::<HashSet<_>>();
3538 let requested_index_segments = indices
3539 .iter()
3540 .filter(|idx| requested_segment_set.contains(&idx.uuid))
3541 .cloned()
3542 .collect::<Vec<_>>();
3543
3544 if requested_index_segments.len() != requested_segment_set.len() {
3545 let found_segment_set = requested_index_segments
3546 .iter()
3547 .map(|idx| idx.uuid)
3548 .collect::<HashSet<_>>();
3549 let missing_segments = requested_segment_set
3550 .difference(&found_segment_set)
3551 .map(ToString::to_string)
3552 .collect::<Vec<_>>();
3553 return Err(Error::invalid_input(format!(
3554 "with_index_segments referenced unknown index segments: {missing_segments:?}",
3555 )));
3556 }
3557
3558 if requested_index_segments
3559 .iter()
3560 .any(|idx| !idx.fields.contains(&column_id))
3561 {
3562 return Err(Error::invalid_input(format!(
3563 "with_index_segments contained a segment that does not belong to vector column '{}'",
3564 q.column
3565 )));
3566 }
3567
3568 let index_name = requested_index_segments[0].name.clone();
3569 if requested_index_segments
3570 .iter()
3571 .any(|idx| idx.name != index_name)
3572 {
3573 return Err(Error::invalid_input(
3574 "with_index_segments must reference segments from a single logical index"
3575 .to_string(),
3576 ));
3577 }
3578
3579 let selected_index_segments =
3580 self.retain_relevant_index_segments(requested_index_segments);
3581 if selected_index_segments.is_empty() {
3582 None
3583 } else {
3584 let idx = self
3585 .dataset
3586 .open_vector_index(
3587 q.column.as_str(),
3588 &selected_index_segments[0].uuid.to_string(),
3589 &NoOpMetricsCollector,
3590 )
3591 .await?;
3592 let index_metric = idx.metric_type();
3593 let use_this_index = match q.metric_type {
3594 Some(user_metric) => {
3595 if user_metric == index_metric {
3596 true
3597 } else {
3598 return Err(Error::invalid_input(format!(
3599 "with_index_segments requested metric {:?} but the selected index segments use {:?}",
3600 user_metric, index_metric
3601 )));
3602 }
3603 }
3604 None => true,
3605 };
3606 if use_this_index {
3607 Some((index_name, selected_index_segments, index_metric))
3608 } else {
3609 None
3610 }
3611 }
3612 } else if let Some(index) = indices.iter().find(|i| i.fields.contains(&column_id)) {
3613 let idx = self
3617 .dataset
3618 .open_vector_index(
3619 q.column.as_str(),
3620 &index.uuid.to_string(),
3621 &NoOpMetricsCollector,
3622 )
3623 .await?;
3624 let index_metric = idx.metric_type();
3625
3626 let use_this_index = match q.metric_type {
3627 Some(user_metric) => {
3628 if user_metric == index_metric {
3629 true
3630 } else {
3631 log::warn!(
3632 "Requested metric {:?} is incompatible with index metric {:?}, falling back to brute-force search",
3633 user_metric,
3634 index_metric
3635 );
3636 false
3637 }
3638 }
3639 None => true,
3640 };
3641
3642 if use_this_index {
3643 let index_segments = self.retain_relevant_index_segments(
3644 self.dataset.load_indices_by_name(&index.name).await?,
3645 );
3646 let index_frags = self.get_indexed_frags(&index_segments);
3647 if !index_segments.is_empty() && !index_frags.is_empty() {
3648 Some((index.name.clone(), index_segments, index_metric))
3649 } else {
3650 None
3651 }
3652 } else {
3653 None
3654 }
3655 } else {
3656 None
3657 }
3658 } else {
3659 None
3660 };
3661
3662 if let Some((index_name, index_segments, index_metric)) = index_and_segments {
3663 log::trace!("index found for vector search");
3664 q.metric_type = Some(index_metric);
3666 validate_distance_type_for(index_metric, &element_type)?;
3667
3668 if matches!(q.refine_factor, Some(0)) {
3669 return Err(Error::invalid_input(
3670 "Refine factor cannot be zero".to_string(),
3671 ));
3672 }
3673 let ann_node = match vector_type {
3674 DataType::FixedSizeList(_, _) => self.ann(&q, &index_segments, filter_plan).await?,
3675 DataType::List(_) => self.multivec_ann(&q, &index_segments, filter_plan).await?,
3676 _ => unreachable!(),
3677 };
3678
3679 let mut knn_node = if q.refine_factor.is_some() {
3680 let vector_projection = self
3681 .dataset
3682 .empty_projection()
3683 .union_column(&q.column, OnMissing::Error)
3684 .unwrap();
3685 let knn_node_with_vector = self.take(ann_node, vector_projection)?;
3686 self.flat_knn(knn_node_with_vector, &q)?
3687 } else {
3688 ann_node
3689 }; if !self.fast_search {
3692 knn_node = self
3693 .knn_combined(&q, &index_name, &index_segments, knn_node, filter_plan)
3694 .await?;
3695 }
3696
3697 Ok(knn_node)
3698 } else {
3699 if self.fast_search {
3700 return Ok(Arc::new(EmptyExec::new(KNN_INDEX_SCHEMA.clone())));
3701 }
3702 let metric = q
3704 .metric_type
3705 .unwrap_or_else(|| default_distance_type_for(&element_type));
3706 q.metric_type = Some(metric);
3707 validate_distance_type_for(metric, &element_type)?;
3708 let mut columns = vec![q.column.clone()];
3710 if let Some(refine_expr) = filter_plan.refine_expr.as_ref() {
3711 columns.extend(Planner::column_names_in_expr(refine_expr));
3712 }
3713 let mut vector_scan_projection = self
3714 .dataset
3715 .empty_projection()
3716 .with_row_id()
3717 .union_columns(&columns, OnMissing::Error)?;
3718
3719 vector_scan_projection.with_row_addr =
3720 self.projection_plan.physical_projection.with_row_addr;
3721
3722 let PlannedFilteredScan { mut plan, .. } = self
3723 .filtered_read(
3724 filter_plan,
3725 vector_scan_projection,
3726 true,
3727 self.fragments.clone().map(Arc::new),
3728 None,
3729 true,
3730 )
3731 .await?;
3732
3733 if let Some(refine_expr) = &filter_plan.refine_expr {
3734 plan = Arc::new(LanceFilterExec::try_new(refine_expr.clone(), plan)?);
3735 }
3736 Ok(self.flat_knn(plan, &q)?)
3737 }
3738 }
3739
3740 async fn knn_combined(
3742 &self,
3743 q: &Query,
3744 index_name: &str,
3745 indexed_segments: &[IndexMetadata],
3746 mut knn_node: Arc<dyn ExecutionPlan>,
3747 filter_plan: &ExprFilterPlan,
3748 ) -> Result<Arc<dyn ExecutionPlan>> {
3749 let fallback_fragments = if let Some(target_fragments) = &self.fragments {
3750 let indexed_fragments = self.get_indexed_frags(indexed_segments);
3751 target_fragments
3752 .iter()
3753 .filter(|fragment| !indexed_fragments.contains(fragment.id as u32))
3754 .cloned()
3755 .collect::<Vec<_>>()
3756 } else if self.index_segments.is_some() {
3757 Vec::new()
3758 } else {
3759 self.dataset.unindexed_fragments(index_name).await?
3760 };
3761
3762 if !fallback_fragments.is_empty() {
3763 let q = q.clone();
3764 debug_assert!(q.metric_type.is_some());
3765
3766 if knn_node.schema().column_with_name(&q.column).is_none() {
3769 let vector_projection = self
3770 .dataset
3771 .empty_projection()
3772 .union_column(&q.column, OnMissing::Error)
3773 .unwrap();
3774 knn_node = self.take(knn_node, vector_projection)?;
3775 }
3776
3777 let mut columns = vec![q.column.clone()];
3778 if let Some(expr) = filter_plan.full_expr.as_ref() {
3779 let filter_columns = Planner::column_names_in_expr(expr);
3780 columns.extend(filter_columns);
3781 }
3782 let vector_scan_projection = Arc::new(self.dataset.schema().project(&columns).unwrap());
3783 let mut scan_node = self.scan_fragments(
3787 true,
3788 false,
3789 false,
3790 false,
3791 false,
3792 vector_scan_projection,
3793 Arc::new(fallback_fragments),
3794 None,
3796 false,
3799 );
3800
3801 if let Some(expr) = filter_plan.full_expr.as_ref() {
3802 scan_node = Arc::new(LanceFilterExec::try_new(expr.clone(), scan_node)?);
3804 }
3805 let topk_appended = self.flat_knn(scan_node, &q)?;
3807
3808 let topk_appended = project(topk_appended, knn_node.schema().as_ref())?;
3812 assert!(
3813 topk_appended
3814 .schema()
3815 .equivalent_names_and_types(&knn_node.schema())
3816 );
3817 let unioned = UnionExec::try_new(vec![Arc::new(topk_appended), knn_node])?;
3819 let unioned = RepartitionExec::try_new(
3821 unioned,
3822 datafusion::physical_plan::Partitioning::RoundRobinBatch(1),
3823 )?;
3824 return self.flat_knn(Arc::new(unioned), &q);
3826 }
3827
3828 Ok(knn_node)
3829 }
3830
3831 #[async_recursion]
3832 async fn fragments_covered_by_index_query(
3833 &self,
3834 index_expr: &ScalarIndexExpr,
3835 ) -> Result<RoaringBitmap> {
3836 match index_expr {
3837 ScalarIndexExpr::And(lhs, rhs) => {
3838 Ok(self.fragments_covered_by_index_query(lhs).await?
3839 & self.fragments_covered_by_index_query(rhs).await?)
3840 }
3841 ScalarIndexExpr::Or(lhs, rhs) => Ok(self.fragments_covered_by_index_query(lhs).await?
3842 & self.fragments_covered_by_index_query(rhs).await?),
3843 ScalarIndexExpr::Not(expr) => self.fragments_covered_by_index_query(expr).await,
3844 ScalarIndexExpr::Query(search) => scalar_index_fragment_bitmap(
3845 self.dataset.as_ref(),
3846 &search.column,
3847 &search.index_name,
3848 )
3849 .await?
3850 .ok_or_else(|| {
3851 crate::Error::internal(format!(
3852 "Index not found even though it must have been found earlier: {}",
3853 search.index_name
3854 ))
3855 }),
3856 }
3857 }
3858
3859 async fn partition_frags_by_coverage(
3867 &self,
3868 index_expr: &ScalarIndexExpr,
3869 fragments: Arc<Vec<Fragment>>,
3870 ) -> Result<(Vec<Fragment>, Vec<Fragment>)> {
3871 let covered_frags = self.fragments_covered_by_index_query(index_expr).await?;
3872 let mut relevant_frags = Vec::with_capacity(fragments.len());
3873 let mut missing_frags = Vec::with_capacity(fragments.len());
3874 for fragment in fragments.iter() {
3875 if covered_frags.contains(fragment.id as u32) {
3876 relevant_frags.push(fragment.clone());
3877 } else {
3878 missing_frags.push(fragment.clone());
3879 }
3880 }
3881 Ok((relevant_frags, missing_frags))
3882 }
3883
3884 async fn scalar_indexed_scan(
3887 &self,
3888 projection: Projection,
3889 filter_plan: &ExprFilterPlan,
3890 fragments: Arc<Vec<Fragment>>,
3891 ) -> Result<Arc<dyn ExecutionPlan>> {
3892 log::trace!("scalar indexed scan");
3893 let index_expr = filter_plan.index_query.as_ref().unwrap();
3900
3901 let needs_recheck = index_expr.needs_recheck();
3902
3903 let (relevant_frags, missing_frags) = self
3905 .partition_frags_by_coverage(index_expr, fragments)
3906 .await?;
3907
3908 let mut plan: Arc<dyn ExecutionPlan> = Arc::new(MaterializeIndexExec::new(
3909 self.dataset.clone(),
3910 index_expr.clone(),
3911 Arc::new(relevant_frags),
3912 ));
3913
3914 let refine_expr = filter_plan.refine_expr.as_ref();
3915
3916 let needs_take =
3919 needs_recheck || projection.has_data_fields() || filter_plan.refine_expr.is_some();
3920 if needs_take {
3921 let mut take_projection = projection.clone();
3922 if needs_recheck {
3923 let filter_expr = index_expr.to_expr();
3925 let filter_cols = Planner::column_names_in_expr(&filter_expr);
3926 take_projection = take_projection.union_columns(filter_cols, OnMissing::Error)?;
3927 }
3928 if let Some(refine_expr) = refine_expr {
3929 let refine_cols = Planner::column_names_in_expr(refine_expr);
3930 take_projection = take_projection.union_columns(refine_cols, OnMissing::Error)?;
3931 }
3932 log::trace!("need to take additional columns for scalar_indexed_scan");
3933 plan = self.take(plan, take_projection)?;
3934 }
3935
3936 let post_take_filter = match (needs_recheck, refine_expr) {
3937 (false, None) => None,
3938 (true, None) => {
3939 Some(index_expr.to_expr())
3941 }
3942 (true, Some(_)) => Some(filter_plan.full_expr.as_ref().unwrap().clone()),
3943 (false, Some(refine_expr)) => Some(refine_expr.clone()),
3944 };
3945
3946 if let Some(post_take_filter) = post_take_filter {
3947 let planner = Planner::new(plan.schema());
3948 let optimized_filter = planner.optimize_expr(post_take_filter)?;
3949
3950 log::trace!("applying post-take filter to indexed scan");
3951 plan = Arc::new(LanceFilterExec::try_new(optimized_filter, plan)?);
3952 }
3953
3954 if self.projection_plan.physical_projection.with_row_addr {
3955 plan = Arc::new(AddRowAddrExec::try_new(plan, self.dataset.clone(), 0)?);
3956 }
3957
3958 let new_data_path: Option<Arc<dyn ExecutionPlan>> = if !missing_frags.is_empty() {
3959 log::trace!(
3960 "scalar_indexed_scan will need full scan of {} missing fragments",
3961 missing_frags.len()
3962 );
3963
3964 let filter = filter_plan.full_expr.as_ref().unwrap();
3977 let filter_cols = Planner::column_names_in_expr(filter);
3978 let scan_projection = projection.union_columns(filter_cols, OnMissing::Error)?;
3979
3980 let scan_schema = Arc::new(scan_projection.to_bare_schema());
3981 let scan_arrow_schema = Arc::new(scan_schema.as_ref().into());
3982 let planner = Planner::new(scan_arrow_schema);
3983 let optimized_filter = planner.optimize_expr(filter.clone())?;
3984
3985 let new_data_scan = self.scan_fragments(
3986 true,
3987 self.projection_plan.physical_projection.with_row_addr,
3988 self.projection_plan
3989 .physical_projection
3990 .with_row_last_updated_at_version,
3991 self.projection_plan
3992 .physical_projection
3993 .with_row_created_at_version,
3994 false,
3995 scan_schema,
3996 missing_frags.into(),
3997 None,
3999 false,
4000 );
4001 let filtered = Arc::new(LanceFilterExec::try_new(optimized_filter, new_data_scan)?);
4002 Some(Arc::new(project(filtered, plan.schema().as_ref())?))
4003 } else {
4004 log::trace!("scalar_indexed_scan will not need full scan of any missing fragments");
4005 None
4006 };
4007
4008 if let Some(new_data_path) = new_data_path {
4009 let unioned = UnionExec::try_new(vec![plan, new_data_path])?;
4010 let unioned = Arc::new(RepartitionExec::try_new(
4012 unioned,
4013 datafusion::physical_plan::Partitioning::RoundRobinBatch(1),
4014 )?);
4015 Ok(unioned)
4016 } else {
4017 Ok(plan)
4018 }
4019 }
4020
4021 fn get_io_buffer_size(&self) -> u64 {
4022 self.io_buffer_size.unwrap_or(*DEFAULT_IO_BUFFER_SIZE)
4023 }
4024
4025 #[allow(clippy::too_many_arguments)]
4030 pub(crate) fn scan(
4031 &self,
4032 with_row_id: bool,
4033 with_row_address: bool,
4034 with_row_last_updated_at_version: bool,
4035 with_row_created_at_version: bool,
4036 with_make_deletions_null: bool,
4037 range: Option<Range<u64>>,
4038 projection: Arc<Schema>,
4039 ) -> Arc<dyn ExecutionPlan> {
4040 let fragments = if let Some(fragment) = self.fragments.as_ref() {
4041 Arc::new(fragment.clone())
4042 } else {
4043 self.dataset.fragments().clone()
4044 };
4045 let ordered = if self.ordering.is_some() || self.nearest.is_some() {
4046 false
4048 } else {
4049 self.ordered
4050 };
4051 self.scan_fragments(
4052 with_row_id,
4053 with_row_address,
4054 with_row_last_updated_at_version,
4055 with_row_created_at_version,
4056 with_make_deletions_null,
4057 projection,
4058 fragments,
4059 range,
4060 ordered,
4061 )
4062 }
4063
4064 #[allow(clippy::too_many_arguments)]
4065 fn scan_fragments(
4066 &self,
4067 with_row_id: bool,
4068 with_row_address: bool,
4069 with_row_last_updated_at_version: bool,
4070 with_row_created_at_version: bool,
4071 with_make_deletions_null: bool,
4072 projection: Arc<Schema>,
4073 fragments: Arc<Vec<Fragment>>,
4074 range: Option<Range<u64>>,
4075 ordered: bool,
4076 ) -> Arc<dyn ExecutionPlan> {
4077 log::trace!("scan_fragments covered {} fragments", fragments.len());
4078 let config = LanceScanConfig {
4079 batch_size: self.get_batch_size(),
4080 batch_readahead: self.batch_readahead,
4081 fragment_readahead: self.fragment_readahead,
4082 io_buffer_size: self.get_io_buffer_size(),
4083 with_row_id,
4084 with_row_address,
4085 with_row_last_updated_at_version,
4086 with_row_created_at_version,
4087 with_make_deletions_null,
4088 ordered_output: ordered,
4089 file_reader_options: self.resolved_file_reader_options(),
4090 };
4091 Arc::new(LanceScanExec::new(
4092 self.dataset.clone(),
4093 fragments,
4094 range,
4095 projection,
4096 config,
4097 ))
4098 }
4099
4100 fn pushdown_scan(
4101 &self,
4102 make_deletions_null: bool,
4103 filter_plan: &ExprFilterPlan,
4104 ) -> Result<Arc<dyn ExecutionPlan>> {
4105 log::trace!("pushdown_scan");
4106
4107 let config = ScanConfig {
4108 batch_readahead: self.batch_readahead,
4109 fragment_readahead: self
4110 .fragment_readahead
4111 .unwrap_or(LEGACY_DEFAULT_FRAGMENT_READAHEAD),
4112 with_row_id: self.projection_plan.physical_projection.with_row_id,
4113 with_row_address: self.projection_plan.physical_projection.with_row_addr,
4114 make_deletions_null,
4115 ordered_output: self.ordered,
4116 file_reader_options: self.resolved_file_reader_options(),
4117 };
4118
4119 let fragments = if let Some(fragment) = self.fragments.as_ref() {
4120 Arc::new(fragment.clone())
4121 } else {
4122 self.dataset.fragments().clone()
4123 };
4124
4125 Ok(Arc::new(LancePushdownScanExec::try_new(
4126 self.dataset.clone(),
4127 fragments,
4128 Arc::new(self.projection_plan.physical_projection.to_bare_schema()),
4129 filter_plan.refine_expr.clone().unwrap(),
4130 config,
4131 )?))
4132 }
4133
4134 async fn flat_fts_filter(
4139 &self,
4140 input: Arc<dyn ExecutionPlan>,
4141 q: &FullTextSearchQuery,
4142 ) -> Result<Arc<dyn ExecutionPlan>> {
4143 let fts_query = if q.columns().is_empty() {
4144 let indexed_columns = fts_indexed_columns(self.dataset.clone()).await?;
4145 fill_fts_query_column(&q.query, &indexed_columns, false)?
4146 } else {
4147 q.query.clone()
4148 };
4149
4150 match &fts_query {
4151 FtsQuery::Match(match_query) => {
4152 let schema = Arc::new((input.schema()).try_with_column(SCORE_FIELD.clone())?);
4153
4154 let column = match_query
4155 .column
4156 .as_ref()
4157 .ok_or(Error::invalid_input(
4158 "the column must be specified in the query".to_string(),
4159 ))?
4160 .clone();
4161 let input = if schema.column_with_name(&column).is_none() {
4162 let projection = self
4163 .dataset
4164 .empty_projection()
4165 .union_column(&column, OnMissing::Error)?;
4166 self.take(input, projection)?
4167 } else {
4168 input
4169 };
4170
4171 Ok(Arc::new(FlatMatchFilterExec::new(
4172 input,
4173 self.dataset.clone(),
4174 match_query.clone(),
4175 q.params(),
4176 )))
4177 }
4178 _ => Err(Error::not_supported(
4179 "Only Match queries are supported currently when using FTS as a post-filter",
4180 )),
4181 }
4182 }
4183
4184 async fn fts_rerank(
4189 &self,
4190 input: Arc<dyn ExecutionPlan>,
4191 q: &FullTextSearchQuery,
4192 ) -> Result<Arc<dyn ExecutionPlan>> {
4193 let fts_query = if q.columns().is_empty() {
4194 let indexed_columns = fts_indexed_columns(self.dataset.clone()).await?;
4195 fill_fts_query_column(&q.query, &indexed_columns, false)?
4196 } else {
4197 q.query.clone()
4198 };
4199
4200 match &fts_query {
4201 FtsQuery::Match(match_query) => {
4202 let schema = Arc::new((input.schema()).try_with_column(SCORE_FIELD.clone())?);
4203
4204 let column = match_query
4205 .column
4206 .as_ref()
4207 .ok_or(Error::invalid_input(
4208 "the column must be specified in the query".to_string(),
4209 ))?
4210 .clone();
4211 let input = if schema.column_with_name(&column).is_none() {
4212 let projection = self
4213 .dataset
4214 .empty_projection()
4215 .union_column(&column, OnMissing::Error)?;
4216 self.take(input, projection)?
4217 } else {
4218 input
4219 };
4220
4221 Ok(Arc::new(FlatMatchQueryExec::new(
4222 self.dataset.clone(),
4223 match_query.clone(),
4224 q.params(),
4225 input,
4226 )))
4227 }
4228 _ => {
4229 let default_filter = ExprFilterPlan::default();
4230 let fts_plan = self.fts(&default_filter, q).await?;
4231
4232 let vector_row_id = Column::new_with_schema(ROW_ID, input.schema().as_ref())?;
4233 let fts_row_id = Column::new_with_schema(ROW_ID, fts_plan.schema().as_ref())?;
4234 let join = HashJoinExec::try_new(
4235 input,
4236 fts_plan,
4237 vec![(Arc::new(vector_row_id), Arc::new(fts_row_id))],
4238 None,
4239 &JoinType::Inner,
4240 None,
4241 PartitionMode::CollectLeft,
4242 NullEquality::NullEqualsNull,
4243 false,
4244 )?;
4245
4246 let schema = join.schema();
4247 let mut projection_exprs = Vec::new();
4248 let mut contain_rowid = false;
4249 for field in schema.fields() {
4250 if field.name() == ROW_ID {
4251 if contain_rowid {
4252 continue;
4253 }
4254 contain_rowid = true;
4255 }
4256 projection_exprs.push((
4257 Arc::new(Column::new_with_schema(field.name(), schema.as_ref())?)
4258 as Arc<dyn PhysicalExpr>,
4259 field.name().clone(),
4260 ));
4261 }
4262
4263 let projection_exec = ProjectionExec::try_new(projection_exprs, Arc::new(join))?;
4264 Ok(Arc::new(projection_exec))
4265 }
4266 }
4267 }
4268
4269 fn flat_knn(&self, input: Arc<dyn ExecutionPlan>, q: &Query) -> Result<Arc<dyn ExecutionPlan>> {
4271 let metric_type = match q.metric_type {
4273 Some(m) => m,
4274 None => {
4275 let (_, element_type) = get_vector_type(self.dataset.schema(), &q.column)?;
4276 default_distance_type_for(&element_type)
4277 }
4278 };
4279 let flat_dist = Arc::new(KNNVectorDistanceExec::try_new(
4280 input,
4281 &q.column,
4282 q.key.clone(),
4283 metric_type,
4284 )?);
4285
4286 let lower: Option<(Expr, Arc<dyn PhysicalExpr>)> = q
4287 .lower_bound
4288 .map(|v| -> Result<(Expr, Arc<dyn PhysicalExpr>)> {
4289 let logical = col(DIST_COL).gt_eq(lit(v));
4290 let schema = flat_dist.schema();
4291 let df_schema = DFSchema::try_from(schema)?;
4292 let physical = create_physical_expr(&logical, &df_schema, &ExecutionProps::new())?;
4293 Ok::<(Expr, Arc<dyn PhysicalExpr>), _>((logical, physical))
4294 })
4295 .transpose()?;
4296
4297 let upper = q
4298 .upper_bound
4299 .map(|v| -> Result<(Expr, Arc<dyn PhysicalExpr>)> {
4300 let logical = col(DIST_COL).lt(lit(v));
4301 let schema = flat_dist.schema();
4302 let df_schema = DFSchema::try_from(schema)?;
4303 let physical = create_physical_expr(&logical, &df_schema, &ExecutionProps::new())?;
4304 Ok::<(Expr, Arc<dyn PhysicalExpr>), _>((logical, physical))
4305 })
4306 .transpose()?;
4307
4308 let filter_expr = match (lower, upper) {
4309 (Some((llog, _)), Some((ulog, _))) => {
4310 let logical = llog.and(ulog);
4311 let schema = flat_dist.schema();
4312 let df_schema = DFSchema::try_from(schema)?;
4313 let physical = create_physical_expr(&logical, &df_schema, &ExecutionProps::new())?;
4314 Some((logical, physical))
4315 }
4316 (Some((llog, lphys)), None) => Some((llog, lphys)),
4317 (None, Some((ulog, uphys))) => Some((ulog, uphys)),
4318 (None, None) => None,
4319 };
4320
4321 let knn_plan: Arc<dyn ExecutionPlan> = if let Some(filter_expr) = filter_expr {
4322 Arc::new(LanceFilterExec::try_new(filter_expr.0, flat_dist)?)
4323 } else {
4324 flat_dist
4325 };
4326
4327 let sort = SortExec::new(
4329 [
4330 PhysicalSortExpr {
4331 expr: expressions::col(DIST_COL, knn_plan.schema().as_ref())?,
4332 options: SortOptions {
4333 descending: false,
4334 nulls_first: false,
4335 },
4336 },
4337 PhysicalSortExpr {
4338 expr: expressions::col(ROW_ID, knn_plan.schema().as_ref())?,
4339 options: SortOptions {
4340 descending: false,
4341 nulls_first: false,
4342 },
4343 },
4344 ]
4345 .into(),
4346 knn_plan,
4347 )
4348 .with_fetch(Some(q.k));
4349
4350 let logical_not_null = col(DIST_COL).is_not_null();
4351 let not_nulls = Arc::new(LanceFilterExec::try_new(logical_not_null, Arc::new(sort))?);
4352
4353 Ok(not_nulls)
4354 }
4355
4356 fn get_fragments_as_bitmap(&self) -> RoaringBitmap {
4357 if let Some(fragments) = &self.fragments {
4358 RoaringBitmap::from_iter(fragments.iter().map(|f| f.id as u32))
4359 } else {
4360 self.dataset.fragment_bitmap.as_ref().clone()
4361 }
4362 }
4363
4364 fn retain_relevant_index_segments(
4365 &self,
4366 index_segments: Vec<IndexMetadata>,
4367 ) -> Vec<IndexMetadata> {
4368 if let Some(fragments) = &self.fragments {
4369 let target_fragments = RoaringBitmap::from_iter(fragments.iter().map(|f| f.id as u32));
4370 index_segments
4371 .into_iter()
4372 .filter(|idx| {
4373 idx.fragment_bitmap
4374 .as_ref()
4375 .is_some_and(|fragmap| !(fragmap & &target_fragments).is_empty())
4376 })
4377 .collect()
4378 } else {
4379 index_segments
4380 }
4381 }
4382
4383 fn retain_target_fragments(&self, mut fragments: Vec<Fragment>) -> Vec<Fragment> {
4386 if let Some(target) = &self.fragments {
4387 let bitmap = RoaringBitmap::from_iter(target.iter().map(|f| f.id as u32));
4388 fragments.retain(|f| bitmap.contains(f.id as u32));
4389 }
4390 fragments
4391 }
4392
4393 fn get_indexed_frags(&self, index: &[IndexMetadata]) -> RoaringBitmap {
4394 let all_fragments = self.get_fragments_as_bitmap();
4395
4396 let mut all_indexed_frags = RoaringBitmap::new();
4397 for idx in index {
4398 if let Some(fragmap) = idx.fragment_bitmap.as_ref() {
4399 all_indexed_frags |= fragmap;
4400 } else {
4401 return all_fragments;
4404 }
4405 }
4406
4407 all_indexed_frags & all_fragments
4408 }
4409
4410 async fn ann(
4412 &self,
4413 q: &Query,
4414 index: &[IndexMetadata],
4415 filter_plan: &ExprFilterPlan,
4416 ) -> Result<Arc<dyn ExecutionPlan>> {
4417 let prefilter_source = self
4418 .prefilter_source(filter_plan, self.get_indexed_frags(index))
4419 .await?;
4420 let inner_fanout_search = new_knn_exec(self.dataset.clone(), index, q, prefilter_source)?;
4421 let sort_expr = PhysicalSortExpr {
4422 expr: expressions::col(DIST_COL, inner_fanout_search.schema().as_ref())?,
4423 options: SortOptions {
4424 descending: false,
4425 nulls_first: false,
4426 },
4427 };
4428 let sort_expr_row_id = PhysicalSortExpr {
4429 expr: expressions::col(ROW_ID, inner_fanout_search.schema().as_ref())?,
4430 options: SortOptions {
4431 descending: false,
4432 nulls_first: false,
4433 },
4434 };
4435 Ok(Arc::new(
4436 SortExec::new([sort_expr, sort_expr_row_id].into(), inner_fanout_search)
4437 .with_fetch(Some(q.k * q.refine_factor.unwrap_or(1) as usize)),
4438 ))
4439 }
4440
4441 async fn multivec_ann(
4443 &self,
4444 q: &Query,
4445 index: &[IndexMetadata],
4446 filter_plan: &ExprFilterPlan,
4447 ) -> Result<Arc<dyn ExecutionPlan>> {
4448 let over_fetch_factor = *DEFAULT_XTR_OVERFETCH;
4453
4454 let prefilter_source = self
4455 .prefilter_source(filter_plan, self.get_indexed_frags(index))
4456 .await?;
4457 let dim = get_vector_dim(self.dataset.schema(), &q.column)?;
4458
4459 let num_queries = q.key.len() / dim;
4460 let new_queries = (0..num_queries)
4461 .map(|i| q.key.slice(i * dim, dim))
4462 .map(|query_vec| {
4463 let mut new_query = q.clone();
4464 new_query.key = query_vec;
4465 new_query.refine_factor = Some(over_fetch_factor);
4469 new_query
4470 });
4471 let mut ann_nodes = Vec::with_capacity(new_queries.len());
4472 for query in new_queries {
4473 let ann_node = new_knn_exec(
4475 self.dataset.clone(),
4476 index,
4477 &query,
4478 prefilter_source.clone(),
4479 )?;
4480 let sort_expr = PhysicalSortExpr {
4481 expr: expressions::col(DIST_COL, ann_node.schema().as_ref())?,
4482 options: SortOptions {
4483 descending: false,
4484 nulls_first: false,
4485 },
4486 };
4487 let sort_expr_row_id = PhysicalSortExpr {
4488 expr: expressions::col(ROW_ID, ann_node.schema().as_ref())?,
4489 options: SortOptions {
4490 descending: false,
4491 nulls_first: false,
4492 },
4493 };
4494 let ann_node = Arc::new(
4495 SortExec::new([sort_expr, sort_expr_row_id].into(), ann_node)
4496 .with_fetch(Some(q.k * over_fetch_factor as usize)),
4497 );
4498 ann_nodes.push(ann_node as Arc<dyn ExecutionPlan>);
4499 }
4500
4501 let ann_node = Arc::new(MultivectorScoringExec::try_new(ann_nodes, q.clone())?);
4502
4503 let sort_expr = PhysicalSortExpr {
4504 expr: expressions::col(DIST_COL, ann_node.schema().as_ref())?,
4505 options: SortOptions {
4506 descending: false,
4507 nulls_first: false,
4508 },
4509 };
4510 let sort_expr_row_id = PhysicalSortExpr {
4511 expr: expressions::col(ROW_ID, ann_node.schema().as_ref())?,
4512 options: SortOptions {
4513 descending: false,
4514 nulls_first: false,
4515 },
4516 };
4517 let ann_node = Arc::new(
4518 SortExec::new([sort_expr, sort_expr_row_id].into(), ann_node)
4519 .with_fetch(Some(q.k * q.refine_factor.unwrap_or(1) as usize)),
4520 );
4521
4522 Ok(ann_node)
4523 }
4524
4525 async fn prefilter_source(
4530 &self,
4531 filter_plan: &ExprFilterPlan,
4532 required_frags: RoaringBitmap,
4533 ) -> Result<PreFilterSource> {
4534 if filter_plan.is_empty() && self.fragments.is_none() {
4535 log::trace!("no filter plan, no prefilter");
4536 return Ok(PreFilterSource::None);
4537 }
4538
4539 let fragments: Vec<Fragment> = self
4541 .dataset
4542 .manifest
4543 .fragments
4544 .iter()
4545 .filter(|f| required_frags.contains(f.id as u32))
4546 .cloned()
4547 .collect();
4548
4549 let fragments = Arc::new(self.retain_target_fragments(fragments));
4551
4552 if filter_plan.is_exact_index_search() && self.fragments.is_none() {
4558 let index_query = filter_plan.index_query.as_ref().expect_ok()?;
4559 let (_, missing_frags) = self
4560 .partition_frags_by_coverage(index_query, fragments.clone())
4561 .await?;
4562
4563 if missing_frags.is_empty() {
4564 log::trace!("prefilter entirely satisfied by exact index search");
4565 return Ok(PreFilterSource::ScalarIndexQuery(Arc::new(
4570 ScalarIndexExec::new(self.dataset.clone(), index_query.clone()),
4571 )));
4572 } else {
4573 log::trace!("exact index search did not cover all fragments");
4574 }
4575 }
4576
4577 log::trace!(
4579 "prefilter is a filtered read of {} fragments",
4580 fragments.len()
4581 );
4582 let PlannedFilteredScan { plan, .. } = self
4583 .filtered_read(
4584 filter_plan,
4585 self.dataset.empty_projection().with_row_id(),
4586 false,
4587 Some(fragments),
4588 None,
4589 true,
4590 )
4591 .await?;
4592 Ok(PreFilterSource::FilteredRowIds(plan))
4593 }
4594
4595 #[allow(deprecated)]
4597 fn take(
4598 &self,
4599 input: Arc<dyn ExecutionPlan>,
4600 output_projection: Projection,
4601 ) -> Result<Arc<dyn ExecutionPlan>> {
4602 let coalesced = Arc::new(CoalesceBatchesExec::new(
4603 input.clone(),
4604 self.get_batch_size(),
4605 ));
4606 if let Some(take_plan) =
4607 TakeExec::try_new(self.dataset.clone(), coalesced, output_projection)?
4608 {
4609 Ok(Arc::new(take_plan))
4610 } else {
4611 Ok(input)
4613 }
4614 }
4615
4616 fn limit_node(&self, plan: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
4618 Arc::new(GlobalLimitExec::new(
4619 plan,
4620 *self.offset.as_ref().unwrap_or(&0) as usize,
4621 self.limit.map(|l| l as usize),
4622 ))
4623 }
4624
4625 #[instrument(level = "info", skip(self))]
4626 pub async fn analyze_plan(&self) -> Result<String> {
4627 let plan = self.create_plan().await?;
4628 analyze_plan(
4629 plan,
4630 LanceExecutionOptions {
4631 batch_size: self.batch_size,
4632 ..Default::default()
4633 },
4634 )
4635 .await
4636 }
4637
4638 #[instrument(level = "info", skip(self))]
4639 pub async fn explain_plan(&self, verbose: bool) -> Result<String> {
4640 let plan = self.create_plan().await?;
4641 let display = DisplayableExecutionPlan::new(plan.as_ref());
4642
4643 Ok(format!("{}", display.indent(verbose)))
4644 }
4645}
4646
4647async fn fts_indexed_columns(dataset: Arc<Dataset>) -> Result<Vec<String>> {
4650 let mut indexed_columns = Vec::new();
4651 for field in dataset.schema().fields_pre_order() {
4652 let is_string_field = match field.data_type() {
4654 DataType::Utf8 | DataType::LargeUtf8 => true,
4655 DataType::List(inner_field) | DataType::LargeList(inner_field) => {
4656 matches!(
4657 inner_field.data_type(),
4658 DataType::Utf8 | DataType::LargeUtf8
4659 )
4660 }
4661 _ => false,
4662 };
4663
4664 if is_string_field {
4665 let column_path =
4667 if let Some(ancestors) = dataset.schema().field_ancestry_by_id(field.id) {
4668 let field_refs: Vec<&str> = ancestors.iter().map(|f| f.name.as_str()).collect();
4669 format_field_path(&field_refs)
4670 } else {
4671 continue; };
4673
4674 let has_fts_index = dataset
4676 .load_scalar_index(
4677 IndexCriteria::default()
4678 .for_column(&column_path)
4679 .supports_fts(),
4680 )
4681 .await?
4682 .is_some();
4683
4684 if has_fts_index {
4685 indexed_columns.push(column_path);
4686 }
4687 }
4688 }
4689 Ok(indexed_columns)
4690}
4691
4692#[pin_project::pin_project]
4696pub struct DatasetRecordBatchStream {
4697 #[pin]
4698 exec_node: SendableRecordBatchStream,
4699 span: Span,
4700}
4701
4702impl DatasetRecordBatchStream {
4703 pub fn new(exec_node: SendableRecordBatchStream) -> Self {
4704 let schema = exec_node.schema();
4705 let adapter = SchemaAdapter::new(schema.clone());
4706 let exec_node = if SchemaAdapter::requires_logical_conversion(&schema) {
4707 adapter.to_logical_stream(exec_node)
4708 } else {
4709 exec_node
4710 };
4711
4712 let span = info_span!("DatasetRecordBatchStream");
4713 Self { exec_node, span }
4714 }
4715}
4716
4717impl RecordBatchStream for DatasetRecordBatchStream {
4718 fn schema(&self) -> SchemaRef {
4719 self.exec_node.schema()
4720 }
4721}
4722
4723impl Stream for DatasetRecordBatchStream {
4724 type Item = Result<RecordBatch>;
4725
4726 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
4727 let mut this = self.project();
4728 let _guard = this.span.enter();
4729 match this.exec_node.poll_next_unpin(cx) {
4730 Poll::Ready(result) => Poll::Ready(result.map(|r| Ok(r?))),
4731 Poll::Pending => Poll::Pending,
4732 }
4733 }
4734}
4735
4736impl From<DatasetRecordBatchStream> for SendableRecordBatchStream {
4737 fn from(stream: DatasetRecordBatchStream) -> Self {
4738 stream.exec_node
4739 }
4740}
4741
4742#[cfg(test)]
4743pub mod test_dataset {
4744
4745 use super::*;
4746
4747 use std::{collections::HashMap, vec};
4748
4749 use arrow_array::{
4750 ArrayRef, FixedSizeListArray, Int32Array, RecordBatch, RecordBatchIterator, StringArray,
4751 types::Float32Type,
4752 };
4753 use arrow_schema::{ArrowError, DataType};
4754 use lance_arrow::FixedSizeListArrayExt;
4755 use lance_core::utils::tempfile::TempStrDir;
4756 use lance_file::version::LanceFileVersion;
4757 use lance_index::{
4758 IndexType,
4759 scalar::{ScalarIndexParams, inverted::tokenizer::InvertedIndexParams},
4760 vector::{
4761 ivf::IvfBuildParams,
4762 kmeans::{KMeansParams, train_kmeans},
4763 },
4764 };
4765 use lance_linalg::distance::DistanceType;
4766 use uuid::Uuid;
4767
4768 use crate::dataset::WriteParams;
4769 use crate::index::vector::VectorIndexParams;
4770
4771 pub struct TestVectorDataset {
4781 pub tmp_dir: TempStrDir,
4782 pub schema: Arc<ArrowSchema>,
4783 pub dataset: Dataset,
4784 dimension: u32,
4785 }
4786
4787 impl TestVectorDataset {
4788 pub async fn new(
4789 data_storage_version: LanceFileVersion,
4790 stable_row_ids: bool,
4791 ) -> Result<Self> {
4792 Self::new_with_dimension(data_storage_version, stable_row_ids, 32).await
4793 }
4794
4795 pub async fn new_with_dimension(
4796 data_storage_version: LanceFileVersion,
4797 stable_row_ids: bool,
4798 dimension: u32,
4799 ) -> Result<Self> {
4800 let path = TempStrDir::default();
4801
4802 let metadata: HashMap<String, String> =
4804 vec![("dataset".to_string(), "vector".to_string())]
4805 .into_iter()
4806 .collect();
4807
4808 let schema = Arc::new(ArrowSchema::new_with_metadata(
4809 vec![
4810 ArrowField::new("i", DataType::Int32, true),
4811 ArrowField::new("s", DataType::Utf8, true),
4812 ArrowField::new(
4813 "vec",
4814 DataType::FixedSizeList(
4815 Arc::new(ArrowField::new("item", DataType::Float32, true)),
4816 dimension as i32,
4817 ),
4818 true,
4819 ),
4820 ],
4821 metadata,
4822 ));
4823
4824 let batches: Vec<RecordBatch> = (0..5)
4825 .map(|i| {
4826 let vector_values: Float32Array =
4827 (0..dimension * 80).map(|v| v as f32).collect();
4828 let vectors =
4829 FixedSizeListArray::try_new_from_values(vector_values, dimension as i32)
4830 .unwrap();
4831 RecordBatch::try_new(
4832 schema.clone(),
4833 vec![
4834 Arc::new(Int32Array::from_iter_values(i * 80..(i + 1) * 80)),
4835 Arc::new(StringArray::from_iter_values(
4836 (i * 80..(i + 1) * 80).map(|v| format!("s-{}", v)),
4837 )),
4838 Arc::new(vectors),
4839 ],
4840 )
4841 })
4842 .collect::<std::result::Result<Vec<_>, ArrowError>>()?;
4843
4844 let params = WriteParams {
4845 max_rows_per_group: 10,
4846 max_rows_per_file: 200,
4847 data_storage_version: Some(data_storage_version),
4848 enable_stable_row_ids: stable_row_ids,
4849 ..Default::default()
4850 };
4851 let reader = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
4852
4853 let dataset = Dataset::write(reader, &path, Some(params)).await?;
4854
4855 Ok(Self {
4856 tmp_dir: path,
4857 schema,
4858 dataset,
4859 dimension,
4860 })
4861 }
4862
4863 pub async fn make_vector_index(&mut self) -> Result<()> {
4864 let params = VectorIndexParams::ivf_pq(2, 8, 2, MetricType::L2, 2);
4865 self.dataset
4866 .create_index(
4867 &["vec"],
4868 IndexType::Vector,
4869 Some("idx".to_string()),
4870 ¶ms,
4871 true,
4872 )
4873 .await?;
4874 Ok(())
4875 }
4876
4877 pub async fn make_segmented_vector_index(&mut self) -> Result<Vec<Uuid>> {
4878 let batch = self
4879 .dataset
4880 .scan()
4881 .project(&["vec"])
4882 .unwrap()
4883 .try_into_batch()
4884 .await?;
4885 let vectors = batch
4886 .column_by_name("vec")
4887 .expect("vector column should exist")
4888 .as_fixed_size_list();
4889 let values = vectors.values().as_primitive::<Float32Type>();
4890 let centroids = train_kmeans::<Float32Type>(
4891 values,
4892 KMeansParams::new(None, 10, 1, DistanceType::L2),
4893 self.dimension as usize,
4894 2,
4895 2,
4896 )
4897 .unwrap()
4898 .centroids
4899 .as_primitive::<Float32Type>()
4900 .clone();
4901 let centroids = Arc::new(
4902 FixedSizeListArray::try_new_from_values(centroids, self.dimension as i32).unwrap(),
4903 );
4904 let params = VectorIndexParams::with_ivf_flat_params(
4905 DistanceType::L2,
4906 IvfBuildParams::try_with_centroids(2, centroids).unwrap(),
4907 );
4908 let fragment_ids = self
4909 .dataset
4910 .get_fragments()
4911 .iter()
4912 .map(|fragment| fragment.id() as u32)
4913 .collect::<Vec<_>>();
4914
4915 let mut segments = Vec::with_capacity(fragment_ids.len());
4916 for fragment_id in fragment_ids {
4917 let mut builder =
4918 self.dataset
4919 .create_index_builder(&["vec"], IndexType::Vector, ¶ms);
4920 builder = builder.name("idx".to_string()).fragments(vec![fragment_id]);
4921 segments.push(builder.execute_uncommitted().await?);
4922 }
4923
4924 let segment_ids = segments
4925 .iter()
4926 .map(|segment| segment.uuid)
4927 .collect::<Vec<_>>();
4928 let segments = self
4929 .dataset
4930 .create_index_segment_builder()
4931 .with_index_type(params.index_type())
4932 .with_segments(segments)
4933 .build_all()
4934 .await?;
4935 self.dataset
4936 .commit_existing_index_segments("idx", "vec", segments)
4937 .await?;
4938 Ok(segment_ids)
4939 }
4940
4941 pub async fn make_scalar_index(&mut self) -> Result<()> {
4942 self.dataset
4943 .create_index(
4944 &["i"],
4945 IndexType::Scalar,
4946 None,
4947 &ScalarIndexParams::default(),
4948 true,
4949 )
4950 .await?;
4951 Ok(())
4952 }
4953
4954 pub async fn make_fts_index(&mut self) -> Result<()> {
4955 let params = InvertedIndexParams::default().with_position(true);
4956 self.dataset
4957 .create_index(&["s"], IndexType::Inverted, None, ¶ms, true)
4958 .await?;
4959 Ok(())
4960 }
4961
4962 pub async fn append_new_data(&mut self) -> Result<()> {
4963 self.append_data_with_range(400, 410).await
4964 }
4965
4966 pub async fn append_data_with_range(&mut self, start: i32, end: i32) -> Result<()> {
4967 let count = (end - start) as usize;
4968 let vector_values: Float32Array = (0..count)
4969 .flat_map(|i| vec![i as f32; self.dimension as usize].into_iter())
4970 .collect();
4971 let new_vectors =
4972 FixedSizeListArray::try_new_from_values(vector_values, self.dimension as i32)
4973 .unwrap();
4974 let new_data: Vec<ArrayRef> = vec![
4975 Arc::new(Int32Array::from_iter_values(start..end)),
4976 Arc::new(StringArray::from_iter_values(
4977 (start..end).map(|v| format!("s-{}", v)),
4978 )),
4979 Arc::new(new_vectors),
4980 ];
4981 let reader = RecordBatchIterator::new(
4982 vec![RecordBatch::try_new(self.schema.clone(), new_data).unwrap()]
4983 .into_iter()
4984 .map(Ok),
4985 self.schema.clone(),
4986 );
4987 self.dataset.append(reader, None).await?;
4988 Ok(())
4989 }
4990 }
4991}
4992
4993#[cfg(test)]
4994mod test {
4995
4996 use std::collections::BTreeSet;
4997 use std::time::{Duration, Instant};
4998 use std::vec;
4999
5000 use arrow::array::as_primitive_array;
5001 use arrow::datatypes::{Float64Type, Int32Type, Int64Type};
5002 use arrow_array::cast::AsArray;
5003 use arrow_array::types::{Float32Type, UInt64Type};
5004 use arrow_array::{
5005 ArrayRef, FixedSizeListArray, Float16Array, Int32Array, LargeStringArray, PrimitiveArray,
5006 RecordBatchIterator, StringArray, StructArray, UInt8Array,
5007 };
5008
5009 use arrow_ord::sort::sort_to_indices;
5010 use arrow_schema::Fields;
5011 use arrow_select::take;
5012 use datafusion::logical_expr::{col, lit};
5013 use half::f16;
5014 use lance_arrow::{FixedSizeListArrayExt, SchemaExt};
5015 use lance_core::utils::tempfile::TempStrDir;
5016 use lance_core::{ROW_CREATED_AT_VERSION, ROW_LAST_UPDATED_AT_VERSION};
5017 use lance_datagen::{
5018 ArrayGeneratorExt, BatchCount, ByteCount, Dimension, RowCount, array, gen_batch,
5019 };
5020 use lance_file::version::LanceFileVersion;
5021 use lance_index::optimize::OptimizeOptions;
5022 use lance_index::scalar::inverted::query::{MatchQuery, PhraseQuery};
5023 use lance_index::vector::hnsw::builder::HnswBuildParams;
5024 use lance_index::vector::ivf::IvfBuildParams;
5025 use lance_index::vector::pq::PQBuildParams;
5026 use lance_index::vector::sq::builder::SQBuildParams;
5027 use lance_index::{IndexType, scalar::ScalarIndexParams};
5028 use lance_io::assert_io_gt;
5029 use lance_io::object_store::ObjectStoreParams;
5030
5031 use lance_linalg::distance::DistanceType;
5032 use lance_testing::datagen::{BatchGenerator, IncrementingInt32, RandomVector};
5033 use object_store::throttle::ThrottleConfig;
5034 use rstest::rstest;
5035
5036 use super::*;
5037 use crate::dataset::WriteMode;
5038 use crate::dataset::WriteParams;
5039 use crate::dataset::optimize::{CompactionOptions, compact_files};
5040 use crate::dataset::scanner::test_dataset::TestVectorDataset;
5041 use crate::index::vector::{StageParams, VectorIndexParams};
5042 use crate::utils::test::{
5043 DatagenExt, FragmentCount, FragmentRowCount, ThrottledStoreWrapper, assert_plan_node_equals,
5044 };
5045
5046 #[test]
5047 fn test_env_var_parsing() {
5048 unsafe {
5052 std::env::set_var("LANCE_DEFAULT_BATCH_SIZE", "not_a_number");
5053 }
5054 let result = get_default_batch_size();
5055 assert_eq!(result, None, "Should return None for invalid batch size");
5056
5057 unsafe {
5059 std::env::set_var("LANCE_DEFAULT_BATCH_SIZE", "2048");
5060 }
5061 let result = get_default_batch_size();
5062 assert_eq!(result, Some(2048), "Should parse valid batch size");
5063
5064 unsafe {
5066 std::env::remove_var("LANCE_DEFAULT_BATCH_SIZE");
5067 }
5068 let result = get_default_batch_size();
5069 assert_eq!(result, None, "Should return None when env var is not set");
5070 }
5071
5072 #[test]
5073 fn test_parse_env_var() {
5074 let test_var = "LANCE_TEST_PARSE_ENV_VAR_USIZE";
5078
5079 unsafe {
5081 std::env::set_var(test_var, "12345");
5082 }
5083 let result: Option<usize> = parse_env_var(test_var, "Using default.");
5084 assert_eq!(result, Some(12345));
5085
5086 unsafe {
5088 std::env::set_var(test_var, "not_a_number");
5089 }
5090 let result: Option<usize> = parse_env_var(test_var, "Using default.");
5091 assert_eq!(result, None);
5092
5093 unsafe {
5095 std::env::remove_var(test_var);
5096 }
5097 let result: Option<usize> = parse_env_var(test_var, "Using default.");
5098 assert_eq!(result, None);
5099
5100 let test_var_u32 = "LANCE_TEST_PARSE_ENV_VAR_U32";
5102 unsafe {
5103 std::env::set_var(test_var_u32, "42");
5104 }
5105 let result: Option<u32> = parse_env_var(test_var_u32, "Using default value.");
5106 assert_eq!(result, Some(42));
5107
5108 unsafe {
5109 std::env::set_var(test_var_u32, "invalid");
5110 }
5111 let result: Option<u32> = parse_env_var(test_var_u32, "Using default value.");
5112 assert_eq!(result, None);
5113
5114 unsafe {
5115 std::env::remove_var(test_var_u32);
5116 }
5117
5118 let test_var_u64 = "LANCE_TEST_PARSE_ENV_VAR_U64";
5120 unsafe {
5121 std::env::set_var(test_var_u64, "9999999999");
5122 }
5123 let result: Option<u64> = parse_env_var(test_var_u64, "Using default value.");
5124 assert_eq!(result, Some(9999999999));
5125
5126 unsafe {
5127 std::env::set_var(test_var_u64, "-1");
5128 }
5129 let result: Option<u64> = parse_env_var(test_var_u64, "Using default value.");
5130 assert_eq!(result, None);
5131
5132 unsafe {
5133 std::env::remove_var(test_var_u64);
5134 }
5135 }
5136
5137 async fn make_binary_vector_dataset() -> Result<(TempStrDir, Dataset)> {
5138 let tmp_dir = TempStrDir::default();
5139 let dim = 4;
5140 let schema = Arc::new(ArrowSchema::new(vec![
5141 ArrowField::new("id", DataType::Int32, false),
5142 ArrowField::new(
5143 "bin",
5144 DataType::FixedSizeList(
5145 Arc::new(ArrowField::new("item", DataType::UInt8, true)),
5146 dim,
5147 ),
5148 false,
5149 ),
5150 ]));
5151
5152 let vectors = FixedSizeListArray::try_new_from_values(
5153 UInt8Array::from(vec![
5154 0b0000_1111u8,
5155 0,
5156 0,
5157 0, 0b0000_0011u8,
5159 0,
5160 0,
5161 0, 0u8,
5163 0,
5164 0,
5165 0,
5166 ]),
5167 dim,
5168 )?;
5169 let ids = Int32Array::from(vec![0, 1, 2]);
5170
5171 let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(ids), Arc::new(vectors)])?;
5172 let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
5173 Dataset::write(reader, &tmp_dir, None).await?;
5174 let dataset = Dataset::open(&tmp_dir).await?;
5175 Ok((tmp_dir, dataset))
5176 }
5177
5178 #[tokio::test]
5179 async fn test_batch_size() {
5180 let schema = Arc::new(ArrowSchema::new(vec![
5181 ArrowField::new("i", DataType::Int32, true),
5182 ArrowField::new("s", DataType::Utf8, true),
5183 ]));
5184
5185 let batches: Vec<RecordBatch> = (0..5)
5186 .map(|i| {
5187 RecordBatch::try_new(
5188 schema.clone(),
5189 vec![
5190 Arc::new(Int32Array::from_iter_values(i * 20..(i + 1) * 20)),
5191 Arc::new(StringArray::from_iter_values(
5192 (i * 20..(i + 1) * 20).map(|v| format!("s-{}", v)),
5193 )),
5194 ],
5195 )
5196 .unwrap()
5197 })
5198 .collect();
5199
5200 for use_filter in [false, true] {
5201 let test_dir = TempStrDir::default();
5202 let test_uri = &test_dir;
5203 let write_params = WriteParams {
5204 max_rows_per_file: 40,
5205 max_rows_per_group: 10,
5206 ..Default::default()
5207 };
5208 let batches =
5209 RecordBatchIterator::new(batches.clone().into_iter().map(Ok), schema.clone());
5210 Dataset::write(batches, test_uri, Some(write_params))
5211 .await
5212 .unwrap();
5213
5214 let dataset = Dataset::open(test_uri).await.unwrap();
5215 let mut builder = dataset.scan();
5216 builder.batch_size(8);
5217 if use_filter {
5218 builder.filter("i IS NOT NULL").unwrap();
5219 }
5220 let mut stream = builder.try_into_stream().await.unwrap();
5221 let mut rows_read = 0;
5222 while let Some(next) = stream.next().await {
5223 let next = next.unwrap();
5224 let expected = 8.min(100 - rows_read);
5225 assert_eq!(next.num_rows(), expected);
5226 rows_read += next.num_rows();
5227 }
5228 }
5229 }
5230
5231 #[tokio::test]
5232 async fn test_strict_batch_size() {
5233 let dataset = lance_datagen::gen_batch()
5234 .col("x", array::step::<Int32Type>())
5235 .anon_col(array::step::<Int64Type>())
5236 .into_ram_dataset(FragmentCount::from(7), FragmentRowCount::from(6))
5237 .await
5238 .unwrap();
5239
5240 let mut scan = dataset.scan();
5241 scan.batch_size(10)
5242 .strict_batch_size(true)
5243 .filter("x % 2 == 0")
5244 .unwrap();
5245
5246 let batches = scan
5247 .try_into_stream()
5248 .await
5249 .unwrap()
5250 .try_collect::<Vec<_>>()
5251 .await
5252 .unwrap();
5253
5254 let batch_sizes = batches.iter().map(|b| b.num_rows()).collect::<Vec<_>>();
5255 assert_eq!(batch_sizes, vec![10, 10, 1]);
5256 }
5257
5258 #[tokio::test]
5259 async fn test_column_not_exist() {
5260 let dataset = lance_datagen::gen_batch()
5261 .col("x", array::step::<Int32Type>())
5262 .into_ram_dataset(FragmentCount::from(7), FragmentRowCount::from(6))
5263 .await
5264 .unwrap();
5265
5266 let check_err_msg = |r: Result<DatasetRecordBatchStream>| {
5267 let Err(err) = r else {
5268 panic!(
5269 "Expected an error to be raised saying column y is not found but got no error"
5270 )
5271 };
5272
5273 assert!(
5274 err.to_string().contains("No field named y"),
5275 "Expected error to contain 'No field named y' but got {}",
5276 err
5277 );
5278 };
5279
5280 let mut scan = dataset.scan();
5281 scan.project(&["x", "y"]).unwrap();
5282 check_err_msg(scan.try_into_stream().await);
5283
5284 let mut scan = dataset.scan();
5285 scan.project(&["y"]).unwrap();
5286 check_err_msg(scan.try_into_stream().await);
5287
5288 let mut scan = dataset.scan();
5291 scan.project_with_transform(&[("foo", "1")]).unwrap();
5292 match scan.try_into_stream().await {
5293 Ok(_) => panic!("Expected an error to be raised saying not supported"),
5294 Err(e) => {
5295 assert!(
5296 e.to_string().contains("Received only dynamic expressions"),
5297 "Expected error to contain 'Received only dynamic expressions' but got {}",
5298 e
5299 );
5300 }
5301 }
5302 }
5303
5304 #[cfg(not(windows))]
5305 #[tokio::test]
5306 async fn test_local_object_store() {
5307 let schema = Arc::new(ArrowSchema::new(vec![
5308 ArrowField::new("i", DataType::Int32, true),
5309 ArrowField::new("s", DataType::Utf8, true),
5310 ]));
5311
5312 let batches: Vec<RecordBatch> = (0..5)
5313 .map(|i| {
5314 RecordBatch::try_new(
5315 schema.clone(),
5316 vec![
5317 Arc::new(Int32Array::from_iter_values(i * 20..(i + 1) * 20)),
5318 Arc::new(StringArray::from_iter_values(
5319 (i * 20..(i + 1) * 20).map(|v| format!("s-{}", v)),
5320 )),
5321 ],
5322 )
5323 .unwrap()
5324 })
5325 .collect();
5326
5327 let test_dir = TempStrDir::default();
5328 let test_uri = &test_dir;
5329 let write_params = WriteParams {
5330 max_rows_per_file: 40,
5331 max_rows_per_group: 10,
5332 ..Default::default()
5333 };
5334 let batches = RecordBatchIterator::new(batches.clone().into_iter().map(Ok), schema.clone());
5335 Dataset::write(batches, test_uri, Some(write_params))
5336 .await
5337 .unwrap();
5338
5339 let dataset = Dataset::open(&format!("file-object-store://{}", test_uri))
5340 .await
5341 .unwrap();
5342 let mut builder = dataset.scan();
5343 builder.batch_size(8);
5344 let mut stream = builder.try_into_stream().await.unwrap();
5345 let mut rows_read = 0;
5346 while let Some(next) = stream.next().await {
5347 let next = next.unwrap();
5348 let expected = 8.min(100 - rows_read);
5349 assert_eq!(next.num_rows(), expected);
5350 rows_read += next.num_rows();
5351 }
5352 }
5353
5354 #[tokio::test]
5355 async fn test_filter_parsing() -> Result<()> {
5356 let test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false).await?;
5357 let dataset = &test_ds.dataset;
5358
5359 let mut scan = dataset.scan();
5360 assert!(scan.filter.is_none());
5361
5362 scan.filter("i > 50")?;
5363 assert_eq!(scan.get_expr_filter().unwrap(), Some(col("i").gt(lit(50))));
5364
5365 for use_stats in [false, true] {
5366 let batches = scan
5367 .project(&["s"])?
5368 .use_stats(use_stats)
5369 .try_into_stream()
5370 .await?
5371 .try_collect::<Vec<_>>()
5372 .await?;
5373 let batch = concat_batches(&batches[0].schema(), &batches)?;
5374
5375 let expected_batch = RecordBatch::try_new(
5376 Arc::new(test_ds.schema.project(&[1])?),
5378 vec![Arc::new(StringArray::from_iter_values(
5379 (51..400).map(|v| format!("s-{}", v)),
5380 ))],
5381 )?;
5382 assert_eq!(batch, expected_batch);
5383 }
5384 Ok(())
5385 }
5386
5387 #[tokio::test]
5388 async fn test_scan_regexp_match_and_non_empty_captions() {
5389 let schema = Arc::new(ArrowSchema::new(vec![
5392 ArrowField::new("keywords", DataType::Utf8, true),
5393 ArrowField::new("natural_caption", DataType::Utf8, true),
5394 ArrowField::new("poetic_caption", DataType::Utf8, true),
5395 ]));
5396
5397 let batch = RecordBatch::try_new(
5398 schema.clone(),
5399 vec![
5400 Arc::new(StringArray::from(vec![
5401 Some("Liberty for all"),
5402 Some("peace"),
5403 Some("revolution now"),
5404 Some("Liberty"),
5405 Some("revolutionary"),
5406 Some("none"),
5407 ])) as ArrayRef,
5408 Arc::new(StringArray::from(vec![
5409 Some("a"),
5410 Some("b"),
5411 None,
5412 Some(""),
5413 Some("c"),
5414 Some("d"),
5415 ])) as ArrayRef,
5416 Arc::new(StringArray::from(vec![
5417 Some("x"),
5418 Some(""),
5419 Some("y"),
5420 Some("z"),
5421 None,
5422 Some("w"),
5423 ])) as ArrayRef,
5424 ],
5425 )
5426 .unwrap();
5427
5428 let reader = RecordBatchIterator::new(vec![Ok(batch.clone())], schema.clone());
5429 let dataset = Dataset::write(reader, "memory://", None).await.unwrap();
5430
5431 let mut scan = dataset.scan();
5432 scan.filter(
5433 "regexp_match(keywords, 'Liberty|revolution') AND \
5434 (natural_caption IS NOT NULL AND natural_caption <> '' AND \
5435 poetic_caption IS NOT NULL AND poetic_caption <> '')",
5436 )
5437 .unwrap();
5438
5439 let out = scan.try_into_batch().await.unwrap();
5440 assert_eq!(out.num_rows(), 1);
5441
5442 let out_keywords = out
5443 .column_by_name("keywords")
5444 .unwrap()
5445 .as_string::<i32>()
5446 .value(0);
5447 let out_nat = out
5448 .column_by_name("natural_caption")
5449 .unwrap()
5450 .as_string::<i32>()
5451 .value(0);
5452 let out_poetic = out
5453 .column_by_name("poetic_caption")
5454 .unwrap()
5455 .as_string::<i32>()
5456 .value(0);
5457
5458 assert_eq!(out_keywords, "Liberty for all");
5459 assert_eq!(out_nat, "a");
5460 assert_eq!(out_poetic, "x");
5461 }
5462
5463 #[tokio::test]
5464 async fn test_nested_projection() {
5465 let point_fields: Fields = vec![
5466 ArrowField::new("x", DataType::Float32, true),
5467 ArrowField::new("y", DataType::Float32, true),
5468 ]
5469 .into();
5470 let metadata_fields: Fields = vec![
5471 ArrowField::new("location", DataType::Struct(point_fields), true),
5472 ArrowField::new("age", DataType::Int32, true),
5473 ]
5474 .into();
5475 let metadata_field = ArrowField::new("metadata", DataType::Struct(metadata_fields), true);
5476 let schema = Arc::new(ArrowSchema::new(vec![
5477 metadata_field,
5478 ArrowField::new("idx", DataType::Int32, true),
5479 ]));
5480 let data = lance_datagen::rand(&schema)
5481 .into_ram_dataset(FragmentCount::from(7), FragmentRowCount::from(6))
5482 .await
5483 .unwrap();
5484
5485 let mut scan = data.scan();
5486 scan.project(&["metadata.location.x", "metadata.age"])
5487 .unwrap();
5488 let batch = scan.try_into_batch().await.unwrap();
5489
5490 assert_eq!(
5491 batch.schema().as_ref(),
5492 &ArrowSchema::new(vec![
5493 ArrowField::new("metadata.location.x", DataType::Float32, true),
5494 ArrowField::new("metadata.age", DataType::Int32, true),
5495 ])
5496 );
5497
5498 let take_schema = data.schema().project_by_ids(&[0, 2, 4], false);
5502
5503 let taken = data.take_rows(&[0, 5], take_schema).await.unwrap();
5504
5505 let part_point_fields = Fields::from(vec![ArrowField::new("x", DataType::Float32, true)]);
5507 let part_metadata_fields = Fields::from(vec![
5508 ArrowField::new("location", DataType::Struct(part_point_fields), true),
5509 ArrowField::new("age", DataType::Int32, true),
5510 ]);
5511 let part_schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
5512 "metadata",
5513 DataType::Struct(part_metadata_fields),
5514 true,
5515 )]));
5516
5517 assert_eq!(taken.schema(), part_schema);
5518 }
5519
5520 #[rstest]
5521 #[tokio::test]
5522 async fn test_limit(
5523 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5524 data_storage_version: LanceFileVersion,
5525 ) -> Result<()> {
5526 let test_ds = TestVectorDataset::new(data_storage_version, false).await?;
5527 let dataset = &test_ds.dataset;
5528
5529 let full_data = dataset.scan().try_into_batch().await?.slice(19, 2);
5530
5531 let actual = dataset
5532 .scan()
5533 .limit(Some(2), Some(19))?
5534 .try_into_batch()
5535 .await?;
5536
5537 assert_eq!(actual.num_rows(), 2);
5538 assert_eq!(actual, full_data);
5539 Ok(())
5540 }
5541
5542 #[test_log::test(tokio::test)]
5543 async fn test_limit_cancel() {
5544 let throttled = Arc::new(ThrottledStoreWrapper {
5553 config: ThrottleConfig {
5554 wait_get_per_call: Duration::from_secs(1),
5555 ..Default::default()
5556 },
5557 });
5558 let write_params = WriteParams {
5559 store_params: Some(ObjectStoreParams {
5560 object_store_wrapper: Some(throttled.clone()),
5561 ..Default::default()
5562 }),
5563 max_rows_per_file: 1,
5564 ..Default::default()
5565 };
5566
5567 let dataset = gen_batch()
5569 .col("i", array::step::<Int32Type>().with_random_nulls(0.1))
5570 .into_ram_dataset_with_params(
5571 FragmentCount::from(2000),
5572 FragmentRowCount::from(1),
5573 Some(write_params),
5574 )
5575 .await
5576 .unwrap();
5577
5578 let mut scan = dataset.scan();
5579 scan.filter("i IS NOT NULL").unwrap();
5580 scan.limit(Some(10), None).unwrap();
5581
5582 let start = Instant::now();
5583 scan.try_into_stream()
5584 .await
5585 .unwrap()
5586 .try_collect::<Vec<_>>()
5587 .await
5588 .unwrap();
5589 let duration = start.elapsed();
5590
5591 assert!(duration < Duration::from_secs(10));
5595 }
5596
5597 #[rstest]
5598 #[tokio::test]
5599 async fn test_knn_nodes(
5600 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5601 data_storage_version: LanceFileVersion,
5602 #[values(false, true)] stable_row_ids: bool,
5603 #[values(false, true)] build_index: bool,
5604 ) {
5605 let mut test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5606 .await
5607 .unwrap();
5608 if build_index {
5609 test_ds.make_vector_index().await.unwrap();
5610 }
5611 let dataset = &test_ds.dataset;
5612
5613 let mut scan = dataset.scan();
5614 let key: Float32Array = (32..64).map(|v| v as f32).collect();
5615 scan.nearest("vec", &key, 5).unwrap();
5616 scan.refine(5);
5617
5618 let batch = scan.try_into_batch().await.unwrap();
5619
5620 assert_eq!(batch.num_rows(), 5);
5621 assert_eq!(
5622 batch.schema().as_ref(),
5623 &ArrowSchema::new(vec![
5624 ArrowField::new("i", DataType::Int32, true),
5625 ArrowField::new("s", DataType::Utf8, true),
5626 ArrowField::new(
5627 "vec",
5628 DataType::FixedSizeList(
5629 Arc::new(ArrowField::new("item", DataType::Float32, true)),
5630 32,
5631 ),
5632 true,
5633 ),
5634 ArrowField::new(DIST_COL, DataType::Float32, true),
5635 ])
5636 .with_metadata([("dataset".into(), "vector".into())].into())
5637 );
5638
5639 let expected_i = BTreeSet::from_iter(vec![1, 81, 161, 241, 321]);
5640 let column_i = batch.column_by_name("i").unwrap();
5641 let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
5642 .values()
5643 .iter()
5644 .copied()
5645 .collect();
5646 assert_eq!(expected_i, actual_i);
5647 }
5648
5649 #[rstest]
5650 #[tokio::test]
5651 async fn test_can_project_distance() {
5652 let test_ds = TestVectorDataset::new(LanceFileVersion::Stable, true)
5653 .await
5654 .unwrap();
5655 let dataset = &test_ds.dataset;
5656
5657 let mut scan = dataset.scan();
5658 let key: Float32Array = (32..64).map(|v| v as f32).collect();
5659 scan.nearest("vec", &key, 5).unwrap();
5660 scan.refine(5);
5661 scan.project(&["_distance"]).unwrap();
5662
5663 let batch = scan.try_into_batch().await.unwrap();
5664
5665 assert_eq!(batch.num_rows(), 5);
5666 assert_eq!(batch.num_columns(), 1);
5667 }
5668
5669 #[rstest]
5670 #[tokio::test]
5671 async fn test_knn_with_new_data(
5672 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5673 data_storage_version: LanceFileVersion,
5674 #[values(false, true)] stable_row_ids: bool,
5675 ) {
5676 let mut test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5677 .await
5678 .unwrap();
5679 test_ds.make_vector_index().await.unwrap();
5680 test_ds.append_new_data().await.unwrap();
5681 let dataset = &test_ds.dataset;
5682
5683 let key: Float32Array = [0f32; 32].into_iter().collect();
5685 let k = 20;
5688
5689 #[derive(Debug)]
5690 struct TestCase {
5691 filter: Option<&'static str>,
5692 limit: Option<i64>,
5693 use_index: bool,
5694 }
5695
5696 let mut cases = vec![];
5697 for filter in [Some("i > 100"), None] {
5698 for limit in [None, Some(10)] {
5699 for use_index in [true, false] {
5700 cases.push(TestCase {
5701 filter,
5702 limit,
5703 use_index,
5704 });
5705 }
5706 }
5707 }
5708
5709 for case in cases {
5711 let mut scanner = dataset.scan();
5712 scanner
5713 .nearest("vec", &key, k)
5714 .unwrap()
5715 .limit(case.limit, None)
5716 .unwrap()
5717 .refine(3)
5718 .use_index(case.use_index);
5719 if let Some(filter) = case.filter {
5720 scanner.filter(filter).unwrap();
5721 }
5722
5723 let result = scanner
5724 .try_into_stream()
5725 .await
5726 .unwrap()
5727 .try_collect::<Vec<_>>()
5728 .await
5729 .unwrap();
5730 assert!(!result.is_empty());
5731 let result = concat_batches(&result[0].schema(), result.iter()).unwrap();
5732
5733 if case.filter.is_some() {
5734 let result_rows = result.num_rows();
5735 let expected_rows = case.limit.unwrap_or(k as i64) as usize;
5736 assert!(
5737 result_rows <= expected_rows,
5738 "Expected less than {} rows, got {}",
5739 expected_rows,
5740 result_rows
5741 );
5742 } else {
5743 assert_eq!(result.num_rows(), case.limit.unwrap_or(k as i64) as usize);
5745 }
5746
5747 assert_eq!(
5749 as_primitive_array::<Int32Type>(result.column(0).as_ref()).value(0),
5750 400
5751 );
5752 }
5753 }
5754
5755 #[rstest]
5756 #[tokio::test]
5757 async fn test_knn_with_prefilter(
5758 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5759 data_storage_version: LanceFileVersion,
5760 #[values(false, true)] stable_row_ids: bool,
5761 ) {
5762 let mut test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5763 .await
5764 .unwrap();
5765 test_ds.make_vector_index().await.unwrap();
5766 let dataset = &test_ds.dataset;
5767
5768 let mut scan = dataset.scan();
5769 let key: Float32Array = (32..64).map(|v| v as f32).collect();
5770 scan.filter("i > 100").unwrap();
5771 scan.prefilter(true);
5772 scan.project(&["i", "vec"]).unwrap();
5773 scan.nearest("vec", &key, 5).unwrap();
5774 scan.use_index(false);
5775
5776 let results = scan
5777 .try_into_stream()
5778 .await
5779 .unwrap()
5780 .try_collect::<Vec<_>>()
5781 .await
5782 .unwrap();
5783
5784 assert_eq!(results.len(), 1);
5785 let batch = &results[0];
5786
5787 assert_eq!(batch.num_rows(), 5);
5788 assert_eq!(
5789 batch.schema().as_ref(),
5790 &ArrowSchema::new(vec![
5791 ArrowField::new("i", DataType::Int32, true),
5792 ArrowField::new(
5793 "vec",
5794 DataType::FixedSizeList(
5795 Arc::new(ArrowField::new("item", DataType::Float32, true)),
5796 32,
5797 ),
5798 true,
5799 ),
5800 ArrowField::new(DIST_COL, DataType::Float32, true),
5801 ])
5802 .with_metadata([("dataset".into(), "vector".into())].into())
5803 );
5804
5805 let exact_i = BTreeSet::from_iter(vec![161, 241, 321]);
5807 let close_i = BTreeSet::from_iter(vec![161, 241, 321, 160, 162, 240, 242, 320, 322]);
5809 let column_i = batch.column_by_name("i").unwrap();
5810 let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
5811 .values()
5812 .iter()
5813 .copied()
5814 .collect();
5815 assert!(exact_i.is_subset(&actual_i));
5816 assert!(actual_i.is_subset(&close_i));
5817 }
5818
5819 #[rstest]
5820 #[tokio::test]
5821 async fn test_knn_filter_new_data(
5822 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5823 data_storage_version: LanceFileVersion,
5824 #[values(false, true)] stable_row_ids: bool,
5825 ) {
5826 let mut test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5830 .await
5831 .unwrap();
5832 test_ds.make_vector_index().await.unwrap();
5833 test_ds.append_new_data().await.unwrap();
5834 let dataset = &test_ds.dataset;
5835
5836 let key: Float32Array = [0f32; 32].into_iter().collect();
5838
5839 let mut query = dataset.scan();
5840 query.nearest("vec", &key, 20).unwrap();
5841
5842 let results = query
5844 .try_into_stream()
5845 .await
5846 .unwrap()
5847 .try_collect::<Vec<_>>()
5848 .await
5849 .unwrap();
5850
5851 let results_i = results[0]["i"]
5852 .as_primitive::<Int32Type>()
5853 .values()
5854 .iter()
5855 .copied()
5856 .collect::<BTreeSet<_>>();
5857
5858 assert!(results_i.contains(&400));
5859
5860 for prefilter in [false, true] {
5862 let mut query = dataset.scan();
5863 query
5864 .filter("i != 400")
5865 .unwrap()
5866 .prefilter(prefilter)
5867 .nearest("vec", &key, 20)
5868 .unwrap();
5869
5870 let results = query
5871 .try_into_stream()
5872 .await
5873 .unwrap()
5874 .try_collect::<Vec<_>>()
5875 .await
5876 .unwrap();
5877
5878 let results_i = results[0]["i"]
5879 .as_primitive::<Int32Type>()
5880 .values()
5881 .iter()
5882 .copied()
5883 .collect::<BTreeSet<_>>();
5884
5885 assert!(!results_i.contains(&400));
5886 }
5887 }
5888
5889 #[rstest]
5890 #[tokio::test]
5891 async fn test_knn_with_filter(
5892 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5893 data_storage_version: LanceFileVersion,
5894 #[values(false, true)] stable_row_ids: bool,
5895 ) {
5896 let test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5897 .await
5898 .unwrap();
5899 let dataset = &test_ds.dataset;
5900
5901 let mut scan = dataset.scan();
5902 let key: Float32Array = (32..64).map(|v| v as f32).collect();
5903 scan.nearest("vec", &key, 5).unwrap();
5904 scan.filter("i > 100").unwrap();
5905 scan.project(&["i", "vec"]).unwrap();
5906 scan.refine(5);
5907
5908 let results = scan
5909 .try_into_stream()
5910 .await
5911 .unwrap()
5912 .try_collect::<Vec<_>>()
5913 .await
5914 .unwrap();
5915
5916 assert_eq!(results.len(), 1);
5917 let batch = &results[0];
5918
5919 assert_eq!(batch.num_rows(), 3);
5920 assert_eq!(
5921 batch.schema().as_ref(),
5922 &ArrowSchema::new(vec![
5923 ArrowField::new("i", DataType::Int32, true),
5924 ArrowField::new(
5925 "vec",
5926 DataType::FixedSizeList(
5927 Arc::new(ArrowField::new("item", DataType::Float32, true)),
5928 32,
5929 ),
5930 true,
5931 ),
5932 ArrowField::new(DIST_COL, DataType::Float32, true),
5933 ])
5934 .with_metadata([("dataset".into(), "vector".into())].into())
5935 );
5936
5937 let expected_i = BTreeSet::from_iter(vec![161, 241, 321]);
5938 let column_i = batch.column_by_name("i").unwrap();
5939 let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
5940 .values()
5941 .iter()
5942 .copied()
5943 .collect();
5944 assert_eq!(expected_i, actual_i);
5945 }
5946
5947 #[rstest]
5948 #[tokio::test]
5949 async fn test_refine_factor(
5950 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5951 data_storage_version: LanceFileVersion,
5952 #[values(false, true)] stable_row_ids: bool,
5953 ) {
5954 let test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5955 .await
5956 .unwrap();
5957 let dataset = &test_ds.dataset;
5958
5959 let mut scan = dataset.scan();
5960 let key: Float32Array = (32..64).map(|v| v as f32).collect();
5961 scan.nearest("vec", &key, 5).unwrap();
5962 scan.refine(5);
5963
5964 let results = scan
5965 .try_into_stream()
5966 .await
5967 .unwrap()
5968 .try_collect::<Vec<_>>()
5969 .await
5970 .unwrap();
5971
5972 assert_eq!(results.len(), 1);
5973 let batch = &results[0];
5974
5975 assert_eq!(batch.num_rows(), 5);
5976 assert_eq!(
5977 batch.schema().as_ref(),
5978 &ArrowSchema::new(vec![
5979 ArrowField::new("i", DataType::Int32, true),
5980 ArrowField::new("s", DataType::Utf8, true),
5981 ArrowField::new(
5982 "vec",
5983 DataType::FixedSizeList(
5984 Arc::new(ArrowField::new("item", DataType::Float32, true)),
5985 32,
5986 ),
5987 true,
5988 ),
5989 ArrowField::new(DIST_COL, DataType::Float32, true),
5990 ])
5991 .with_metadata([("dataset".into(), "vector".into())].into())
5992 );
5993
5994 let expected_i = BTreeSet::from_iter(vec![1, 81, 161, 241, 321]);
5995 let column_i = batch.column_by_name("i").unwrap();
5996 let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
5997 .values()
5998 .iter()
5999 .copied()
6000 .collect();
6001 assert_eq!(expected_i, actual_i);
6002 }
6003
6004 #[tokio::test]
6005 async fn test_binary_vectors_default_to_hamming() {
6006 let (_tmp_dir, dataset) = make_binary_vector_dataset().await.unwrap();
6007 let query = UInt8Array::from(vec![0b0000_1111u8, 0, 0, 0]);
6008
6009 let mut scan = dataset.scan();
6010 scan.nearest("bin", &query, 3).unwrap();
6011
6012 assert_eq!(scan.nearest.as_ref().unwrap().metric_type, None);
6014
6015 let batch = scan.try_into_batch().await.unwrap();
6016 let ids = batch
6017 .column_by_name("id")
6018 .unwrap()
6019 .as_primitive::<Int32Type>()
6020 .values();
6021 assert_eq!(ids, &[0, 1, 2]);
6022 let distances = batch
6023 .column_by_name(DIST_COL)
6024 .unwrap()
6025 .as_primitive::<Float32Type>()
6026 .values();
6027 assert_eq!(distances, &[0.0, 2.0, 4.0]);
6028 }
6029
6030 #[tokio::test]
6031 async fn test_binary_vectors_invalid_distance_error() {
6032 let (_tmp_dir, dataset) = make_binary_vector_dataset().await.unwrap();
6033 let query = UInt8Array::from(vec![0b0000_1111u8, 0, 0, 0]);
6034
6035 let mut scan = dataset.scan();
6036 scan.nearest("bin", &query, 1).unwrap();
6037 scan.distance_metric(DistanceType::L2);
6038
6039 let err = scan.try_into_batch().await.unwrap_err();
6040 assert!(matches!(err, Error::InvalidInput { .. }));
6041 let message = err.to_string();
6042 assert!(
6043 message.contains("l2") && message.contains("UInt8"),
6044 "unexpected message: {message}"
6045 );
6046 }
6047
6048 #[tokio::test]
6052 async fn test_knn_metric_mismatch_falls_back_to_flat_search() {
6053 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, true)
6054 .await
6055 .unwrap();
6056 test_ds.make_vector_index().await.unwrap();
6058
6059 let dataset = &test_ds.dataset;
6060 let key: Float32Array = (32..64).map(|v| v as f32).collect();
6061
6062 let mut scan = dataset.scan();
6064 scan.nearest("vec", &key, 5).unwrap();
6065 scan.distance_metric(DistanceType::Dot);
6066
6067 let plan = scan.explain_plan(false).await.unwrap();
6069 assert!(
6070 !plan.contains("ANNSubIndex"),
6071 "Expected flat search, but got ANN index in plan:\n{}",
6072 plan
6073 );
6074 assert!(
6076 plan.contains("KNNVectorDistance") && plan.to_lowercase().contains("dot"),
6077 "Expected flat KNN with Dot metric in plan:\n{}",
6078 plan
6079 );
6080
6081 let dot_batch = dataset
6083 .scan()
6084 .nearest("vec", &key, 5)
6085 .unwrap()
6086 .distance_metric(DistanceType::Dot)
6087 .try_into_batch()
6088 .await
6089 .unwrap();
6090
6091 let l2_batch = dataset
6092 .scan()
6093 .nearest("vec", &key, 5)
6094 .unwrap()
6095 .distance_metric(DistanceType::L2)
6096 .try_into_batch()
6097 .await
6098 .unwrap();
6099
6100 let dot_distances: Vec<f32> = dot_batch
6101 .column_by_name(DIST_COL)
6102 .unwrap()
6103 .as_primitive::<Float32Type>()
6104 .values()
6105 .to_vec();
6106 let l2_distances: Vec<f32> = l2_batch
6107 .column_by_name(DIST_COL)
6108 .unwrap()
6109 .as_primitive::<Float32Type>()
6110 .values()
6111 .to_vec();
6112
6113 assert_ne!(dot_distances, l2_distances);
6115 }
6116
6117 #[tokio::test]
6120 async fn test_knn_no_metric_uses_index_metric() {
6121 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, true)
6122 .await
6123 .unwrap();
6124 test_ds.make_vector_index().await.unwrap();
6126
6127 let dataset = &test_ds.dataset;
6128 let key: Float32Array = (32..64).map(|v| v as f32).collect();
6129
6130 let mut scan = dataset.scan();
6132 scan.nearest("vec", &key, 5).unwrap();
6133 let plan = scan.explain_plan(false).await.unwrap();
6137 assert!(
6138 plan.contains("ANNSubIndex") && plan.to_lowercase().contains("l2"),
6139 "Expected ANN index with L2 metric in plan:\n{}",
6140 plan
6141 );
6142 }
6143
6144 #[rstest]
6145 #[tokio::test]
6146 async fn test_only_row_id(
6147 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6148 data_storage_version: LanceFileVersion,
6149 ) {
6150 let test_ds = TestVectorDataset::new(data_storage_version, false)
6151 .await
6152 .unwrap();
6153 let dataset = &test_ds.dataset;
6154
6155 let mut scan = dataset.scan();
6156 scan.project::<&str>(&[]).unwrap().with_row_id();
6157
6158 let batch = scan.try_into_batch().await.unwrap();
6159
6160 assert_eq!(batch.num_columns(), 1);
6161 assert_eq!(batch.num_rows(), 400);
6162 let expected_schema =
6163 ArrowSchema::new(vec![ArrowField::new(ROW_ID, DataType::UInt64, true)])
6164 .with_metadata(dataset.schema().metadata.clone());
6165 assert_eq!(batch.schema().as_ref(), &expected_schema,);
6166
6167 let expected_row_ids: Vec<u64> = (0..200_u64).chain((1 << 32)..((1 << 32) + 200)).collect();
6168 let actual_row_ids: Vec<u64> = as_primitive_array::<UInt64Type>(batch.column(0).as_ref())
6169 .values()
6170 .iter()
6171 .copied()
6172 .collect();
6173 assert_eq!(expected_row_ids, actual_row_ids);
6174 }
6175
6176 #[tokio::test]
6177 async fn test_scan_unordered_with_row_id() {
6178 let test_ds = TestVectorDataset::new(LanceFileVersion::Legacy, false)
6180 .await
6181 .unwrap();
6182 let dataset = &test_ds.dataset;
6183
6184 let mut scan = dataset.scan();
6185 scan.with_row_id();
6186
6187 let ordered_batches = scan
6188 .try_into_stream()
6189 .await
6190 .unwrap()
6191 .try_collect::<Vec<RecordBatch>>()
6192 .await
6193 .unwrap();
6194 assert!(ordered_batches.len() > 2);
6195 let ordered_batch =
6196 concat_batches(&ordered_batches[0].schema(), ordered_batches.iter()).unwrap();
6197
6198 scan.scan_in_order(false);
6200 for _ in 0..10 {
6201 let unordered_batches = scan
6202 .try_into_stream()
6203 .await
6204 .unwrap()
6205 .try_collect::<Vec<RecordBatch>>()
6206 .await
6207 .unwrap();
6208 let unordered_batch =
6209 concat_batches(&unordered_batches[0].schema(), unordered_batches.iter()).unwrap();
6210
6211 assert_eq!(ordered_batch.num_rows(), unordered_batch.num_rows());
6212
6213 if ordered_batch != unordered_batch {
6215 let sort_indices = sort_to_indices(&unordered_batch[ROW_ID], None, None).unwrap();
6216
6217 let ordered_i = ordered_batch["i"].clone();
6218 let sorted_i = take::take(&unordered_batch["i"], &sort_indices, None).unwrap();
6219
6220 assert_eq!(&ordered_i, &sorted_i);
6221
6222 break;
6223 }
6224 }
6225 }
6226
6227 #[tokio::test]
6228 async fn test_scan_with_wildcard() {
6229 let data = gen_batch()
6230 .col("x", array::step::<Float64Type>())
6231 .col("y", array::step::<Float64Type>())
6232 .into_ram_dataset(FragmentCount::from(1), FragmentRowCount::from(100))
6233 .await
6234 .unwrap();
6235
6236 let check_cols = async |projection: &[&str], expected_cols: &[&str]| {
6237 let mut scan = data.scan();
6238 scan.project(projection).unwrap();
6239 let stream = scan.try_into_stream().await.unwrap();
6240 let schema = stream.schema();
6241 let field_names = schema.field_names();
6242 assert_eq!(field_names, expected_cols);
6243 };
6244
6245 check_cols(&["*"], &["x", "y"]).await;
6246 check_cols(&["x", "y"], &["x", "y"]).await;
6247 check_cols(&["x"], &["x"]).await;
6248 check_cols(&["_rowid", "*"], &["_rowid", "x", "y"]).await;
6249 check_cols(&["*", "_rowid"], &["x", "y", "_rowid"]).await;
6250 check_cols(
6251 &["_rowid", "*", "_rowoffset"],
6252 &["_rowid", "x", "y", "_rowoffset"],
6253 )
6254 .await;
6255
6256 let check_exprs = async |exprs: &[&str], expected_cols: &[&str]| {
6257 let mut scan = data.scan();
6258 let projection = exprs
6259 .iter()
6260 .map(|e| (e.to_string(), e.to_string()))
6261 .collect::<Vec<_>>();
6262 scan.project_with_transform(&projection).unwrap();
6263 let stream = scan.try_into_stream().await.unwrap();
6264 let schema = stream.schema();
6265 let field_names = schema.field_names();
6266 assert_eq!(field_names, expected_cols);
6267 };
6268
6269 check_exprs(&["_rowid", "*", "x * 2"], &["_rowid", "x", "y", "x * 2"]).await;
6271
6272 let check_fails = |projection: &[&str]| {
6273 let mut scan = data.scan();
6274 assert!(scan.project(projection).is_err());
6275 };
6276
6277 check_fails(&["x", "*"]);
6279 check_fails(&["_rowid", "_rowid"]);
6280 }
6281
6282 #[rstest]
6283 #[tokio::test]
6284 async fn test_scan_order(
6285 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6286 data_storage_version: LanceFileVersion,
6287 ) {
6288 let test_dir = TempStrDir::default();
6289 let test_uri = &test_dir;
6290
6291 let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
6292 "i",
6293 DataType::Int32,
6294 true,
6295 )]));
6296
6297 let batch1 = RecordBatch::try_new(
6298 schema.clone(),
6299 vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5]))],
6300 )
6301 .unwrap();
6302
6303 let batch2 = RecordBatch::try_new(
6304 schema.clone(),
6305 vec![Arc::new(Int32Array::from(vec![6, 7, 8]))],
6306 )
6307 .unwrap();
6308
6309 let params = WriteParams {
6310 mode: WriteMode::Append,
6311 data_storage_version: Some(data_storage_version),
6312 ..Default::default()
6313 };
6314
6315 let write_batch = |batch: RecordBatch| async {
6316 let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
6317 Dataset::write(reader, test_uri, Some(params)).await
6318 };
6319
6320 write_batch.clone()(batch1.clone()).await.unwrap();
6321 write_batch(batch2.clone()).await.unwrap();
6322
6323 let dataset = Arc::new(Dataset::open(test_uri).await.unwrap());
6324 let fragment1 = dataset.get_fragment(0).unwrap().metadata().clone();
6325 let fragment2 = dataset.get_fragment(1).unwrap().metadata().clone();
6326
6327 let mut scanner = dataset.scan();
6329 scanner.with_fragments(vec![fragment1.clone(), fragment2.clone()]);
6330 let output = scanner
6331 .try_into_stream()
6332 .await
6333 .unwrap()
6334 .try_collect::<Vec<_>>()
6335 .await
6336 .unwrap();
6337 assert_eq!(output.len(), 2);
6338 assert_eq!(output[0], batch1);
6339 assert_eq!(output[1], batch2);
6340
6341 let mut scanner = dataset.scan();
6343 scanner.with_fragments(vec![fragment2, fragment1]);
6344 let output = scanner
6345 .try_into_stream()
6346 .await
6347 .unwrap()
6348 .try_collect::<Vec<_>>()
6349 .await
6350 .unwrap();
6351 assert_eq!(output.len(), 2);
6352 assert_eq!(output[0], batch2);
6353 assert_eq!(output[1], batch1);
6354 }
6355
6356 #[rstest]
6357 #[tokio::test]
6358 async fn test_scan_sort(
6359 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6360 data_storage_version: LanceFileVersion,
6361 ) {
6362 let test_dir = TempStrDir::default();
6363 let test_uri = &test_dir;
6364
6365 let data = gen_batch()
6366 .col("int", array::cycle::<Int32Type>(vec![5, 4, 1, 2, 3]))
6367 .col(
6368 "str",
6369 array::cycle_utf8_literals(&["a", "b", "c", "e", "d"]),
6370 );
6371
6372 let sorted_by_int = gen_batch()
6373 .col("int", array::cycle::<Int32Type>(vec![1, 2, 3, 4, 5]))
6374 .col(
6375 "str",
6376 array::cycle_utf8_literals(&["c", "e", "d", "b", "a"]),
6377 )
6378 .into_batch_rows(RowCount::from(5))
6379 .unwrap();
6380
6381 let sorted_by_str = gen_batch()
6382 .col("int", array::cycle::<Int32Type>(vec![5, 4, 1, 3, 2]))
6383 .col(
6384 "str",
6385 array::cycle_utf8_literals(&["a", "b", "c", "d", "e"]),
6386 )
6387 .into_batch_rows(RowCount::from(5))
6388 .unwrap();
6389
6390 Dataset::write(
6391 data.into_reader_rows(RowCount::from(5), BatchCount::from(1)),
6392 test_uri,
6393 Some(WriteParams {
6394 data_storage_version: Some(data_storage_version),
6395 ..Default::default()
6396 }),
6397 )
6398 .await
6399 .unwrap();
6400
6401 let dataset = Arc::new(Dataset::open(test_uri).await.unwrap());
6402
6403 let batches_by_int = dataset
6404 .scan()
6405 .order_by(Some(vec![ColumnOrdering::asc_nulls_first(
6406 "int".to_string(),
6407 )]))
6408 .unwrap()
6409 .try_into_stream()
6410 .await
6411 .unwrap()
6412 .try_collect::<Vec<_>>()
6413 .await
6414 .unwrap();
6415
6416 assert_eq!(batches_by_int[0], sorted_by_int);
6417
6418 let batches_by_str = dataset
6419 .scan()
6420 .order_by(Some(vec![ColumnOrdering::asc_nulls_first(
6421 "str".to_string(),
6422 )]))
6423 .unwrap()
6424 .try_into_stream()
6425 .await
6426 .unwrap()
6427 .try_collect::<Vec<_>>()
6428 .await
6429 .unwrap();
6430
6431 assert_eq!(batches_by_str[0], sorted_by_str);
6432
6433 dataset
6435 .scan()
6436 .order_by(Some(vec![]))
6437 .unwrap()
6438 .try_into_stream()
6439 .await
6440 .unwrap()
6441 .try_collect::<Vec<_>>()
6442 .await
6443 .unwrap();
6444 }
6445
6446 #[rstest]
6447 #[tokio::test]
6448 async fn test_sort_multi_columns(
6449 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6450 data_storage_version: LanceFileVersion,
6451 ) {
6452 let test_dir = TempStrDir::default();
6453 let test_uri = &test_dir;
6454
6455 let data = gen_batch()
6456 .col("int", array::cycle::<Int32Type>(vec![5, 5, 1, 1, 3]))
6457 .col(
6458 "float",
6459 array::cycle::<Float32Type>(vec![7.3, -f32::NAN, f32::NAN, 4.3, f32::INFINITY]),
6460 );
6461
6462 let sorted_by_int_then_float = gen_batch()
6463 .col("int", array::cycle::<Int32Type>(vec![1, 1, 3, 5, 5]))
6464 .col(
6465 "float",
6466 array::cycle::<Float32Type>(vec![4.3, f32::NAN, f32::INFINITY, -f32::NAN, 7.3]),
6468 )
6469 .into_batch_rows(RowCount::from(5))
6470 .unwrap();
6471
6472 Dataset::write(
6473 data.into_reader_rows(RowCount::from(5), BatchCount::from(1)),
6474 test_uri,
6475 Some(WriteParams {
6476 data_storage_version: Some(data_storage_version),
6477 ..Default::default()
6478 }),
6479 )
6480 .await
6481 .unwrap();
6482
6483 let dataset = Arc::new(Dataset::open(test_uri).await.unwrap());
6484
6485 let batches_by_int_then_float = dataset
6486 .scan()
6487 .order_by(Some(vec![
6488 ColumnOrdering::asc_nulls_first("int".to_string()),
6489 ColumnOrdering::asc_nulls_first("float".to_string()),
6490 ]))
6491 .unwrap()
6492 .try_into_stream()
6493 .await
6494 .unwrap()
6495 .try_collect::<Vec<_>>()
6496 .await
6497 .unwrap();
6498
6499 assert_eq!(batches_by_int_then_float[0], sorted_by_int_then_float);
6500 }
6501
6502 #[rstest]
6503 #[tokio::test]
6504 async fn test_ann_prefilter(
6505 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6506 data_storage_version: LanceFileVersion,
6507 #[values(false, true)] stable_row_ids: bool,
6508 #[values(
6509 VectorIndexParams::ivf_pq(2, 8, 2, MetricType::L2, 2),
6510 VectorIndexParams::with_ivf_hnsw_sq_params(
6511 MetricType::L2,
6512 IvfBuildParams::new(2),
6513 HnswBuildParams::default(),
6514 SQBuildParams::default()
6515 )
6516 )]
6517 index_params: VectorIndexParams,
6518 ) {
6519 use lance_arrow::{FixedSizeListArrayExt, fixed_size_list_type};
6520
6521 let test_dir = TempStrDir::default();
6522 let test_uri = &test_dir;
6523
6524 let schema = Arc::new(ArrowSchema::new(vec![
6525 ArrowField::new("filterable", DataType::Int32, true),
6526 ArrowField::new("vector", fixed_size_list_type(2, DataType::Float32), true),
6527 ]));
6528
6529 let vector_values = Float32Array::from_iter_values((0..600).map(|x| x as f32));
6530
6531 let batches = vec![
6532 RecordBatch::try_new(
6533 schema.clone(),
6534 vec![
6535 Arc::new(Int32Array::from_iter_values(0..300)),
6536 Arc::new(FixedSizeListArray::try_new_from_values(vector_values, 2).unwrap()),
6537 ],
6538 )
6539 .unwrap(),
6540 ];
6541
6542 let write_params = WriteParams {
6543 data_storage_version: Some(data_storage_version),
6544 max_rows_per_file: 300, enable_stable_row_ids: stable_row_ids,
6546 ..Default::default()
6547 };
6548 let batches = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6549 let mut dataset = Dataset::write(batches, test_uri, Some(write_params))
6550 .await
6551 .unwrap();
6552
6553 dataset
6554 .create_index(&["vector"], IndexType::Vector, None, &index_params, false)
6555 .await
6556 .unwrap();
6557
6558 let query_key = Arc::new(Float32Array::from_iter_values((0..2).map(|x| x as f32)));
6559 let mut scan = dataset.scan();
6560 scan.filter("filterable > 5").unwrap();
6561 scan.nearest("vector", query_key.as_ref(), 1).unwrap();
6562 scan.minimum_nprobes(100);
6563 scan.ef(100);
6564 scan.with_row_id();
6565
6566 let batches = scan
6567 .try_into_stream()
6568 .await
6569 .unwrap()
6570 .try_collect::<Vec<_>>()
6571 .await
6572 .unwrap();
6573
6574 assert_eq!(batches.len(), 0);
6575
6576 scan.prefilter(true);
6577
6578 let batches = scan
6579 .try_into_stream()
6580 .await
6581 .unwrap()
6582 .try_collect::<Vec<_>>()
6583 .await
6584 .unwrap();
6585 assert_eq!(batches.len(), 1);
6586
6587 let first_match = batches[0][ROW_ID].as_primitive::<UInt64Type>().values()[0];
6588
6589 assert_eq!(6, first_match);
6590 }
6591
6592 #[rstest]
6593 #[tokio::test]
6594 async fn test_filter_on_large_utf8(
6595 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6596 data_storage_version: LanceFileVersion,
6597 ) {
6598 let test_dir = TempStrDir::default();
6599 let test_uri = &test_dir;
6600
6601 let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
6602 "ls",
6603 DataType::LargeUtf8,
6604 true,
6605 )]));
6606
6607 let batches = vec![
6608 RecordBatch::try_new(
6609 schema.clone(),
6610 vec![Arc::new(LargeStringArray::from_iter_values(
6611 (0..10).map(|v| format!("s-{}", v)),
6612 ))],
6613 )
6614 .unwrap(),
6615 ];
6616
6617 let write_params = WriteParams {
6618 data_storage_version: Some(data_storage_version),
6619 ..Default::default()
6620 };
6621 let batches = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6622 Dataset::write(batches, test_uri, Some(write_params))
6623 .await
6624 .unwrap();
6625
6626 let dataset = Dataset::open(test_uri).await.unwrap();
6627 let mut scan = dataset.scan();
6628 scan.filter("ls = 's-8'").unwrap();
6629
6630 let batches = scan
6631 .try_into_stream()
6632 .await
6633 .unwrap()
6634 .try_collect::<Vec<_>>()
6635 .await
6636 .unwrap();
6637 let batch = &batches[0];
6638
6639 let expected = RecordBatch::try_new(
6640 schema.clone(),
6641 vec![Arc::new(LargeStringArray::from_iter_values(
6642 (8..9).map(|v| format!("s-{}", v)),
6643 ))],
6644 )
6645 .unwrap();
6646
6647 assert_eq!(batch, &expected);
6648 }
6649
6650 #[rstest]
6651 #[tokio::test]
6652 async fn test_filter_with_regex(
6653 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6654 data_storage_version: LanceFileVersion,
6655 ) {
6656 let test_dir = TempStrDir::default();
6657 let test_uri = &test_dir;
6658
6659 let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
6660 "ls",
6661 DataType::Utf8,
6662 true,
6663 )]));
6664
6665 let batches = vec![
6666 RecordBatch::try_new(
6667 schema.clone(),
6668 vec![Arc::new(StringArray::from_iter_values(
6669 (0..20).map(|v| format!("s-{}", v)),
6670 ))],
6671 )
6672 .unwrap(),
6673 ];
6674
6675 let write_params = WriteParams {
6676 data_storage_version: Some(data_storage_version),
6677 ..Default::default()
6678 };
6679 let batches = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6680 Dataset::write(batches, test_uri, Some(write_params))
6681 .await
6682 .unwrap();
6683
6684 let dataset = Dataset::open(test_uri).await.unwrap();
6685 let mut scan = dataset.scan();
6686 scan.filter("regexp_match(ls, 's-1.')").unwrap();
6687
6688 let stream = scan.try_into_stream().await.unwrap();
6689 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
6690 let batch = &batches[0];
6691
6692 let expected = RecordBatch::try_new(
6693 schema.clone(),
6694 vec![Arc::new(StringArray::from_iter_values(
6695 (10..=19).map(|v| format!("s-{}", v)),
6696 ))],
6697 )
6698 .unwrap();
6699
6700 assert_eq!(batch, &expected);
6701 }
6702
6703 #[tokio::test]
6704 async fn test_filter_proj_bug() {
6705 let struct_i_field = ArrowField::new("i", DataType::Int32, true);
6706 let struct_o_field = ArrowField::new("o", DataType::Utf8, true);
6707 let schema = Arc::new(ArrowSchema::new(vec![
6708 ArrowField::new(
6709 "struct",
6710 DataType::Struct(vec![struct_i_field.clone(), struct_o_field.clone()].into()),
6711 true,
6712 ),
6713 ArrowField::new("s", DataType::Utf8, true),
6714 ]));
6715
6716 let input_batches: Vec<RecordBatch> = (0..5)
6717 .map(|i| {
6718 let struct_i_arr: Arc<Int32Array> =
6719 Arc::new(Int32Array::from_iter_values(i * 20..(i + 1) * 20));
6720 let struct_o_arr: Arc<StringArray> = Arc::new(StringArray::from_iter_values(
6721 (i * 20..(i + 1) * 20).map(|v| format!("o-{:02}", v)),
6722 ));
6723 RecordBatch::try_new(
6724 schema.clone(),
6725 vec![
6726 Arc::new(StructArray::from(vec![
6727 (Arc::new(struct_i_field.clone()), struct_i_arr as ArrayRef),
6728 (Arc::new(struct_o_field.clone()), struct_o_arr as ArrayRef),
6729 ])),
6730 Arc::new(StringArray::from_iter_values(
6731 (i * 20..(i + 1) * 20).map(|v| format!("s-{}", v)),
6732 )),
6733 ],
6734 )
6735 .unwrap()
6736 })
6737 .collect();
6738 let batches =
6739 RecordBatchIterator::new(input_batches.clone().into_iter().map(Ok), schema.clone());
6740 let test_dir = TempStrDir::default();
6741 let test_uri = &test_dir;
6742 let write_params = WriteParams {
6743 max_rows_per_file: 40,
6744 max_rows_per_group: 10,
6745 data_storage_version: Some(LanceFileVersion::Legacy),
6746 ..Default::default()
6747 };
6748 Dataset::write(batches, test_uri, Some(write_params))
6749 .await
6750 .unwrap();
6751
6752 let dataset = Dataset::open(test_uri).await.unwrap();
6753 let batches = dataset
6754 .scan()
6755 .filter("struct.i >= 20")
6756 .unwrap()
6757 .try_into_stream()
6758 .await
6759 .unwrap()
6760 .try_collect::<Vec<_>>()
6761 .await
6762 .unwrap();
6763 let batch = concat_batches(&batches[0].schema(), &batches).unwrap();
6764
6765 let expected_batch = concat_batches(&schema, &input_batches.as_slice()[1..]).unwrap();
6766 assert_eq!(batch, expected_batch);
6767
6768 let batches = dataset
6770 .scan()
6771 .filter("struct.o >= 'o-20'")
6772 .unwrap()
6773 .try_into_stream()
6774 .await
6775 .unwrap()
6776 .try_collect::<Vec<_>>()
6777 .await
6778 .unwrap();
6779 let batch = concat_batches(&batches[0].schema(), &batches).unwrap();
6780 assert_eq!(batch, expected_batch);
6781
6782 let batches = dataset
6784 .scan()
6785 .project(vec!["struct"].as_slice())
6786 .unwrap()
6787 .try_into_stream()
6788 .await
6789 .unwrap()
6790 .try_collect::<Vec<_>>()
6791 .await
6792 .unwrap();
6793 concat_batches(&batches[0].schema(), &batches).unwrap();
6794 }
6795
6796 #[rstest]
6797 #[tokio::test]
6798 async fn test_ann_with_deletion(
6799 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6800 data_storage_version: LanceFileVersion,
6801 #[values(false, true)] stable_row_ids: bool,
6802 ) {
6803 let vec_params = vec![
6804 VectorIndexParams::ivf_pq(4, 8, 2, MetricType::L2, 2),
6807 ];
6808 for params in vec_params {
6809 use lance_arrow::FixedSizeListArrayExt;
6810
6811 let test_dir = TempStrDir::default();
6812 let test_uri = &test_dir;
6813
6814 let schema = Arc::new(ArrowSchema::new(vec![
6816 ArrowField::new("i", DataType::Int32, true),
6817 ArrowField::new(
6818 "vec",
6819 DataType::FixedSizeList(
6820 Arc::new(ArrowField::new("item", DataType::Float32, true)),
6821 32,
6822 ),
6823 true,
6824 ),
6825 ]));
6826
6827 let vector_values: Float32Array =
6829 (0..32 * 512).map(|v| (v / 32) as f32 + 1.0).collect();
6830 let vectors = FixedSizeListArray::try_new_from_values(vector_values, 32).unwrap();
6831
6832 let batches = vec![
6833 RecordBatch::try_new(
6834 schema.clone(),
6835 vec![
6836 Arc::new(Int32Array::from_iter_values(0..512)),
6837 Arc::new(vectors.clone()),
6838 ],
6839 )
6840 .unwrap(),
6841 ];
6842
6843 let reader = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6844 let mut dataset = Dataset::write(
6845 reader,
6846 test_uri,
6847 Some(WriteParams {
6848 data_storage_version: Some(data_storage_version),
6849 enable_stable_row_ids: stable_row_ids,
6850 ..Default::default()
6851 }),
6852 )
6853 .await
6854 .unwrap();
6855
6856 assert_eq!(dataset.index_cache_entry_count().await, 0);
6857 dataset
6858 .create_index(
6859 &["vec"],
6860 IndexType::Vector,
6861 Some("idx".to_string()),
6862 ¶ms,
6863 true,
6864 )
6865 .await
6866 .unwrap();
6867
6868 let mut scan = dataset.scan();
6869 let key: Float32Array = (0..32).map(|_v| 1.0_f32).collect();
6871 scan.nearest("vec", &key, 5).unwrap();
6872 scan.refine(100);
6873 scan.minimum_nprobes(100);
6874
6875 assert_eq!(
6876 dataset.index_cache_entry_count().await,
6877 2, );
6879 let results = scan
6880 .try_into_stream()
6881 .await
6882 .unwrap()
6883 .try_collect::<Vec<_>>()
6884 .await
6885 .unwrap();
6886
6887 assert_eq!(
6888 dataset.index_cache_entry_count().await,
6889 5 + dataset.versions().await.unwrap().len()
6890 );
6891 assert_eq!(results.len(), 1);
6892 let batch = &results[0];
6893
6894 let expected_i = BTreeSet::from_iter(vec![0, 1, 2, 3, 4]);
6895 let column_i = batch.column_by_name("i").unwrap();
6896 let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
6897 .values()
6898 .iter()
6899 .copied()
6900 .collect();
6901 assert_eq!(expected_i, actual_i);
6902
6903 dataset.delete("i = 1").await.unwrap();
6906 let mut scan = dataset.scan();
6907 scan.nearest("vec", &key, 5).unwrap();
6908 scan.refine(100);
6909 scan.minimum_nprobes(100);
6910
6911 let results = scan
6912 .try_into_stream()
6913 .await
6914 .unwrap()
6915 .try_collect::<Vec<_>>()
6916 .await
6917 .unwrap();
6918
6919 assert_eq!(results.len(), 1);
6920 let batch = &results[0];
6921
6922 let expected_i = BTreeSet::from_iter(vec![0, 2, 3, 4, 5]);
6924 let column_i = batch.column_by_name("i").unwrap();
6925 let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
6926 .values()
6927 .iter()
6928 .copied()
6929 .collect();
6930 assert_eq!(expected_i, actual_i);
6931
6932 let batches = vec![
6935 RecordBatch::try_new(
6936 schema.clone(),
6937 vec![
6938 Arc::new(Int32Array::from_iter_values(512..1024)),
6939 Arc::new(vectors),
6940 ],
6941 )
6942 .unwrap(),
6943 ];
6944
6945 let reader = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6946 let mut dataset = Dataset::write(
6947 reader,
6948 test_uri,
6949 Some(WriteParams {
6950 mode: WriteMode::Append,
6951 data_storage_version: Some(data_storage_version),
6952 ..Default::default()
6953 }),
6954 )
6955 .await
6956 .unwrap();
6957 dataset
6958 .create_index(
6959 &["vec"],
6960 IndexType::Vector,
6961 Some("idx".to_string()),
6962 ¶ms,
6963 true,
6964 )
6965 .await
6966 .unwrap();
6967
6968 dataset.delete("i < 512").await.unwrap();
6969
6970 let mut scan = dataset.scan();
6971 scan.nearest("vec", &key, 5).unwrap();
6972 scan.refine(100);
6973 scan.minimum_nprobes(100);
6974
6975 let results = scan
6976 .try_into_stream()
6977 .await
6978 .unwrap()
6979 .try_collect::<Vec<_>>()
6980 .await
6981 .unwrap();
6982
6983 assert_eq!(results.len(), 1);
6984 let batch = &results[0];
6985
6986 let expected_i = BTreeSet::from_iter(vec![512, 513, 514, 515, 516]);
6988 let column_i = batch.column_by_name("i").unwrap();
6989 let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
6990 .values()
6991 .iter()
6992 .copied()
6993 .collect();
6994 assert_eq!(expected_i, actual_i);
6995 }
6996 }
6997
6998 #[tokio::test]
6999 async fn test_projection_order() {
7000 let vec_params = VectorIndexParams::ivf_pq(4, 8, 2, MetricType::L2, 2);
7001 let mut data = gen_batch()
7002 .col("vec", array::rand_vec::<Float32Type>(Dimension::from(4)))
7003 .col("text", array::rand_utf8(ByteCount::from(10), false))
7004 .into_ram_dataset(FragmentCount::from(3), FragmentRowCount::from(100))
7005 .await
7006 .unwrap();
7007 data.create_index(&["vec"], IndexType::Vector, None, &vec_params, true)
7008 .await
7009 .unwrap();
7010
7011 let mut scan = data.scan();
7012 scan.nearest("vec", &Float32Array::from(vec![1.0, 1.0, 1.0, 1.0]), 5)
7013 .unwrap();
7014 scan.with_row_id().project(&["text"]).unwrap();
7015
7016 let results = scan
7017 .try_into_stream()
7018 .await
7019 .unwrap()
7020 .try_collect::<Vec<_>>()
7021 .await
7022 .unwrap();
7023
7024 assert_eq!(
7025 results[0].schema().field_names(),
7026 vec!["text", "_distance", "_rowid"]
7027 );
7028 }
7029
7030 #[rstest]
7031 #[tokio::test]
7032 async fn test_count_rows_with_filter(
7033 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
7034 data_storage_version: LanceFileVersion,
7035 ) {
7036 let test_dir = TempStrDir::default();
7037 let test_uri = &test_dir;
7038 let mut data_gen = BatchGenerator::new().col(Box::new(
7039 IncrementingInt32::new().named("Filter_me".to_owned()),
7040 ));
7041 Dataset::write(
7042 data_gen.batch(32),
7043 test_uri,
7044 Some(WriteParams {
7045 data_storage_version: Some(data_storage_version),
7046 ..Default::default()
7047 }),
7048 )
7049 .await
7050 .unwrap();
7051
7052 let dataset = Dataset::open(test_uri).await.unwrap();
7053 assert_eq!(32, dataset.count_rows(None).await.unwrap());
7054 assert_eq!(
7055 16,
7056 dataset
7057 .count_rows(Some("`Filter_me` > 15".to_string()))
7058 .await
7059 .unwrap()
7060 );
7061 }
7062
7063 #[rstest]
7064 #[tokio::test]
7065 async fn test_dynamic_projection(
7066 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
7067 data_storage_version: LanceFileVersion,
7068 ) {
7069 let test_dir = TempStrDir::default();
7070 let test_uri = &test_dir;
7071 let mut data_gen =
7072 BatchGenerator::new().col(Box::new(IncrementingInt32::new().named("i".to_owned())));
7073 Dataset::write(
7074 data_gen.batch(32),
7075 test_uri,
7076 Some(WriteParams {
7077 data_storage_version: Some(data_storage_version),
7078 ..Default::default()
7079 }),
7080 )
7081 .await
7082 .unwrap();
7083
7084 let dataset = Dataset::open(test_uri).await.unwrap();
7085 assert_eq!(dataset.count_rows(None).await.unwrap(), 32);
7086
7087 let mut scanner = dataset.scan();
7088
7089 let scan_res = scanner
7090 .project_with_transform(&[("bool", "i > 15")])
7091 .unwrap()
7092 .try_into_batch()
7093 .await
7094 .unwrap();
7095
7096 assert_eq!(1, scan_res.num_columns());
7097
7098 let bool_col = scan_res
7099 .column_by_name("bool")
7100 .expect("bool column should exist");
7101 let bool_arr = bool_col.as_boolean();
7102 for i in 0..32 {
7103 if i > 15 {
7104 assert!(bool_arr.value(i));
7105 } else {
7106 assert!(!bool_arr.value(i));
7107 }
7108 }
7109 }
7110
7111 #[rstest]
7112 #[tokio::test]
7113 async fn test_column_casting_function(
7114 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
7115 data_storage_version: LanceFileVersion,
7116 ) {
7117 let test_dir = TempStrDir::default();
7118 let test_uri = &test_dir;
7119 let mut data_gen =
7120 BatchGenerator::new().col(Box::new(RandomVector::new().named("vec".to_owned())));
7121 Dataset::write(
7122 data_gen.batch(32),
7123 test_uri,
7124 Some(WriteParams {
7125 data_storage_version: Some(data_storage_version),
7126 ..Default::default()
7127 }),
7128 )
7129 .await
7130 .unwrap();
7131
7132 let dataset = Dataset::open(test_uri).await.unwrap();
7133 assert_eq!(dataset.count_rows(None).await.unwrap(), 32);
7134
7135 let mut scanner = dataset.scan();
7136
7137 let scan_res = scanner
7138 .project_with_transform(&[("f16", "_cast_list_f16(vec)")])
7139 .unwrap()
7140 .try_into_batch()
7141 .await
7142 .unwrap();
7143
7144 assert_eq!(1, scan_res.num_columns());
7145 assert_eq!(32, scan_res.num_rows());
7146 assert_eq!("f16", scan_res.schema().field(0).name());
7147
7148 let mut scanner = dataset.scan();
7149 let scan_res_original = scanner
7150 .project(&["vec"])
7151 .unwrap()
7152 .try_into_batch()
7153 .await
7154 .unwrap();
7155
7156 let f32_col: &Float32Array = scan_res_original
7157 .column_by_name("vec")
7158 .unwrap()
7159 .as_fixed_size_list()
7160 .values()
7161 .as_primitive();
7162 let f16_col: &Float16Array = scan_res
7163 .column_by_name("f16")
7164 .unwrap()
7165 .as_fixed_size_list()
7166 .values()
7167 .as_primitive();
7168
7169 for (f32_val, f16_val) in f32_col.iter().zip(f16_col.iter()) {
7170 let f32_val = f32_val.unwrap();
7171 let f16_val = f16_val.unwrap();
7172 assert_eq!(f16::from_f32(f32_val), f16_val);
7173 }
7174 }
7175
7176 struct ScalarIndexTestFixture {
7177 _test_dir: TempStrDir,
7178 dataset: Dataset,
7179 sample_query: Arc<dyn Array>,
7180 delete_query: Arc<dyn Array>,
7181 original_version: u64,
7183 compact_version: u64,
7185 append_version: u64,
7187 updated_version: u64,
7189 delete_version: u64,
7191 append_then_delete_version: u64,
7193 }
7194
7195 #[derive(Debug, PartialEq)]
7196 struct ScalarTestParams {
7197 use_index: bool,
7198 use_projection: bool,
7199 use_deleted_data: bool,
7200 use_new_data: bool,
7201 with_row_id: bool,
7202 use_compaction: bool,
7203 use_updated: bool,
7204 }
7205
7206 impl ScalarIndexTestFixture {
7207 async fn new(data_storage_version: LanceFileVersion, use_stable_row_ids: bool) -> Self {
7208 let test_dir = TempStrDir::default();
7209 let test_uri = &test_dir;
7210
7211 let data = gen_batch()
7217 .col(
7218 "vector",
7219 array::rand_vec::<Float32Type>(Dimension::from(32)),
7220 )
7221 .col("indexed", array::step::<Int32Type>())
7222 .col("not_indexed", array::step::<Int32Type>())
7223 .into_batch_rows(RowCount::from(1000))
7224 .unwrap();
7225
7226 let mut dataset = Dataset::write(
7228 RecordBatchIterator::new(vec![Ok(data.clone())], data.schema().clone()),
7229 test_uri,
7230 Some(WriteParams {
7231 max_rows_per_file: 500,
7232 data_storage_version: Some(data_storage_version),
7233 enable_stable_row_ids: use_stable_row_ids,
7234 ..Default::default()
7235 }),
7236 )
7237 .await
7238 .unwrap();
7239
7240 dataset
7241 .create_index(
7242 &["vector"],
7243 IndexType::Vector,
7244 None,
7245 &VectorIndexParams::ivf_pq(2, 8, 2, MetricType::L2, 2),
7246 false,
7247 )
7248 .await
7249 .unwrap();
7250
7251 dataset
7252 .create_index(
7253 &["indexed"],
7254 IndexType::Scalar,
7255 None,
7256 &ScalarIndexParams::default(),
7257 false,
7258 )
7259 .await
7260 .unwrap();
7261
7262 let original_version = dataset.version().version;
7263 let sample_query = data["vector"].as_fixed_size_list().value(50);
7264 let delete_query = data["vector"].as_fixed_size_list().value(75);
7265
7266 let new_indexed =
7271 arrow_arith::numeric::add(&data["indexed"], &Int32Array::new_scalar(1000)).unwrap();
7272 let new_not_indexed =
7273 arrow_arith::numeric::add(&data["indexed"], &Int32Array::new_scalar(1000)).unwrap();
7274 let append_data = RecordBatch::try_new(
7275 data.schema(),
7276 vec![data["vector"].clone(), new_indexed, new_not_indexed],
7277 )
7278 .unwrap();
7279
7280 dataset
7281 .append(
7282 RecordBatchIterator::new(vec![Ok(append_data)], data.schema()),
7283 Some(WriteParams {
7284 data_storage_version: Some(data_storage_version),
7285 ..Default::default()
7286 }),
7287 )
7288 .await
7289 .unwrap();
7290
7291 let append_version = dataset.version().version;
7292
7293 dataset
7296 .optimize_indices(&OptimizeOptions::merge(1))
7297 .await
7298 .unwrap();
7299 let updated_version = dataset.version().version;
7300
7301 dataset.checkout_version(append_version).await.unwrap();
7304 dataset.restore().await.unwrap();
7305
7306 dataset.delete("not_indexed = 75").await.unwrap();
7307
7308 let append_then_delete_version = dataset.version().version;
7309
7310 let mut dataset = dataset.checkout_version(original_version).await.unwrap();
7313 dataset.restore().await.unwrap();
7314
7315 dataset.delete("not_indexed = 75").await.unwrap();
7316
7317 let delete_version = dataset.version().version;
7318
7319 compact_files(&mut dataset, CompactionOptions::default(), None)
7321 .await
7322 .unwrap();
7323 let compact_version = dataset.version().version;
7324 dataset.checkout_version(original_version).await.unwrap();
7325 dataset.restore().await.unwrap();
7326
7327 Self {
7328 _test_dir: test_dir,
7329 dataset,
7330 sample_query,
7331 delete_query,
7332 original_version,
7333 compact_version,
7334 append_version,
7335 updated_version,
7336 delete_version,
7337 append_then_delete_version,
7338 }
7339 }
7340
7341 fn sample_query(&self) -> &PrimitiveArray<Float32Type> {
7342 self.sample_query.as_primitive::<Float32Type>()
7343 }
7344
7345 fn delete_query(&self) -> &PrimitiveArray<Float32Type> {
7346 self.delete_query.as_primitive::<Float32Type>()
7347 }
7348
7349 async fn get_dataset(&self, params: &ScalarTestParams) -> Dataset {
7350 let version = if params.use_compaction {
7351 if params.use_deleted_data || params.use_new_data || params.use_updated {
7353 panic!(
7354 "There is no test data combining new/deleted/updated data with compaction"
7355 );
7356 } else {
7357 self.compact_version
7358 }
7359 } else if params.use_updated {
7360 if params.use_deleted_data || params.use_new_data || params.use_compaction {
7362 panic!(
7363 "There is no test data combining updated data with new/deleted/compaction"
7364 );
7365 } else {
7366 self.updated_version
7367 }
7368 } else {
7369 match (params.use_new_data, params.use_deleted_data) {
7370 (false, false) => self.original_version,
7371 (false, true) => self.delete_version,
7372 (true, false) => self.append_version,
7373 (true, true) => self.append_then_delete_version,
7374 }
7375 };
7376 self.dataset.checkout_version(version).await.unwrap()
7377 }
7378
7379 async fn run_query(
7380 &self,
7381 query: &str,
7382 vector: Option<&PrimitiveArray<Float32Type>>,
7383 params: &ScalarTestParams,
7384 ) -> (String, RecordBatch) {
7385 let dataset = self.get_dataset(params).await;
7386 let mut scan = dataset.scan();
7387 if let Some(vector) = vector {
7388 scan.nearest("vector", vector, 10).unwrap();
7389 }
7390 if params.use_projection {
7391 scan.project(&["indexed"]).unwrap();
7392 }
7393 if params.with_row_id {
7394 scan.with_row_id();
7395 }
7396 scan.scan_in_order(true);
7397 scan.use_index(params.use_index);
7398 scan.filter(query).unwrap();
7399 scan.prefilter(true);
7400
7401 let plan = scan.explain_plan(true).await.unwrap();
7402 let batch = scan.try_into_batch().await.unwrap();
7403
7404 if params.use_projection {
7405 let mut expected_columns = 1;
7407 if vector.is_some() {
7408 expected_columns += 1;
7410 }
7411 if params.with_row_id {
7412 expected_columns += 1;
7413 }
7414 assert_eq!(batch.num_columns(), expected_columns);
7415 } else {
7416 let mut expected_columns = 3;
7417 if vector.is_some() {
7418 expected_columns += 1;
7420 }
7421 if params.with_row_id {
7422 expected_columns += 1;
7423 }
7424 assert_eq!(batch.num_columns(), expected_columns);
7426 }
7427
7428 (plan, batch)
7429 }
7430
7431 fn assert_none<F: Fn(i32) -> bool>(
7432 &self,
7433 batch: &RecordBatch,
7434 predicate: F,
7435 message: &str,
7436 ) {
7437 let indexed = batch["indexed"].as_primitive::<Int32Type>();
7438 if indexed.iter().map(|val| val.unwrap()).any(predicate) {
7439 panic!("{}", message);
7440 }
7441 }
7442
7443 fn assert_one<F: Fn(i32) -> bool>(&self, batch: &RecordBatch, predicate: F, message: &str) {
7444 let indexed = batch["indexed"].as_primitive::<Int32Type>();
7445 if !indexed.iter().map(|val| val.unwrap()).any(predicate) {
7446 panic!("{}", message);
7447 }
7448 }
7449
7450 async fn check_vector_scalar_indexed_and_refine(&self, params: &ScalarTestParams) {
7451 let (query_plan, batch) = self
7452 .run_query(
7453 "indexed != 50 AND ((not_indexed < 100) OR (not_indexed >= 1000 AND not_indexed < 1100))",
7454 Some(self.sample_query()),
7455 params,
7456 )
7457 .await;
7458 if self.dataset.is_legacy_storage() {
7460 assert!(query_plan.contains("MaterializeIndex"));
7461 }
7462 self.assert_none(
7464 &batch,
7465 |val| val == 50,
7466 "The query contained 50 even though it was filtered",
7467 );
7468 if !params.use_new_data {
7469 self.assert_none(
7471 &batch,
7472 |val| (100..1000).contains(&val) || (val >= 1100),
7473 "The non-indexed refine filter was not applied",
7474 );
7475 }
7476
7477 if params.use_new_data || params.use_updated {
7479 self.assert_one(
7480 &batch,
7481 |val| val == 1050,
7482 "The query did not contain 1050 from the new data",
7483 );
7484 }
7485 }
7486
7487 async fn check_vector_scalar_indexed_only(&self, params: &ScalarTestParams) {
7488 let (query_plan, batch) = self
7489 .run_query("indexed != 50", Some(self.sample_query()), params)
7490 .await;
7491 if self.dataset.is_legacy_storage() {
7492 if params.use_index {
7493 assert!(query_plan.contains("ScalarIndexQuery"));
7496 } else {
7497 assert!(query_plan.contains("MaterializeIndex"));
7499 }
7500 }
7501 self.assert_none(
7503 &batch,
7504 |val| val == 50,
7505 "The query contained 50 even though it was filtered",
7506 );
7507 if params.use_new_data {
7509 self.assert_one(
7510 &batch,
7511 |val| val == 1050,
7512 "The query did not contain 1050 from the new data",
7513 );
7514 if !params.use_new_data {
7515 let (_, batch) = self
7517 .run_query("indexed == 1050", Some(self.sample_query()), params)
7518 .await;
7519 assert_eq!(batch.num_rows(), 1);
7520 }
7521 }
7522 if params.use_deleted_data {
7523 let (_, batch) = self
7524 .run_query("indexed == 75", Some(self.delete_query()), params)
7525 .await;
7526 if !params.use_new_data {
7527 assert_eq!(batch.num_rows(), 0);
7528 }
7529 }
7530 }
7531
7532 async fn check_vector_queries(&self, params: &ScalarTestParams) {
7533 self.check_vector_scalar_indexed_only(params).await;
7534 self.check_vector_scalar_indexed_and_refine(params).await;
7535 }
7536
7537 async fn check_simple_indexed_only(&self, params: &ScalarTestParams) {
7538 let (query_plan, batch) = self.run_query("indexed != 50", None, params).await;
7539 if self.dataset.is_legacy_storage() {
7541 assert!(query_plan.contains("MaterializeIndex"));
7542 } else {
7543 assert!(query_plan.contains("LanceRead"));
7544 }
7545 self.assert_none(
7547 &batch,
7548 |val| val == 50,
7549 "The query contained 50 even though it was filtered",
7550 );
7551 let mut expected_num_rows = if params.use_new_data || params.use_updated {
7552 1999
7553 } else {
7554 999
7555 };
7556 if params.use_deleted_data || params.use_compaction {
7557 expected_num_rows -= 1;
7558 }
7559 assert_eq!(batch.num_rows(), expected_num_rows);
7560
7561 if params.use_new_data || params.use_updated {
7563 let (_, batch) = self.run_query("indexed == 1050", None, params).await;
7564 assert_eq!(batch.num_rows(), 1);
7565 }
7566
7567 if params.use_deleted_data || params.use_compaction {
7569 let (_, batch) = self.run_query("indexed == 75", None, params).await;
7570 assert_eq!(batch.num_rows(), 0);
7571 }
7572 }
7573
7574 async fn check_simple_indexed_and_refine(&self, params: &ScalarTestParams) {
7575 let (query_plan, batch) = self.run_query(
7576 "indexed != 50 AND ((not_indexed < 100) OR (not_indexed >= 1000 AND not_indexed < 1100))",
7577 None,
7578 params
7579 ).await;
7580 if self.dataset.is_legacy_storage() {
7582 assert!(query_plan.contains("MaterializeIndex"));
7583 } else {
7584 assert!(query_plan.contains("LanceRead"));
7585 }
7586 self.assert_none(
7588 &batch,
7589 |val| val == 50,
7590 "The query contained 50 even though it was filtered",
7591 );
7592 self.assert_none(
7594 &batch,
7595 |val| (100..1000).contains(&val) || (val >= 1100),
7596 "The non-indexed refine filter was not applied",
7597 );
7598
7599 let mut expected_num_rows = if params.use_new_data || params.use_updated {
7600 199
7601 } else {
7602 99
7603 };
7604 if params.use_deleted_data || params.use_compaction {
7605 expected_num_rows -= 1;
7606 }
7607 assert_eq!(batch.num_rows(), expected_num_rows);
7608 }
7609
7610 async fn check_simple_queries(&self, params: &ScalarTestParams) {
7611 self.check_simple_indexed_only(params).await;
7612 self.check_simple_indexed_and_refine(params).await;
7613 }
7614 }
7615
7616 #[rstest]
7620 #[tokio::test]
7621 async fn test_secondary_index_scans(
7622 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
7623 data_storage_version: LanceFileVersion,
7624 #[values(false, true)] use_stable_row_ids: bool,
7625 ) {
7626 let fixture = Box::pin(ScalarIndexTestFixture::new(
7627 data_storage_version,
7628 use_stable_row_ids,
7629 ))
7630 .await;
7631
7632 for use_index in [false, true] {
7633 for use_projection in [false, true] {
7634 for use_deleted_data in [false, true] {
7635 for use_new_data in [false, true] {
7636 let compaction_choices =
7641 if use_deleted_data || use_new_data || use_stable_row_ids {
7642 vec![false]
7643 } else {
7644 vec![false, true]
7645 };
7646 for use_compaction in compaction_choices {
7647 let updated_choices =
7648 if use_deleted_data || use_new_data || use_compaction {
7649 vec![false]
7650 } else {
7651 vec![false, true]
7652 };
7653 for use_updated in updated_choices {
7654 for with_row_id in [false, true] {
7655 let params = ScalarTestParams {
7656 use_index,
7657 use_projection,
7658 use_deleted_data,
7659 use_new_data,
7660 with_row_id,
7661 use_compaction,
7662 use_updated,
7663 };
7664 fixture.check_vector_queries(¶ms).await;
7665 fixture.check_simple_queries(¶ms).await;
7666 }
7667 }
7668 }
7669 }
7670 }
7671 }
7672 }
7673 }
7674
7675 #[tokio::test]
7676 async fn can_filter_row_id() {
7677 let dataset = lance_datagen::gen_batch()
7678 .col("x", array::step::<Int32Type>())
7679 .into_ram_dataset(FragmentCount::from(1), FragmentRowCount::from(1000))
7680 .await
7681 .unwrap();
7682
7683 let mut scan = dataset.scan();
7684 scan.with_row_id();
7685 scan.project::<&str>(&[]).unwrap();
7686 scan.filter("_rowid == 50").unwrap();
7687 let batch = scan.try_into_batch().await.unwrap();
7688 assert_eq!(batch.num_rows(), 1);
7689 assert_eq!(batch.column(0).as_primitive::<UInt64Type>().values()[0], 50);
7690 }
7691
7692 #[rstest]
7693 #[tokio::test]
7694 async fn test_index_take_batch_size() {
7695 let fixture = Box::pin(ScalarIndexTestFixture::new(LanceFileVersion::Stable, false)).await;
7696 let stream = fixture
7697 .dataset
7698 .scan()
7699 .filter("indexed > 0")
7700 .unwrap()
7701 .batch_size(16)
7702 .try_into_stream()
7703 .await
7704 .unwrap();
7705 let batches = stream.collect::<Vec<_>>().await;
7706 assert_eq!(batches.len(), 1000_usize.div_ceil(16));
7707 }
7708
7709 async fn assert_plan_equals(
7713 dataset: &Dataset,
7714 plan: impl Fn(&mut Scanner) -> Result<&mut Scanner>,
7715 expected: &str,
7716 ) -> Result<()> {
7717 let mut scan = dataset.scan();
7718 plan(&mut scan)?;
7719 let exec_plan = scan.create_plan().await?;
7720 assert_plan_node_equals(exec_plan, expected).await
7721 }
7722
7723 #[tokio::test]
7724 async fn test_inexact_scalar_index_plans() {
7725 let data = gen_batch()
7726 .col("ngram", array::rand_utf8(ByteCount::from(5), false))
7727 .col("exact", array::rand_type(&DataType::UInt32))
7728 .col("no_index", array::rand_type(&DataType::UInt32))
7729 .into_reader_rows(RowCount::from(1000), BatchCount::from(5));
7730
7731 let mut dataset = Dataset::write(data, "memory://test", None).await.unwrap();
7732 dataset
7733 .create_index(
7734 &["ngram"],
7735 IndexType::NGram,
7736 None,
7737 &ScalarIndexParams::default(),
7738 true,
7739 )
7740 .await
7741 .unwrap();
7742 dataset
7743 .create_index(
7744 &["exact"],
7745 IndexType::BTree,
7746 None,
7747 &ScalarIndexParams::default(),
7748 true,
7749 )
7750 .await
7751 .unwrap();
7752
7753 assert_plan_equals(
7755 &dataset,
7756 |scanner| scanner.filter("contains(ngram, 'test string')"),
7757 "LanceRead: uri=..., projection=[ngram, exact, no_index], num_fragments=1, \
7758 range_before=None, range_after=None, row_id=false, row_addr=false, \
7759 full_filter=contains(ngram, Utf8(\"test string\")), refine_filter=--
7760 ScalarIndexQuery: query=[contains(ngram, Utf8(\"test string\"))]@ngram_idx",
7761 )
7762 .await
7763 .unwrap();
7764
7765 assert_plan_equals(
7767 &dataset,
7768 |scanner| scanner.filter("contains(ngram, 'test string') and exact < 50"),
7769 "LanceRead: uri=..., projection=[ngram, exact, no_index], num_fragments=1, \
7770 range_before=None, range_after=None, row_id=false, row_addr=false, \
7771 full_filter=contains(ngram, Utf8(\"test string\")) AND exact < UInt32(50), \
7772 refine_filter=--
7773 ScalarIndexQuery: query=AND([contains(ngram, Utf8(\"test string\"))]@ngram_idx,[exact < 50]@exact_idx)",
7774 )
7775 .await
7776 .unwrap();
7777
7778 assert_plan_equals(
7780 &dataset,
7781 |scanner| {
7782 scanner.filter("contains(ngram, 'test string') and exact < 50 AND no_index > 100")
7783 },
7784 "ProjectionExec: expr=[ngram@0 as ngram, exact@1 as exact, no_index@2 as no_index]
7785 LanceRead: uri=..., projection=[ngram, exact, no_index], num_fragments=1, range_before=None, \
7786 range_after=None, row_id=true, row_addr=false, full_filter=contains(ngram, Utf8(\"test string\")) AND exact < UInt32(50) AND no_index > UInt32(100), \
7787 refine_filter=no_index > UInt32(100)
7788 ScalarIndexQuery: query=AND([contains(ngram, Utf8(\"test string\"))]@ngram_idx,[exact < 50]@exact_idx)",
7789 )
7790 .await
7791 .unwrap();
7792 }
7793
7794 #[tokio::test]
7795 async fn test_like_prefix_with_btree_index() {
7796 let data = gen_batch()
7799 .col(
7800 "name",
7801 array::cycle_utf8_literals(&[
7802 "apple",
7803 "application",
7804 "app",
7805 "banana",
7806 "band",
7807 "testns1",
7808 "testns2",
7809 "test",
7810 "testing",
7811 "zoo",
7812 ]),
7813 )
7814 .col("id", array::step::<Int32Type>())
7815 .into_reader_rows(RowCount::from(100), BatchCount::from(1));
7816
7817 let mut dataset = Dataset::write(data, "memory://test_like", None)
7818 .await
7819 .unwrap();
7820
7821 dataset
7823 .create_index(
7824 &["name"],
7825 IndexType::BTree,
7826 None,
7827 &ScalarIndexParams::default(),
7828 true,
7829 )
7830 .await
7831 .unwrap();
7832
7833 assert_plan_equals(
7835 &dataset,
7836 |scanner| scanner.filter("name LIKE 'app%'"),
7837 "LanceRead: uri=..., projection=[name, id], num_fragments=1, \
7838 range_before=None, range_after=None, row_id=false, row_addr=false, \
7839 full_filter=name LIKE Utf8(\"app%\"), refine_filter=--
7840 ScalarIndexQuery: query=[name LIKE 'app%']@name_idx",
7841 )
7842 .await
7843 .unwrap();
7844
7845 let results = dataset
7847 .scan()
7848 .filter("name LIKE 'app%'")
7849 .unwrap()
7850 .try_into_batch()
7851 .await
7852 .unwrap();
7853 let names: Vec<&str> = results
7854 .column_by_name("name")
7855 .unwrap()
7856 .as_any()
7857 .downcast_ref::<StringArray>()
7858 .unwrap()
7859 .iter()
7860 .map(|s| s.unwrap())
7861 .collect();
7862 assert!(names.iter().all(|n| n.starts_with("app")));
7864 assert!(!names.is_empty());
7865
7866 assert_plan_equals(
7869 &dataset,
7870 |scanner| scanner.filter("starts_with(name, 'ban')"),
7871 "LanceRead: uri=..., projection=[name, id], num_fragments=1, \
7872 range_before=None, range_after=None, row_id=false, row_addr=false, \
7873 full_filter=name LIKE Utf8(\"ban%\"), refine_filter=--
7874 ScalarIndexQuery: query=[name LIKE 'ban%']@name_idx",
7875 )
7876 .await
7877 .unwrap();
7878
7879 let results = dataset
7881 .scan()
7882 .filter("starts_with(name, 'ban')")
7883 .unwrap()
7884 .try_into_batch()
7885 .await
7886 .unwrap();
7887 let names: Vec<&str> = results
7888 .column_by_name("name")
7889 .unwrap()
7890 .as_any()
7891 .downcast_ref::<StringArray>()
7892 .unwrap()
7893 .iter()
7894 .map(|s| s.unwrap())
7895 .collect();
7896 assert!(names.iter().all(|n| n.starts_with("ban")));
7898 assert!(!names.is_empty());
7899
7900 assert_plan_equals(
7902 &dataset,
7903 |scanner| scanner.filter("name LIKE 'test%2'"),
7904 "ProjectionExec: expr=[name@0 as name, id@1 as id]
7905 LanceRead: uri=..., projection=[name, id], num_fragments=1, \
7906range_before=None, range_after=None, row_id=true, row_addr=false, \
7907full_filter=name LIKE Utf8(\"test%2\"), refine_filter=name LIKE Utf8(\"test%2\")
7908 ScalarIndexQuery: query=[name LIKE 'test%']@name_idx",
7909 )
7910 .await
7911 .unwrap();
7912
7913 let results = dataset
7915 .scan()
7916 .filter("name LIKE 'test%2'")
7917 .unwrap()
7918 .try_into_batch()
7919 .await
7920 .unwrap();
7921 let names: Vec<&str> = results
7922 .column_by_name("name")
7923 .unwrap()
7924 .as_any()
7925 .downcast_ref::<StringArray>()
7926 .unwrap()
7927 .iter()
7928 .map(|s| s.unwrap())
7929 .collect();
7930 assert!(
7932 names
7933 .iter()
7934 .all(|n| n.starts_with("test") && n.ends_with("2"))
7935 );
7936
7937 let mut scanner = dataset.scan();
7940 scanner.filter("name LIKE '%app%'").unwrap();
7941 let plan = scanner.create_plan().await.unwrap();
7942 let plan_str = format!("{:?}", plan);
7943 assert!(
7944 !plan_str.contains("ScalarIndexQuery"),
7945 "LIKE '%app%' should not use scalar index, but got: {}",
7946 plan_str
7947 );
7948
7949 let results = dataset
7951 .scan()
7952 .filter("name LIKE '%app%'")
7953 .unwrap()
7954 .try_into_batch()
7955 .await
7956 .unwrap();
7957 let names: Vec<&str> = results
7958 .column_by_name("name")
7959 .unwrap()
7960 .as_any()
7961 .downcast_ref::<StringArray>()
7962 .unwrap()
7963 .iter()
7964 .map(|s| s.unwrap())
7965 .collect();
7966 assert!(names.iter().all(|n| n.contains("app")));
7968
7969 let mut scanner = dataset.scan();
7971 scanner.filter("name NOT LIKE 'app%'").unwrap();
7972 let plan = scanner.create_plan().await.unwrap();
7973 let plan_str = format!("{:?}", plan);
7974 assert!(
7975 !plan_str.contains("ScalarIndexQuery"),
7976 "NOT LIKE should not use scalar index, but got: {}",
7977 plan_str
7978 );
7979 }
7980
7981 #[tokio::test]
7982 async fn test_like_prefix_correctness_with_btree_index() {
7983 let names: Vec<&str> = vec![
7985 "alpha", "alphabet", "beta", "gamma", "delta", "epsilon", "eta", "theta", "iota",
7986 "kappa",
7987 ];
7988 let data = RecordBatch::try_new(
7989 Arc::new(ArrowSchema::new(vec![
7990 ArrowField::new("name", DataType::Utf8, false),
7991 ArrowField::new("id", DataType::Int32, false),
7992 ])),
7993 vec![
7994 Arc::new(StringArray::from(names.clone())),
7995 Arc::new(Int32Array::from_iter_values(0..10)),
7996 ],
7997 )
7998 .unwrap();
7999
8000 let reader = RecordBatchIterator::new(
8001 vec![Ok(data)],
8002 Arc::new(ArrowSchema::new(vec![
8003 ArrowField::new("name", DataType::Utf8, false),
8004 ArrowField::new("id", DataType::Int32, false),
8005 ])),
8006 );
8007
8008 let mut dataset = Dataset::write(reader, "memory://test_like_correctness", None)
8009 .await
8010 .unwrap();
8011
8012 dataset
8014 .create_index(
8015 &["name"],
8016 IndexType::BTree,
8017 None,
8018 &ScalarIndexParams::default(),
8019 true,
8020 )
8021 .await
8022 .unwrap();
8023
8024 let with_index = dataset
8026 .scan()
8027 .filter("name LIKE 'alpha%'")
8028 .unwrap()
8029 .try_into_batch()
8030 .await
8031 .unwrap();
8032
8033 let without_index = dataset
8035 .scan()
8036 .use_scalar_index(false)
8037 .filter("name LIKE 'alpha%'")
8038 .unwrap()
8039 .try_into_batch()
8040 .await
8041 .unwrap();
8042
8043 assert_eq!(with_index.num_rows(), without_index.num_rows());
8045 assert_eq!(with_index.num_rows(), 2);
8046
8047 let with_index_names: BTreeSet<String> = with_index
8048 .column_by_name("name")
8049 .unwrap()
8050 .as_any()
8051 .downcast_ref::<StringArray>()
8052 .unwrap()
8053 .iter()
8054 .map(|s| s.unwrap().to_string())
8055 .collect();
8056
8057 let without_index_names: BTreeSet<String> = without_index
8058 .column_by_name("name")
8059 .unwrap()
8060 .as_any()
8061 .downcast_ref::<StringArray>()
8062 .unwrap()
8063 .iter()
8064 .map(|s| s.unwrap().to_string())
8065 .collect();
8066
8067 assert_eq!(with_index_names, without_index_names);
8068 assert_eq!(
8069 with_index_names,
8070 BTreeSet::from(["alpha".to_string(), "alphabet".to_string()])
8071 );
8072
8073 let starts_with_result = dataset
8075 .scan()
8076 .filter("starts_with(name, 'e')")
8077 .unwrap()
8078 .try_into_batch()
8079 .await
8080 .unwrap();
8081
8082 let starts_with_names: BTreeSet<String> = starts_with_result
8083 .column_by_name("name")
8084 .unwrap()
8085 .as_any()
8086 .downcast_ref::<StringArray>()
8087 .unwrap()
8088 .iter()
8089 .map(|s| s.unwrap().to_string())
8090 .collect();
8091
8092 assert_eq!(
8094 starts_with_names,
8095 BTreeSet::from(["epsilon".to_string(), "eta".to_string()])
8096 );
8097 }
8098
8099 #[tokio::test]
8100 async fn test_like_prefix_with_zone_map() {
8101 use lance_index::scalar::BuiltinIndexType;
8102
8103 let data = gen_batch()
8105 .col(
8106 "name",
8107 array::cycle_utf8_literals(&[
8108 "apple",
8109 "application",
8110 "app",
8111 "banana",
8112 "band",
8113 "testns1",
8114 "testns2",
8115 "test",
8116 "testing",
8117 "zoo",
8118 ]),
8119 )
8120 .col("id", array::step::<Int32Type>())
8121 .into_reader_rows(RowCount::from(100), BatchCount::from(1));
8122
8123 let mut dataset = Dataset::write(data, "memory://test_like_zonemap", None)
8124 .await
8125 .unwrap();
8126
8127 let params = ScalarIndexParams::for_builtin(BuiltinIndexType::ZoneMap);
8129 dataset
8130 .create_index(
8131 &["name"],
8132 IndexType::Scalar,
8133 Some("name_zonemap".to_string()),
8134 ¶ms,
8135 true,
8136 )
8137 .await
8138 .unwrap();
8139
8140 let mut scanner = dataset.scan();
8142 scanner.filter("name LIKE 'app%'").unwrap();
8143 let plan = scanner.create_plan().await.unwrap();
8144 let plan_str = format!("{:?}", plan);
8145 assert!(
8147 plan_str.contains("ScalarIndexExec") && plan_str.contains("LikePrefix"),
8148 "LIKE 'app%' should use zone map index with LikePrefix, but got: {}",
8149 plan_str
8150 );
8151
8152 let results = dataset
8154 .scan()
8155 .filter("name LIKE 'app%'")
8156 .unwrap()
8157 .try_into_batch()
8158 .await
8159 .unwrap();
8160 let names: Vec<&str> = results
8161 .column_by_name("name")
8162 .unwrap()
8163 .as_any()
8164 .downcast_ref::<StringArray>()
8165 .unwrap()
8166 .iter()
8167 .map(|s| s.unwrap())
8168 .collect();
8169 assert!(names.iter().all(|n| n.starts_with("app")));
8170 assert!(!names.is_empty());
8171
8172 let mut scanner = dataset.scan();
8174 scanner.filter("starts_with(name, 'ban')").unwrap();
8175 let plan = scanner.create_plan().await.unwrap();
8176 let plan_str = format!("{:?}", plan);
8177 assert!(
8178 plan_str.contains("ScalarIndexExec") && plan_str.contains("LikePrefix"),
8179 "starts_with should use zone map index with LikePrefix, but got: {}",
8180 plan_str
8181 );
8182
8183 let results = dataset
8185 .scan()
8186 .filter("starts_with(name, 'ban')")
8187 .unwrap()
8188 .try_into_batch()
8189 .await
8190 .unwrap();
8191 let names: Vec<&str> = results
8192 .column_by_name("name")
8193 .unwrap()
8194 .as_any()
8195 .downcast_ref::<StringArray>()
8196 .unwrap()
8197 .iter()
8198 .map(|s| s.unwrap())
8199 .collect();
8200 assert!(names.iter().all(|n| n.starts_with("ban")));
8201
8202 let mut scanner = dataset.scan();
8204 scanner.filter("name LIKE 'test%2'").unwrap();
8205 let plan = scanner.create_plan().await.unwrap();
8206 let plan_str = format!("{:?}", plan);
8207 assert!(
8208 plan_str.contains("ScalarIndexExec") && plan_str.contains("LikePrefix"),
8209 "LIKE 'test%2' should use zone map index for prefix, but got: {}",
8210 plan_str
8211 );
8212
8213 let mut scanner = dataset.scan();
8215 scanner.filter("name LIKE '%app%'").unwrap();
8216 let plan = scanner.create_plan().await.unwrap();
8217 let plan_str = format!("{:?}", plan);
8218 assert!(
8219 !plan_str.contains("LikePrefix"),
8220 "LIKE '%app%' should not use LikePrefix index, but got: {}",
8221 plan_str
8222 );
8223 }
8224
8225 #[tokio::test]
8226 async fn test_like_prefix_with_segmented_zone_map() {
8227 use lance_index::scalar::BuiltinIndexType;
8228
8229 let data = gen_batch()
8230 .col(
8231 "name",
8232 array::cycle_utf8_literals(&[
8233 "apple",
8234 "application",
8235 "app",
8236 "banana",
8237 "band",
8238 "testns1",
8239 "testns2",
8240 "test",
8241 "testing",
8242 "zoo",
8243 ]),
8244 )
8245 .col("id", array::step::<Int32Type>())
8246 .into_reader_rows(RowCount::from(150), BatchCount::from(6));
8247
8248 let write_params = WriteParams {
8249 max_rows_per_file: 25,
8250 max_rows_per_group: 10,
8251 ..Default::default()
8252 };
8253
8254 let mut dataset = Dataset::write(
8255 data,
8256 "memory://test_like_segmented_zonemap",
8257 Some(write_params),
8258 )
8259 .await
8260 .unwrap();
8261
8262 let fragments = dataset.get_fragments();
8263 assert!(fragments.len() > 1, "expected multiple fragments");
8264
8265 let params = ScalarIndexParams::for_builtin(BuiltinIndexType::ZoneMap);
8266 let mut segments = Vec::with_capacity(fragments.len());
8267 for fragment in &fragments {
8268 let mut builder = dataset.create_index_builder(&["name"], IndexType::Scalar, ¶ms);
8269 builder = builder
8270 .name("name_zonemap".to_string())
8271 .fragments(vec![fragment.id() as u32]);
8272 segments.push(builder.execute_uncommitted().await.unwrap());
8273 }
8274
8275 dataset
8276 .commit_existing_index_segments("name_zonemap", "name", segments)
8277 .await
8278 .unwrap();
8279
8280 let committed = dataset.load_indices_by_name("name_zonemap").await.unwrap();
8281 assert_eq!(committed.len(), fragments.len());
8282
8283 let mut scanner = dataset.scan();
8284 scanner.filter("name LIKE 'app%'").unwrap();
8285 let plan = scanner.create_plan().await.unwrap();
8286 let plan_str = format!("{:?}", plan);
8287 assert!(
8288 plan_str.contains("ScalarIndexExec") && plan_str.contains("LikePrefix"),
8289 "segmented zonemap should use LikePrefix pruning, but got: {}",
8290 plan_str
8291 );
8292
8293 let with_index = dataset
8294 .scan()
8295 .filter("name LIKE 'app%'")
8296 .unwrap()
8297 .try_into_batch()
8298 .await
8299 .unwrap();
8300 let without_index = dataset
8301 .scan()
8302 .use_scalar_index(false)
8303 .filter("name LIKE 'app%'")
8304 .unwrap()
8305 .try_into_batch()
8306 .await
8307 .unwrap();
8308
8309 let with_index_ids = with_index
8310 .column_by_name("id")
8311 .unwrap()
8312 .as_primitive::<Int32Type>()
8313 .values()
8314 .iter()
8315 .copied()
8316 .collect::<BTreeSet<_>>();
8317 let without_index_ids = without_index
8318 .column_by_name("id")
8319 .unwrap()
8320 .as_primitive::<Int32Type>()
8321 .values()
8322 .iter()
8323 .copied()
8324 .collect::<BTreeSet<_>>();
8325 assert_eq!(with_index_ids, without_index_ids);
8326 assert!(!with_index_ids.is_empty());
8327
8328 let names = with_index
8329 .column_by_name("name")
8330 .unwrap()
8331 .as_any()
8332 .downcast_ref::<StringArray>()
8333 .unwrap()
8334 .iter()
8335 .map(|value| value.unwrap())
8336 .collect::<Vec<_>>();
8337 assert!(names.iter().all(|name| name.starts_with("app")));
8338 }
8339
8340 #[tokio::test]
8341 async fn test_like_prefix_correctness_with_zone_map() {
8342 use lance_index::scalar::BuiltinIndexType;
8343
8344 let names: Vec<&str> = vec![
8346 "alpha", "alphabet", "beta", "gamma", "delta", "epsilon", "eta", "theta", "iota",
8347 "kappa",
8348 ];
8349 let data = RecordBatch::try_new(
8350 Arc::new(ArrowSchema::new(vec![
8351 ArrowField::new("name", DataType::Utf8, false),
8352 ArrowField::new("id", DataType::Int32, false),
8353 ])),
8354 vec![
8355 Arc::new(StringArray::from(names.clone())),
8356 Arc::new(Int32Array::from_iter_values(0..10)),
8357 ],
8358 )
8359 .unwrap();
8360
8361 let reader = RecordBatchIterator::new(
8362 vec![Ok(data)],
8363 Arc::new(ArrowSchema::new(vec![
8364 ArrowField::new("name", DataType::Utf8, false),
8365 ArrowField::new("id", DataType::Int32, false),
8366 ])),
8367 );
8368
8369 let mut dataset = Dataset::write(reader, "memory://test_like_correctness_zonemap", None)
8370 .await
8371 .unwrap();
8372
8373 let params = ScalarIndexParams::for_builtin(BuiltinIndexType::ZoneMap);
8375 dataset
8376 .create_index(
8377 &["name"],
8378 IndexType::Scalar,
8379 Some("name_zonemap".to_string()),
8380 ¶ms,
8381 true,
8382 )
8383 .await
8384 .unwrap();
8385
8386 let with_index = dataset
8388 .scan()
8389 .filter("name LIKE 'alpha%'")
8390 .unwrap()
8391 .try_into_batch()
8392 .await
8393 .unwrap();
8394
8395 let without_index = dataset
8397 .scan()
8398 .use_scalar_index(false)
8399 .filter("name LIKE 'alpha%'")
8400 .unwrap()
8401 .try_into_batch()
8402 .await
8403 .unwrap();
8404
8405 assert_eq!(with_index.num_rows(), without_index.num_rows());
8407 assert_eq!(with_index.num_rows(), 2);
8408
8409 let with_index_names: BTreeSet<String> = with_index
8410 .column_by_name("name")
8411 .unwrap()
8412 .as_any()
8413 .downcast_ref::<StringArray>()
8414 .unwrap()
8415 .iter()
8416 .map(|s| s.unwrap().to_string())
8417 .collect();
8418
8419 let without_index_names: BTreeSet<String> = without_index
8420 .column_by_name("name")
8421 .unwrap()
8422 .as_any()
8423 .downcast_ref::<StringArray>()
8424 .unwrap()
8425 .iter()
8426 .map(|s| s.unwrap().to_string())
8427 .collect();
8428
8429 assert_eq!(with_index_names, without_index_names);
8430 assert_eq!(
8431 with_index_names,
8432 BTreeSet::from(["alpha".to_string(), "alphabet".to_string()])
8433 );
8434
8435 let starts_with_result = dataset
8437 .scan()
8438 .filter("starts_with(name, 'e')")
8439 .unwrap()
8440 .try_into_batch()
8441 .await
8442 .unwrap();
8443
8444 let starts_with_names: BTreeSet<String> = starts_with_result
8445 .column_by_name("name")
8446 .unwrap()
8447 .as_any()
8448 .downcast_ref::<StringArray>()
8449 .unwrap()
8450 .iter()
8451 .map(|s| s.unwrap().to_string())
8452 .collect();
8453
8454 assert_eq!(
8456 starts_with_names,
8457 BTreeSet::from(["epsilon".to_string(), "eta".to_string()])
8458 );
8459 }
8460
8461 #[rstest]
8462 #[tokio::test]
8463 async fn test_late_materialization(
8464 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
8465 data_storage_version: LanceFileVersion,
8466 ) {
8467 use lance_io::assert_io_lt;
8468 use lance_table::io::commit::RenameCommitHandler;
8471 let data = gen_batch()
8472 .col(
8473 "vector",
8474 array::rand_vec::<Float32Type>(Dimension::from(32)),
8475 )
8476 .col("indexed", array::step::<Int32Type>())
8477 .col("not_indexed", array::step::<Int32Type>())
8478 .into_reader_rows(RowCount::from(1000), BatchCount::from(20));
8479
8480 let mut dataset = Dataset::write(
8481 data,
8482 "memory://test",
8483 Some(WriteParams {
8484 commit_handler: Some(Arc::new(RenameCommitHandler)),
8485 data_storage_version: Some(data_storage_version),
8486 ..Default::default()
8487 }),
8488 )
8489 .await
8490 .unwrap();
8491 dataset
8492 .create_index(
8493 &["indexed"],
8494 IndexType::Scalar,
8495 None,
8496 &ScalarIndexParams::default(),
8497 false,
8498 )
8499 .await
8500 .unwrap();
8501
8502 let _ = dataset.object_store().io_stats_incremental(); dataset.scan().try_into_batch().await.unwrap();
8505 let io_stats = dataset.object_store().io_stats_incremental();
8506 let full_scan_bytes = io_stats.read_bytes;
8507
8508 dataset
8510 .scan()
8511 .use_stats(false)
8512 .filter("not_indexed = 50")
8513 .unwrap()
8514 .try_into_batch()
8515 .await
8516 .unwrap();
8517 let io_stats = dataset.object_store().io_stats_incremental();
8518 assert_io_lt!(io_stats, read_bytes, full_scan_bytes);
8519 let filtered_scan_bytes = io_stats.read_bytes;
8520
8521 if data_storage_version == LanceFileVersion::Legacy {
8524 dataset
8525 .scan()
8526 .filter("not_indexed = 50")
8527 .unwrap()
8528 .try_into_batch()
8529 .await
8530 .unwrap();
8531 let io_stats = dataset.object_store().io_stats_incremental();
8532 assert_io_lt!(io_stats, read_bytes, filtered_scan_bytes);
8533 }
8534
8535 dataset
8539 .scan()
8540 .filter("indexed = 50")
8541 .unwrap()
8542 .try_into_batch()
8543 .await
8544 .unwrap();
8545 let io_stats = dataset.object_store().io_stats_incremental();
8546 assert_io_lt!(io_stats, read_bytes, full_scan_bytes);
8547 let index_scan_bytes = io_stats.read_bytes;
8548
8549 dataset
8552 .scan()
8553 .filter("indexed = 50")
8554 .unwrap()
8555 .try_into_batch()
8556 .await
8557 .unwrap();
8558 let io_stats = dataset.object_store().io_stats_incremental();
8559 assert_io_lt!(io_stats, read_bytes, index_scan_bytes);
8560 }
8561
8562 #[rstest]
8563 #[tokio::test]
8564 async fn test_project_nested(
8565 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
8566 data_storage_version: LanceFileVersion,
8567 ) -> Result<()> {
8568 let struct_i_field = ArrowField::new("i", DataType::Int32, true);
8569 let struct_o_field = ArrowField::new("o", DataType::Utf8, true);
8570 let schema = Arc::new(ArrowSchema::new(vec![
8571 ArrowField::new(
8572 "struct",
8573 DataType::Struct(vec![struct_i_field.clone(), struct_o_field.clone()].into()),
8574 true,
8575 ),
8576 ArrowField::new("s", DataType::Utf8, true),
8577 ]));
8578
8579 let input_batches: Vec<RecordBatch> = (0..5)
8580 .map(|i| {
8581 let struct_i_arr: Arc<Int32Array> =
8582 Arc::new(Int32Array::from_iter_values(i * 20..(i + 1) * 20));
8583 let struct_o_arr: Arc<StringArray> = Arc::new(StringArray::from_iter_values(
8584 (i * 20..(i + 1) * 20).map(|v| format!("o-{:02}", v)),
8585 ));
8586 RecordBatch::try_new(
8587 schema.clone(),
8588 vec![
8589 Arc::new(StructArray::from(vec![
8590 (Arc::new(struct_i_field.clone()), struct_i_arr as ArrayRef),
8591 (Arc::new(struct_o_field.clone()), struct_o_arr as ArrayRef),
8592 ])),
8593 Arc::new(StringArray::from_iter_values(
8594 (i * 20..(i + 1) * 20).map(|v| format!("s-{}", v)),
8595 )),
8596 ],
8597 )
8598 .unwrap()
8599 })
8600 .collect();
8601 let batches =
8602 RecordBatchIterator::new(input_batches.clone().into_iter().map(Ok), schema.clone());
8603 let test_dir = TempStrDir::default();
8604 let test_uri = &test_dir;
8605 let write_params = WriteParams {
8606 max_rows_per_file: 40,
8607 max_rows_per_group: 10,
8608 data_storage_version: Some(data_storage_version),
8609 ..Default::default()
8610 };
8611 Dataset::write(batches, test_uri, Some(write_params))
8612 .await
8613 .unwrap();
8614
8615 let dataset = Dataset::open(test_uri).await.unwrap();
8616
8617 let batches = dataset
8618 .scan()
8619 .project(&["struct.i"])
8620 .unwrap()
8621 .try_into_stream()
8622 .await
8623 .unwrap()
8624 .try_collect::<Vec<_>>()
8625 .await
8626 .unwrap();
8627 let batch = concat_batches(&batches[0].schema(), &batches).unwrap();
8628 assert!(batch.column_by_name("struct.i").is_some());
8629 Ok(())
8630 }
8631
8632 #[rstest]
8633 #[tokio::test]
8634 async fn test_plans(
8635 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
8636 data_storage_version: LanceFileVersion,
8637 #[values(false, true)] stable_row_id: bool,
8638 ) -> Result<()> {
8639 use lance_index::scalar::inverted::query::BoostQuery;
8642 let dim = 256;
8643 let mut dataset =
8644 TestVectorDataset::new_with_dimension(data_storage_version, stable_row_id, dim).await?;
8645 let lance_schema = dataset.dataset.schema();
8646
8647 if data_storage_version == LanceFileVersion::Legacy {
8651 log::info!("Test case: Pushdown scan");
8652 assert_plan_equals(
8653 &dataset.dataset,
8654 |scan| scan.project(&["s"])?.filter("i > 10 and i < 20"),
8655 "LancePushdownScan: uri=..., projection=[s], predicate=i > Int32(10) AND i < Int32(20), row_id=false, row_addr=false, ordered=true"
8656 ).await?;
8657 }
8658
8659 log::info!("Test case: Project and filter");
8660 let expected = if data_storage_version == LanceFileVersion::Legacy {
8661 "ProjectionExec: expr=[s@2 as s]
8662 Take: columns=\"i, _rowid, (s)\"
8663 CoalesceBatchesExec: target_batch_size=8192
8664 FilterExec: i@0 > 10 AND i@0 < 20
8665 LanceScan: uri..., projection=[i], row_id=true, row_addr=false, ordered=true, range=None"
8666 } else {
8667 "ProjectionExec: expr=[s@2 as s]
8668 Take: columns=\"i, _rowid, (s)\"
8669 CoalesceBatchesExec: target_batch_size=8192
8670 LanceRead: ..., projection=[i], num_fragments=2, range_before=None, range_after=None, row_id=true, row_addr=false, full_filter=i > Int32(10) AND i < Int32(20), refine_filter=i > Int32(10) AND i < Int32(20)"
8671 };
8672 assert_plan_equals(
8673 &dataset.dataset,
8674 |scan| {
8675 scan.use_stats(false)
8676 .project(&["s"])?
8677 .filter("i > 10 and i < 20")
8678 },
8679 expected,
8680 )
8681 .await?;
8682
8683 log::info!("Test case: Late materialization");
8686 let expected = if data_storage_version == LanceFileVersion::Legacy {
8687 "ProjectionExec: expr=[i@0 as i, s@1 as s, vec@3 as vec]
8688 Take: columns=\"i, s, _rowid, (vec)\"
8689 CoalesceBatchesExec: target_batch_size=8192
8690 FilterExec: s@1 IS NOT NULL
8691 LanceScan: uri..., projection=[i, s], row_id=true, row_addr=false, ordered=true, range=None"
8692 } else {
8693 "ProjectionExec: expr=[i@0 as i, s@1 as s, vec@3 as vec]
8694 Take: columns=\"i, s, _rowid, (vec)\"
8695 CoalesceBatchesExec: target_batch_size=8192
8696 LanceRead: uri=..., projection=[i, s], num_fragments=2, range_before=None, range_after=None, \
8697 row_id=true, row_addr=false, full_filter=s IS NOT NULL, refine_filter=s IS NOT NULL"
8698 };
8699 assert_plan_equals(
8700 &dataset.dataset,
8701 |scan| scan.use_stats(false).filter("s IS NOT NULL"),
8702 expected,
8703 )
8704 .await?;
8705
8706 log::info!("Test case: Custom materialization (all early)");
8708 let expected = if data_storage_version == LanceFileVersion::Legacy {
8709 "ProjectionExec: expr=[i@0 as i, s@1 as s, vec@2 as vec]
8710 FilterExec: s@1 IS NOT NULL
8711 LanceScan: uri..., projection=[i, s, vec], row_id=true, row_addr=false, ordered=true, range=None"
8712 } else {
8713 "ProjectionExec: expr=[i@0 as i, s@1 as s, vec@2 as vec]
8714 LanceRead: uri=..., projection=[i, s, vec], num_fragments=2, range_before=None, \
8715 range_after=None, row_id=true, row_addr=false, full_filter=s IS NOT NULL, refine_filter=s IS NOT NULL"
8716 };
8717 assert_plan_equals(
8718 &dataset.dataset,
8719 |scan| {
8720 scan.use_stats(false)
8721 .materialization_style(MaterializationStyle::AllEarly)
8722 .filter("s IS NOT NULL")
8723 },
8724 expected,
8725 )
8726 .await?;
8727
8728 log::info!("Test case: Custom materialization 2 (all late)");
8729 let expected = if data_storage_version == LanceFileVersion::Legacy {
8730 "ProjectionExec: expr=[i@2 as i, s@0 as s, vec@3 as vec]
8731 Take: columns=\"s, _rowid, (i), (vec)\"
8732 CoalesceBatchesExec: target_batch_size=8192
8733 FilterExec: s@0 IS NOT NULL
8734 LanceScan: uri..., projection=[s], row_id=true, row_addr=false, ordered=true, range=None"
8735 } else {
8736 "ProjectionExec: expr=[i@2 as i, s@0 as s, vec@3 as vec]
8737 Take: columns=\"s, _rowid, (i), (vec)\"
8738 CoalesceBatchesExec: target_batch_size=8192
8739 LanceRead: uri=..., projection=[s], num_fragments=2, range_before=None, \
8740 range_after=None, row_id=true, row_addr=false, full_filter=s IS NOT NULL, refine_filter=s IS NOT NULL"
8741 };
8742 assert_plan_equals(
8743 &dataset.dataset,
8744 |scan| {
8745 scan.use_stats(false)
8746 .materialization_style(MaterializationStyle::AllLate)
8747 .filter("s IS NOT NULL")
8748 },
8749 expected,
8750 )
8751 .await?;
8752
8753 log::info!("Test case: Custom materialization 3 (mixed)");
8754 let expected = if data_storage_version == LanceFileVersion::Legacy {
8755 "ProjectionExec: expr=[i@3 as i, s@0 as s, vec@1 as vec]
8756 Take: columns=\"s, vec, _rowid, (i)\"
8757 CoalesceBatchesExec: target_batch_size=8192
8758 FilterExec: s@0 IS NOT NULL
8759 LanceScan: uri..., projection=[s, vec], row_id=true, row_addr=false, ordered=true, range=None"
8760 } else {
8761 "ProjectionExec: expr=[i@3 as i, s@0 as s, vec@1 as vec]
8762 Take: columns=\"s, vec, _rowid, (i)\"
8763 CoalesceBatchesExec: target_batch_size=8192
8764 LanceRead: uri=..., projection=[s, vec], num_fragments=2, range_before=None, range_after=None, \
8765 row_id=true, row_addr=false, full_filter=s IS NOT NULL, refine_filter=s IS NOT NULL"
8766 };
8767 assert_plan_equals(
8768 &dataset.dataset,
8769 |scan| {
8770 scan.use_stats(false)
8771 .materialization_style(
8772 MaterializationStyle::all_early_except(&["i"], lance_schema).unwrap(),
8773 )
8774 .filter("s IS NOT NULL")
8775 },
8776 expected,
8777 )
8778 .await?;
8779
8780 log::info!("Test case: Scan out of order");
8781 let expected = if data_storage_version == LanceFileVersion::Legacy {
8782 "LanceScan: uri=..., projection=[s], row_id=true, row_addr=false, ordered=false, range=None"
8783 } else {
8784 "LanceRead: uri=..., projection=[s], num_fragments=2, range_before=None, range_after=None, row_id=true, \
8785 row_addr=false, full_filter=--, refine_filter=--"
8786 };
8787 assert_plan_equals(
8788 &dataset.dataset,
8789 |scan| Ok(scan.project(&["s"])?.with_row_id().scan_in_order(false)),
8790 expected,
8791 )
8792 .await?;
8793
8794 let q: Float32Array = (32..32 + dim).map(|v| v as f32).collect();
8797 log::info!("Test case: Basic KNN");
8798 let expected = if data_storage_version == LanceFileVersion::Legacy {
8799 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8800 Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8801 CoalesceBatchesExec: target_batch_size=8192
8802 FilterExec: _distance@2 IS NOT NULL
8803 SortExec: TopK(fetch=5), expr=...
8804 KNNVectorDistance: metric=l2
8805 LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None"
8806 } else {
8807 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8808 Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8809 CoalesceBatchesExec: target_batch_size=8192
8810 FilterExec: _distance@2 IS NOT NULL
8811 SortExec: TopK(fetch=5), expr=...
8812 KNNVectorDistance: metric=l2
8813 LanceRead: uri=..., projection=[vec], num_fragments=2, range_before=None, range_after=None, \
8814 row_id=true, row_addr=false, full_filter=--, refine_filter=--"
8815 };
8816 assert_plan_equals(
8817 &dataset.dataset,
8818 |scan| scan.nearest("vec", &q, 5),
8819 expected,
8820 )
8821 .await?;
8822
8823 let q: Float32Array = (32..32 + dim).map(|v| v as f32).collect();
8826 log::info!("Test case: KNN with extraneous limit");
8827 let expected = if data_storage_version == LanceFileVersion::Legacy {
8828 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8829 Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8830 CoalesceBatchesExec: target_batch_size=8192
8831 GlobalLimitExec: skip=0, fetch=1
8832 FilterExec: _distance@2 IS NOT NULL
8833 SortExec: TopK(fetch=5), expr=...
8834 KNNVectorDistance: metric=l2
8835 LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None"
8836 } else {
8837 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8838 Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8839 CoalesceBatchesExec: target_batch_size=8192
8840 GlobalLimitExec: skip=0, fetch=1
8841 FilterExec: _distance@2 IS NOT NULL
8842 SortExec: TopK(fetch=5), expr=...
8843 KNNVectorDistance: metric=l2
8844 LanceRead: uri=..., projection=[vec], num_fragments=2, range_before=None, range_after=None, \
8845 row_id=true, row_addr=false, full_filter=--, refine_filter=--"
8846 };
8847 assert_plan_equals(
8848 &dataset.dataset,
8849 |scan| scan.nearest("vec", &q, 5)?.limit(Some(1), None),
8850 expected,
8851 )
8852 .await?;
8853
8854 dataset.make_vector_index().await?;
8857 log::info!("Test case: Basic ANN");
8858 let expected =
8859 "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8860 Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8861 CoalesceBatchesExec: target_batch_size=8192
8862 SortExec: TopK(fetch=42), expr=...
8863 ANNSubIndex: name=..., k=42, deltas=1, metric=L2
8864 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
8865 assert_plan_equals(
8866 &dataset.dataset,
8867 |scan| scan.nearest("vec", &q, 42),
8868 expected,
8869 )
8870 .await?;
8871
8872 log::info!("Test case: ANN with refine");
8873 let expected =
8874 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8875 Take: columns=\"_rowid, vec, _distance, (i), (s)\"
8876 CoalesceBatchesExec: target_batch_size=8192
8877 FilterExec: _distance@... IS NOT NULL
8878 SortExec: TopK(fetch=10), expr=...
8879 KNNVectorDistance: metric=l2
8880 Take: columns=\"_distance, _rowid, (vec)\"
8881 CoalesceBatchesExec: target_batch_size=8192
8882 SortExec: TopK(fetch=40), expr=...
8883 ANNSubIndex: name=..., k=40, deltas=1, metric=L2
8884 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
8885 assert_plan_equals(
8886 &dataset.dataset,
8887 |scan| Ok(scan.nearest("vec", &q, 10)?.refine(4)),
8888 expected,
8889 )
8890 .await?;
8891
8892 log::info!("Test case: ANN with index disabled");
8894 let expected = if data_storage_version == LanceFileVersion::Legacy {
8895 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8896 Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8897 CoalesceBatchesExec: target_batch_size=8192
8898 FilterExec: _distance@... IS NOT NULL
8899 SortExec: TopK(fetch=13), expr=...
8900 KNNVectorDistance: metric=l2
8901 LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None"
8902 } else {
8903 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8904 Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8905 CoalesceBatchesExec: target_batch_size=8192
8906 FilterExec: _distance@... IS NOT NULL
8907 SortExec: TopK(fetch=13), expr=...
8908 KNNVectorDistance: metric=l2
8909 LanceRead: uri=..., projection=[vec], num_fragments=2, range_before=None, range_after=None, \
8910 row_id=true, row_addr=false, full_filter=--, refine_filter=--"
8911 };
8912 assert_plan_equals(
8913 &dataset.dataset,
8914 |scan| Ok(scan.nearest("vec", &q, 13)?.use_index(false)),
8915 expected,
8916 )
8917 .await?;
8918
8919 log::info!("Test case: ANN with postfilter");
8920 let expected = "ProjectionExec: expr=[s@3 as s, vec@4 as vec, _distance@0 as _distance, _rowid@1 as _rowid]
8921 Take: columns=\"_distance, _rowid, i, (s), (vec)\"
8922 CoalesceBatchesExec: target_batch_size=8192
8923 FilterExec: i@2 > 10
8924 Take: columns=\"_distance, _rowid, (i)\"
8925 CoalesceBatchesExec: target_batch_size=8192
8926 SortExec: TopK(fetch=17), expr=...
8927 ANNSubIndex: name=..., k=17, deltas=1, metric=L2
8928 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
8929 assert_plan_equals(
8930 &dataset.dataset,
8931 |scan| {
8932 Ok(scan
8933 .nearest("vec", &q, 17)?
8934 .filter("i > 10")?
8935 .project(&["s", "vec"])?
8936 .with_row_id())
8937 },
8938 expected,
8939 )
8940 .await?;
8941
8942 log::info!("Test case: ANN with prefilter");
8943 let expected = if data_storage_version == LanceFileVersion::Legacy {
8944 "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8945 Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8946 CoalesceBatchesExec: target_batch_size=8192
8947 SortExec: TopK(fetch=17), expr=...
8948 ANNSubIndex: name=..., k=17, deltas=1, metric=L2
8949 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8950 FilterExec: i@0 > 10
8951 LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None"
8952 } else {
8953 "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8954 Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8955 CoalesceBatchesExec: target_batch_size=8192
8956 SortExec: TopK(fetch=17), expr=...
8957 ANNSubIndex: name=..., k=17, deltas=1, metric=L2
8958 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8959 LanceRead: uri=..., projection=[], num_fragments=2, range_before=None, range_after=None, \
8960 row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=i > Int32(10)
8961"
8962 };
8963 assert_plan_equals(
8964 &dataset.dataset,
8965 |scan| {
8966 Ok(scan
8967 .nearest("vec", &q, 17)?
8968 .filter("i > 10")?
8969 .prefilter(true))
8970 },
8971 expected,
8972 )
8973 .await?;
8974
8975 dataset.append_new_data().await?;
8976 log::info!("Test case: Combined KNN/ANN");
8977 let expected = "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8978 Take: columns=\"_rowid, vec, _distance, (i), (s)\"
8979 CoalesceBatchesExec: target_batch_size=8192
8980 FilterExec: _distance@... IS NOT NULL
8981 SortExec: TopK(fetch=6), expr=...
8982 KNNVectorDistance: metric=l2
8983 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8984 UnionExec
8985 ProjectionExec: expr=[_distance@2 as _distance, _rowid@1 as _rowid, vec@0 as vec]
8986 FilterExec: _distance@... IS NOT NULL
8987 SortExec: TopK(fetch=6), expr=...
8988 KNNVectorDistance: metric=l2
8989 LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None
8990 Take: columns=\"_distance, _rowid, (vec)\"
8991 CoalesceBatchesExec: target_batch_size=8192
8992 SortExec: TopK(fetch=6), expr=...
8993 ANNSubIndex: name=..., k=6, deltas=1, metric=L2
8994 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
8995 assert_plan_equals(
8996 &dataset.dataset,
8997 |scan| scan.nearest("vec", &q, 6),
8998 expected,
9001 )
9002 .await?;
9003
9004 log::info!("Test case: Combined KNN/ANN with postfilter");
9006 let expected = "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
9007 Take: columns=\"_rowid, vec, _distance, i, (s)\"
9008 CoalesceBatchesExec: target_batch_size=8192
9009 FilterExec: i@3 > 10
9010 Take: columns=\"_rowid, vec, _distance, (i)\"
9011 CoalesceBatchesExec: target_batch_size=8192
9012 FilterExec: _distance@... IS NOT NULL
9013 SortExec: TopK(fetch=15), expr=...
9014 KNNVectorDistance: metric=l2
9015 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9016 UnionExec
9017 ProjectionExec: expr=[_distance@2 as _distance, _rowid@1 as _rowid, vec@0 as vec]
9018 FilterExec: _distance@... IS NOT NULL
9019 SortExec: TopK(fetch=15), expr=...
9020 KNNVectorDistance: metric=l2
9021 LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None
9022 Take: columns=\"_distance, _rowid, (vec)\"
9023 CoalesceBatchesExec: target_batch_size=8192
9024 SortExec: TopK(fetch=15), expr=...
9025 ANNSubIndex: name=..., k=15, deltas=1, metric=L2
9026 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
9027 assert_plan_equals(
9028 &dataset.dataset,
9029 |scan| scan.nearest("vec", &q, 15)?.filter("i > 10"),
9030 expected,
9031 )
9032 .await?;
9033
9034 log::info!("Test case: Combined KNN/ANN with prefilter");
9036 let expected = if data_storage_version == LanceFileVersion::Legacy {
9037 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
9038 Take: columns=\"_rowid, vec, _distance, (i), (s)\"
9039 CoalesceBatchesExec: target_batch_size=8192
9040 FilterExec: _distance@... IS NOT NULL
9041 SortExec: TopK(fetch=5), expr=...
9042 KNNVectorDistance: metric=l2
9043 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9044 UnionExec
9045 ProjectionExec: expr=[_distance@3 as _distance, _rowid@2 as _rowid, vec@0 as vec]
9046 FilterExec: _distance@... IS NOT NULL
9047 SortExec: TopK(fetch=5), expr=...
9048 KNNVectorDistance: metric=l2
9049 FilterExec: i@1 > 10
9050 LanceScan: uri=..., projection=[vec, i], row_id=true, row_addr=false, ordered=false, range=None
9051 Take: columns=\"_distance, _rowid, (vec)\"
9052 CoalesceBatchesExec: target_batch_size=8192
9053 SortExec: TopK(fetch=5), expr=...
9054 ANNSubIndex: name=..., k=5, deltas=1, metric=L2
9055 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
9056 FilterExec: i@0 > 10
9057 LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None"
9058 } else {
9059 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
9060 Take: columns=\"_rowid, vec, _distance, (i), (s)\"
9061 CoalesceBatchesExec: target_batch_size=8192
9062 FilterExec: _distance@... IS NOT NULL
9063 SortExec: TopK(fetch=5), expr=...
9064 KNNVectorDistance: metric=l2
9065 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9066 UnionExec
9067 ProjectionExec: expr=[_distance@3 as _distance, _rowid@2 as _rowid, vec@0 as vec]
9068 FilterExec: _distance@... IS NOT NULL
9069 SortExec: TopK(fetch=5), expr=...
9070 KNNVectorDistance: metric=l2
9071 FilterExec: i@1 > 10
9072 LanceScan: uri=..., projection=[vec, i], row_id=true, row_addr=false, ordered=false, range=None
9073 Take: columns=\"_distance, _rowid, (vec)\"
9074 CoalesceBatchesExec: target_batch_size=8192
9075 SortExec: TopK(fetch=5), expr=...
9076 ANNSubIndex: name=..., k=5, deltas=1, metric=L2
9077 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
9078 LanceRead: uri=..., projection=[], num_fragments=2, range_before=None, range_after=None, \
9079 row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=i > Int32(10)"
9080 };
9081 assert_plan_equals(
9082 &dataset.dataset,
9083 |scan| {
9084 Ok(scan
9085 .nearest("vec", &q, 5)?
9086 .filter("i > 10")?
9087 .prefilter(true))
9088 },
9089 expected,
9092 )
9093 .await?;
9094
9095 dataset.make_vector_index().await?;
9099 dataset.make_scalar_index().await?;
9100
9101 log::info!("Test case: ANN with scalar index");
9102 let expected =
9103 "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
9104 Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
9105 CoalesceBatchesExec: target_batch_size=8192
9106 SortExec: TopK(fetch=5), expr=...
9107 ANNSubIndex: name=..., k=5, deltas=1, metric=L2
9108 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
9109 ScalarIndexQuery: query=[i > 10]@i_idx";
9110 assert_plan_equals(
9111 &dataset.dataset,
9112 |scan| {
9113 Ok(scan
9114 .nearest("vec", &q, 5)?
9115 .filter("i > 10")?
9116 .prefilter(true))
9117 },
9118 expected,
9119 )
9120 .await?;
9121
9122 log::info!("Test case: ANN with scalar index disabled");
9123 let expected = if data_storage_version == LanceFileVersion::Legacy {
9124 "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
9125 Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
9126 CoalesceBatchesExec: target_batch_size=8192
9127 SortExec: TopK(fetch=5), expr=...
9128 ANNSubIndex: name=..., k=5, deltas=1, metric=L2
9129 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
9130 FilterExec: i@0 > 10
9131 LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None"
9132 } else {
9133 "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
9134 Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
9135 CoalesceBatchesExec: target_batch_size=8192
9136 SortExec: TopK(fetch=5), expr=...
9137 ANNSubIndex: name=..., k=5, deltas=1, metric=L2
9138 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
9139 LanceRead: uri=..., projection=[], num_fragments=3, range_before=None, \
9140 range_after=None, row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=i > Int32(10)"
9141 };
9142 assert_plan_equals(
9143 &dataset.dataset,
9144 |scan| {
9145 Ok(scan
9146 .nearest("vec", &q, 5)?
9147 .use_scalar_index(false)
9148 .filter("i > 10")?
9149 .prefilter(true))
9150 },
9151 expected,
9152 )
9153 .await?;
9154
9155 dataset.append_new_data().await?;
9156
9157 log::info!("Test case: Combined KNN/ANN with scalar index");
9158 let expected = "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
9159 Take: columns=\"_rowid, vec, _distance, (i), (s)\"
9160 CoalesceBatchesExec: target_batch_size=8192
9161 FilterExec: _distance@... IS NOT NULL
9162 SortExec: TopK(fetch=8), expr=...
9163 KNNVectorDistance: metric=l2
9164 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9165 UnionExec
9166 ProjectionExec: expr=[_distance@3 as _distance, _rowid@2 as _rowid, vec@0 as vec]
9167 FilterExec: _distance@... IS NOT NULL
9168 SortExec: TopK(fetch=8), expr=...
9169 KNNVectorDistance: metric=l2
9170 FilterExec: i@1 > 10
9171 LanceScan: uri=..., projection=[vec, i], row_id=true, row_addr=false, ordered=false, range=None
9172 Take: columns=\"_distance, _rowid, (vec)\"
9173 CoalesceBatchesExec: target_batch_size=8192
9174 SortExec: TopK(fetch=8), expr=...
9175 ANNSubIndex: name=..., k=8, deltas=1, metric=L2
9176 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
9177 ScalarIndexQuery: query=[i > 10]@i_idx";
9178 assert_plan_equals(
9179 &dataset.dataset,
9180 |scan| {
9181 Ok(scan
9182 .nearest("vec", &q, 8)?
9183 .filter("i > 10")?
9184 .prefilter(true))
9185 },
9186 expected,
9187 )
9188 .await?;
9189
9190 log::info!(
9192 "Test case: Combined KNN/ANN with updated scalar index and outdated vector index"
9193 );
9194 let expected = "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
9195 Take: columns=\"_rowid, vec, _distance, (i), (s)\"
9196 CoalesceBatchesExec: target_batch_size=8192
9197 FilterExec: _distance@... IS NOT NULL
9198 SortExec: TopK(fetch=11), expr=...
9199 KNNVectorDistance: metric=l2
9200 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9201 UnionExec
9202 ProjectionExec: expr=[_distance@3 as _distance, _rowid@2 as _rowid, vec@0 as vec]
9203 FilterExec: _distance@... IS NOT NULL
9204 SortExec: TopK(fetch=11), expr=...
9205 KNNVectorDistance: metric=l2
9206 FilterExec: i@1 > 10
9207 LanceScan: uri=..., projection=[vec, i], row_id=true, row_addr=false, ordered=false, range=None
9208 Take: columns=\"_distance, _rowid, (vec)\"
9209 CoalesceBatchesExec: target_batch_size=8192
9210 SortExec: TopK(fetch=11), expr=...
9211 ANNSubIndex: name=..., k=11, deltas=1, metric=L2
9212 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
9213 ScalarIndexQuery: query=[i > 10]@i_idx";
9214 dataset.make_scalar_index().await?;
9215 assert_plan_equals(
9216 &dataset.dataset,
9217 |scan| {
9218 Ok(scan
9219 .nearest("vec", &q, 11)?
9220 .filter("i > 10")?
9221 .prefilter(true))
9222 },
9223 expected,
9224 )
9225 .await?;
9226
9227 log::info!("Test case: Filtered read with scalar index");
9230 let expected = if data_storage_version == LanceFileVersion::Legacy {
9231 "ProjectionExec: expr=[s@1 as s]
9232 Take: columns=\"_rowid, (s)\"
9233 CoalesceBatchesExec: target_batch_size=8192
9234 MaterializeIndex: query=[i > 10]@i_idx"
9235 } else {
9236 "LanceRead: uri=..., projection=[s], num_fragments=4, range_before=None, \
9237 range_after=None, row_id=false, row_addr=false, full_filter=i > Int32(10), refine_filter=--
9238 ScalarIndexQuery: query=[i > 10]@i_idx"
9239 };
9240 assert_plan_equals(
9241 &dataset.dataset,
9242 |scan| scan.project(&["s"])?.filter("i > 10"),
9243 expected,
9244 )
9245 .await?;
9246
9247 if data_storage_version != LanceFileVersion::Legacy {
9248 log::info!(
9249 "Test case: Filtered read with scalar index disabled (late materialization)"
9250 );
9251 assert_plan_equals(
9252 &dataset.dataset,
9253 |scan| {
9254 scan.project(&["s"])?
9255 .use_scalar_index(false)
9256 .filter("i > 10")
9257 },
9258 "ProjectionExec: expr=[s@2 as s]
9259 Take: columns=\"i, _rowid, (s)\"
9260 CoalesceBatchesExec: target_batch_size=8192
9261 LanceRead: uri=..., projection=[i], num_fragments=4, range_before=None, \
9262 range_after=None, row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=i > Int32(10)",
9263 )
9264 .await?;
9265 }
9266
9267 log::info!("Test case: Empty projection");
9268 let expected = if data_storage_version == LanceFileVersion::Legacy {
9269 "ProjectionExec: expr=[_rowaddr@0 as _rowaddr]
9270 AddRowAddrExec
9271 MaterializeIndex: query=[i > 10]@i_idx"
9272 } else {
9273 "LanceRead: uri=..., projection=[], num_fragments=4, range_before=None, \
9274 range_after=None, row_id=false, row_addr=true, full_filter=i > Int32(10), refine_filter=--
9275 ScalarIndexQuery: query=[i > 10]@i_idx"
9276 };
9277 assert_plan_equals(
9278 &dataset.dataset,
9279 |scan| {
9280 scan.filter("i > 10")
9281 .unwrap()
9282 .with_row_address()
9283 .project::<&str>(&[])
9284 },
9285 expected,
9286 )
9287 .await?;
9288
9289 dataset.append_new_data().await?;
9290 log::info!("Test case: Combined Scalar/non-scalar filtered read");
9291 let expected = if data_storage_version == LanceFileVersion::Legacy {
9292 "ProjectionExec: expr=[s@1 as s]
9293 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9294 UnionExec
9295 Take: columns=\"_rowid, (s)\"
9296 CoalesceBatchesExec: target_batch_size=8192
9297 MaterializeIndex: query=[i > 10]@i_idx
9298 ProjectionExec: expr=[_rowid@2 as _rowid, s@1 as s]
9299 FilterExec: i@0 > 10
9300 LanceScan: uri=..., projection=[i, s], row_id=true, row_addr=false, ordered=false, range=None"
9301 } else {
9302 "LanceRead: uri=..., projection=[s], num_fragments=5, range_before=None, \
9303 range_after=None, row_id=false, row_addr=false, full_filter=i > Int32(10), refine_filter=--
9304 ScalarIndexQuery: query=[i > 10]@i_idx"
9305 };
9306 assert_plan_equals(
9307 &dataset.dataset,
9308 |scan| scan.project(&["s"])?.filter("i > 10"),
9309 expected,
9310 )
9311 .await?;
9312
9313 log::info!("Test case: Combined Scalar/non-scalar filtered read with empty projection");
9314 let expected = if data_storage_version == LanceFileVersion::Legacy {
9315 "ProjectionExec: expr=[_rowaddr@0 as _rowaddr]
9316 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9317 UnionExec
9318 AddRowAddrExec
9319 MaterializeIndex: query=[i > 10]@i_idx
9320 ProjectionExec: expr=[_rowaddr@2 as _rowaddr, _rowid@1 as _rowid]
9321 FilterExec: i@0 > 10
9322 LanceScan: uri=..., projection=[i], row_id=true, row_addr=true, ordered=false, range=None"
9323 } else {
9324 "LanceRead: uri=..., projection=[], num_fragments=5, range_before=None, \
9325 range_after=None, row_id=false, row_addr=true, full_filter=i > Int32(10), refine_filter=--
9326 ScalarIndexQuery: query=[i > 10]@i_idx"
9327 };
9328 assert_plan_equals(
9329 &dataset.dataset,
9330 |scan| {
9331 scan.filter("i > 10")
9332 .unwrap()
9333 .with_row_address()
9334 .project::<&str>(&[])
9335 },
9336 expected,
9337 )
9338 .await?;
9339
9340 log::info!("Test case: Dynamic projection");
9343 let expected = if data_storage_version == LanceFileVersion::Legacy {
9344 "ProjectionExec: expr=[regexp_match(s@1, .*) as matches]
9345 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9346 UnionExec
9347 Take: columns=\"_rowid, (s)\"
9348 CoalesceBatchesExec: target_batch_size=8192
9349 MaterializeIndex: query=[i > 10]@i_idx
9350 ProjectionExec: expr=[_rowid@2 as _rowid, s@1 as s]
9351 FilterExec: i@0 > 10
9352 LanceScan: uri=..., row_id=true, row_addr=false, ordered=false, range=None"
9353 } else {
9354 "ProjectionExec: expr=[regexp_match(s@0, .*) as matches]
9355 LanceRead: uri=..., projection=[s], num_fragments=5, range_before=None, \
9356 range_after=None, row_id=false, row_addr=false, full_filter=i > Int32(10), refine_filter=--
9357 ScalarIndexQuery: query=[i > 10]@i_idx"
9358 };
9359 assert_plan_equals(
9360 &dataset.dataset,
9361 |scan| {
9362 scan.project_with_transform(&[("matches", "regexp_match(s, \".*\")")])?
9363 .filter("i > 10")
9364 },
9365 expected,
9366 )
9367 .await?;
9368
9369 dataset.make_fts_index().await?;
9373 log::info!("Test case: Full text search (match query)");
9374 let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9375 Take: columns="_rowid, _score, (s)"
9376 CoalesceBatchesExec: target_batch_size=8192
9377 MatchQuery: column=s, query=hello"#;
9378 assert_plan_equals(
9379 &dataset.dataset,
9380 |scan| {
9381 scan.project(&["s"])?
9382 .with_row_id()
9383 .full_text_search(FullTextSearchQuery::new("hello".to_owned()))
9384 },
9385 expected,
9386 )
9387 .await?;
9388
9389 log::info!("Test case: Full text search (phrase query)");
9390 let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9391 Take: columns="_rowid, _score, (s)"
9392 CoalesceBatchesExec: target_batch_size=8192
9393 PhraseQuery: column=s, query=hello world"#;
9394 assert_plan_equals(
9395 &dataset.dataset,
9396 |scan| {
9397 let query = PhraseQuery::new("hello world".to_owned());
9398 scan.project(&["s"])?
9399 .with_row_id()
9400 .full_text_search(FullTextSearchQuery::new_query(query.into()))
9401 },
9402 expected,
9403 )
9404 .await?;
9405
9406 log::info!("Test case: Full text search (boost query)");
9407 let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9408 Take: columns="_rowid, _score, (s)"
9409 CoalesceBatchesExec: target_batch_size=8192
9410 BoostQuery: negative_boost=1
9411 MatchQuery: column=s, query=hello
9412 MatchQuery: column=s, query=world"#;
9413 assert_plan_equals(
9414 &dataset.dataset,
9415 |scan| {
9416 let positive =
9417 MatchQuery::new("hello".to_owned()).with_column(Some("s".to_owned()));
9418 let negative =
9419 MatchQuery::new("world".to_owned()).with_column(Some("s".to_owned()));
9420 let query = BoostQuery::new(positive.into(), negative.into(), Some(1.0));
9421 scan.project(&["s"])?
9422 .with_row_id()
9423 .full_text_search(FullTextSearchQuery::new_query(query.into()))
9424 },
9425 expected,
9426 )
9427 .await?;
9428
9429 log::info!("Test case: Full text search with prefilter");
9430 let expected = if data_storage_version == LanceFileVersion::Legacy {
9431 r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9432 Take: columns="_rowid, _score, (s)"
9433 CoalesceBatchesExec: target_batch_size=8192
9434 MatchQuery: column=s, query=hello
9435 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9436 UnionExec
9437 MaterializeIndex: query=[i > 10]@i_idx
9438 ProjectionExec: expr=[_rowid@1 as _rowid]
9439 FilterExec: i@0 > 10
9440 LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None"#
9441 } else {
9442 r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9443 Take: columns="_rowid, _score, (s)"
9444 CoalesceBatchesExec: target_batch_size=8192
9445 MatchQuery: column=s, query=hello
9446 LanceRead: uri=..., projection=[], num_fragments=5, range_before=None, range_after=None, row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=--
9447 ScalarIndexQuery: query=[i > 10]@i_idx"#
9448 };
9449 assert_plan_equals(
9450 &dataset.dataset,
9451 |scan| {
9452 scan.project(&["s"])?
9453 .with_row_id()
9454 .filter("i > 10")?
9455 .prefilter(true)
9456 .full_text_search(FullTextSearchQuery::new("hello".to_owned()))
9457 },
9458 expected,
9459 )
9460 .await?;
9461
9462 log::info!("Test case: Full text search with unindexed rows");
9463 let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9464 Take: columns="_rowid, _score, (s)"
9465 CoalesceBatchesExec: target_batch_size=8192
9466 SortExec: expr=[_score@1 DESC NULLS LAST], preserve_partitioning=[false]
9467 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9468 UnionExec
9469 MatchQuery: column=s, query=hello
9470 FlatMatchQuery: column=s, query=hello
9471 LanceScan: uri=..., projection=[s], row_id=true, row_addr=false, ordered=false, range=None"#;
9472 dataset.append_new_data().await?;
9473 assert_plan_equals(
9474 &dataset.dataset,
9475 |scan| {
9476 scan.project(&["s"])?
9477 .with_row_id()
9478 .full_text_search(FullTextSearchQuery::new("hello".to_owned()))
9479 },
9480 expected,
9481 )
9482 .await?;
9483
9484 log::info!("Test case: Full text search with unindexed rows and fast_search");
9485 let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9486 Take: columns="_rowid, _score, (s)"
9487 CoalesceBatchesExec: target_batch_size=8192
9488 MatchQuery: column=s, query=hello"#;
9489 assert_plan_equals(
9490 &dataset.dataset,
9491 |scan| {
9492 let scan = scan
9493 .project(&["s"])?
9494 .with_row_id()
9495 .full_text_search(FullTextSearchQuery::new("hello".to_owned()))?;
9496 scan.fast_search();
9497 Ok(scan)
9498 },
9499 expected,
9500 )
9501 .await?;
9502
9503 log::info!("Test case: Full text search with unindexed rows and prefilter");
9504 let expected = if data_storage_version == LanceFileVersion::Legacy {
9505 r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9506 Take: columns="_rowid, _score, (s)"
9507 CoalesceBatchesExec: target_batch_size=8192
9508 SortExec: expr=[_score@1 DESC NULLS LAST], preserve_partitioning=[false]
9509 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9510 UnionExec
9511 MatchQuery: column=s, query=hello
9512 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9513 UnionExec
9514 MaterializeIndex: query=[i > 10]@i_idx
9515 ProjectionExec: expr=[_rowid@1 as _rowid]
9516 FilterExec: i@0 > 10
9517 LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None
9518 FlatMatchQuery: column=s, query=hello
9519 FilterExec: i@1 > 10
9520 LanceScan: uri=..., projection=[s, i], row_id=true, row_addr=false, ordered=false, range=None"#
9521 } else {
9522 r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9523 Take: columns="_rowid, _score, (s)"
9524 CoalesceBatchesExec: target_batch_size=8192
9525 SortExec: expr=[_score@1 DESC NULLS LAST], preserve_partitioning=[false]
9526 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9527 UnionExec
9528 MatchQuery: column=s, query=hello
9529 LanceRead: uri=..., projection=[], num_fragments=5, range_before=None, range_after=None, row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=--
9530 ScalarIndexQuery: query=[i > 10]@i_idx
9531 FlatMatchQuery: column=s, query=hello
9532 FilterExec: i@1 > 10
9533 LanceScan: uri=..., projection=[s, i], row_id=true, row_addr=false, ordered=false, range=None"#
9534 };
9535 assert_plan_equals(
9536 &dataset.dataset,
9537 |scan| {
9538 scan.project(&["s"])?
9539 .with_row_id()
9540 .filter("i > 10")?
9541 .prefilter(true)
9542 .full_text_search(FullTextSearchQuery::new("hello".to_owned()))
9543 },
9544 expected,
9545 )
9546 .await?;
9547
9548 Ok(())
9549 }
9550
9551 #[tokio::test]
9552 async fn test_fast_search_plan() {
9553 let mut dataset = TestVectorDataset::new(LanceFileVersion::Stable, true)
9555 .await
9556 .unwrap();
9557 dataset.make_vector_index().await.unwrap();
9558 dataset.append_new_data().await.unwrap();
9559
9560 let q: Float32Array = (32..64).map(|v| v as f32).collect();
9561
9562 assert_plan_equals(
9563 &dataset.dataset,
9564 |scan| {
9565 scan.nearest("vec", &q, 32)?
9566 .fast_search()
9567 .project(&["_distance", "_rowid"])
9568 },
9569 "SortExec: TopK(fetch=32), expr=[_distance@0 ASC NULLS LAST, _rowid@1 ASC NULLS LAST]...
9570 ANNSubIndex: name=idx, k=32, deltas=1, metric=L2
9571 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1",
9572 )
9573 .await
9574 .unwrap();
9575
9576 assert_plan_equals(
9577 &dataset.dataset,
9578 |scan| {
9579 scan.nearest("vec", &q, 33)?
9580 .fast_search()
9581 .with_row_id()
9582 .project(&["_distance", "_rowid"])
9583 },
9584 "SortExec: TopK(fetch=33), expr=[_distance@0 ASC NULLS LAST, _rowid@1 ASC NULLS LAST]...
9585 ANNSubIndex: name=idx, k=33, deltas=1, metric=L2
9586 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1",
9587 )
9588 .await
9589 .unwrap();
9590
9591 assert_plan_equals(
9593 &dataset.dataset,
9594 |scan| {
9595 scan.nearest("vec", &q, 34)?
9596 .with_row_id()
9597 .project(&["_distance", "_rowid"])
9598 },
9599 "ProjectionExec: expr=[_distance@2 as _distance, _rowid@0 as _rowid]
9600 FilterExec: _distance@2 IS NOT NULL
9601 SortExec: TopK(fetch=34), expr=[_distance@2 ASC NULLS LAST, _rowid@0 ASC NULLS LAST]...
9602 KNNVectorDistance: metric=l2
9603 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9604 UnionExec
9605 ProjectionExec: expr=[_distance@2 as _distance, _rowid@1 as _rowid, vec@0 as vec]
9606 FilterExec: _distance@2 IS NOT NULL
9607 SortExec: TopK(fetch=34), expr=[_distance@2 ASC NULLS LAST, _rowid@1 ASC NULLS LAST]...
9608 KNNVectorDistance: metric=l2
9609 LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None
9610 Take: columns=\"_distance, _rowid, (vec)\"
9611 CoalesceBatchesExec: target_batch_size=8192
9612 SortExec: TopK(fetch=34), expr=[_distance@0 ASC NULLS LAST, _rowid@1 ASC NULLS LAST]...
9613 ANNSubIndex: name=idx, k=34, deltas=1, metric=L2
9614 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1",
9615 )
9616 .await
9617 .unwrap();
9618 }
9619
9620 #[tokio::test]
9621 async fn test_fast_search_without_vector_index_returns_empty() {
9622 let dataset = TestVectorDataset::new(LanceFileVersion::Stable, true)
9623 .await
9624 .unwrap();
9625 let q: Float32Array = (32..64).map(|v| v as f32).collect();
9626
9627 let mut scanner = dataset.dataset.scan();
9628 scanner.nearest("vec", &q, 10).unwrap();
9629 let normal_rows = scanner.try_into_batch().await.unwrap().num_rows();
9630
9631 let mut scanner = dataset.dataset.scan();
9632 scanner.nearest("vec", &q, 10).unwrap().fast_search();
9633 let fast_rows = scanner.try_into_batch().await.unwrap().num_rows();
9634
9635 assert_eq!(normal_rows, 10);
9636 assert_eq!(fast_rows, 0);
9637 }
9638
9639 #[rstest]
9640 #[tokio::test]
9641 pub async fn test_scan_planning_io(
9642 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
9643 data_storage_version: LanceFileVersion,
9644 ) {
9645 use lance_index::scalar::inverted::tokenizer::InvertedIndexParams;
9649 use lance_io::assert_io_eq;
9650 let data = gen_batch()
9651 .col(
9652 "vector",
9653 array::rand_vec::<Float32Type>(Dimension::from(32)),
9654 )
9655 .col("text", array::rand_utf8(ByteCount::from(4), false))
9656 .col("indexed", array::step::<Int32Type>())
9657 .col("not_indexed", array::step::<Int32Type>())
9658 .into_reader_rows(RowCount::from(100), BatchCount::from(5));
9659
9660 let mut dataset = Dataset::write(
9661 data,
9662 "memory://test",
9663 Some(WriteParams {
9664 data_storage_version: Some(data_storage_version),
9665 ..Default::default()
9666 }),
9667 )
9668 .await
9669 .unwrap();
9670 dataset
9671 .create_index(
9672 &["indexed"],
9673 IndexType::Scalar,
9674 None,
9675 &ScalarIndexParams::default(),
9676 false,
9677 )
9678 .await
9679 .unwrap();
9680 dataset
9681 .create_index(
9682 &["text"],
9683 IndexType::Inverted,
9684 None,
9685 &InvertedIndexParams::default(),
9686 false,
9687 )
9688 .await
9689 .unwrap();
9690 dataset
9691 .create_index(
9692 &["vector"],
9693 IndexType::Vector,
9694 None,
9695 &VectorIndexParams {
9696 metric_type: DistanceType::L2,
9697 stages: vec![
9698 StageParams::Ivf(IvfBuildParams {
9699 max_iters: 2,
9700 num_partitions: Some(2),
9701 sample_rate: 2,
9702 ..Default::default()
9703 }),
9704 StageParams::PQ(PQBuildParams {
9705 max_iters: 2,
9706 num_sub_vectors: 2,
9707 ..Default::default()
9708 }),
9709 ],
9710 version: crate::index::vector::IndexFileVersion::Legacy,
9711 skip_transpose: false,
9712 },
9713 false,
9714 )
9715 .await
9716 .unwrap();
9717
9718 dataset
9720 .scan()
9721 .prefilter(true)
9722 .filter("indexed > 10")
9723 .unwrap()
9724 .explain_plan(true)
9725 .await
9726 .unwrap();
9727
9728 let io_stats = dataset.object_store().io_stats_incremental();
9730 assert_io_gt!(io_stats, read_iops, 0);
9731
9732 dataset
9734 .scan()
9735 .prefilter(true)
9736 .filter("indexed > 10")
9737 .unwrap()
9738 .explain_plan(true)
9739 .await
9740 .unwrap();
9741
9742 let io_stats = dataset.object_store().io_stats_incremental();
9743 assert_io_eq!(io_stats, read_iops, 0);
9744
9745 dataset
9746 .scan()
9747 .prefilter(true)
9748 .filter("true")
9749 .unwrap()
9750 .explain_plan(true)
9751 .await
9752 .unwrap();
9753
9754 let io_stats = dataset.object_store().io_stats_incremental();
9755 assert_io_eq!(io_stats, read_iops, 0);
9756
9757 dataset
9758 .scan()
9759 .prefilter(true)
9760 .materialization_style(MaterializationStyle::AllEarly)
9761 .filter("true")
9762 .unwrap()
9763 .explain_plan(true)
9764 .await
9765 .unwrap();
9766
9767 let io_stats = dataset.object_store().io_stats_incremental();
9768 assert_io_eq!(io_stats, read_iops, 0);
9769
9770 dataset
9771 .scan()
9772 .prefilter(true)
9773 .materialization_style(MaterializationStyle::AllLate)
9774 .filter("true")
9775 .unwrap()
9776 .explain_plan(true)
9777 .await
9778 .unwrap();
9779
9780 let io_stats = dataset.object_store().io_stats_incremental();
9781 assert_io_eq!(io_stats, read_iops, 0);
9782 }
9783
9784 #[rstest]
9785 #[tokio::test]
9786 pub async fn test_row_meta_columns(
9787 #[values(
9788 (true, false), (false, true), (true, true) )]
9792 columns: (bool, bool),
9793 ) {
9794 let (with_row_id, with_row_address) = columns;
9795 let test_dir = TempStrDir::default();
9796 let uri = &test_dir;
9797
9798 let schema = Arc::new(arrow_schema::Schema::new(vec![
9799 arrow_schema::Field::new("data_item_id", arrow_schema::DataType::Int32, false),
9800 arrow_schema::Field::new("a", arrow_schema::DataType::Int32, false),
9801 ]));
9802
9803 let data = RecordBatch::try_new(
9804 schema.clone(),
9805 vec![
9806 Arc::new(Int32Array::from(vec![1001, 1002, 1003])),
9807 Arc::new(Int32Array::from(vec![1, 2, 3])),
9808 ],
9809 )
9810 .unwrap();
9811
9812 let dataset = Dataset::write(
9813 RecordBatchIterator::new(vec![Ok(data)], schema.clone()),
9814 uri,
9815 None,
9816 )
9817 .await
9818 .unwrap();
9819
9820 let mut scanner = dataset.scan();
9822
9823 let mut projection = vec!["data_item_id".to_string()];
9824 if with_row_id {
9825 scanner.with_row_id();
9826 projection.push(ROW_ID.to_string());
9827 }
9828 if with_row_address {
9829 scanner.with_row_address();
9830 projection.push(ROW_ADDR.to_string());
9831 }
9832
9833 scanner.project(&projection).unwrap();
9834 let stream = scanner.try_into_stream().await.unwrap();
9835 let batch = stream.try_collect::<Vec<_>>().await.unwrap().pop().unwrap();
9836
9837 if with_row_id {
9839 let column = batch.column_by_name(ROW_ID).unwrap();
9840 assert_eq!(column.data_type(), &DataType::UInt64);
9841 }
9842 if with_row_address {
9843 let column = batch.column_by_name(ROW_ADDR).unwrap();
9844 assert_eq!(column.data_type(), &DataType::UInt64);
9845 }
9846
9847 let mut scanner = dataset.scan();
9849 if with_row_id {
9850 scanner.with_row_id();
9851 }
9852 if with_row_address {
9853 scanner.with_row_address();
9854 }
9855 scanner.project(&["data_item_id"]).unwrap();
9856 let stream = scanner.try_into_stream().await.unwrap();
9857 let batch = stream.try_collect::<Vec<_>>().await.unwrap().pop().unwrap();
9858 let meta_column = batch.column_by_name(if with_row_id { ROW_ID } else { ROW_ADDR });
9859 assert!(meta_column.is_some());
9860
9861 let mut scanner = dataset.scan();
9863 if with_row_id {
9864 scanner.project(&[ROW_ID]).unwrap();
9865 } else {
9866 scanner.project(&[ROW_ADDR]).unwrap();
9867 };
9868 let stream = scanner.try_into_stream().await.unwrap();
9869 assert_eq!(stream.schema().fields().len(), 1);
9870 if with_row_id {
9871 assert!(stream.schema().field_with_name(ROW_ID).is_ok());
9872 } else {
9873 assert!(stream.schema().field_with_name(ROW_ADDR).is_ok());
9874 }
9875 }
9876
9877 async fn limit_offset_equivalency_test(scanner: &Scanner) {
9878 async fn test_one(
9879 scanner: &Scanner,
9880 full_result: &RecordBatch,
9881 limit: Option<i64>,
9882 offset: Option<i64>,
9883 ) {
9884 let mut new_scanner = scanner.clone();
9885 new_scanner.limit(limit, offset).unwrap();
9886 if let Some(nearest) = new_scanner.nearest_mut() {
9887 nearest.k = offset.unwrap_or(0).saturating_add(limit.unwrap_or(10_000)) as usize;
9888 }
9889 let result = new_scanner.try_into_batch().await.unwrap();
9890
9891 let resolved_offset = offset.unwrap_or(0).min(full_result.num_rows() as i64);
9892 let resolved_length = limit
9893 .unwrap_or(i64::MAX)
9894 .min(full_result.num_rows() as i64 - resolved_offset);
9895
9896 let expected = full_result.slice(resolved_offset as usize, resolved_length as usize);
9897
9898 if expected != result {
9899 let plan = new_scanner.analyze_plan().await.unwrap();
9900 assert_eq!(
9901 &expected, &result,
9902 "Limit: {:?}, Offset: {:?}, Plan: \n{}",
9903 limit, offset, plan
9904 );
9905 }
9906 }
9907
9908 let mut scanner_full = scanner.clone();
9909 if let Some(nearest) = scanner_full.nearest_mut() {
9910 nearest.k = 500;
9911 }
9912 let full_results = scanner_full.try_into_batch().await.unwrap();
9913
9914 test_one(scanner, &full_results, Some(1), None).await;
9915 test_one(scanner, &full_results, Some(1), Some(1)).await;
9916 test_one(scanner, &full_results, Some(1), Some(2)).await;
9917 test_one(scanner, &full_results, Some(1), Some(10)).await;
9918
9919 test_one(scanner, &full_results, Some(3), None).await;
9920 test_one(scanner, &full_results, Some(3), Some(2)).await;
9921 test_one(scanner, &full_results, Some(3), Some(4)).await;
9922
9923 test_one(scanner, &full_results, None, Some(3)).await;
9924 test_one(scanner, &full_results, None, Some(10)).await;
9925 }
9926
9927 #[tokio::test]
9928 async fn test_scan_limit_offset() {
9929 let test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9930 .await
9931 .unwrap();
9932 let scanner = test_ds.dataset.scan();
9933 limit_offset_equivalency_test(&scanner).await;
9934 }
9935
9936 #[tokio::test]
9937 async fn test_knn_limit_offset() {
9938 let test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9939 .await
9940 .unwrap();
9941 let query_vector = Float32Array::from(vec![0.0; 32]);
9942 let mut scanner = test_ds.dataset.scan();
9943 scanner
9944 .nearest("vec", &query_vector, 5)
9945 .unwrap()
9946 .project(&["i"])
9947 .unwrap();
9948 limit_offset_equivalency_test(&scanner).await;
9949 }
9950
9951 #[tokio::test]
9952 async fn test_knn_query_parallelism_defaults_and_setter() {
9953 let test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9954 .await
9955 .unwrap();
9956 let query_vector = Float32Array::from(vec![0.0; 32]);
9957 let mut scanner = test_ds.dataset.scan();
9958 scanner.nearest("vec", &query_vector, 5).unwrap();
9959 assert_eq!(
9960 scanner.nearest_mut().unwrap().query_parallelism,
9961 DEFAULT_QUERY_PARALLELISM
9962 );
9963
9964 scanner.query_parallelism(4);
9965 assert_eq!(scanner.nearest_mut().unwrap().query_parallelism, 4);
9966
9967 scanner.query_parallelism(-1);
9968 assert_eq!(scanner.nearest_mut().unwrap().query_parallelism, -1);
9969 }
9970
9971 #[tokio::test]
9972 async fn test_ivf_pq_query_parallelism_returns_same_results() {
9973 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9974 .await
9975 .unwrap();
9976 test_ds.make_vector_index().await.unwrap();
9977
9978 let query_vector = Float32Array::from(vec![0.0; 32]);
9979
9980 let mut sequential = test_ds.dataset.scan();
9981 sequential.nearest("vec", &query_vector, 50).unwrap();
9982 let sequential_results = sequential.try_into_batch().await.unwrap();
9983
9984 let mut parallel = test_ds.dataset.scan();
9985 parallel
9986 .nearest("vec", &query_vector, 50)
9987 .unwrap()
9988 .query_parallelism(4);
9989 let parallel_results = parallel.try_into_batch().await.unwrap();
9990
9991 assert_eq!(sequential_results, parallel_results);
9992 }
9993
9994 #[tokio::test]
9995 async fn test_ivf_pq_limit_offset() {
9996 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9997 .await
9998 .unwrap();
9999 test_ds.make_vector_index().await.unwrap();
10000 test_ds.append_new_data().await.unwrap();
10001 let query_vector = Float32Array::from(vec![0.0; 32]);
10002 let mut scanner = test_ds.dataset.scan();
10003 scanner.nearest("vec", &query_vector, 500).unwrap();
10004 limit_offset_equivalency_test(&scanner).await;
10005 }
10006
10007 #[tokio::test]
10008 async fn test_fts_limit_offset() {
10009 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10010 .await
10011 .unwrap();
10012 test_ds.make_fts_index().await.unwrap();
10013 test_ds.append_new_data().await.unwrap();
10014 let mut scanner = test_ds.dataset.scan();
10015 scanner
10016 .full_text_search(FullTextSearchQuery::new("4".into()))
10017 .unwrap();
10018 limit_offset_equivalency_test(&scanner).await;
10019 }
10020
10021 #[tokio::test]
10022 async fn test_fts_fast_search_excludes_unindexed_rows() {
10023 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10024 .await
10025 .unwrap();
10026 test_ds.make_fts_index().await.unwrap();
10027 test_ds.append_data_with_range(10, 20).await.unwrap();
10029
10030 let mut scanner = test_ds.dataset.scan();
10031 scanner
10032 .full_text_search(FullTextSearchQuery::new_query(
10033 MatchQuery::new("15".to_owned())
10034 .with_column(Some("s".to_owned()))
10035 .into(),
10036 ))
10037 .unwrap();
10038 let normal_rows = scanner.try_into_batch().await.unwrap().num_rows();
10039
10040 let mut scanner = test_ds.dataset.scan();
10041 scanner
10042 .full_text_search(FullTextSearchQuery::new_query(
10043 MatchQuery::new("15".to_owned())
10044 .with_column(Some("s".to_owned()))
10045 .into(),
10046 ))
10047 .unwrap()
10048 .fast_search();
10049 let fast_rows = scanner.try_into_batch().await.unwrap().num_rows();
10050
10051 assert_eq!(normal_rows, 2);
10052 assert_eq!(fast_rows, 1);
10053 }
10054
10055 async fn test_row_offset_read_helper(
10056 ds: &Dataset,
10057 scan_builder: impl FnOnce(&mut Scanner) -> &mut Scanner,
10058 expected_cols: &[&str],
10059 expected_row_offsets: &[u64],
10060 ) {
10061 let mut scanner = ds.scan();
10062 let scanner = scan_builder(&mut scanner);
10063 let stream = scanner.try_into_stream().await.unwrap();
10064
10065 let schema = stream.schema();
10066 let actual_cols = schema
10067 .fields()
10068 .iter()
10069 .map(|f| f.name().as_str())
10070 .collect::<Vec<_>>();
10071 assert_eq!(&actual_cols, expected_cols);
10072
10073 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
10074 let batch = arrow_select::concat::concat_batches(&schema, &batches).unwrap();
10075
10076 let row_offsets = batch
10077 .column_by_name(ROW_OFFSET)
10078 .unwrap()
10079 .as_primitive::<UInt64Type>()
10080 .values();
10081 assert_eq!(row_offsets.as_ref(), expected_row_offsets);
10082 }
10083
10084 #[tokio::test]
10085 async fn test_row_offset_read() {
10086 let mut ds = lance_datagen::gen_batch()
10087 .col("idx", array::step::<Int32Type>())
10088 .into_ram_dataset(FragmentCount::from(3), FragmentRowCount::from(3))
10089 .await
10090 .unwrap();
10091 ds.delete("idx >= 2 AND idx <= 6").await.unwrap();
10095
10096 test_row_offset_read_helper(
10098 &ds,
10099 |scanner| scanner.project(&["idx", ROW_OFFSET]).unwrap(),
10100 &["idx", ROW_OFFSET],
10101 &[0, 1, 2, 3],
10102 )
10103 .await;
10104
10105 test_row_offset_read_helper(
10107 &ds,
10108 |scanner| scanner.project(&[ROW_OFFSET]).unwrap(),
10109 &[ROW_OFFSET],
10110 &[0, 1, 2, 3],
10111 )
10112 .await;
10113
10114 test_row_offset_read_helper(
10116 &ds,
10117 |scanner| {
10118 scanner
10119 .filter("idx > 1")
10120 .unwrap()
10121 .project(&[ROW_OFFSET])
10122 .unwrap()
10123 },
10124 &[ROW_OFFSET],
10125 &[2, 3],
10126 )
10127 .await;
10128 }
10129
10130 #[tokio::test]
10131 async fn test_filter_to_take() {
10132 let mut ds = lance_datagen::gen_batch()
10133 .col("idx", array::step::<Int32Type>())
10134 .into_ram_dataset(FragmentCount::from(3), FragmentRowCount::from(100))
10135 .await
10136 .unwrap();
10137
10138 let row_ids = ds
10139 .scan()
10140 .project(&Vec::<&str>::default())
10141 .unwrap()
10142 .with_row_id()
10143 .try_into_stream()
10144 .await
10145 .unwrap()
10146 .try_collect::<Vec<_>>()
10147 .await
10148 .unwrap();
10149 let schema = row_ids[0].schema();
10150 let row_ids = concat_batches(&schema, row_ids.iter()).unwrap();
10151 let row_ids = row_ids.column(0).as_primitive::<UInt64Type>().clone();
10152
10153 let row_addrs = ds
10154 .scan()
10155 .project(&Vec::<&str>::default())
10156 .unwrap()
10157 .with_row_address()
10158 .try_into_stream()
10159 .await
10160 .unwrap()
10161 .try_collect::<Vec<_>>()
10162 .await
10163 .unwrap();
10164 let schema = row_addrs[0].schema();
10165 let row_addrs = concat_batches(&schema, row_addrs.iter()).unwrap();
10166 let row_addrs = row_addrs.column(0).as_primitive::<UInt64Type>().clone();
10167
10168 ds.delete("idx >= 190 AND idx < 210").await.unwrap();
10169
10170 let ds_copy = ds.clone();
10171 let do_check = async move |filt: &str, expected_idx: &[i32], applies_optimization: bool| {
10172 let mut scanner = ds_copy.scan();
10173 scanner.filter(filt).unwrap();
10174 let plan = scanner.explain_plan(true).await.unwrap();
10176 if applies_optimization {
10177 assert!(
10178 plan.contains("OneShotStream"),
10179 "expected take optimization to be applied. Filter: '{}'. Plan:\n{}",
10180 filt,
10181 plan
10182 );
10183 } else {
10184 assert!(
10185 !plan.contains("OneShotStream"),
10186 "expected take optimization to not be applied. Filter: '{}'. Plan:\n{}",
10187 filt,
10188 plan
10189 );
10190 }
10191
10192 let stream = scanner.try_into_stream().await.unwrap();
10194 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
10195 let idx = batches
10196 .iter()
10197 .map(|b| b.column_by_name("idx").unwrap().as_ref())
10198 .collect::<Vec<_>>();
10199
10200 if idx.is_empty() {
10201 assert!(expected_idx.is_empty());
10202 return;
10203 }
10204
10205 let idx = arrow::compute::concat(&idx).unwrap();
10206 assert_eq!(idx.as_primitive::<Int32Type>().values(), expected_idx);
10207 };
10208 let check =
10209 async |filt: &str, expected_idx: &[i32]| do_check(filt, expected_idx, true).await;
10210 let check_no_opt = async |filt: &str, expected_idx: &[i32]| {
10211 do_check(filt, expected_idx, false).await;
10212 };
10213
10214 check("_rowid = 50", &[50]).await;
10216 check("_rowaddr = 50", &[50]).await;
10217 check("_rowoffset = 50", &[50]).await;
10218
10219 check(
10220 "_rowid = 50 OR _rowid = 51 OR _rowid = 52 OR _rowid = 49",
10221 &[49, 50, 51, 52],
10222 )
10223 .await;
10224 check(
10225 "_rowaddr = 50 OR _rowaddr = 51 OR _rowaddr = 52 OR _rowaddr = 49",
10226 &[49, 50, 51, 52],
10227 )
10228 .await;
10229 check(
10230 "_rowoffset = 50 OR _rowoffset = 51 OR _rowoffset = 52 OR _rowoffset = 49",
10231 &[49, 50, 51, 52],
10232 )
10233 .await;
10234
10235 check("_rowid IN (52, 51, 50, 17)", &[17, 50, 51, 52]).await;
10236 check("_rowaddr IN (52, 51, 50, 17)", &[17, 50, 51, 52]).await;
10237 check("_rowoffset IN (52, 51, 50, 17)", &[17, 50, 51, 52]).await;
10238
10239 check(&format!("_rowid = {}", row_ids.value(190)), &[]).await;
10243 check(&format!("_rowaddr = {}", row_addrs.value(190)), &[]).await;
10244 check("_rowoffset = 190", &[210]).await;
10247
10248 check(&format!("_rowid = {}", row_ids.value(250)), &[250]).await;
10250 check(&format!("_rowaddr = {}", row_addrs.value(250)), &[250]).await;
10251 check("_rowoffset = 250", &[270]).await;
10252
10253 check("_rowoffset = 1000", &[]).await;
10255
10256 check("_rowid IN (5, 10, 15) AND idx > 10", &[15]).await;
10258 check("_rowaddr IN (5, 10, 15) AND idx > 10", &[15]).await;
10259 check("_rowoffset IN (5, 10, 15) AND idx > 10", &[15]).await;
10260 check("idx > 10 AND _rowid IN (5, 10, 15)", &[15]).await;
10261 check("idx > 10 AND _rowaddr IN (5, 10, 15)", &[15]).await;
10262 check("idx > 10 AND _rowoffset IN (5, 10, 15)", &[15]).await;
10263 check("_rowid = 50 AND _rowid = 50", &[50]).await;
10265
10266 check_no_opt("_rowid = 50 AND _rowid = 51", &[]).await;
10268 check_no_opt("(_rowid = 50 AND idx < 100) OR _rowid = 51", &[50, 51]).await;
10269
10270 let mut scanner = ds.scan();
10272 scanner.filter("_rowoffset = 77").unwrap();
10273 scanner
10274 .project_with_transform(&[("foo", "idx * 2")])
10275 .unwrap();
10276 let stream = scanner.try_into_stream().await.unwrap();
10277 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
10278 assert_eq!(batches[0].schema().field(0).name(), "foo");
10279 let val = batches[0].column(0).as_primitive::<Int32Type>().values()[0];
10280 assert_eq!(val, 154);
10281 }
10282
10283 #[tokio::test]
10284 async fn test_nested_field_ordering() {
10285 use arrow_array::StructArray;
10286
10287 let id_array = Int32Array::from(vec![3, 1, 2]);
10289 let nested_values = Int32Array::from(vec![30, 10, 20]);
10290 let nested_struct = StructArray::from(vec![(
10291 Arc::new(ArrowField::new("value", DataType::Int32, false)),
10292 Arc::new(nested_values) as ArrayRef,
10293 )]);
10294
10295 let schema = Arc::new(ArrowSchema::new(vec![
10296 ArrowField::new("id", DataType::Int32, false),
10297 ArrowField::new(
10298 "nested",
10299 DataType::Struct(vec![ArrowField::new("value", DataType::Int32, false)].into()),
10300 false,
10301 ),
10302 ]));
10303
10304 let batch = RecordBatch::try_new(
10305 schema.clone(),
10306 vec![Arc::new(id_array), Arc::new(nested_struct)],
10307 )
10308 .unwrap();
10309
10310 let test_dir = TempStrDir::default();
10311 let test_uri = &test_dir;
10312 let reader = RecordBatchIterator::new(vec![Ok(batch)].into_iter(), schema.clone());
10313
10314 let dataset = Dataset::write(reader, test_uri, None).await.unwrap();
10315
10316 let mut scanner = dataset.scan();
10318 scanner
10319 .order_by(Some(vec![ColumnOrdering {
10320 column_name: "nested.value".to_string(),
10321 ascending: true,
10322 nulls_first: true,
10323 }]))
10324 .unwrap(); let stream = scanner.try_into_stream().await.unwrap();
10327 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
10328
10329 let sorted_ids = batches[0].column(0).as_primitive::<Int32Type>().values();
10331 assert_eq!(sorted_ids[0], 1); assert_eq!(sorted_ids[1], 2); assert_eq!(sorted_ids[2], 3); }
10335
10336 #[tokio::test]
10337 async fn test_limit_with_ordering_not_pushed_down() {
10338 let id_array = Int32Array::from(vec![5, 2, 8, 1, 3, 7, 4, 6]);
10344 let value_array = Int32Array::from(vec![50, 20, 80, 10, 30, 70, 40, 60]);
10345
10346 let schema = Arc::new(ArrowSchema::new(vec![
10347 ArrowField::new("id", DataType::Int32, false),
10348 ArrowField::new("value", DataType::Int32, false),
10349 ]));
10350
10351 let batch = RecordBatch::try_new(
10352 schema.clone(),
10353 vec![Arc::new(id_array), Arc::new(value_array)],
10354 )
10355 .unwrap();
10356
10357 let test_dir = TempStrDir::default();
10358 let test_uri = &test_dir;
10359 let reader = RecordBatchIterator::new(vec![Ok(batch)].into_iter(), schema.clone());
10360
10361 let dataset = Dataset::write(reader, test_uri, None).await.unwrap();
10362
10363 let mut scanner = dataset.scan();
10365 scanner
10366 .order_by(Some(vec![ColumnOrdering {
10367 column_name: "value".to_string(),
10368 ascending: true,
10369 nulls_first: true,
10370 }]))
10371 .unwrap();
10372 scanner.limit(Some(3), None).unwrap();
10373
10374 let stream = scanner.try_into_stream().await.unwrap();
10375 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
10376
10377 let sorted_ids = batches[0].column(0).as_primitive::<Int32Type>().values();
10379 let sorted_values = batches[0].column(1).as_primitive::<Int32Type>().values();
10380 assert_eq!(batches[0].num_rows(), 3);
10381 assert_eq!(sorted_ids[0], 1); assert_eq!(sorted_ids[1], 2); assert_eq!(sorted_ids[2], 3); assert_eq!(sorted_values[0], 10);
10385 assert_eq!(sorted_values[1], 20);
10386 assert_eq!(sorted_values[2], 30);
10387
10388 let mut scanner = dataset.scan();
10390 scanner
10391 .order_by(Some(vec![ColumnOrdering {
10392 column_name: "value".to_string(),
10393 ascending: true,
10394 nulls_first: true,
10395 }]))
10396 .unwrap();
10397 scanner.limit(Some(3), Some(2)).unwrap(); let stream = scanner.try_into_stream().await.unwrap();
10400 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
10401
10402 let sorted_ids = batches[0].column(0).as_primitive::<Int32Type>().values();
10403 let sorted_values = batches[0].column(1).as_primitive::<Int32Type>().values();
10404 assert_eq!(batches[0].num_rows(), 3);
10405 assert_eq!(sorted_ids[0], 3); assert_eq!(sorted_ids[1], 4); assert_eq!(sorted_ids[2], 5); assert_eq!(sorted_values[0], 30);
10409 assert_eq!(sorted_values[1], 40);
10410 assert_eq!(sorted_values[2], 50);
10411
10412 let mut scanner = dataset.scan();
10414 scanner.limit(Some(3), None).unwrap();
10415
10416 let stream = scanner.try_into_stream().await.unwrap();
10417 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
10418
10419 assert_eq!(batches[0].num_rows(), 3);
10421 let unsorted_values = batches[0].column(1).as_primitive::<Int32Type>().values();
10422 assert_eq!(unsorted_values[0], 50);
10424 assert_eq!(unsorted_values[1], 20);
10425 assert_eq!(unsorted_values[2], 80);
10426 }
10427
10428 #[tokio::test]
10429 async fn test_scan_with_version_columns() {
10430 use arrow_array::{Int32Array, RecordBatch, RecordBatchIterator};
10431 use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
10432
10433 let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
10435 "id",
10436 DataType::Int32,
10437 false,
10438 )]));
10439
10440 let batch = RecordBatch::try_new(
10441 schema.clone(),
10442 vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
10443 )
10444 .unwrap();
10445
10446 let test_dir = lance_core::utils::tempfile::TempStrDir::default();
10447 let test_uri = test_dir.as_str();
10448
10449 let reader = RecordBatchIterator::new(vec![Ok(batch)], schema);
10450 let write_params = WriteParams {
10451 enable_stable_row_ids: true,
10452 ..Default::default()
10453 };
10454 Dataset::write(reader, test_uri, Some(write_params))
10455 .await
10456 .unwrap();
10457
10458 let dataset = Dataset::open(test_uri).await.unwrap();
10459 let mut scanner = dataset.scan();
10460
10461 scanner
10462 .project(&[ROW_CREATED_AT_VERSION, ROW_LAST_UPDATED_AT_VERSION])
10463 .unwrap();
10464
10465 let output_schema = scanner.schema().await.unwrap();
10467 assert!(
10468 output_schema
10469 .column_with_name("_row_last_updated_at_version")
10470 .is_some(),
10471 "Schema should include _row_last_updated_at_version"
10472 );
10473 assert!(
10474 output_schema
10475 .column_with_name("_row_created_at_version")
10476 .is_some(),
10477 "Schema should include _row_created_at_version"
10478 );
10479
10480 let batches = scanner
10482 .try_into_stream()
10483 .await
10484 .unwrap()
10485 .try_collect::<Vec<_>>()
10486 .await
10487 .unwrap();
10488
10489 assert_eq!(batches.len(), 1);
10490 let batch = &batches[0];
10491
10492 let last_updated = batch
10494 .column_by_name("_row_last_updated_at_version")
10495 .expect("Should have _row_last_updated_at_version column");
10496 let created_at = batch
10497 .column_by_name("_row_created_at_version")
10498 .expect("Should have _row_created_at_version column");
10499
10500 let last_updated_array = last_updated
10502 .as_any()
10503 .downcast_ref::<arrow_array::UInt64Array>()
10504 .unwrap();
10505 let created_at_array = created_at
10506 .as_any()
10507 .downcast_ref::<arrow_array::UInt64Array>()
10508 .unwrap();
10509
10510 for i in 0..batch.num_rows() {
10511 assert_eq!(
10512 last_updated_array.value(i),
10513 1,
10514 "All rows last updated at version 1"
10515 );
10516 assert_eq!(
10517 created_at_array.value(i),
10518 1,
10519 "All rows created at version 1"
10520 );
10521 }
10522 }
10523
10524 #[test_log::test(test)]
10525 fn test_scan_finishes_all_tasks() {
10526 let runtime = tokio::runtime::Builder::new_multi_thread()
10528 .enable_time()
10529 .build()
10530 .unwrap();
10531
10532 runtime.block_on(async move {
10533 let ds = lance_datagen::gen_batch()
10534 .col("id", lance_datagen::array::step::<Int32Type>())
10535 .into_ram_dataset(FragmentCount::from(1000), FragmentRowCount::from(10))
10536 .await
10537 .unwrap();
10538
10539 let mut stream = ds
10547 .scan()
10548 .fragment_readahead(1000)
10549 .batch_size(1)
10550 .io_buffer_size(1)
10551 .batch_readahead(1)
10552 .try_into_stream()
10553 .await
10554 .unwrap();
10555 stream.next().await.unwrap().unwrap();
10556 });
10557
10558 let start = Instant::now();
10559 while start.elapsed() < Duration::from_secs(10) {
10560 if runtime.handle().metrics().num_alive_tasks() == 0 {
10561 break;
10562 }
10563 std::thread::sleep(Duration::from_millis(100));
10564 }
10565
10566 assert!(
10567 runtime.handle().metrics().num_alive_tasks() == 0,
10568 "Tasks should have finished within 10 seconds but there are still {} tasks running",
10569 runtime.handle().metrics().num_alive_tasks()
10570 );
10571 }
10572
10573 fn find_filtered_read(plan: &dyn ExecutionPlan) -> Option<&FilteredReadExec> {
10574 if let Some(f) = plan.as_any().downcast_ref::<FilteredReadExec>() {
10575 return Some(f);
10576 }
10577 for child in plan.children() {
10578 if let Some(f) = find_filtered_read(child.as_ref()) {
10579 return Some(f);
10580 }
10581 }
10582 None
10583 }
10584
10585 #[tokio::test]
10586 async fn test_io_buffer_size_explicit_propagated() {
10587 let data = lance_datagen::gen_batch()
10592 .col("x", lance_datagen::array::step::<Int32Type>())
10593 .into_reader_rows(RowCount::from(8), BatchCount::from(1));
10594 let dataset = Dataset::write(data, "memory://test_io_buffer_explicit", None)
10595 .await
10596 .unwrap();
10597
10598 let plan = dataset.scan().create_plan().await.unwrap();
10599 let filtered = find_filtered_read(plan.as_ref())
10600 .expect("expected a FilteredReadExec in the scan plan");
10601 assert_eq!(filtered.options().io_buffer_size_bytes, None);
10602
10603 let mut scanner = dataset.scan();
10604 scanner.io_buffer_size(7777);
10605 let plan = scanner.create_plan().await.unwrap();
10606 let filtered = find_filtered_read(plan.as_ref())
10607 .expect("expected a FilteredReadExec in the scan plan");
10608 assert_eq!(filtered.options().io_buffer_size_bytes, Some(7777));
10609 }
10610
10611 #[test]
10615 #[serial_test::serial(LANCE_DEFAULT_IO_BUFFER_SIZE)]
10616 fn test_default_io_buffer_size_override_env_var() {
10617 let _ = *DEFAULT_IO_BUFFER_SIZE;
10622
10623 unsafe {
10627 std::env::set_var("LANCE_DEFAULT_IO_BUFFER_SIZE", "4096");
10628 }
10629 assert_eq!(get_default_io_buffer_size_override(), Some(4096));
10630
10631 unsafe {
10632 std::env::set_var("LANCE_DEFAULT_IO_BUFFER_SIZE", "not_a_number");
10633 }
10634 assert_eq!(get_default_io_buffer_size_override(), None);
10635
10636 unsafe {
10637 std::env::remove_var("LANCE_DEFAULT_IO_BUFFER_SIZE");
10638 }
10639 assert_eq!(get_default_io_buffer_size_override(), None);
10640 }
10641
10642 fn assert_values_in_range(array: &Int32Array, range: std::ops::Range<i32>, msg: &str) {
10643 assert!(!array.is_empty(), "Expected some results but got none");
10644 assert!(
10645 array
10646 .iter()
10647 .all(|v| v.is_some_and(|val| range.contains(&val))),
10648 "{msg} (expected range {range:?})"
10649 );
10650 }
10651
10652 fn assert_has_all_fragments(array: &Int32Array) {
10654 assert!(
10655 array
10656 .iter()
10657 .any(|v| v.is_some_and(|val| (0..200).contains(&val)))
10658 && array
10659 .iter()
10660 .any(|v| v.is_some_and(|val| (200..400).contains(&val)))
10661 && array
10662 .iter()
10663 .any(|v| v.is_some_and(|val| (400..410).contains(&val)))
10664 && array
10665 .iter()
10666 .any(|v| v.is_some_and(|val| (410..420).contains(&val))),
10667 "Expected results from all fragments"
10668 );
10669 }
10670
10671 async fn test_fragment_list_filtering(
10673 test_ds: &TestVectorDataset,
10674 fragments: &[Fragment],
10675 mut build_scanner: impl FnMut(&Dataset) -> Scanner,
10676 ) {
10677 let batch = build_scanner(&test_ds.dataset)
10679 .try_into_batch()
10680 .await
10681 .unwrap();
10682 let i_array = batch
10683 .column_by_name("i")
10684 .unwrap()
10685 .as_any()
10686 .downcast_ref::<Int32Array>()
10687 .unwrap();
10688 assert_has_all_fragments(i_array);
10689
10690 let mut scanner = build_scanner(&test_ds.dataset);
10692 scanner.with_fragments(vec![fragments[2].clone()]);
10693 let batch = scanner.try_into_batch().await.unwrap();
10694 let i_array = batch
10695 .column_by_name("i")
10696 .unwrap()
10697 .as_any()
10698 .downcast_ref::<Int32Array>()
10699 .unwrap();
10700 assert_values_in_range(i_array, 400..410, "Should only get results from fragment 2");
10701
10702 let mut scanner = build_scanner(&test_ds.dataset);
10704 scanner.with_fragments(vec![fragments[0].clone()]);
10705 let batch = scanner.try_into_batch().await.unwrap();
10706 let i_array = batch
10707 .column_by_name("i")
10708 .unwrap()
10709 .as_any()
10710 .downcast_ref::<Int32Array>()
10711 .unwrap();
10712 assert_values_in_range(i_array, 0..200, "Should only get results from fragment 0");
10713
10714 let mut scanner = build_scanner(&test_ds.dataset);
10716 scanner.with_fragments(vec![
10717 fragments[0].clone(),
10718 fragments[1].clone(),
10719 fragments[2].clone(),
10720 ]);
10721 let batch = scanner.try_into_batch().await.unwrap();
10722 let i_array = batch
10723 .column_by_name("i")
10724 .unwrap()
10725 .as_any()
10726 .downcast_ref::<Int32Array>()
10727 .unwrap();
10728 assert_values_in_range(
10729 i_array,
10730 0..410,
10731 "Should get results from fragments 0, 1, and 2, excluding fragment 3",
10732 );
10733
10734 let mut scanner = build_scanner(&test_ds.dataset);
10736 scanner.with_fragments(vec![fragments[0].clone(), fragments[2].clone()]);
10737 let batch = scanner.try_into_batch().await.unwrap();
10738 let i_array = batch
10739 .column_by_name("i")
10740 .unwrap()
10741 .as_any()
10742 .downcast_ref::<Int32Array>()
10743 .unwrap();
10744 assert!(
10745 i_array
10746 .iter()
10747 .all(|v| v.is_some_and(|val| (0..200).contains(&val) || (400..410).contains(&val)))
10748 && i_array
10749 .iter()
10750 .any(|v| v.is_some_and(|val| (0..200).contains(&val)))
10751 && i_array
10752 .iter()
10753 .any(|v| v.is_some_and(|val| (400..410).contains(&val))),
10754 "Should only get results from fragment 0 (indexed) and fragment 2 (unindexed)"
10755 );
10756 }
10757
10758 #[tokio::test]
10759 async fn test_vector_search_respects_fragment_list() {
10760 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10761 .await
10762 .unwrap();
10763
10764 test_ds.make_segmented_vector_index().await.unwrap();
10766
10767 let query: Float32Array = (0..32).map(|v| v as f32).collect();
10768
10769 test_ds.append_data_with_range(400, 410).await.unwrap();
10771 test_ds.append_data_with_range(410, 420).await.unwrap();
10772
10773 let fragments = test_ds.dataset.fragments();
10776 assert_eq!(fragments.len(), 4);
10777
10778 test_fragment_list_filtering(&test_ds, fragments, |dataset| {
10779 let mut scanner = dataset.scan();
10780 scanner.nearest("vec", &query, 420).unwrap();
10781 scanner
10782 })
10783 .await;
10784 }
10785
10786 #[tokio::test]
10787 async fn test_vector_search_fragment_filter_prunes_segment_fanout() {
10788 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10789 .await
10790 .unwrap();
10791 test_ds.make_segmented_vector_index().await.unwrap();
10792
10793 let query: Float32Array = (0..32).map(|v| v as f32).collect();
10794 test_ds.append_data_with_range(400, 410).await.unwrap();
10795 test_ds.append_data_with_range(410, 420).await.unwrap();
10796 let fragments = test_ds.dataset.fragments();
10797
10798 let mut scanner = test_ds.dataset.scan();
10799 scanner.nearest("vec", &query, 420).unwrap();
10800 let full_plan = scanner.explain_plan(true).await.unwrap();
10801 assert!(
10802 full_plan.contains("ANNSubIndex: name=idx, k=420, deltas=2, metric=L2"),
10803 "expected two ANN deltas without fragment filter, plan was:\n{full_plan}"
10804 );
10805
10806 let mut scanner = test_ds.dataset.scan();
10807 scanner
10808 .nearest("vec", &query, 420)
10809 .unwrap()
10810 .with_fragments(vec![fragments[0].clone()]);
10811 let filtered_plan = scanner.explain_plan(true).await.unwrap();
10812 assert!(
10813 filtered_plan.contains("ANNSubIndex: name=idx, k=420, deltas=1, metric=L2"),
10814 "expected one ANN delta with fragment filter, plan was:\n{filtered_plan}"
10815 );
10816 }
10817
10818 #[tokio::test]
10819 async fn test_vector_search_respects_index_segments() {
10820 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10821 .await
10822 .unwrap();
10823 let segment_ids = test_ds.make_segmented_vector_index().await.unwrap();
10824
10825 let query: Float32Array = (0..32).map(|v| v as f32).collect();
10826 test_ds.append_data_with_range(400, 410).await.unwrap();
10827 test_ds.append_data_with_range(410, 420).await.unwrap();
10828
10829 let mut scanner = test_ds.dataset.scan();
10830 scanner
10831 .nearest("vec", &query, 420)
10832 .unwrap()
10833 .with_index_segments(vec![segment_ids[0]])
10834 .unwrap();
10835 let batch = scanner.try_into_batch().await.unwrap();
10836 let i_array = batch
10837 .column_by_name("i")
10838 .unwrap()
10839 .as_any()
10840 .downcast_ref::<Int32Array>()
10841 .unwrap();
10842 assert_eq!(batch.num_rows(), 200);
10843 assert_values_in_range(
10844 i_array,
10845 0..200,
10846 "Should only get results from the selected index segment",
10847 );
10848 }
10849
10850 #[tokio::test]
10851 async fn test_vector_search_intersects_fragments_and_index_segments() {
10852 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10853 .await
10854 .unwrap();
10855 let segment_ids = test_ds.make_segmented_vector_index().await.unwrap();
10856
10857 let query: Float32Array = (0..32).map(|v| v as f32).collect();
10858 test_ds.append_data_with_range(400, 410).await.unwrap();
10859 test_ds.append_data_with_range(410, 420).await.unwrap();
10860 let fragments = test_ds.dataset.fragments();
10861
10862 let mut scanner = test_ds.dataset.scan();
10863 scanner
10864 .nearest("vec", &query, 420)
10865 .unwrap()
10866 .with_fragments(vec![fragments[0].clone(), fragments[2].clone()])
10867 .with_index_segments(vec![segment_ids[0]])
10868 .unwrap();
10869 let batch = scanner.try_into_batch().await.unwrap();
10870 let i_array = batch
10871 .column_by_name("i")
10872 .unwrap()
10873 .as_any()
10874 .downcast_ref::<Int32Array>()
10875 .unwrap();
10876 assert!(
10877 i_array
10878 .iter()
10879 .all(|v| v.is_some_and(|val| (0..200).contains(&val) || (400..410).contains(&val)))
10880 && i_array
10881 .iter()
10882 .any(|v| v.is_some_and(|val| (0..200).contains(&val)))
10883 && i_array
10884 .iter()
10885 .any(|v| v.is_some_and(|val| (400..410).contains(&val))),
10886 "Should get selected segment rows plus flat fallback for target fragments outside the selected segments"
10887 );
10888 }
10889
10890 #[tokio::test]
10891 async fn test_vector_search_rejects_unknown_index_segment() {
10892 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10893 .await
10894 .unwrap();
10895 test_ds.make_segmented_vector_index().await.unwrap();
10896
10897 let query: Float32Array = (0..32).map(|v| v as f32).collect();
10898 let err = test_ds
10899 .dataset
10900 .scan()
10901 .nearest("vec", &query, 10)
10902 .unwrap()
10903 .with_index_segments(vec![Uuid::new_v4()])
10904 .unwrap()
10905 .try_into_batch()
10906 .await
10907 .unwrap_err();
10908 assert!(
10909 err.to_string().contains("unknown index segments"),
10910 "unexpected error: {err}"
10911 );
10912 }
10913
10914 #[tokio::test]
10915 async fn test_vector_search_rejects_metric_mismatch_for_index_segments() {
10916 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10917 .await
10918 .unwrap();
10919 let segment_ids = test_ds.make_segmented_vector_index().await.unwrap();
10920
10921 let query: Float32Array = (0..32).map(|v| v as f32).collect();
10922 let err = test_ds
10923 .dataset
10924 .scan()
10925 .nearest("vec", &query, 10)
10926 .unwrap()
10927 .distance_metric(DistanceType::Dot)
10928 .with_index_segments(vec![segment_ids[0]])
10929 .unwrap()
10930 .try_into_batch()
10931 .await
10932 .unwrap_err();
10933 assert!(
10934 err.to_string()
10935 .contains("with_index_segments requested metric"),
10936 "unexpected error: {err}"
10937 );
10938 }
10939
10940 #[tokio::test]
10941 async fn test_with_index_segments_rejects_empty_list() {
10942 let test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10943 .await
10944 .unwrap();
10945 let query: Float32Array = (0..32).map(|v| v as f32).collect();
10946
10947 let Err(err) = test_ds
10948 .dataset
10949 .scan()
10950 .nearest("vec", &query, 10)
10951 .unwrap()
10952 .with_index_segments(vec![])
10953 else {
10954 panic!("expected empty index segments to be rejected");
10955 };
10956 assert!(
10957 err.to_string()
10958 .contains("with_index_segments does not accept an empty segment list"),
10959 "unexpected error: {err}"
10960 );
10961 }
10962
10963 #[tokio::test]
10964 async fn test_with_index_segments_rejected_for_non_vector_query() {
10965 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10966 .await
10967 .unwrap();
10968 let segment_ids = test_ds.make_segmented_vector_index().await.unwrap();
10969
10970 let err = test_ds
10971 .dataset
10972 .scan()
10973 .project(&["i"])
10974 .unwrap()
10975 .with_index_segments(vec![segment_ids[0]])
10976 .unwrap()
10977 .try_into_batch()
10978 .await
10979 .unwrap_err();
10980 assert!(
10981 err.to_string()
10982 .contains("with_index_segments is only supported for vector search"),
10983 "unexpected error: {err}"
10984 );
10985 }
10986
10987 #[tokio::test]
10988 async fn test_fts_respects_fragment_list() {
10989 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10990 .await
10991 .unwrap();
10992
10993 test_ds.make_fts_index().await.unwrap();
10995
10996 test_ds.append_data_with_range(400, 410).await.unwrap();
10998 test_ds.append_data_with_range(410, 420).await.unwrap();
10999
11000 let fragments = test_ds.dataset.fragments();
11003 assert_eq!(fragments.len(), 4);
11004
11005 test_fragment_list_filtering(&test_ds, fragments, |dataset| {
11007 let mut scanner = dataset.scan();
11008 scanner
11009 .full_text_search(FullTextSearchQuery::new("s-5".into()))
11010 .unwrap();
11011 scanner
11012 })
11013 .await;
11014 }
11015}