1use std::ops::Range;
5use std::pin::Pin;
6use std::sync::{Arc, LazyLock};
7use std::task::{Context, Poll};
8
9use arrow::array::AsArray;
10use arrow_array::{Array, Float32Array, Int64Array, RecordBatch};
11use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema, SchemaRef, SortOptions};
12use arrow_select::concat::concat_batches;
13use async_recursion::async_recursion;
14use chrono::Utc;
15use datafusion::common::{DFSchema, JoinType, NullEquality, SchemaExt, exec_datafusion_err};
16use datafusion::functions_aggregate;
17use datafusion::logical_expr::{Expr, ScalarUDF, col, lit};
18use datafusion::physical_expr::PhysicalSortExpr;
19use datafusion::physical_plan::coalesce_batches::CoalesceBatchesExec;
20use datafusion::physical_plan::expressions;
21use datafusion::physical_plan::projection::ProjectionExec as DFProjectionExec;
22use datafusion::physical_plan::sorts::sort::SortExec;
23use datafusion::physical_plan::{
24 ExecutionPlan, SendableRecordBatchStream,
25 aggregates::{AggregateExec, AggregateMode, PhysicalGroupBy},
26 display::DisplayableExecutionPlan,
27 limit::GlobalLimitExec,
28 repartition::RepartitionExec,
29 union::UnionExec,
30};
31use datafusion::scalar::ScalarValue;
32use datafusion_expr::ExprSchemable;
33use datafusion_expr::execution_props::ExecutionProps;
34use datafusion_functions::core::getfield::GetFieldFunc;
35use datafusion_physical_expr::expressions::Column;
36use datafusion_physical_expr::{LexOrdering, Partitioning, PhysicalExpr, create_physical_expr};
37use datafusion_physical_plan::joins::PartitionMode;
38use datafusion_physical_plan::projection::ProjectionExec;
39use datafusion_physical_plan::stream::RecordBatchStreamAdapter;
40use datafusion_physical_plan::{empty::EmptyExec, joins::HashJoinExec};
41use futures::future::BoxFuture;
42use futures::stream::{Stream, StreamExt};
43use futures::{FutureExt, TryStreamExt};
44use lance_arrow::floats::{FloatType, coerce_float_vector};
45use lance_arrow::{DataTypeExt, SchemaExt as ArrowSchemaExt};
46use lance_core::datatypes::{
47 BlobHandling, Field, OnMissing, Projection, escape_field_path_for_project, format_field_path,
48};
49use lance_core::error::LanceOptionExt;
50use lance_core::utils::address::RowAddress;
51use lance_core::utils::mask::{RowAddrMask, RowAddrTreeMap};
52use lance_core::utils::tokio::get_num_compute_intensive_cpus;
53use lance_core::{ROW_ADDR, ROW_ID, ROW_OFFSET};
54use lance_datafusion::aggregate::Aggregate;
55use lance_datafusion::exec::{
56 LanceExecutionOptions, OneShotExec, StrictBatchSizeExec, analyze_plan, execute_plan,
57};
58use lance_datafusion::expr::safe_coerce_scalar;
59use lance_datafusion::projection::ProjectionPlan;
60use lance_file::reader::FileReaderOptions;
61use lance_index::IndexCriteria;
62use lance_index::scalar::FullTextSearchQuery;
63use lance_index::scalar::expression::{INDEX_EXPR_RESULT_SCHEMA, IndexExprResult, PlannerIndexExt};
64use lance_index::scalar::inverted::query::{
65 FtsQuery, FtsQueryNode, FtsSearchParams, MatchQuery, PhraseQuery, fill_fts_query_column,
66};
67use lance_index::scalar::inverted::{SCORE_COL, SCORE_FIELD};
68use lance_index::vector::{DIST_COL, Query};
69use lance_index::{DatasetIndexExt, scalar::expression::ScalarIndexExpr};
70use lance_index::{metrics::NoOpMetricsCollector, scalar::inverted::FTS_SCHEMA};
71use lance_io::stream::RecordBatchStream;
72use lance_linalg::distance::MetricType;
73use lance_table::format::{Fragment, IndexMetadata};
74use roaring::RoaringBitmap;
75use tracing::{Span, info_span, instrument};
76
77use super::Dataset;
78use crate::dataset::row_offsets_to_row_addresses;
79use crate::dataset::utils::SchemaAdapter;
80use crate::index::DatasetIndexInternalExt;
81use crate::index::vector::utils::{
82 default_distance_type_for, get_vector_dim, get_vector_type, validate_distance_type_for,
83};
84use crate::io::exec::filtered_read::{FilteredReadExec, FilteredReadOptions};
85use crate::io::exec::fts::{
86 BoostQueryExec, FlatMatchFilterExec, FlatMatchQueryExec, MatchQueryExec, PhraseQueryExec,
87};
88use crate::io::exec::knn::MultivectorScoringExec;
89use crate::io::exec::scalar_index::{MaterializeIndexExec, ScalarIndexExec};
90use crate::io::exec::{
91 AddRowAddrExec, FilterPlan as ExprFilterPlan, KNNVectorDistanceExec, LancePushdownScanExec,
92 LanceScanExec, Planner, PreFilterSource, ScanConfig, TakeExec,
93 knn::{KNN_INDEX_SCHEMA, new_knn_exec},
94 project,
95};
96use crate::io::exec::{AddRowOffsetExec, LanceFilterExec, LanceScanConfig, get_physical_optimizer};
97use crate::{Error, Result};
98use crate::{datatypes::Schema, io::exec::fts::BooleanQueryExec};
99
100pub use lance_datafusion::exec::{ExecutionStatsCallback, ExecutionSummaryCounts};
101#[cfg(feature = "substrait")]
102use lance_datafusion::substrait::parse_substrait;
103
104pub(crate) const BATCH_SIZE_FALLBACK: usize = 8192;
105
106fn parse_env_var<T: std::str::FromStr>(env_var_name: &str, default_val: &str) -> Option<T>
108where
109 T::Err: std::fmt::Display,
110{
111 std::env::var(env_var_name)
112 .ok()
113 .and_then(|val| match val.parse() {
114 Ok(value) => Some(value),
115 Err(e) => {
116 log::warn!(
117 "Failed to parse the environment variable {}='{}': {}, the default value is: {}.",
118 env_var_name,
119 val,
120 e,
121 default_val
122 );
123 None
124 }
125 })
126}
127
128pub fn get_default_batch_size() -> Option<usize> {
131 parse_env_var("LANCE_DEFAULT_BATCH_SIZE", &BATCH_SIZE_FALLBACK.to_string())
132}
133
134pub const LEGACY_DEFAULT_FRAGMENT_READAHEAD: usize = 4;
135
136pub static DEFAULT_FRAGMENT_READAHEAD: LazyLock<Option<usize>> = LazyLock::new(|| {
137 parse_env_var(
138 "LANCE_DEFAULT_FRAGMENT_READAHEAD",
139 &LEGACY_DEFAULT_FRAGMENT_READAHEAD.to_string(),
140 )
141});
142
143const DEFAULT_XTR_OVERFETCH_VALUE: u32 = 10;
144
145pub static DEFAULT_XTR_OVERFETCH: LazyLock<u32> = LazyLock::new(|| {
146 parse_env_var(
147 "LANCE_XTR_OVERFETCH",
148 &DEFAULT_XTR_OVERFETCH_VALUE.to_string(),
149 )
150 .unwrap_or(DEFAULT_XTR_OVERFETCH_VALUE)
151});
152
153const DEFAULT_IO_BUFFER_SIZE_VALUE: u64 = 2 * 1024 * 1024 * 1024;
157
158pub static DEFAULT_IO_BUFFER_SIZE: LazyLock<u64> = LazyLock::new(|| {
159 parse_env_var(
160 "LANCE_DEFAULT_IO_BUFFER_SIZE",
161 &DEFAULT_IO_BUFFER_SIZE_VALUE.to_string(),
162 )
163 .unwrap_or(DEFAULT_IO_BUFFER_SIZE_VALUE)
164});
165
166#[derive(Debug, Clone)]
171pub struct ColumnOrdering {
172 pub ascending: bool,
173 pub nulls_first: bool,
174 pub column_name: String,
175}
176
177impl ColumnOrdering {
178 pub fn asc_nulls_first(column_name: String) -> Self {
179 Self {
180 ascending: true,
181 nulls_first: true,
182 column_name,
183 }
184 }
185
186 pub fn asc_nulls_last(column_name: String) -> Self {
187 Self {
188 ascending: true,
189 nulls_first: false,
190 column_name,
191 }
192 }
193
194 pub fn desc_nulls_first(column_name: String) -> Self {
195 Self {
196 ascending: false,
197 nulls_first: true,
198 column_name,
199 }
200 }
201
202 pub fn desc_nulls_last(column_name: String) -> Self {
203 Self {
204 ascending: false,
205 nulls_first: false,
206 column_name,
207 }
208 }
209}
210
211#[derive(Clone)]
225pub enum MaterializationStyle {
226 Heuristic,
238 AllLate,
240 AllEarly,
242 AllEarlyExcept(Vec<u32>),
244}
245
246impl MaterializationStyle {
247 pub fn all_early_except(columns: &[impl AsRef<str>], schema: &Schema) -> Result<Self> {
248 let field_ids = schema
249 .project(columns)?
250 .field_ids()
251 .into_iter()
252 .map(|id| id as u32)
253 .collect();
254 Ok(Self::AllEarlyExcept(field_ids))
255 }
256}
257
258#[derive(Debug)]
259struct PlannedFilteredScan {
260 plan: Arc<dyn ExecutionPlan>,
261 limit_pushed_down: bool,
262 filter_pushed_down: bool,
263}
264
265pub struct FilterPlan {
266 query_filter: Option<QueryFilter>,
268 refine_query_filter: bool,
269 expr_filter_plan: ExprFilterPlan,
271}
272
273impl FilterPlan {
274 pub fn new(query_filter: Option<QueryFilter>, expr_filter_plan: ExprFilterPlan) -> Self {
275 Self {
276 query_filter,
277 refine_query_filter: false,
278 expr_filter_plan,
279 }
280 }
281
282 pub fn disable_refine(&mut self) {
283 self.expr_filter_plan = ExprFilterPlan::default();
284 self.refine_query_filter = false;
285 }
286
287 pub fn make_refine_only(&mut self) {
288 self.expr_filter_plan.make_refine_only();
289 self.refine_query_filter = true;
290 }
291
292 pub fn fts_filter(&self) -> Option<FullTextSearchQuery> {
293 match &self.query_filter {
294 Some(QueryFilter::Fts(query)) => Some(query.clone()),
295 _ => None,
296 }
297 }
298
299 pub fn vector_filter(&self) -> Option<Query> {
300 match &self.query_filter {
301 Some(QueryFilter::Vector(query)) => Some(query.clone()),
302 _ => None,
303 }
304 }
305
306 pub fn has_refine(&self) -> bool {
307 self.expr_filter_plan.has_refine() || self.refine_query_filter
308 }
309
310 pub async fn refine_columns(&self, dataset: &Arc<Dataset>) -> Result<Vec<String>> {
311 let mut columns = vec![];
312
313 if self.expr_filter_plan.has_refine() {
314 columns.extend(self.expr_filter_plan.refine_columns());
315 }
316
317 if self.refine_query_filter {
318 match &self.query_filter {
319 Some(QueryFilter::Fts(fts_query)) => {
320 let cols = if fts_query.columns().is_empty() {
321 let indexed_columns = fts_indexed_columns(dataset.clone()).await?;
322 let q = fill_fts_query_column(&fts_query.query, &indexed_columns, false)?;
323 q.columns()
324 } else {
325 fts_query.columns()
326 };
327
328 if let FtsQuery::Match(_) = &fts_query.query {
331 columns.extend(cols.iter().cloned().collect::<Vec<_>>());
332 }
333 }
334 Some(QueryFilter::Vector(vector_query)) => {
335 columns.push(vector_query.column.clone());
336 }
337 None => {}
338 }
339 }
340
341 Ok(columns)
342 }
343
344 pub async fn refine_filter(
345 &self,
346 input: Arc<dyn ExecutionPlan>,
347 scanner: &Scanner,
348 ) -> Result<Arc<dyn ExecutionPlan>> {
349 let mut plan = input;
350
351 if self.refine_query_filter {
352 match &self.query_filter {
353 Some(QueryFilter::Fts(fts_query)) => {
354 plan = scanner.flat_fts_filter(plan, fts_query).await?;
355 }
356 Some(QueryFilter::Vector(vector_query)) => {
357 plan = scanner.flat_knn(plan, vector_query)?;
358 }
359 None => {}
360 }
361 }
362
363 if let Some(refine_expr) = &self.expr_filter_plan.refine_expr {
364 plan = Arc::new(LanceFilterExec::try_new(refine_expr.clone(), plan)?);
367 }
368
369 Ok(plan)
370 }
371}
372
373#[derive(Debug, Clone, Default)]
374pub struct LanceFilter {
375 query_filter: Option<QueryFilter>,
376 expr_filter: Option<ExprFilter>,
377}
378
379impl LanceFilter {
380 pub fn is_none(&self) -> bool {
381 self.query_filter.is_none() && self.expr_filter.is_none()
382 }
383}
384
385#[derive(Debug, Clone)]
387pub enum QueryFilter {
388 Fts(FullTextSearchQuery),
389 Vector(Query),
390}
391
392#[derive(Debug, Clone)]
394pub enum ExprFilter {
395 Sql(String),
397 Substrait(Vec<u8>),
399 Datafusion(Expr),
401}
402
403impl ExprFilter {
404 #[allow(unused)]
413 #[instrument(level = "trace", name = "filter_to_df", skip_all)]
414 pub fn to_datafusion(&self, dataset_schema: &Schema, full_schema: &Schema) -> Result<Expr> {
415 match self {
416 Self::Sql(sql) => {
417 let schema = Arc::new(ArrowSchema::from(full_schema));
418 let planner = Planner::new(schema.clone());
419 let filter = planner.parse_filter(sql)?;
420
421 let df_schema = DFSchema::try_from(schema)?;
422 let ret_field = filter.to_field(&df_schema)?.1;
423 let ret_type = ret_field.data_type();
424 if ret_type != &DataType::Boolean {
425 return Err(Error::invalid_input_source(
426 format!("The filter {} does not return a boolean", filter).into(),
427 ));
428 }
429
430 let optimized = planner.optimize_expr(filter).map_err(|e| {
431 Error::invalid_input(format!("Error optimizing sql filter: {sql} ({e})"))
432 })?;
433 Ok(optimized)
434 }
435 #[cfg(feature = "substrait")]
436 Self::Substrait(expr) => {
437 use lance_datafusion::exec::{LanceExecutionOptions, get_session_context};
438
439 let ctx = get_session_context(&LanceExecutionOptions::default());
440 let state = ctx.state();
441 let schema = Arc::new(ArrowSchema::from(dataset_schema));
442 let expr = parse_substrait(expr, schema.clone(), &ctx.state())
443 .now_or_never()
444 .expect("could not parse the Substrait filter in a synchronous fashion")?;
445 let planner = Planner::new(schema);
446 planner.optimize_expr(expr.clone()).map_err(|e| {
447 Error::invalid_input(format!(
448 "Error optimizing substrait filter: {expr:?} ({e})"
449 ))
450 })
451 }
452 #[cfg(not(feature = "substrait"))]
453 Self::Substrait(_) => Err(Error::not_supported_source(
454 "Substrait filter is not supported in this build".into(),
455 )),
456 Self::Datafusion(expr) => Ok(expr.clone()),
457 }
458 }
459}
460
461#[derive(Debug, Clone)]
463pub enum AggregateExpr {
464 #[cfg(feature = "substrait")]
465 Substrait(Vec<u8>),
466 Datafusion {
467 group_by: Vec<Expr>,
468 aggregates: Vec<Expr>,
469 },
470}
471
472impl AggregateExpr {
473 pub fn builder() -> AggregateExprBuilder<false> {
486 AggregateExprBuilder::new()
487 }
488
489 #[cfg(feature = "substrait")]
491 pub fn substrait(bytes: impl Into<Vec<u8>>) -> Self {
492 Self::Substrait(bytes.into())
493 }
494
495 pub fn datafusion(group_by: Vec<Expr>, aggregates: Vec<Expr>) -> Self {
498 Self::Datafusion {
499 group_by,
500 aggregates,
501 }
502 }
503
504 fn parse(self, #[allow(unused_variables)] schema: Arc<ArrowSchema>) -> Result<Aggregate> {
511 match self {
512 #[cfg(feature = "substrait")]
513 Self::Substrait(bytes) => {
514 use lance_datafusion::exec::{LanceExecutionOptions, get_session_context};
515 use lance_datafusion::substrait::parse_substrait_aggregate;
516
517 let ctx = get_session_context(&LanceExecutionOptions::default());
518 parse_substrait_aggregate(&bytes, schema, &ctx.state())
519 .now_or_never()
520 .expect("could not parse the Substrait aggregate in a synchronous fashion")
521 }
522 Self::Datafusion {
523 group_by,
524 aggregates,
525 } => Ok(Aggregate::new(group_by, aggregates)),
526 }
527 }
528}
529
530#[derive(Debug, Clone)]
535pub struct AggregateExprBuilder<const HAS_PENDING: bool> {
536 group_by: Vec<Expr>,
537 aggregates: Vec<Expr>,
538}
539
540impl Default for AggregateExprBuilder<false> {
541 fn default() -> Self {
542 Self {
543 group_by: Vec::new(),
544 aggregates: Vec::new(),
545 }
546 }
547}
548
549impl AggregateExprBuilder<false> {
550 pub fn new() -> Self {
552 Self::default()
553 }
554
555 pub fn build(self) -> AggregateExpr {
557 AggregateExpr::Datafusion {
558 group_by: self.group_by,
559 aggregates: self.aggregates,
560 }
561 }
562}
563
564impl<const HAS_PENDING: bool> AggregateExprBuilder<HAS_PENDING> {
565 pub fn group_by(mut self, column: impl Into<String>) -> AggregateExprBuilder<false> {
570 self.group_by.push(col(column.into()));
571 AggregateExprBuilder {
572 group_by: self.group_by,
573 aggregates: self.aggregates,
574 }
575 }
576
577 pub fn group_by_columns(
582 mut self,
583 columns: impl IntoIterator<Item = impl Into<String>>,
584 ) -> AggregateExprBuilder<false> {
585 for column in columns {
586 self.group_by.push(col(column.into()));
587 }
588 AggregateExprBuilder {
589 group_by: self.group_by,
590 aggregates: self.aggregates,
591 }
592 }
593
594 pub fn count_star(mut self) -> AggregateExprBuilder<true> {
596 self.aggregates
597 .push(functions_aggregate::count::count(lit(1)));
598 AggregateExprBuilder {
599 group_by: self.group_by,
600 aggregates: self.aggregates,
601 }
602 }
603
604 pub fn count(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
609 self.aggregates
610 .push(functions_aggregate::count::count(col(column.into())));
611 AggregateExprBuilder {
612 group_by: self.group_by,
613 aggregates: self.aggregates,
614 }
615 }
616
617 pub fn sum(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
619 self.aggregates
620 .push(functions_aggregate::sum::sum(col(column.into())));
621 AggregateExprBuilder {
622 group_by: self.group_by,
623 aggregates: self.aggregates,
624 }
625 }
626
627 pub fn avg(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
629 self.aggregates
630 .push(functions_aggregate::average::avg(col(column.into())));
631 AggregateExprBuilder {
632 group_by: self.group_by,
633 aggregates: self.aggregates,
634 }
635 }
636
637 pub fn min(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
639 self.aggregates
640 .push(functions_aggregate::min_max::min(col(column.into())));
641 AggregateExprBuilder {
642 group_by: self.group_by,
643 aggregates: self.aggregates,
644 }
645 }
646
647 pub fn max(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
649 self.aggregates
650 .push(functions_aggregate::min_max::max(col(column.into())));
651 AggregateExprBuilder {
652 group_by: self.group_by,
653 aggregates: self.aggregates,
654 }
655 }
656}
657
658impl AggregateExprBuilder<true> {
659 pub fn alias(mut self, name: impl Into<String>) -> AggregateExprBuilder<false> {
661 let pending = self.aggregates.pop().expect("pending aggregate must exist");
662 self.aggregates.push(pending.alias(name.into()));
663 AggregateExprBuilder {
664 group_by: self.group_by,
665 aggregates: self.aggregates,
666 }
667 }
668
669 pub fn build(self) -> AggregateExpr {
671 AggregateExpr::Datafusion {
672 group_by: self.group_by,
673 aggregates: self.aggregates,
674 }
675 }
676}
677
678#[derive(Clone)]
692pub struct Scanner {
693 dataset: Arc<Dataset>,
694
695 projection_plan: ProjectionPlan,
702 blob_handling: BlobHandling,
703
704 prefilter: bool,
706
707 materialization_style: MaterializationStyle,
709
710 filter: LanceFilter,
712
713 full_text_query: Option<FullTextSearchQuery>,
715
716 batch_size: Option<usize>,
718
719 batch_readahead: usize,
721
722 fragment_readahead: Option<usize>,
724
725 io_buffer_size: Option<u64>,
727
728 limit: Option<i64>,
729 offset: Option<i64>,
730
731 ordering: Option<Vec<ColumnOrdering>>,
740
741 nearest: Option<Query>,
742
743 use_scalar_index: bool,
749
750 use_stats: bool,
754
755 ordered: bool,
759
760 fragments: Option<Vec<Fragment>>,
762
763 fast_search: bool,
770
771 include_deleted_rows: bool,
773
774 scan_stats_callback: Option<ExecutionStatsCallback>,
776
777 strict_batch_size: bool,
782
783 file_reader_options: Option<FileReaderOptions>,
785
786 aggregate: Option<Aggregate>,
787
788 legacy_with_row_id: bool,
809 legacy_with_row_addr: bool,
811 explicit_projection: bool,
814 autoproject_scoring_columns: bool,
816}
817
818#[derive(Debug, Clone)]
820pub enum TakeOperation {
821 RowIds(Vec<u64>),
823 RowAddrs(Vec<u64>),
825 RowOffsets(Vec<u64>),
830}
831
832impl TakeOperation {
833 fn extract_u64_list(list: &[Expr]) -> Option<Vec<u64>> {
834 let mut u64s = Vec::with_capacity(list.len());
835 for expr in list {
836 if let Expr::Literal(lit, _) = expr {
837 if let Some(ScalarValue::UInt64(Some(val))) =
838 safe_coerce_scalar(lit, &DataType::UInt64)
839 {
840 u64s.push(val);
841 } else {
842 return None;
843 }
844 } else {
845 return None;
846 }
847 }
848 Some(u64s)
849 }
850
851 fn merge(self, other: Self) -> Option<Self> {
852 match (self, other) {
853 (Self::RowIds(mut left), Self::RowIds(right)) => {
854 left.extend(right);
855 Some(Self::RowIds(left))
856 }
857 (Self::RowAddrs(mut left), Self::RowAddrs(right)) => {
858 left.extend(right);
859 Some(Self::RowAddrs(left))
860 }
861 (Self::RowOffsets(mut left), Self::RowOffsets(right)) => {
862 left.extend(right);
863 Some(Self::RowOffsets(left))
864 }
865 _ => None,
866 }
867 }
868
869 fn try_from_expr(expr: &Expr) -> Option<(Self, Option<Expr>)> {
887 if let Expr::BinaryExpr(binary) = expr {
888 match binary.op {
889 datafusion_expr::Operator::And => {
890 let left_take = Self::try_from_expr(&binary.left);
891 let right_take = Self::try_from_expr(&binary.right);
892 match (left_take, right_take) {
893 (Some(_), Some(_)) => {
894 return None;
900 }
901 (Some((left_op, left_rem)), None) => {
902 let remainder = match left_rem {
903 Some(expr) => Expr::and(expr, binary.right.as_ref().clone()),
907 None => binary.right.as_ref().clone(),
908 };
909 return Some((left_op, Some(remainder)));
910 }
911 (None, Some((right_op, right_rem))) => {
912 let remainder = match right_rem {
913 Some(expr) => Expr::and(expr, binary.left.as_ref().clone()),
914 None => binary.left.as_ref().clone(),
915 };
916 return Some((right_op, Some(remainder)));
917 }
918 (None, None) => {
919 return None;
920 }
921 }
922 }
923 datafusion_expr::Operator::Eq => {
924 if let (Expr::Column(col), Expr::Literal(lit, _)) =
926 (binary.left.as_ref(), binary.right.as_ref())
927 && let Some(ScalarValue::UInt64(Some(val))) =
928 safe_coerce_scalar(lit, &DataType::UInt64)
929 {
930 if col.name == ROW_ID {
931 return Some((Self::RowIds(vec![val]), None));
932 } else if col.name == ROW_ADDR {
933 return Some((Self::RowAddrs(vec![val]), None));
934 } else if col.name == ROW_OFFSET {
935 return Some((Self::RowOffsets(vec![val]), None));
936 }
937 }
938 }
939 datafusion_expr::Operator::Or => {
940 let left_take = Self::try_from_expr(&binary.left);
941 let right_take = Self::try_from_expr(&binary.right);
942 if let (Some(left), Some(right)) = (left_take, right_take) {
943 if left.1.is_some() || right.1.is_some() {
944 return None;
951 }
952 return left.0.merge(right.0).map(|op| (op, None));
953 }
954 }
955 _ => {}
956 }
957 } else if let Expr::InList(in_expr) = expr
958 && let Expr::Column(col) = in_expr.expr.as_ref()
959 && let Some(u64s) = Self::extract_u64_list(&in_expr.list)
960 {
961 if col.name == ROW_ID {
962 return Some((Self::RowIds(u64s), None));
963 } else if col.name == ROW_ADDR {
964 return Some((Self::RowAddrs(u64s), None));
965 } else if col.name == ROW_OFFSET {
966 return Some((Self::RowOffsets(u64s), None));
967 }
968 }
969 None
970 }
971}
972
973impl Scanner {
974 pub fn new(dataset: Arc<Dataset>) -> Self {
975 let projection_plan = ProjectionPlan::full(dataset.clone()).unwrap();
976 let file_reader_options = dataset.file_reader_options.clone();
977 let mut scanner = Self {
978 dataset,
979 projection_plan,
980 blob_handling: BlobHandling::default(),
981 prefilter: false,
982 materialization_style: MaterializationStyle::Heuristic,
983 filter: LanceFilter::default(),
984 full_text_query: None,
985 batch_size: None,
986 batch_readahead: get_num_compute_intensive_cpus(),
987 fragment_readahead: None,
988 io_buffer_size: None,
989 limit: None,
990 offset: None,
991 ordering: None,
992 nearest: None,
993 use_stats: true,
994 ordered: true,
995 fragments: None,
996 fast_search: false,
997 use_scalar_index: true,
998 include_deleted_rows: false,
999 scan_stats_callback: None,
1000 strict_batch_size: false,
1001 file_reader_options,
1002 aggregate: None,
1003 legacy_with_row_addr: false,
1004 legacy_with_row_id: false,
1005 explicit_projection: false,
1006 autoproject_scoring_columns: true,
1007 };
1008 scanner.apply_blob_handling();
1009 scanner
1010 }
1011
1012 fn apply_blob_handling(&mut self) {
1013 let projection = self
1014 .projection_plan
1015 .physical_projection
1016 .clone()
1017 .with_blob_handling(self.blob_handling.clone());
1018 self.projection_plan.physical_projection = projection;
1019 }
1020
1021 pub fn blob_handling(&mut self, blob_handling: BlobHandling) -> &mut Self {
1022 self.blob_handling = blob_handling;
1023 self.apply_blob_handling();
1024 self
1025 }
1026
1027 pub fn from_fragment(dataset: Arc<Dataset>, fragment: Fragment) -> Self {
1028 Self {
1029 fragments: Some(vec![fragment]),
1030 ..Self::new(dataset)
1031 }
1032 }
1033
1034 pub fn with_fragments(&mut self, fragments: Vec<Fragment>) -> &mut Self {
1038 self.fragments = Some(fragments);
1039 self
1040 }
1041
1042 fn get_batch_size(&self) -> usize {
1043 get_default_batch_size().unwrap_or_else(|| {
1049 self.batch_size.unwrap_or_else(|| {
1050 std::cmp::max(
1051 self.dataset.object_store().block_size() / 4,
1052 BATCH_SIZE_FALLBACK,
1053 )
1054 })
1055 })
1056 }
1057
1058 fn ensure_not_fragment_scan(&self) -> Result<()> {
1059 if self.is_fragment_scan() {
1060 Err(Error::not_supported(
1061 "This operation is not supported for fragment scan".to_string(),
1062 ))
1063 } else {
1064 Ok(())
1065 }
1066 }
1067
1068 fn is_fragment_scan(&self) -> bool {
1069 self.fragments.is_some()
1070 }
1071
1072 pub fn empty_project(&mut self) -> Result<&mut Self> {
1076 self.project(&[] as &[&str])
1077 }
1078
1079 pub fn project<T: AsRef<str>>(&mut self, columns: &[T]) -> Result<&mut Self> {
1083 let transformed_columns: Vec<(&str, String)> = columns
1084 .iter()
1085 .map(|c| (c.as_ref(), escape_field_path_for_project(c.as_ref())))
1086 .collect();
1087
1088 self.project_with_transform(&transformed_columns)
1089 }
1090
1091 pub fn project_with_transform(
1095 &mut self,
1096 columns: &[(impl AsRef<str>, impl AsRef<str>)],
1097 ) -> Result<&mut Self> {
1098 self.explicit_projection = true;
1099 self.projection_plan = ProjectionPlan::from_expressions(self.dataset.clone(), columns)?;
1100 if self.legacy_with_row_id {
1101 self.projection_plan.include_row_id();
1102 }
1103 if self.legacy_with_row_addr {
1104 self.projection_plan.include_row_addr();
1105 }
1106 self.apply_blob_handling();
1107 Ok(self)
1108 }
1109
1110 pub fn prefilter(&mut self, should_prefilter: bool) -> &mut Self {
1119 self.prefilter = should_prefilter;
1120 self
1121 }
1122
1123 pub fn scan_stats_callback(&mut self, callback: ExecutionStatsCallback) -> &mut Self {
1125 self.scan_stats_callback = Some(callback);
1126 self
1127 }
1128
1129 pub fn materialization_style(&mut self, style: MaterializationStyle) -> &mut Self {
1141 self.materialization_style = style;
1142 self
1143 }
1144
1145 pub fn filter(&mut self, filter: &str) -> Result<&mut Self> {
1161 self.filter.expr_filter = Some(ExprFilter::Sql(filter.to_string()));
1162 Ok(self)
1163 }
1164
1165 pub fn filter_query(&mut self, filter: QueryFilter) -> Result<&mut Self> {
1184 self.filter.query_filter = Some(filter);
1185 Ok(self)
1186 }
1187
1188 pub fn full_text_search(&mut self, query: FullTextSearchQuery) -> Result<&mut Self> {
1202 let fields = query.columns();
1203 if !fields.is_empty() {
1204 for field in fields.iter() {
1205 if self.dataset.schema().field(field).is_none() {
1206 return Err(Error::invalid_input(format!("Column {} not found", field)));
1207 }
1208 }
1209 }
1210
1211 self.full_text_query = Some(query);
1212 Ok(self)
1213 }
1214
1215 pub fn filter_substrait(&mut self, filter: &[u8]) -> Result<&mut Self> {
1220 self.filter.expr_filter = Some(ExprFilter::Substrait(filter.to_vec()));
1221 Ok(self)
1222 }
1223
1224 pub fn filter_expr(&mut self, filter: Expr) -> &mut Self {
1225 self.filter.expr_filter = Some(ExprFilter::Datafusion(filter));
1226 self
1227 }
1228
1229 pub fn aggregate(&mut self, aggregate: AggregateExpr) -> Result<&mut Self> {
1234 let schema: Arc<ArrowSchema> = Arc::new(self.dataset.schema().into());
1235 let parsed = aggregate.parse(schema)?;
1236 self.aggregate = Some(parsed);
1237 Ok(self)
1238 }
1239
1240 pub fn batch_size(&mut self, batch_size: usize) -> &mut Self {
1242 self.batch_size = Some(batch_size);
1243 self
1244 }
1245
1246 pub fn include_deleted_rows(&mut self) -> &mut Self {
1257 self.include_deleted_rows = true;
1258 self
1259 }
1260
1261 pub fn io_buffer_size(&mut self, size: u64) -> &mut Self {
1280 self.io_buffer_size = Some(size);
1281 self
1282 }
1283
1284 pub fn batch_readahead(&mut self, nbatches: usize) -> &mut Self {
1287 self.batch_readahead = nbatches;
1288 self
1289 }
1290
1291 pub fn fragment_readahead(&mut self, nfragments: usize) -> &mut Self {
1295 self.fragment_readahead = Some(nfragments);
1296 self
1297 }
1298
1299 pub fn scan_in_order(&mut self, ordered: bool) -> &mut Self {
1313 self.ordered = ordered;
1314 self
1315 }
1316
1317 pub fn use_scalar_index(&mut self, use_scalar_index: bool) -> &mut Self {
1323 self.use_scalar_index = use_scalar_index;
1324 self
1325 }
1326
1327 pub fn strict_batch_size(&mut self, strict_batch_size: bool) -> &mut Self {
1334 self.strict_batch_size = strict_batch_size;
1335 self
1336 }
1337
1338 pub fn limit(&mut self, limit: Option<i64>, offset: Option<i64>) -> Result<&mut Self> {
1345 if limit.unwrap_or_default() < 0 {
1346 return Err(Error::invalid_input(
1347 "Limit must be non-negative".to_string(),
1348 ));
1349 }
1350 if let Some(off) = offset
1351 && off < 0
1352 {
1353 return Err(Error::invalid_input(
1354 "Offset must be non-negative".to_string(),
1355 ));
1356 }
1357 self.limit = limit;
1358 self.offset = offset;
1359 Ok(self)
1360 }
1361
1362 pub fn nearest(&mut self, column: &str, q: &dyn Array, k: usize) -> Result<&mut Self> {
1366 if !self.prefilter {
1367 self.ensure_not_fragment_scan()?;
1370 }
1371
1372 if k == 0 {
1373 return Err(Error::invalid_input("k must be positive".to_string()));
1374 }
1375 if q.is_empty() {
1376 return Err(Error::invalid_input(
1377 "Query vector must have non-zero length".to_string(),
1378 ));
1379 }
1380 let (vector_type, element_type) = get_vector_type(self.dataset.schema(), column)?;
1382 let dim = get_vector_dim(self.dataset.schema(), column)?;
1383
1384 let q = match q.data_type() {
1385 DataType::List(_) | DataType::FixedSizeList(_, _) => {
1386 if !matches!(vector_type, DataType::List(_)) {
1387 return Err(Error::invalid_input(format!(
1388 "Query is multivector but column {}({})is not multivector",
1389 column, vector_type,
1390 )));
1391 }
1392
1393 if let Some(list_array) = q.as_list_opt::<i32>() {
1394 for i in 0..list_array.len() {
1395 let vec = list_array.value(i);
1396 if vec.len() != dim {
1397 return Err(Error::invalid_input(format!(
1398 "query dim({}) doesn't match the column {} vector dim({})",
1399 vec.len(),
1400 column,
1401 dim,
1402 )));
1403 }
1404 }
1405 list_array.values().clone()
1406 } else {
1407 let fsl = q.as_fixed_size_list();
1408 if fsl.value_length() as usize != dim {
1409 return Err(Error::invalid_input(format!(
1410 "query dim({}) doesn't match the column {} vector dim({})",
1411 fsl.value_length(),
1412 column,
1413 dim,
1414 )));
1415 }
1416 fsl.values().clone()
1417 }
1418 }
1419 _ => {
1420 if q.len() != dim {
1421 return Err(Error::invalid_input(format!(
1422 "query dim({}) doesn't match the column {} vector dim({})",
1423 q.len(),
1424 column,
1425 dim,
1426 )));
1427 }
1428 q.slice(0, q.len())
1429 }
1430 };
1431
1432 let key = match &element_type {
1433 dt if dt == q.data_type() => q,
1434 dt if dt.is_floating() => coerce_float_vector(
1435 q.as_any().downcast_ref::<Float32Array>().unwrap(),
1436 FloatType::try_from(dt)?,
1437 )?,
1438 _ => {
1439 return Err(Error::invalid_input(format!(
1440 "Column {} has element type {} and the query vector is {}",
1441 column,
1442 element_type,
1443 q.data_type(),
1444 )));
1445 }
1446 };
1447
1448 self.nearest = Some(Query {
1449 column: column.to_string(),
1450 key,
1451 k,
1452 lower_bound: None,
1453 upper_bound: None,
1454 minimum_nprobes: 1,
1455 maximum_nprobes: None,
1456 ef: None,
1457 refine_factor: None,
1458 metric_type: None,
1459 use_index: true,
1460 dist_q_c: 0.0,
1461 });
1462 Ok(self)
1463 }
1464
1465 #[cfg(test)]
1466 fn nearest_mut(&mut self) -> Option<&mut Query> {
1467 self.nearest.as_mut()
1468 }
1469
1470 pub fn distance_range(
1472 &mut self,
1473 lower_bound: Option<f32>,
1474 upper_bound: Option<f32>,
1475 ) -> &mut Self {
1476 if let Some(q) = self.nearest.as_mut() {
1477 q.lower_bound = lower_bound;
1478 q.upper_bound = upper_bound;
1479 }
1480 self
1481 }
1482
1483 pub fn nprobes(&mut self, n: usize) -> &mut Self {
1488 if let Some(q) = self.nearest.as_mut() {
1489 q.minimum_nprobes = n;
1490 q.maximum_nprobes = Some(n);
1491 } else {
1492 log::warn!("nprobes is not set because nearest has not been called yet");
1493 }
1494 self
1495 }
1496
1497 #[deprecated(note = "Use nprobes instead")]
1502 pub fn nprobs(&mut self, n: usize) -> &mut Self {
1503 if let Some(q) = self.nearest.as_mut() {
1504 q.minimum_nprobes = n;
1505 q.maximum_nprobes = Some(n);
1506 } else {
1507 log::warn!("nprobes is not set because nearest has not been called yet");
1508 }
1509 self
1510 }
1511
1512 pub fn minimum_nprobes(&mut self, n: usize) -> &mut Self {
1520 if let Some(q) = self.nearest.as_mut() {
1521 q.minimum_nprobes = n;
1522 } else {
1523 log::warn!("minimum_nprobes is not set because nearest has not been called yet");
1524 }
1525 self
1526 }
1527
1528 pub fn maximum_nprobes(&mut self, n: usize) -> &mut Self {
1540 if let Some(q) = self.nearest.as_mut() {
1541 q.maximum_nprobes = Some(n);
1542 } else {
1543 log::warn!("maximum_nprobes is not set because nearest has not been called yet");
1544 }
1545 self
1546 }
1547
1548 pub fn ef(&mut self, ef: usize) -> &mut Self {
1549 if let Some(q) = self.nearest.as_mut() {
1550 q.ef = Some(ef);
1551 }
1552 self
1553 }
1554
1555 pub fn fast_search(&mut self) -> &mut Self {
1561 if let Some(q) = self.nearest.as_mut() {
1562 q.use_index = true;
1563 }
1564 self.fast_search = true;
1565 self.projection_plan.include_row_id(); self
1567 }
1568
1569 pub fn refine(&mut self, factor: u32) -> &mut Self {
1579 if let Some(q) = self.nearest.as_mut() {
1580 q.refine_factor = Some(factor)
1581 };
1582 self
1583 }
1584
1585 pub fn distance_metric(&mut self, metric_type: MetricType) -> &mut Self {
1587 if let Some(q) = self.nearest.as_mut() {
1588 q.metric_type = Some(metric_type)
1589 }
1590 self
1591 }
1592
1593 pub fn order_by(&mut self, ordering: Option<Vec<ColumnOrdering>>) -> Result<&mut Self> {
1599 if let Some(ordering) = &ordering {
1600 if ordering.is_empty() {
1601 self.ordering = None;
1602 return Ok(self);
1603 }
1604 for column in ordering {
1606 self.dataset
1607 .schema()
1608 .field(&column.column_name)
1609 .ok_or(Error::invalid_input(format!(
1610 "Column {} not found",
1611 &column.column_name
1612 )))?;
1613 }
1614 }
1615 self.ordering = ordering;
1616 Ok(self)
1617 }
1618
1619 pub fn use_index(&mut self, use_index: bool) -> &mut Self {
1621 if let Some(q) = self.nearest.as_mut() {
1622 q.use_index = use_index
1623 }
1624 self
1625 }
1626
1627 pub fn with_row_id(&mut self) -> &mut Self {
1629 self.legacy_with_row_id = true;
1630 self.projection_plan.include_row_id();
1631 self
1632 }
1633
1634 pub fn with_row_address(&mut self) -> &mut Self {
1636 self.legacy_with_row_addr = true;
1637 self.projection_plan.include_row_addr();
1638 self
1639 }
1640
1641 pub fn disable_scoring_autoprojection(&mut self) -> &mut Self {
1657 self.autoproject_scoring_columns = false;
1658 self
1659 }
1660
1661 pub fn with_file_reader_options(&mut self, options: FileReaderOptions) -> &mut Self {
1663 self.file_reader_options = Some(options);
1664 self
1665 }
1666
1667 fn create_column_expr(
1669 column_name: &str,
1670 dataset: &Dataset,
1671 arrow_schema: &ArrowSchema,
1672 ) -> Result<Arc<dyn PhysicalExpr>> {
1673 let lance_schema = dataset.schema();
1674 let field_path = lance_schema
1675 .resolve_case_insensitive(column_name)
1676 .ok_or_else(|| {
1677 Error::invalid_input(format!("Field '{}' not found in schema", column_name))
1678 })?;
1679
1680 if field_path.len() == 1 {
1681 expressions::col(&field_path[0].name, arrow_schema).map_err(|e| {
1683 Error::internal(format!(
1684 "Failed to create column expression for '{}': {}",
1685 column_name, e
1686 ))
1687 })
1688 } else {
1689 let get_field_func = ScalarUDF::from(GetFieldFunc::default());
1691
1692 let mut expr = Expr::Column(datafusion::common::Column::new_unqualified(
1695 &field_path[0].name,
1696 ));
1697 for nested_field in &field_path[1..] {
1698 expr = get_field_func.call(vec![expr, lit(&nested_field.name)]);
1699 }
1700
1701 let df_schema = Arc::new(DFSchema::try_from(arrow_schema.clone())?);
1703 let execution_props = ExecutionProps::new().with_query_execution_start_time(Utc::now());
1704 create_physical_expr(&expr, &df_schema, &execution_props).map_err(|e| {
1705 Error::internal(format!(
1706 "Failed to create physical expression for nested field '{}': {}",
1707 column_name, e
1708 ))
1709 })
1710 }
1711 }
1712
1713 pub fn use_stats(&mut self, use_stats: bool) -> &mut Self {
1717 self.use_stats = use_stats;
1718 self
1719 }
1720
1721 pub async fn schema(&self) -> Result<SchemaRef> {
1723 let plan = self.create_plan().await?;
1724 Ok(plan.schema())
1725 }
1726
1727 pub fn get_expr_filter(&self) -> Result<Option<Expr>> {
1734 if let Some(filter) = &self.filter.expr_filter {
1735 let filter_schema = self.filterable_schema()?;
1736 Ok(Some(filter.to_datafusion(
1737 self.dataset.schema(),
1738 filter_schema.as_ref(),
1739 )?))
1740 } else {
1741 Ok(None)
1742 }
1743 }
1744
1745 fn add_extra_columns(&self, schema: Schema) -> Result<Schema> {
1746 let mut extra_columns = vec![ArrowField::new(ROW_OFFSET, DataType::UInt64, true)];
1747
1748 if self.nearest.as_ref().is_some() {
1749 extra_columns.push(ArrowField::new(DIST_COL, DataType::Float32, true));
1750 };
1751
1752 if self.full_text_query.is_some() {
1753 extra_columns.push(ArrowField::new(SCORE_COL, DataType::Float32, true));
1754 }
1755
1756 schema.merge(&ArrowSchema::new(extra_columns))
1757 }
1758
1759 fn filterable_schema(&self) -> Result<Arc<Schema>> {
1764 let base_schema = Projection::full(self.dataset.clone())
1765 .with_row_id()
1766 .with_row_addr()
1767 .with_row_last_updated_at_version()
1768 .with_row_created_at_version()
1769 .to_schema();
1770
1771 Ok(Arc::new(self.add_extra_columns(base_schema)?))
1772 }
1773
1774 pub(crate) fn calculate_final_projection(
1779 &self,
1780 current_schema: &ArrowSchema,
1781 ) -> Result<Vec<(Arc<dyn PhysicalExpr>, String)>> {
1782 let mut output_expr = self.projection_plan.to_physical_exprs(current_schema)?;
1785
1786 if self.autoproject_scoring_columns {
1789 if self.nearest.is_some() && output_expr.iter().all(|(_, name)| name != DIST_COL) {
1790 if self.explicit_projection {
1791 log::warn!(
1792 "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"
1793 );
1794 }
1795 let vector_expr = expressions::col(DIST_COL, current_schema)?;
1796 output_expr.push((vector_expr, DIST_COL.to_string()));
1797 }
1798 if self.full_text_query.is_some()
1799 && output_expr.iter().all(|(_, name)| name != SCORE_COL)
1800 {
1801 if self.explicit_projection {
1802 log::warn!(
1803 "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"
1804 );
1805 }
1806 let score_expr = expressions::col(SCORE_COL, current_schema)?;
1807 output_expr.push((score_expr, SCORE_COL.to_string()));
1808 }
1809 }
1810
1811 if self.legacy_with_row_id {
1812 let row_id_pos = output_expr
1813 .iter()
1814 .position(|(_, name)| name == ROW_ID)
1815 .ok_or_else(|| {
1816 Error::internal(
1817 "user specified with_row_id but the _rowid column was not in the output"
1818 .to_string(),
1819 )
1820 })?;
1821 if row_id_pos != output_expr.len() - 1 {
1822 let row_id_expr = output_expr.remove(row_id_pos);
1824 output_expr.push(row_id_expr);
1825 }
1826 }
1827
1828 if self.legacy_with_row_addr {
1829 let row_addr_pos = output_expr.iter().position(|(_, name)| name == ROW_ADDR).ok_or_else(|| {
1830 Error::internal("user specified with_row_address but the _rowaddr column was not in the output".to_string())
1831 })?;
1832 if row_addr_pos != output_expr.len() - 1 {
1833 let row_addr_expr = output_expr.remove(row_addr_pos);
1835 output_expr.push(row_addr_expr);
1836 }
1837 }
1838
1839 Ok(output_expr)
1840 }
1841
1842 #[instrument(skip_all)]
1844 pub fn try_into_stream(&self) -> BoxFuture<'_, Result<DatasetRecordBatchStream>> {
1845 async move {
1847 let plan = self.create_plan().await?;
1848
1849 Ok(DatasetRecordBatchStream::new(execute_plan(
1850 plan,
1851 LanceExecutionOptions {
1852 batch_size: self.batch_size,
1853 execution_stats_callback: self.scan_stats_callback.clone(),
1854 ..Default::default()
1855 },
1856 )?))
1857 }
1858 .boxed()
1859 }
1860
1861 pub(crate) async fn try_into_dfstream(
1862 &self,
1863 mut options: LanceExecutionOptions,
1864 ) -> Result<SendableRecordBatchStream> {
1865 let plan = self.create_plan().await?;
1866
1867 if options.execution_stats_callback.is_none() {
1869 options.execution_stats_callback = self.scan_stats_callback.clone();
1870 }
1871
1872 execute_plan(plan, options)
1873 }
1874
1875 pub async fn try_into_batch(&self) -> Result<RecordBatch> {
1876 let stream = self.try_into_stream().await?;
1877 let schema = stream.schema();
1878 let batches = stream.try_collect::<Vec<_>>().await?;
1879 Ok(concat_batches(&schema, &batches)?)
1880 }
1881
1882 #[instrument(skip_all)]
1887 pub fn count_rows(&self) -> BoxFuture<'_, Result<u64>> {
1888 async move {
1890 let mut scanner = self.clone();
1891 scanner.aggregate(AggregateExpr::builder().count_star().build())?;
1892
1893 let plan = scanner.create_plan().await?;
1894 let mut stream = execute_plan(plan, LanceExecutionOptions::default())?;
1895
1896 if let Some(first_batch) = stream.next().await {
1898 let batch = first_batch?;
1899 let array = batch
1900 .column(0)
1901 .as_any()
1902 .downcast_ref::<Int64Array>()
1903 .ok_or(Error::invalid_input(
1904 "Count plan did not return an Int64Array".to_string(),
1905 ))?;
1906 Ok(array.value(0) as u64)
1907 } else {
1908 Ok(0)
1909 }
1910 }
1911 .boxed()
1912 }
1913
1914 #[deprecated(note = "Use create_plan() instead, which now applies aggregate automatically")]
1918 pub fn create_aggregate_plan(&self) -> BoxFuture<'_, Result<Arc<dyn ExecutionPlan>>> {
1919 async move {
1920 if self.aggregate.is_none() {
1921 return Err(Error::invalid_input(
1922 "create_aggregate_plan called but no aggregate was set",
1923 ));
1924 }
1925 self.create_plan().await
1927 }
1928 .boxed()
1929 }
1930
1931 async fn apply_aggregate(
1932 &self,
1933 plan: Arc<dyn ExecutionPlan>,
1934 agg: &Aggregate,
1935 ) -> Result<Arc<dyn ExecutionPlan>> {
1936 use datafusion_physical_expr::aggregate::AggregateFunctionExpr;
1937
1938 let schema = plan.schema();
1939 let df_schema = DFSchema::try_from(schema.as_ref().clone())?;
1940
1941 let group_exprs: Vec<(Arc<dyn PhysicalExpr>, String)> = agg
1942 .group_by
1943 .iter()
1944 .map(|expr| {
1945 let name = expr.schema_name().to_string();
1946 let physical_expr =
1947 create_physical_expr(expr, &df_schema, &ExecutionProps::default())?;
1948 Ok((physical_expr, name))
1949 })
1950 .collect::<Result<_>>()?;
1951
1952 #[allow(clippy::type_complexity)]
1953 let aggr_results: Vec<(Arc<AggregateFunctionExpr>, Option<Arc<dyn PhysicalExpr>>)> = agg
1954 .aggregates
1955 .iter()
1956 .map(|expr| self.build_physical_aggregate_expr(expr, &df_schema, &schema))
1957 .collect::<Result<_>>()?;
1958
1959 let (aggr_exprs, filters): (Vec<_>, Vec<_>) = aggr_results.into_iter().unzip();
1960
1961 Ok(Arc::new(AggregateExec::try_new(
1962 AggregateMode::Single,
1963 PhysicalGroupBy::new_single(group_exprs),
1964 aggr_exprs,
1965 filters,
1966 plan,
1967 schema,
1968 )?) as Arc<dyn ExecutionPlan>)
1969 }
1970
1971 #[allow(clippy::type_complexity)]
1972 fn build_physical_aggregate_expr(
1973 &self,
1974 expr: &Expr,
1975 df_schema: &DFSchema,
1976 input_schema: &SchemaRef,
1977 ) -> Result<(
1978 Arc<datafusion_physical_expr::aggregate::AggregateFunctionExpr>,
1979 Option<Arc<dyn PhysicalExpr>>,
1980 )> {
1981 use datafusion::physical_planner::create_aggregate_expr_and_maybe_filter;
1982
1983 let coerced_expr = self.coerce_aggregate_expr(expr, df_schema)?;
1984
1985 let (agg_expr, filter, _order_by) = create_aggregate_expr_and_maybe_filter(
1987 &coerced_expr,
1988 df_schema,
1989 input_schema.as_ref(),
1990 &ExecutionProps::default(),
1991 )?;
1992
1993 Ok((agg_expr, filter))
1994 }
1995
1996 fn coerce_aggregate_expr(&self, expr: &Expr, schema: &DFSchema) -> Result<Expr> {
2004 Self::coerce_aggregate_expr_impl(expr, schema)
2005 }
2006
2007 fn coerce_aggregate_expr_impl(expr: &Expr, schema: &DFSchema) -> Result<Expr> {
2008 use datafusion::logical_expr::Expr;
2009 use datafusion::logical_expr::expr::AggregateFunction;
2010 use datafusion::logical_expr::type_coercion::functions::fields_with_udf;
2011
2012 match expr {
2013 Expr::AggregateFunction(agg_func) => {
2014 let func = &agg_func.func;
2015 let args = &agg_func.params.args;
2016
2017 if args.is_empty() {
2018 return Ok(expr.clone());
2019 }
2020
2021 let current_fields: Vec<arrow_schema::FieldRef> = args
2022 .iter()
2023 .enumerate()
2024 .map(|(i, e)| {
2025 let dt = e.get_type(schema)?;
2026 Ok(Arc::new(arrow_schema::Field::new(
2027 format!("arg_{i}"),
2028 dt,
2029 true,
2030 )))
2031 })
2032 .collect::<std::result::Result<_, datafusion::common::DataFusionError>>()?;
2033
2034 let coerced_fields = fields_with_udf(¤t_fields, func.as_ref())?;
2035 let coerced_args: Vec<Expr> = args
2036 .iter()
2037 .zip(coerced_fields.iter())
2038 .map(|(arg, target_field)| {
2039 let arg_type = arg.get_type(schema)?;
2040 let target_type = target_field.data_type();
2041 if arg_type == *target_type {
2042 Ok(arg.clone())
2043 } else {
2044 arg.clone().cast_to(target_type, schema)
2045 }
2046 })
2047 .collect::<std::result::Result<_, _>>()?;
2048
2049 Ok(Expr::AggregateFunction(AggregateFunction::new_udf(
2050 func.clone(),
2051 coerced_args,
2052 agg_func.params.distinct,
2053 agg_func.params.filter.clone(),
2054 agg_func.params.order_by.clone(),
2055 agg_func.params.null_treatment,
2056 )))
2057 }
2058 Expr::Alias(alias) => {
2059 let coerced_inner = Self::coerce_aggregate_expr_impl(&alias.expr, schema)?;
2061 Ok(coerced_inner.alias(&alias.name))
2062 }
2063 other => Err(Error::invalid_input(format!(
2064 "Expected aggregate function expression, got {:?}",
2065 other.variant_name()
2066 ))),
2067 }
2068 }
2069
2070 fn is_early_field(&self, field: &Field) -> bool {
2091 match self.materialization_style {
2092 MaterializationStyle::AllEarly => true,
2093 MaterializationStyle::AllLate => false,
2094 MaterializationStyle::AllEarlyExcept(ref cols) => !cols.contains(&(field.id as u32)),
2095 MaterializationStyle::Heuristic => {
2096 if field.is_blob() {
2097 return true;
2102 }
2103
2104 let byte_width = field.data_type().byte_width_opt();
2105 let is_cloud = self.dataset.object_store().is_cloud();
2106 if is_cloud {
2107 byte_width.is_some_and(|bw| bw < 1000)
2108 } else {
2109 byte_width.is_some_and(|bw| bw < 10)
2110 }
2111 }
2112 }
2113 }
2114
2115 fn calc_eager_projection(
2120 &self,
2121 filter_plan: &ExprFilterPlan,
2122 desired_projection: &Projection,
2123 ) -> Result<Projection> {
2124 let filter_columns = filter_plan.all_columns();
2131
2132 let filter_schema = self
2133 .dataset
2134 .empty_projection()
2135 .union_columns(filter_columns, OnMissing::Error)?
2136 .into_schema();
2137
2138 Ok(desired_projection
2140 .clone()
2141 .subtract_predicate(|f| !self.is_early_field(f))
2143 .union_schema(&filter_schema))
2145 }
2146
2147 fn validate_options(&self) -> Result<()> {
2148 if self.include_deleted_rows && !self.projection_plan.physical_projection.with_row_id {
2149 return Err(Error::invalid_input_source(
2150 "include_deleted_rows is set but with_row_id is false".into(),
2151 ));
2152 }
2153
2154 if self.aggregate.is_some() {
2155 if self.limit.is_some() || self.offset.is_some() {
2156 return Err(Error::invalid_input_source(
2157 "Cannot use limit/offset with aggregate. Apply limit to the result instead."
2158 .into(),
2159 ));
2160 }
2161 if self.ordering.is_some() {
2162 return Err(Error::invalid_input_source(
2163 "Cannot use order_by with aggregate. Apply ordering to the result instead."
2164 .into(),
2165 ));
2166 }
2167 }
2168
2169 Ok(())
2170 }
2171
2172 async fn create_filter_plan(&self, use_scalar_index: bool) -> Result<FilterPlan> {
2173 let filter_schema = self.filterable_schema()?;
2174 let planner = Planner::new(Arc::new(filter_schema.as_ref().into()));
2175
2176 let filter_plan = if let Some(filter) = self.filter.expr_filter.as_ref() {
2178 let expr = filter.to_datafusion(self.dataset.schema(), filter_schema.as_ref())?;
2179 let index_info = self.dataset.scalar_index_info().await?;
2180 let filter_plan =
2181 planner.create_filter_plan(expr.clone(), &index_info, use_scalar_index)?;
2182
2183 if filter_plan.index_query.is_some() {
2186 let fragments = if let Some(fragments) = self.fragments.as_ref() {
2187 fragments
2188 } else {
2189 self.dataset.fragments()
2190 };
2191 let mut has_missing_row_count = false;
2192 for frag in fragments {
2193 if frag.physical_rows.is_none() {
2194 has_missing_row_count = true;
2195 break;
2196 }
2197 }
2198 if has_missing_row_count {
2199 let filter_plan =
2202 planner.create_filter_plan(expr.clone(), &index_info, false)?;
2203 FilterPlan::new(self.filter.query_filter.clone(), filter_plan)
2204 } else {
2205 FilterPlan::new(self.filter.query_filter.clone(), filter_plan)
2206 }
2207 } else {
2208 FilterPlan::new(self.filter.query_filter.clone(), filter_plan)
2209 }
2210 } else {
2211 FilterPlan::new(self.filter.query_filter.clone(), ExprFilterPlan::default())
2212 };
2213
2214 if filter_plan.query_filter.is_some()
2216 && self.nearest.is_none()
2217 && self.full_text_query.is_none()
2218 {
2219 return Err(Error::invalid_input_source(
2220 "Query filter can only be used with full text search or vector search".into(),
2221 ));
2222 }
2223 if self.nearest.is_some() && filter_plan.vector_filter().is_some() {
2224 return Err(Error::invalid_input_source(
2225 "Query filter can't be used with vector search".into(),
2226 ));
2227 }
2228 if self.full_text_query.is_some() && filter_plan.fts_filter().is_some() {
2229 return Err(Error::invalid_input_source(
2230 "Fts filter can't be used with fts search".into(),
2231 ));
2232 }
2233
2234 Ok(filter_plan)
2235 }
2236
2237 async fn get_scan_range(&self, filter_plan: &ExprFilterPlan) -> Result<Option<Range<u64>>> {
2238 if filter_plan.has_any_filter() {
2239 Ok(None)
2241 } else if self.ordering.is_some() {
2242 Ok(None)
2245 } else {
2246 match (self.limit, self.offset) {
2247 (None, None) => Ok(None),
2248 (Some(limit), None) => {
2249 let num_rows = self.dataset.count_all_rows().await? as i64;
2250 Ok(Some(0..limit.min(num_rows) as u64))
2251 }
2252 (None, Some(offset)) => {
2253 let num_rows = self.dataset.count_all_rows().await? as i64;
2254 Ok(Some(offset.min(num_rows) as u64..num_rows as u64))
2255 }
2256 (Some(limit), Some(offset)) => {
2257 let num_rows = self.dataset.count_all_rows().await? as i64;
2258 Ok(Some(
2259 offset.min(num_rows) as u64..(offset + limit).min(num_rows) as u64,
2260 ))
2261 }
2262 }
2263 }
2264 }
2265
2266 #[instrument(level = "debug", skip_all)]
2312 pub async fn create_plan(&self) -> Result<Arc<dyn ExecutionPlan>> {
2313 log::trace!("creating scanner plan");
2314 self.validate_options()?;
2315
2316 let use_scalar_index = self.use_scalar_index && (self.prefilter || self.nearest.is_none());
2318 let mut filter_plan = self.create_filter_plan(use_scalar_index).await?;
2319
2320 let mut use_limit_node = true;
2321 let mut plan: Arc<dyn ExecutionPlan> = match (&self.nearest, &self.full_text_query) {
2323 (Some(_), None) => self.vector_search_source(&mut filter_plan).await?,
2324 (None, Some(query)) => self.fts_search_source(&mut filter_plan, query).await?,
2325 (None, None) => {
2326 if self.projection_plan.has_output_cols()
2327 && self.projection_plan.physical_projection.is_empty()
2328 {
2329 let output_expr = self.calculate_final_projection(&ArrowSchema::empty())?;
2340 return Err(Error::not_supported_source(format!("Scans must request at least one column. Received only dynamic expressions: {:?}", output_expr).into()));
2341 }
2342
2343 let take_op = filter_plan
2344 .expr_filter_plan
2345 .full_expr
2346 .as_ref()
2347 .and_then(TakeOperation::try_from_expr);
2348 if let Some((take_op, remainder)) = take_op {
2349 filter_plan.expr_filter_plan = remainder
2352 .map(ExprFilterPlan::new_refine_only)
2353 .unwrap_or(ExprFilterPlan::default());
2354 self.take_source(take_op).await?
2355 } else {
2356 let planned_read = self
2357 .filtered_read_source(&mut filter_plan.expr_filter_plan)
2358 .await?;
2359 if planned_read.limit_pushed_down {
2360 use_limit_node = false;
2361 }
2362 if planned_read.filter_pushed_down {
2363 filter_plan.disable_refine();
2364 }
2365 planned_read.plan
2366 }
2367 }
2368 _ => {
2369 return Err(Error::invalid_input_source(
2370 "Cannot have both nearest and full text search".into(),
2371 ));
2372 }
2373 };
2374
2375 let mut pre_filter_projection = self.dataset.empty_projection();
2377
2378 if filter_plan.has_refine() {
2381 pre_filter_projection = pre_filter_projection.union_columns(
2383 filter_plan.refine_columns(&self.dataset).await?,
2384 OnMissing::Ignore,
2385 )?;
2386 }
2387
2388 if let Some(ordering) = &self.ordering {
2393 pre_filter_projection = pre_filter_projection.union_columns(
2394 ordering.iter().map(|col| &col.column_name),
2395 OnMissing::Error,
2396 )?;
2397 }
2398
2399 plan = self.take(plan, pre_filter_projection)?;
2400
2401 plan = filter_plan.refine_filter(plan, self).await?;
2403
2404 if let Some(agg) = &self.aggregate {
2406 let required_columns = agg.required_columns();
2409 let agg_projection = if required_columns.is_empty() {
2410 self.dataset.empty_projection()
2411 } else {
2412 self.dataset
2413 .empty_projection()
2414 .union_columns(&required_columns, OnMissing::Error)?
2415 };
2416 plan = self.take(plan, agg_projection)?;
2417 plan = self.apply_aggregate(plan, agg).await?;
2418
2419 let optimizer = get_physical_optimizer();
2420 let options = Default::default();
2421 for rule in optimizer.rules {
2422 plan = rule.optimize(plan, &options)?;
2423 }
2424
2425 return Ok(plan);
2426 }
2427
2428 if let Some(ordering) = &self.ordering {
2430 let ordering_columns = ordering.iter().map(|col| &col.column_name);
2431 let projection_with_ordering = self
2432 .dataset
2433 .empty_projection()
2434 .union_columns(ordering_columns, OnMissing::Error)?;
2435 plan = self.take(plan, projection_with_ordering)?;
2437 let col_exprs = ordering
2438 .iter()
2439 .map(|col| {
2440 Ok(PhysicalSortExpr {
2441 expr: Self::create_column_expr(
2442 &col.column_name,
2443 &self.dataset,
2444 plan.schema().as_ref(),
2445 )?,
2446 options: SortOptions {
2447 descending: !col.ascending,
2448 nulls_first: col.nulls_first,
2449 },
2450 })
2451 })
2452 .collect::<Result<Vec<_>>>()?;
2453 plan = Arc::new(SortExec::new(
2454 LexOrdering::new(col_exprs)
2455 .ok_or(exec_datafusion_err!("Unexpected empty sort expressions"))?,
2456 plan,
2457 ));
2458 }
2459
2460 if use_limit_node && (self.limit.unwrap_or(0) > 0 || self.offset.is_some()) {
2462 plan = self.limit_node(plan);
2463 }
2464
2465 plan = self.take(plan, self.projection_plan.physical_projection.clone())?;
2467
2468 if self.projection_plan.must_add_row_offset {
2470 plan = Arc::new(AddRowOffsetExec::try_new(plan, self.dataset.clone()).await?);
2471 }
2472
2473 let final_projection = self.calculate_final_projection(plan.schema().as_ref())?;
2475
2476 plan = Arc::new(DFProjectionExec::try_new(final_projection, plan)?);
2477
2478 if self.strict_batch_size {
2480 plan = Arc::new(StrictBatchSizeExec::new(plan, self.get_batch_size()));
2481 }
2482
2483 let optimizer = get_physical_optimizer();
2484 let options = Default::default();
2485 for rule in optimizer.rules {
2486 plan = rule.optimize(plan, &options)?;
2487 }
2488
2489 Ok(plan)
2490 }
2491
2492 fn filter_references_version_columns(&self, filter_plan: &ExprFilterPlan) -> bool {
2494 use lance_core::{ROW_CREATED_AT_VERSION, ROW_LAST_UPDATED_AT_VERSION};
2495
2496 if let Some(refine_expr) = &filter_plan.refine_expr {
2497 let column_names = Planner::column_names_in_expr(refine_expr);
2498 for col_name in column_names {
2499 if col_name == ROW_CREATED_AT_VERSION || col_name == ROW_LAST_UPDATED_AT_VERSION {
2500 return true;
2501 }
2502 }
2503 }
2504 false
2505 }
2506
2507 async fn legacy_filtered_read(
2513 &self,
2514 filter_plan: &ExprFilterPlan,
2515 projection: Projection,
2516 make_deletions_null: bool,
2517 fragments: Option<Arc<Vec<Fragment>>>,
2518 scan_range: Option<Range<u64>>,
2519 is_prefilter: bool,
2520 ) -> Result<PlannedFilteredScan> {
2521 let fragments = fragments.unwrap_or(self.dataset.fragments().clone());
2522 let mut filter_pushed_down = false;
2523
2524 let plan: Arc<dyn ExecutionPlan> = if filter_plan.has_index_query() {
2525 if self.include_deleted_rows {
2526 return Err(Error::invalid_input_source(
2527 "Cannot include deleted rows in a scalar indexed scan".into(),
2528 ));
2529 }
2530 self.scalar_indexed_scan(projection, filter_plan, fragments)
2531 .await
2532 } else if !is_prefilter
2533 && filter_plan.has_refine()
2534 && self.batch_size.is_none()
2535 && self.use_stats
2536 && !self.filter_references_version_columns(filter_plan)
2537 {
2538 filter_pushed_down = true;
2539 self.pushdown_scan(false, filter_plan)
2540 } else {
2541 let ordered = if self.ordering.is_some() || self.nearest.is_some() {
2542 false
2544 } else if projection.with_row_last_updated_at_version
2545 || projection.with_row_created_at_version
2546 {
2547 true
2550 } else {
2551 self.ordered
2552 };
2553
2554 let projection = if let Some(refine_expr) = filter_plan.refine_expr.as_ref() {
2555 if is_prefilter {
2556 let refine_cols = Planner::column_names_in_expr(refine_expr);
2557 projection.union_columns(refine_cols, OnMissing::Error)?
2558 } else {
2559 projection
2560 }
2561 } else {
2562 projection
2563 };
2564
2565 let scan_range = if filter_plan.has_refine() {
2567 None
2568 } else {
2569 scan_range
2570 };
2571
2572 let scan = self.scan_fragments(
2573 projection.with_row_id,
2574 self.projection_plan.physical_projection.with_row_addr,
2575 self.projection_plan
2576 .physical_projection
2577 .with_row_last_updated_at_version,
2578 self.projection_plan
2579 .physical_projection
2580 .with_row_created_at_version,
2581 make_deletions_null,
2582 Arc::new(projection.to_bare_schema()),
2583 fragments,
2584 scan_range,
2585 ordered,
2586 );
2587
2588 if filter_plan.has_refine() && is_prefilter {
2589 Ok(Arc::new(LanceFilterExec::try_new(
2590 filter_plan.refine_expr.clone().unwrap(),
2591 scan,
2592 )?) as Arc<dyn ExecutionPlan>)
2593 } else {
2594 Ok(scan)
2595 }
2596 }?;
2597 Ok(PlannedFilteredScan {
2598 plan,
2599 limit_pushed_down: false,
2600 filter_pushed_down,
2601 })
2602 }
2603
2604 async fn new_filtered_read(
2608 &self,
2609 filter_plan: &ExprFilterPlan,
2610 projection: Projection,
2611 make_deletions_null: bool,
2612 fragments: Option<Arc<Vec<Fragment>>>,
2613 scan_range: Option<Range<u64>>,
2614 ) -> Result<Arc<dyn ExecutionPlan>> {
2615 let mut read_options = FilteredReadOptions::basic_full_read(&self.dataset)
2616 .with_filter_plan(filter_plan.clone())
2617 .with_projection(projection);
2618
2619 if let Some(fragments) = fragments {
2620 read_options = read_options.with_fragments(fragments);
2621 }
2622
2623 if let Some(scan_range) = scan_range {
2624 read_options = read_options.with_scan_range_before_filter(scan_range)?;
2625 }
2626
2627 if let Some(batch_size) = self.batch_size {
2628 read_options = read_options.with_batch_size(batch_size as u32);
2629 }
2630
2631 if let Some(fragment_readahead) = self.fragment_readahead {
2632 read_options = read_options.with_fragment_readahead(fragment_readahead);
2633 }
2634
2635 if make_deletions_null {
2636 read_options = read_options.with_deleted_rows()?;
2637 }
2638
2639 if let Some(io_buffer_size_bytes) = self.io_buffer_size {
2640 read_options = read_options.with_io_buffer_size(io_buffer_size_bytes);
2641 }
2642
2643 let index_input = filter_plan.index_query.clone().map(|index_query| {
2644 Arc::new(ScalarIndexExec::new(self.dataset.clone(), index_query))
2645 as Arc<dyn ExecutionPlan>
2646 });
2647
2648 Ok(Arc::new(FilteredReadExec::try_new(
2649 self.dataset.clone(),
2650 read_options,
2651 index_input,
2652 )?))
2653 }
2654
2655 async fn filtered_read(
2659 &self,
2660 filter_plan: &ExprFilterPlan,
2661 projection: Projection,
2662 make_deletions_null: bool,
2663 fragments: Option<Arc<Vec<Fragment>>>,
2664 scan_range: Option<Range<u64>>,
2665 is_prefilter: bool,
2666 ) -> Result<PlannedFilteredScan> {
2667 if self.dataset.is_legacy_storage() {
2669 self.legacy_filtered_read(
2670 filter_plan,
2671 projection,
2672 make_deletions_null,
2673 fragments,
2674 scan_range,
2675 is_prefilter,
2676 )
2677 .await
2678 } else {
2679 let limit_pushed_down = scan_range.is_some();
2680 let plan = self
2681 .new_filtered_read(
2682 filter_plan,
2683 projection,
2684 make_deletions_null,
2685 fragments,
2686 scan_range,
2687 )
2688 .await?;
2689 Ok(PlannedFilteredScan {
2690 filter_pushed_down: true,
2691 limit_pushed_down,
2692 plan,
2693 })
2694 }
2695 }
2696
2697 fn u64s_as_take_input(&self, u64s: Vec<u64>) -> Result<Arc<dyn ExecutionPlan>> {
2698 let row_addrs = RowAddrTreeMap::from_iter(u64s);
2699 let row_addr_mask = RowAddrMask::from_allowed(row_addrs);
2700 let index_result = IndexExprResult::Exact(row_addr_mask);
2701 let fragments_covered = self.dataset.fragment_bitmap.as_ref().clone();
2702 let batch = index_result.serialize_to_arrow(&fragments_covered)?;
2703 let stream = futures::stream::once(async move { Ok(batch) });
2704 let stream = Box::pin(RecordBatchStreamAdapter::new(
2705 INDEX_EXPR_RESULT_SCHEMA.clone(),
2706 stream,
2707 ));
2708 Ok(Arc::new(OneShotExec::new(stream)))
2709 }
2710
2711 async fn take_source(&self, take_op: TakeOperation) -> Result<Arc<dyn ExecutionPlan>> {
2712 let projection = self.projection_plan.physical_projection.clone();
2715
2716 let input = match take_op {
2717 TakeOperation::RowIds(ids) => self.u64s_as_take_input(ids),
2718 TakeOperation::RowAddrs(addrs) => self.u64s_as_take_input(addrs),
2719 TakeOperation::RowOffsets(offsets) => {
2720 let mut addrs =
2721 row_offsets_to_row_addresses(self.dataset.as_ref(), &offsets).await?;
2722 addrs.retain(|addr| *addr != RowAddress::TOMBSTONE_ROW);
2723 self.u64s_as_take_input(addrs)
2724 }
2725 }?;
2726
2727 let mut filtered_read_options = FilteredReadOptions::new(projection);
2728 if let Some(fragment) = self.fragments.as_ref() {
2729 filtered_read_options =
2730 filtered_read_options.with_fragments(Arc::new(fragment.clone()));
2731 }
2732
2733 Ok(Arc::new(FilteredReadExec::try_new(
2734 self.dataset.clone(),
2735 filtered_read_options,
2736 Some(input),
2737 )?))
2738 }
2739
2740 async fn filtered_read_source(
2741 &self,
2742 filter_plan: &mut ExprFilterPlan,
2743 ) -> Result<PlannedFilteredScan> {
2744 log::trace!("source is a filtered read");
2745
2746 let effective_projection = if let Some(agg) = &self.aggregate {
2750 let required_columns = agg.required_columns();
2751 if required_columns.is_empty() {
2752 self.dataset.empty_projection()
2754 } else {
2755 self.dataset
2757 .empty_projection()
2758 .union_columns(&required_columns, OnMissing::Error)?
2759 }
2760 } else {
2761 self.projection_plan.physical_projection.clone()
2762 };
2763
2764 let mut projection = if filter_plan.has_refine() {
2765 self.calc_eager_projection(filter_plan, &effective_projection)?
2769 .with_row_id()
2770 } else {
2771 effective_projection
2774 };
2775
2776 if projection.is_empty() {
2777 projection.with_row_addr = true;
2780 }
2781
2782 let scan_range = if filter_plan.is_empty() {
2783 log::trace!("pushing scan_range into filtered_read");
2784 self.get_scan_range(filter_plan).await?
2785 } else {
2786 None
2787 };
2788
2789 self.filtered_read(
2790 filter_plan,
2791 projection,
2792 self.include_deleted_rows,
2793 self.fragments.clone().map(Arc::new),
2794 scan_range,
2795 false,
2796 )
2797 .await
2798 }
2799
2800 async fn fts_search_source(
2801 &self,
2802 filter_plan: &mut FilterPlan,
2803 query: &FullTextSearchQuery,
2804 ) -> Result<Arc<dyn ExecutionPlan>> {
2805 log::trace!("source is an fts search");
2806 if self.include_deleted_rows {
2807 return Err(Error::invalid_input_source(
2808 "Cannot include deleted rows in an FTS search".into(),
2809 ));
2810 }
2811
2812 if self.prefilter {
2814 let source: Arc<dyn ExecutionPlan> = match &filter_plan.vector_filter() {
2815 Some(vector_query) => {
2816 let vector_plan = self
2818 .vector_search(&filter_plan.expr_filter_plan, vector_query)
2819 .await?;
2820 self.fts_rerank(vector_plan, query).await?
2821 }
2822 None => self.fts(&filter_plan.expr_filter_plan, query).await?,
2823 };
2824 filter_plan.disable_refine();
2826 Ok(source)
2827 } else {
2828 filter_plan.make_refine_only();
2831 self.fts(&ExprFilterPlan::default(), query).await
2832 }
2833 }
2834
2835 async fn vector_search_source(
2836 &self,
2837 filter_plan: &mut FilterPlan,
2838 ) -> Result<Arc<dyn ExecutionPlan>> {
2839 if self.include_deleted_rows {
2840 return Err(Error::invalid_input_source(
2841 "Cannot include deleted rows in a nearest neighbor search".into(),
2842 ));
2843 }
2844 let Some(query) = self.nearest.as_ref() else {
2845 return Err(Error::invalid_input("No nearest query".to_string()));
2846 };
2847
2848 if self.prefilter {
2849 log::trace!("source is a vector search (prefilter)");
2850 let source: Arc<dyn ExecutionPlan> = match &filter_plan.fts_filter() {
2852 Some(fts_query) => {
2853 let fts_plan = self.fts(&filter_plan.expr_filter_plan, fts_query).await?;
2854 let projection = self
2855 .dataset
2856 .empty_projection()
2857 .union_column(&query.column, OnMissing::Error)?;
2858 let plan = self.take(fts_plan, projection)?;
2859
2860 self.flat_knn(plan, query)?
2861 }
2862 None => {
2863 self.vector_search(&filter_plan.expr_filter_plan, query)
2864 .await?
2865 }
2866 };
2867
2868 filter_plan.disable_refine();
2869 Ok(source)
2870 } else {
2871 log::trace!("source is a vector search (postfilter)");
2872 filter_plan.make_refine_only();
2875 self.vector_search(&ExprFilterPlan::default(), query).await
2876 }
2877 }
2878
2879 async fn fragments_covered_by_fts_leaf(
2880 &self,
2881 column: &str,
2882 accum: &mut RoaringBitmap,
2883 ) -> Result<bool> {
2884 let index = self
2885 .dataset
2886 .load_scalar_index(IndexCriteria::default().for_column(column).supports_fts())
2887 .await?;
2888 match index {
2889 Some(index) => match &index.fragment_bitmap {
2890 Some(fragmap) => {
2891 *accum |= fragmap;
2892 Ok(true)
2893 }
2894 None => Ok(false),
2895 },
2896 None => Ok(false),
2897 }
2898 }
2899
2900 #[async_recursion]
2901 async fn fragments_covered_by_fts_query_helper(
2902 &self,
2903 query: &FtsQuery,
2904 accum: &mut RoaringBitmap,
2905 ) -> Result<bool> {
2906 match query {
2907 FtsQuery::Match(match_query) => {
2908 self.fragments_covered_by_fts_leaf(
2909 match_query.column.as_ref().ok_or(Error::invalid_input(
2910 "the column must be specified in the query".to_string(),
2911 ))?,
2912 accum,
2913 )
2914 .await
2915 }
2916 FtsQuery::Boost(boost) => Ok(self
2917 .fragments_covered_by_fts_query_helper(&boost.negative, accum)
2918 .await?
2919 & self
2920 .fragments_covered_by_fts_query_helper(&boost.positive, accum)
2921 .await?),
2922 FtsQuery::MultiMatch(multi_match) => {
2923 for mq in &multi_match.match_queries {
2924 if !self
2925 .fragments_covered_by_fts_leaf(
2926 mq.column.as_ref().ok_or(Error::invalid_input(
2927 "the column must be specified in the query".to_string(),
2928 ))?,
2929 accum,
2930 )
2931 .await?
2932 {
2933 return Ok(false);
2934 }
2935 }
2936 Ok(true)
2937 }
2938 FtsQuery::Phrase(phrase_query) => {
2939 self.fragments_covered_by_fts_leaf(
2940 phrase_query.column.as_ref().ok_or(Error::invalid_input(
2941 "the column must be specified in the query".to_string(),
2942 ))?,
2943 accum,
2944 )
2945 .await
2946 }
2947 FtsQuery::Boolean(bool_query) => {
2948 for query in bool_query.must.iter() {
2949 if !self
2950 .fragments_covered_by_fts_query_helper(query, accum)
2951 .await?
2952 {
2953 return Ok(false);
2954 }
2955 }
2956 for query in &bool_query.should {
2957 if !self
2958 .fragments_covered_by_fts_query_helper(query, accum)
2959 .await?
2960 {
2961 return Ok(false);
2962 }
2963 }
2964 Ok(true)
2965 }
2966 }
2967 }
2968
2969 async fn fragments_covered_by_fts_query(&self, query: &FtsQuery) -> Result<RoaringBitmap> {
2970 let all_fragments = self.get_fragments_as_bitmap();
2971
2972 let mut referenced_fragments = RoaringBitmap::new();
2973 if !self
2974 .fragments_covered_by_fts_query_helper(query, &mut referenced_fragments)
2975 .await?
2976 {
2977 Ok(all_fragments)
2979 } else {
2980 Ok(all_fragments & referenced_fragments)
2982 }
2983 }
2984
2985 async fn fts(
2987 &self,
2988 filter_plan: &ExprFilterPlan,
2989 query: &FullTextSearchQuery,
2990 ) -> Result<Arc<dyn ExecutionPlan>> {
2991 let columns = query.columns();
2992 let mut params = query.params();
2993 if params.limit.is_none() {
2994 let search_limit = match (self.limit, self.offset) {
2995 (Some(limit), Some(offset)) => Some((limit + offset) as usize),
2996 (Some(limit), None) => Some(limit as usize),
2997 (None, Some(_)) => None, (None, None) => None,
2999 };
3000 params = params.with_limit(search_limit);
3001 }
3002 let query = if columns.is_empty() {
3003 let indexed_columns = fts_indexed_columns(self.dataset.clone()).await?;
3006 fill_fts_query_column(&query.query, &indexed_columns, false)?
3007 } else {
3008 query.query.clone()
3009 };
3010
3011 let prefilter_source = self
3015 .prefilter_source(
3016 filter_plan,
3017 self.fragments_covered_by_fts_query(&query).await?,
3018 )
3019 .await?;
3020 let fts_exec = self
3021 .plan_fts(&query, ¶ms, filter_plan, &prefilter_source)
3022 .await?;
3023 Ok(fts_exec)
3024 }
3025
3026 async fn plan_fts(
3027 &self,
3028 query: &FtsQuery,
3029 params: &FtsSearchParams,
3030 filter_plan: &ExprFilterPlan,
3031 prefilter_source: &PreFilterSource,
3032 ) -> Result<Arc<dyn ExecutionPlan>> {
3033 let plan: Arc<dyn ExecutionPlan> = match query {
3034 FtsQuery::Match(query) => {
3035 self.plan_match_query(query, params, filter_plan, prefilter_source)
3036 .await?
3037 }
3038 FtsQuery::Phrase(query) => {
3039 self.plan_phrase_query(query, params, prefilter_source)
3040 .await?
3041 }
3042
3043 FtsQuery::Boost(query) => {
3044 let unlimited_params = params.clone().with_limit(None);
3048 let positive_exec = Box::pin(self.plan_fts(
3049 &query.positive,
3050 &unlimited_params,
3051 filter_plan,
3052 prefilter_source,
3053 ));
3054 let negative_exec = Box::pin(self.plan_fts(
3055 &query.negative,
3056 &unlimited_params,
3057 filter_plan,
3058 prefilter_source,
3059 ));
3060 let (positive_exec, negative_exec) =
3061 futures::future::try_join(positive_exec, negative_exec).await?;
3062 Arc::new(BoostQueryExec::new(
3063 query.clone(),
3064 params.clone(),
3065 positive_exec,
3066 negative_exec,
3067 ))
3068 }
3069
3070 FtsQuery::MultiMatch(query) => {
3071 let mut children = Vec::with_capacity(query.match_queries.len());
3072 for match_query in &query.match_queries {
3073 let child =
3074 self.plan_match_query(match_query, params, filter_plan, prefilter_source);
3075 children.push(child);
3076 }
3077 let children = futures::future::try_join_all(children).await?;
3078
3079 let schema = children[0].schema();
3080 let group_expr = vec![(
3081 expressions::col(ROW_ID, schema.as_ref())?,
3082 ROW_ID.to_string(),
3083 )];
3084
3085 let fts_node = UnionExec::try_new(children)?;
3086 let fts_node = Arc::new(RepartitionExec::try_new(
3087 fts_node,
3088 Partitioning::RoundRobinBatch(1),
3089 )?);
3090 let fts_node = Arc::new(AggregateExec::try_new(
3092 AggregateMode::Single,
3093 PhysicalGroupBy::new_single(group_expr),
3094 vec![Arc::new(
3095 datafusion_physical_expr::aggregate::AggregateExprBuilder::new(
3096 functions_aggregate::min_max::max_udaf(),
3097 vec![expressions::col(SCORE_COL, &schema)?],
3098 )
3099 .schema(schema.clone())
3100 .alias(SCORE_COL)
3101 .build()?,
3102 )],
3103 vec![None],
3104 fts_node,
3105 schema,
3106 )?);
3107 let sort_expr = PhysicalSortExpr {
3108 expr: expressions::col(SCORE_COL, fts_node.schema().as_ref())?,
3109 options: SortOptions {
3110 descending: true,
3111 nulls_first: false,
3112 },
3113 };
3114
3115 Arc::new(
3116 SortExec::new([sort_expr].into(), fts_node)
3117 .with_fetch(self.limit.map(|l| l as usize)),
3118 )
3119 }
3120 FtsQuery::Boolean(query) => {
3121 let unlimited_params = params.clone().with_limit(None);
3126
3127 let mut should = Vec::with_capacity(query.should.len());
3129 for subquery in &query.should {
3130 let plan = Box::pin(self.plan_fts(
3131 subquery,
3132 &unlimited_params,
3133 filter_plan,
3134 prefilter_source,
3135 ))
3136 .await?;
3137 should.push(plan);
3138 }
3139 let should = if should.is_empty() {
3140 Arc::new(EmptyExec::new(FTS_SCHEMA.clone()))
3141 } else if should.len() == 1 {
3142 should.pop().unwrap()
3143 } else {
3144 let unioned = UnionExec::try_new(should)?;
3145 Arc::new(RepartitionExec::try_new(
3146 unioned,
3147 Partitioning::RoundRobinBatch(1),
3148 )?)
3149 };
3150
3151 let mut must = None;
3153 for query in &query.must {
3154 let plan = Box::pin(self.plan_fts(
3155 query,
3156 &unlimited_params,
3157 filter_plan,
3158 prefilter_source,
3159 ))
3160 .await?;
3161 if let Some(joined_plan) = must {
3162 must = Some(Arc::new(HashJoinExec::try_new(
3163 joined_plan,
3164 plan,
3165 vec![(
3166 Arc::new(Column::new_with_schema(ROW_ID, &FTS_SCHEMA)?),
3167 Arc::new(Column::new_with_schema(ROW_ID, &FTS_SCHEMA)?),
3168 )],
3169 None,
3170 &datafusion_expr::JoinType::Inner,
3171 None,
3172 datafusion_physical_plan::joins::PartitionMode::CollectLeft,
3173 NullEquality::NullEqualsNothing,
3174 )?) as _);
3175 } else {
3176 must = Some(plan);
3177 }
3178 }
3179
3180 let mut must_not = Vec::with_capacity(query.must_not.len());
3182 for query in &query.must_not {
3183 let plan = Box::pin(self.plan_fts(
3184 query,
3185 &unlimited_params,
3186 filter_plan,
3187 prefilter_source,
3188 ))
3189 .await?;
3190 must_not.push(plan);
3191 }
3192 let must_not = if must_not.is_empty() {
3193 Arc::new(EmptyExec::new(FTS_SCHEMA.clone()))
3194 } else if must_not.len() == 1 {
3195 must_not.pop().unwrap()
3196 } else {
3197 let unioned = UnionExec::try_new(must_not)?;
3198 Arc::new(RepartitionExec::try_new(
3199 unioned,
3200 Partitioning::RoundRobinBatch(1),
3201 )?)
3202 };
3203
3204 if query.should.is_empty() && must.is_none() {
3205 return Err(Error::invalid_input(
3206 "boolean query must have at least one should/must query".to_string(),
3207 ));
3208 }
3209
3210 Arc::new(BooleanQueryExec::new(
3211 query.clone(),
3212 params.clone(),
3213 should,
3214 must,
3215 must_not,
3216 ))
3217 }
3218 };
3219
3220 Ok(plan)
3221 }
3222
3223 async fn plan_phrase_query(
3224 &self,
3225 query: &PhraseQuery,
3226 params: &FtsSearchParams,
3227 prefilter_source: &PreFilterSource,
3228 ) -> Result<Arc<dyn ExecutionPlan>> {
3229 let column = query.column.clone().ok_or(Error::invalid_input(
3230 "the column must be specified in the query".to_string(),
3231 ))?;
3232
3233 let index_meta = self
3234 .dataset
3235 .load_scalar_index(IndexCriteria::default().for_column(&column).supports_fts())
3236 .await?
3237 .ok_or(Error::invalid_input(format!(
3238 "No Inverted index found for column {}",
3239 column
3240 )))?;
3241
3242 let details_any =
3243 crate::index::scalar::fetch_index_details(&self.dataset, &column, &index_meta).await?;
3244 let details = details_any
3245 .as_ref()
3246 .to_msg::<lance_index::pbold::InvertedIndexDetails>()?;
3247 if !details.with_position {
3248 return Err(Error::invalid_input("position is not found but required for phrase queries, try recreating the index with position"
3249 .to_string()));
3250 }
3251
3252 Ok(Arc::new(PhraseQueryExec::new(
3253 self.dataset.clone(),
3254 query.clone(),
3255 params.clone(),
3256 prefilter_source.clone(),
3257 )))
3258 }
3259
3260 async fn plan_match_query(
3261 &self,
3262 query: &MatchQuery,
3263 params: &FtsSearchParams,
3264 filter_plan: &ExprFilterPlan,
3265 prefilter_source: &PreFilterSource,
3266 ) -> Result<Arc<dyn ExecutionPlan>> {
3267 let column = query
3268 .column
3269 .as_ref()
3270 .ok_or(Error::invalid_input(
3271 "the column must be specified in the query".to_string(),
3272 ))?
3273 .clone();
3274
3275 let index = self
3276 .dataset
3277 .load_scalar_index(IndexCriteria::default().for_column(&column).supports_fts())
3278 .await?;
3279
3280 let target_fragments = self
3282 .fragments
3283 .clone()
3284 .unwrap_or_else(|| self.dataset.fragments().to_vec());
3285
3286 let (match_plan, flat_match_plan) = match &index {
3287 Some(index) => {
3288 let unindexed_fragments = self
3290 .retain_target_fragments(self.dataset.unindexed_fragments(&index.name).await?);
3291
3292 if unindexed_fragments.len() == target_fragments.len() {
3294 if self.fast_search {
3295 return Ok(Arc::new(EmptyExec::new(FTS_SCHEMA.clone())));
3296 }
3297 let flat_match_plan = self
3298 .plan_flat_match_query(unindexed_fragments, query, params, filter_plan)
3299 .await?;
3300 return Ok(flat_match_plan);
3301 }
3302
3303 let match_plan: Arc<dyn ExecutionPlan> = Arc::new(MatchQueryExec::new(
3305 self.dataset.clone(),
3306 query.clone(),
3307 params.clone(),
3308 prefilter_source.clone(),
3309 ));
3310
3311 if self.fast_search || unindexed_fragments.is_empty() {
3312 (Some(match_plan), None)
3313 } else {
3314 let flat_match_plan = self
3315 .plan_flat_match_query(unindexed_fragments, query, params, filter_plan)
3316 .await?;
3317 (Some(match_plan), Some(flat_match_plan))
3318 }
3319 }
3320 None => {
3321 if self.fast_search {
3322 return Ok(Arc::new(EmptyExec::new(FTS_SCHEMA.clone())));
3323 }
3324 let flat_match_plan = self
3326 .plan_flat_match_query(target_fragments.clone(), query, params, filter_plan)
3327 .await?;
3328 (None, Some(flat_match_plan))
3329 }
3330 };
3331
3332 let plan = match (match_plan, flat_match_plan) {
3334 (Some(match_plan), Some(flat_match_plan)) => {
3335 let match_plan = UnionExec::try_new(vec![match_plan, flat_match_plan])?;
3336 let match_plan = Arc::new(RepartitionExec::try_new(
3337 match_plan,
3338 Partitioning::RoundRobinBatch(1),
3339 )?);
3340 let sort_expr = PhysicalSortExpr {
3341 expr: expressions::col(SCORE_COL, match_plan.schema().as_ref())?,
3342 options: SortOptions {
3343 descending: true,
3344 nulls_first: false,
3345 },
3346 };
3347 Arc::new(SortExec::new([sort_expr].into(), match_plan).with_fetch(params.limit))
3348 }
3349 (Some(match_plan), None) => match_plan,
3350 (None, Some(flat_match_plan)) => flat_match_plan,
3351 (None, None) => unreachable!(),
3352 };
3353
3354 Ok(plan)
3355 }
3356
3357 async fn plan_flat_match_query(
3359 &self,
3360 fragments: Vec<Fragment>,
3361 query: &MatchQuery,
3362 params: &FtsSearchParams,
3363 filter_plan: &ExprFilterPlan,
3364 ) -> Result<Arc<dyn ExecutionPlan>> {
3365 let column = query
3366 .column
3367 .as_ref()
3368 .ok_or(Error::invalid_input(
3369 "the column must be specified in the query".to_string(),
3370 ))?
3371 .clone();
3372
3373 let mut columns = vec![column];
3374 if let Some(expr) = filter_plan.full_expr.as_ref() {
3375 let filter_columns = Planner::column_names_in_expr(expr);
3376 columns.extend(filter_columns);
3377 }
3378 let flat_fts_scan_schema = Arc::new(self.dataset.schema().project(&columns).unwrap());
3379 let mut scan_node = self.scan_fragments(
3380 true,
3381 false,
3382 false,
3383 false,
3384 false,
3385 flat_fts_scan_schema,
3386 Arc::new(fragments),
3387 None,
3388 false,
3389 );
3390
3391 if let Some(expr) = filter_plan.full_expr.as_ref() {
3392 scan_node = Arc::new(LanceFilterExec::try_new(expr.clone(), scan_node)?);
3394 }
3395
3396 let flat_match_plan = Arc::new(FlatMatchQueryExec::new(
3397 self.dataset.clone(),
3398 query.clone(),
3399 params.clone(),
3400 scan_node,
3401 ));
3402 Ok(flat_match_plan)
3403 }
3404
3405 async fn vector_search(
3407 &self,
3408 filter_plan: &ExprFilterPlan,
3409 q: &Query,
3410 ) -> Result<Arc<dyn ExecutionPlan>> {
3411 let mut q = q.clone();
3412
3413 let (vector_type, element_type) = get_vector_type(self.dataset.schema(), &q.column)?;
3415
3416 let column_id = self.dataset.schema().field_id(q.column.as_str())?;
3417 let use_index = q.use_index;
3418 let indices = if use_index {
3419 self.dataset.load_indices().await?
3420 } else {
3421 Arc::new(vec![])
3422 };
3423 let matching_index = if let Some(index) =
3425 indices.iter().find(|i| i.fields.contains(&column_id))
3426 {
3427 let idx = self
3431 .dataset
3432 .open_vector_index(
3433 q.column.as_str(),
3434 &index.uuid.to_string(),
3435 &NoOpMetricsCollector,
3436 )
3437 .await?;
3438 let index_metric = idx.metric_type();
3439
3440 let use_this_index = match q.metric_type {
3442 Some(user_metric) => {
3443 if user_metric == index_metric {
3444 true
3445 } else {
3446 log::warn!(
3447 "Requested metric {:?} is incompatible with index metric {:?}, falling back to brute-force search",
3448 user_metric,
3449 index_metric
3450 );
3451 false
3452 }
3453 }
3454 None => true, };
3456
3457 if use_this_index {
3458 Some((index, idx, index_metric))
3459 } else {
3460 None
3461 }
3462 } else {
3463 None
3464 };
3465
3466 let index_and_deltas = if let Some((index, _idx, index_metric)) = matching_index {
3468 let deltas = self.dataset.load_indices_by_name(&index.name).await?;
3469 let index_frags = self.get_indexed_frags(&deltas);
3470 if !index_frags.is_empty() {
3471 Some((index, deltas, index_metric))
3472 } else {
3473 None
3474 }
3475 } else {
3476 None
3477 };
3478
3479 if let Some((index, deltas, index_metric)) = index_and_deltas {
3480 log::trace!("index found for vector search");
3481 q.metric_type = Some(index_metric);
3483 validate_distance_type_for(index_metric, &element_type)?;
3484
3485 if matches!(q.refine_factor, Some(0)) {
3486 return Err(Error::invalid_input(
3487 "Refine factor cannot be zero".to_string(),
3488 ));
3489 }
3490 let ann_node = match vector_type {
3491 DataType::FixedSizeList(_, _) => self.ann(&q, &deltas, filter_plan).await?,
3492 DataType::List(_) => self.multivec_ann(&q, &deltas, filter_plan).await?,
3493 _ => unreachable!(),
3494 };
3495
3496 let mut knn_node = if q.refine_factor.is_some() {
3497 let vector_projection = self
3498 .dataset
3499 .empty_projection()
3500 .union_column(&q.column, OnMissing::Error)
3501 .unwrap();
3502 let knn_node_with_vector = self.take(ann_node, vector_projection)?;
3503 self.flat_knn(knn_node_with_vector, &q)?
3504 } else {
3505 ann_node
3506 }; if !self.fast_search {
3509 knn_node = self.knn_combined(&q, index, knn_node, filter_plan).await?;
3510 }
3511
3512 Ok(knn_node)
3513 } else {
3514 if self.fast_search {
3515 return Ok(Arc::new(EmptyExec::new(KNN_INDEX_SCHEMA.clone())));
3516 }
3517 let metric = q
3519 .metric_type
3520 .unwrap_or_else(|| default_distance_type_for(&element_type));
3521 q.metric_type = Some(metric);
3522 validate_distance_type_for(metric, &element_type)?;
3523 let mut columns = vec![q.column.clone()];
3525 if let Some(refine_expr) = filter_plan.refine_expr.as_ref() {
3526 columns.extend(Planner::column_names_in_expr(refine_expr));
3527 }
3528 let mut vector_scan_projection = self
3529 .dataset
3530 .empty_projection()
3531 .with_row_id()
3532 .union_columns(&columns, OnMissing::Error)?;
3533
3534 vector_scan_projection.with_row_addr =
3535 self.projection_plan.physical_projection.with_row_addr;
3536
3537 let PlannedFilteredScan { mut plan, .. } = self
3538 .filtered_read(
3539 filter_plan,
3540 vector_scan_projection,
3541 true,
3542 self.fragments.clone().map(Arc::new),
3543 None,
3544 true,
3545 )
3546 .await?;
3547
3548 if let Some(refine_expr) = &filter_plan.refine_expr {
3549 plan = Arc::new(LanceFilterExec::try_new(refine_expr.clone(), plan)?);
3550 }
3551 Ok(self.flat_knn(plan, &q)?)
3552 }
3553 }
3554
3555 async fn knn_combined(
3557 &self,
3558 q: &Query,
3559 index: &IndexMetadata,
3560 mut knn_node: Arc<dyn ExecutionPlan>,
3561 filter_plan: &ExprFilterPlan,
3562 ) -> Result<Arc<dyn ExecutionPlan>> {
3563 let unindexed_fragments =
3565 self.retain_target_fragments(self.dataset.unindexed_fragments(&index.name).await?);
3566
3567 if !unindexed_fragments.is_empty() {
3568 let idx = self
3571 .dataset
3572 .open_vector_index(
3573 q.column.as_str(),
3574 &index.uuid.to_string(),
3575 &NoOpMetricsCollector,
3576 )
3577 .await?;
3578 let mut q = q.clone();
3579 q.metric_type = Some(idx.metric_type());
3580
3581 if knn_node.schema().column_with_name(&q.column).is_none() {
3584 let vector_projection = self
3585 .dataset
3586 .empty_projection()
3587 .union_column(&q.column, OnMissing::Error)
3588 .unwrap();
3589 knn_node = self.take(knn_node, vector_projection)?;
3590 }
3591
3592 let mut columns = vec![q.column.clone()];
3593 if let Some(expr) = filter_plan.full_expr.as_ref() {
3594 let filter_columns = Planner::column_names_in_expr(expr);
3595 columns.extend(filter_columns);
3596 }
3597 let vector_scan_projection = Arc::new(self.dataset.schema().project(&columns).unwrap());
3598 let mut scan_node = self.scan_fragments(
3602 true,
3603 false,
3604 false,
3605 false,
3606 false,
3607 vector_scan_projection,
3608 Arc::new(unindexed_fragments),
3609 None,
3611 false,
3614 );
3615
3616 if let Some(expr) = filter_plan.full_expr.as_ref() {
3617 scan_node = Arc::new(LanceFilterExec::try_new(expr.clone(), scan_node)?);
3619 }
3620 let topk_appended = self.flat_knn(scan_node, &q)?;
3622
3623 let topk_appended = project(topk_appended, knn_node.schema().as_ref())?;
3627 assert!(
3628 topk_appended
3629 .schema()
3630 .equivalent_names_and_types(&knn_node.schema())
3631 );
3632 let unioned = UnionExec::try_new(vec![Arc::new(topk_appended), knn_node])?;
3634 let unioned = RepartitionExec::try_new(
3636 unioned,
3637 datafusion::physical_plan::Partitioning::RoundRobinBatch(1),
3638 )?;
3639 return self.flat_knn(Arc::new(unioned), &q);
3641 }
3642
3643 Ok(knn_node)
3644 }
3645
3646 #[async_recursion]
3647 async fn fragments_covered_by_index_query(
3648 &self,
3649 index_expr: &ScalarIndexExpr,
3650 ) -> Result<RoaringBitmap> {
3651 match index_expr {
3652 ScalarIndexExpr::And(lhs, rhs) => {
3653 Ok(self.fragments_covered_by_index_query(lhs).await?
3654 & self.fragments_covered_by_index_query(rhs).await?)
3655 }
3656 ScalarIndexExpr::Or(lhs, rhs) => Ok(self.fragments_covered_by_index_query(lhs).await?
3657 & self.fragments_covered_by_index_query(rhs).await?),
3658 ScalarIndexExpr::Not(expr) => self.fragments_covered_by_index_query(expr).await,
3659 ScalarIndexExpr::Query(search) => {
3660 let idx = self
3661 .dataset
3662 .load_scalar_index(IndexCriteria::default().with_name(&search.index_name))
3663 .await?
3664 .expect("Index not found even though it must have been found earlier");
3665 Ok(idx
3666 .fragment_bitmap
3667 .expect("scalar indices should always have a fragment bitmap"))
3668 }
3669 }
3670 }
3671
3672 async fn partition_frags_by_coverage(
3680 &self,
3681 index_expr: &ScalarIndexExpr,
3682 fragments: Arc<Vec<Fragment>>,
3683 ) -> Result<(Vec<Fragment>, Vec<Fragment>)> {
3684 let covered_frags = self.fragments_covered_by_index_query(index_expr).await?;
3685 let mut relevant_frags = Vec::with_capacity(fragments.len());
3686 let mut missing_frags = Vec::with_capacity(fragments.len());
3687 for fragment in fragments.iter() {
3688 if covered_frags.contains(fragment.id as u32) {
3689 relevant_frags.push(fragment.clone());
3690 } else {
3691 missing_frags.push(fragment.clone());
3692 }
3693 }
3694 Ok((relevant_frags, missing_frags))
3695 }
3696
3697 async fn scalar_indexed_scan(
3700 &self,
3701 projection: Projection,
3702 filter_plan: &ExprFilterPlan,
3703 fragments: Arc<Vec<Fragment>>,
3704 ) -> Result<Arc<dyn ExecutionPlan>> {
3705 log::trace!("scalar indexed scan");
3706 let index_expr = filter_plan.index_query.as_ref().unwrap();
3713
3714 let needs_recheck = index_expr.needs_recheck();
3715
3716 let (relevant_frags, missing_frags) = self
3718 .partition_frags_by_coverage(index_expr, fragments)
3719 .await?;
3720
3721 let mut plan: Arc<dyn ExecutionPlan> = Arc::new(MaterializeIndexExec::new(
3722 self.dataset.clone(),
3723 index_expr.clone(),
3724 Arc::new(relevant_frags),
3725 ));
3726
3727 let refine_expr = filter_plan.refine_expr.as_ref();
3728
3729 let needs_take =
3732 needs_recheck || projection.has_data_fields() || filter_plan.refine_expr.is_some();
3733 if needs_take {
3734 let mut take_projection = projection.clone();
3735 if needs_recheck {
3736 let filter_expr = index_expr.to_expr();
3738 let filter_cols = Planner::column_names_in_expr(&filter_expr);
3739 take_projection = take_projection.union_columns(filter_cols, OnMissing::Error)?;
3740 }
3741 if let Some(refine_expr) = refine_expr {
3742 let refine_cols = Planner::column_names_in_expr(refine_expr);
3743 take_projection = take_projection.union_columns(refine_cols, OnMissing::Error)?;
3744 }
3745 log::trace!("need to take additional columns for scalar_indexed_scan");
3746 plan = self.take(plan, take_projection)?;
3747 }
3748
3749 let post_take_filter = match (needs_recheck, refine_expr) {
3750 (false, None) => None,
3751 (true, None) => {
3752 Some(index_expr.to_expr())
3754 }
3755 (true, Some(_)) => Some(filter_plan.full_expr.as_ref().unwrap().clone()),
3756 (false, Some(refine_expr)) => Some(refine_expr.clone()),
3757 };
3758
3759 if let Some(post_take_filter) = post_take_filter {
3760 let planner = Planner::new(plan.schema());
3761 let optimized_filter = planner.optimize_expr(post_take_filter)?;
3762
3763 log::trace!("applying post-take filter to indexed scan");
3764 plan = Arc::new(LanceFilterExec::try_new(optimized_filter, plan)?);
3765 }
3766
3767 if self.projection_plan.physical_projection.with_row_addr {
3768 plan = Arc::new(AddRowAddrExec::try_new(plan, self.dataset.clone(), 0)?);
3769 }
3770
3771 let new_data_path: Option<Arc<dyn ExecutionPlan>> = if !missing_frags.is_empty() {
3772 log::trace!(
3773 "scalar_indexed_scan will need full scan of {} missing fragments",
3774 missing_frags.len()
3775 );
3776
3777 let filter = filter_plan.full_expr.as_ref().unwrap();
3790 let filter_cols = Planner::column_names_in_expr(filter);
3791 let scan_projection = projection.union_columns(filter_cols, OnMissing::Error)?;
3792
3793 let scan_schema = Arc::new(scan_projection.to_bare_schema());
3794 let scan_arrow_schema = Arc::new(scan_schema.as_ref().into());
3795 let planner = Planner::new(scan_arrow_schema);
3796 let optimized_filter = planner.optimize_expr(filter.clone())?;
3797
3798 let new_data_scan = self.scan_fragments(
3799 true,
3800 self.projection_plan.physical_projection.with_row_addr,
3801 self.projection_plan
3802 .physical_projection
3803 .with_row_last_updated_at_version,
3804 self.projection_plan
3805 .physical_projection
3806 .with_row_created_at_version,
3807 false,
3808 scan_schema,
3809 missing_frags.into(),
3810 None,
3812 false,
3813 );
3814 let filtered = Arc::new(LanceFilterExec::try_new(optimized_filter, new_data_scan)?);
3815 Some(Arc::new(project(filtered, plan.schema().as_ref())?))
3816 } else {
3817 log::trace!("scalar_indexed_scan will not need full scan of any missing fragments");
3818 None
3819 };
3820
3821 if let Some(new_data_path) = new_data_path {
3822 let unioned = UnionExec::try_new(vec![plan, new_data_path])?;
3823 let unioned = Arc::new(RepartitionExec::try_new(
3825 unioned,
3826 datafusion::physical_plan::Partitioning::RoundRobinBatch(1),
3827 )?);
3828 Ok(unioned)
3829 } else {
3830 Ok(plan)
3831 }
3832 }
3833
3834 fn get_io_buffer_size(&self) -> u64 {
3835 self.io_buffer_size.unwrap_or(*DEFAULT_IO_BUFFER_SIZE)
3836 }
3837
3838 #[allow(clippy::too_many_arguments)]
3843 pub(crate) fn scan(
3844 &self,
3845 with_row_id: bool,
3846 with_row_address: bool,
3847 with_row_last_updated_at_version: bool,
3848 with_row_created_at_version: bool,
3849 with_make_deletions_null: bool,
3850 range: Option<Range<u64>>,
3851 projection: Arc<Schema>,
3852 ) -> Arc<dyn ExecutionPlan> {
3853 let fragments = if let Some(fragment) = self.fragments.as_ref() {
3854 Arc::new(fragment.clone())
3855 } else {
3856 self.dataset.fragments().clone()
3857 };
3858 let ordered = if self.ordering.is_some() || self.nearest.is_some() {
3859 false
3861 } else {
3862 self.ordered
3863 };
3864 self.scan_fragments(
3865 with_row_id,
3866 with_row_address,
3867 with_row_last_updated_at_version,
3868 with_row_created_at_version,
3869 with_make_deletions_null,
3870 projection,
3871 fragments,
3872 range,
3873 ordered,
3874 )
3875 }
3876
3877 #[allow(clippy::too_many_arguments)]
3878 fn scan_fragments(
3879 &self,
3880 with_row_id: bool,
3881 with_row_address: bool,
3882 with_row_last_updated_at_version: bool,
3883 with_row_created_at_version: bool,
3884 with_make_deletions_null: bool,
3885 projection: Arc<Schema>,
3886 fragments: Arc<Vec<Fragment>>,
3887 range: Option<Range<u64>>,
3888 ordered: bool,
3889 ) -> Arc<dyn ExecutionPlan> {
3890 log::trace!("scan_fragments covered {} fragments", fragments.len());
3891 let config = LanceScanConfig {
3892 batch_size: self.get_batch_size(),
3893 batch_readahead: self.batch_readahead,
3894 fragment_readahead: self.fragment_readahead,
3895 io_buffer_size: self.get_io_buffer_size(),
3896 with_row_id,
3897 with_row_address,
3898 with_row_last_updated_at_version,
3899 with_row_created_at_version,
3900 with_make_deletions_null,
3901 ordered_output: ordered,
3902 };
3903 Arc::new(LanceScanExec::new(
3904 self.dataset.clone(),
3905 fragments,
3906 range,
3907 projection,
3908 config,
3909 ))
3910 }
3911
3912 fn pushdown_scan(
3913 &self,
3914 make_deletions_null: bool,
3915 filter_plan: &ExprFilterPlan,
3916 ) -> Result<Arc<dyn ExecutionPlan>> {
3917 log::trace!("pushdown_scan");
3918
3919 let config = ScanConfig {
3920 batch_readahead: self.batch_readahead,
3921 fragment_readahead: self
3922 .fragment_readahead
3923 .unwrap_or(LEGACY_DEFAULT_FRAGMENT_READAHEAD),
3924 with_row_id: self.projection_plan.physical_projection.with_row_id,
3925 with_row_address: self.projection_plan.physical_projection.with_row_addr,
3926 make_deletions_null,
3927 ordered_output: self.ordered,
3928 file_reader_options: self
3929 .file_reader_options
3930 .clone()
3931 .or_else(|| self.dataset.file_reader_options.clone()),
3932 };
3933
3934 let fragments = if let Some(fragment) = self.fragments.as_ref() {
3935 Arc::new(fragment.clone())
3936 } else {
3937 self.dataset.fragments().clone()
3938 };
3939
3940 Ok(Arc::new(LancePushdownScanExec::try_new(
3941 self.dataset.clone(),
3942 fragments,
3943 Arc::new(self.projection_plan.physical_projection.to_bare_schema()),
3944 filter_plan.refine_expr.clone().unwrap(),
3945 config,
3946 )?))
3947 }
3948
3949 async fn flat_fts_filter(
3954 &self,
3955 input: Arc<dyn ExecutionPlan>,
3956 q: &FullTextSearchQuery,
3957 ) -> Result<Arc<dyn ExecutionPlan>> {
3958 let fts_query = if q.columns().is_empty() {
3959 let indexed_columns = fts_indexed_columns(self.dataset.clone()).await?;
3960 fill_fts_query_column(&q.query, &indexed_columns, false)?
3961 } else {
3962 q.query.clone()
3963 };
3964
3965 match &fts_query {
3966 FtsQuery::Match(match_query) => {
3967 let schema = Arc::new((input.schema()).try_with_column(SCORE_FIELD.clone())?);
3968
3969 let column = match_query
3970 .column
3971 .as_ref()
3972 .ok_or(Error::invalid_input(
3973 "the column must be specified in the query".to_string(),
3974 ))?
3975 .clone();
3976 let input = if schema.column_with_name(&column).is_none() {
3977 let projection = self
3978 .dataset
3979 .empty_projection()
3980 .union_column(&column, OnMissing::Error)?;
3981 self.take(input, projection)?
3982 } else {
3983 input
3984 };
3985
3986 Ok(Arc::new(FlatMatchFilterExec::new(
3987 input,
3988 self.dataset.clone(),
3989 match_query.clone(),
3990 q.params(),
3991 )))
3992 }
3993 _ => Err(Error::not_supported(
3994 "Only Match queries are supported currently when using FTS as a post-filter",
3995 )),
3996 }
3997 }
3998
3999 async fn fts_rerank(
4004 &self,
4005 input: Arc<dyn ExecutionPlan>,
4006 q: &FullTextSearchQuery,
4007 ) -> Result<Arc<dyn ExecutionPlan>> {
4008 let fts_query = if q.columns().is_empty() {
4009 let indexed_columns = fts_indexed_columns(self.dataset.clone()).await?;
4010 fill_fts_query_column(&q.query, &indexed_columns, false)?
4011 } else {
4012 q.query.clone()
4013 };
4014
4015 match &fts_query {
4016 FtsQuery::Match(match_query) => {
4017 let schema = Arc::new((input.schema()).try_with_column(SCORE_FIELD.clone())?);
4018
4019 let column = match_query
4020 .column
4021 .as_ref()
4022 .ok_or(Error::invalid_input(
4023 "the column must be specified in the query".to_string(),
4024 ))?
4025 .clone();
4026 let input = if schema.column_with_name(&column).is_none() {
4027 let projection = self
4028 .dataset
4029 .empty_projection()
4030 .union_column(&column, OnMissing::Error)?;
4031 self.take(input, projection)?
4032 } else {
4033 input
4034 };
4035
4036 Ok(Arc::new(FlatMatchQueryExec::new(
4037 self.dataset.clone(),
4038 match_query.clone(),
4039 q.params(),
4040 input,
4041 )))
4042 }
4043 _ => {
4044 let default_filter = ExprFilterPlan::default();
4045 let fts_plan = self.fts(&default_filter, q).await?;
4046
4047 let vector_row_id = Column::new_with_schema(ROW_ID, input.schema().as_ref())?;
4048 let fts_row_id = Column::new_with_schema(ROW_ID, fts_plan.schema().as_ref())?;
4049 let join = HashJoinExec::try_new(
4050 input,
4051 fts_plan,
4052 vec![(Arc::new(vector_row_id), Arc::new(fts_row_id))],
4053 None,
4054 &JoinType::Inner,
4055 None,
4056 PartitionMode::CollectLeft,
4057 NullEquality::NullEqualsNull,
4058 )?;
4059
4060 let schema = join.schema();
4061 let mut projection_exprs = Vec::new();
4062 let mut contain_rowid = false;
4063 for field in schema.fields() {
4064 if field.name() == ROW_ID {
4065 if contain_rowid {
4066 continue;
4067 }
4068 contain_rowid = true;
4069 }
4070 projection_exprs.push((
4071 Arc::new(Column::new_with_schema(field.name(), schema.as_ref())?)
4072 as Arc<dyn PhysicalExpr>,
4073 field.name().clone(),
4074 ));
4075 }
4076
4077 let projection_exec = ProjectionExec::try_new(projection_exprs, Arc::new(join))?;
4078 Ok(Arc::new(projection_exec))
4079 }
4080 }
4081 }
4082
4083 fn flat_knn(&self, input: Arc<dyn ExecutionPlan>, q: &Query) -> Result<Arc<dyn ExecutionPlan>> {
4085 let metric_type = match q.metric_type {
4087 Some(m) => m,
4088 None => {
4089 let (_, element_type) = get_vector_type(self.dataset.schema(), &q.column)?;
4090 default_distance_type_for(&element_type)
4091 }
4092 };
4093 let flat_dist = Arc::new(KNNVectorDistanceExec::try_new(
4094 input,
4095 &q.column,
4096 q.key.clone(),
4097 metric_type,
4098 )?);
4099
4100 let lower: Option<(Expr, Arc<dyn PhysicalExpr>)> = q
4101 .lower_bound
4102 .map(|v| -> Result<(Expr, Arc<dyn PhysicalExpr>)> {
4103 let logical = col(DIST_COL).gt_eq(lit(v));
4104 let schema = flat_dist.schema();
4105 let df_schema = DFSchema::try_from(schema)?;
4106 let physical = create_physical_expr(&logical, &df_schema, &ExecutionProps::new())?;
4107 Ok::<(Expr, Arc<dyn PhysicalExpr>), _>((logical, physical))
4108 })
4109 .transpose()?;
4110
4111 let upper = q
4112 .upper_bound
4113 .map(|v| -> Result<(Expr, Arc<dyn PhysicalExpr>)> {
4114 let logical = col(DIST_COL).lt(lit(v));
4115 let schema = flat_dist.schema();
4116 let df_schema = DFSchema::try_from(schema)?;
4117 let physical = create_physical_expr(&logical, &df_schema, &ExecutionProps::new())?;
4118 Ok::<(Expr, Arc<dyn PhysicalExpr>), _>((logical, physical))
4119 })
4120 .transpose()?;
4121
4122 let filter_expr = match (lower, upper) {
4123 (Some((llog, _)), Some((ulog, _))) => {
4124 let logical = llog.and(ulog);
4125 let schema = flat_dist.schema();
4126 let df_schema = DFSchema::try_from(schema)?;
4127 let physical = create_physical_expr(&logical, &df_schema, &ExecutionProps::new())?;
4128 Some((logical, physical))
4129 }
4130 (Some((llog, lphys)), None) => Some((llog, lphys)),
4131 (None, Some((ulog, uphys))) => Some((ulog, uphys)),
4132 (None, None) => None,
4133 };
4134
4135 let knn_plan: Arc<dyn ExecutionPlan> = if let Some(filter_expr) = filter_expr {
4136 Arc::new(LanceFilterExec::try_new(filter_expr.0, flat_dist)?)
4137 } else {
4138 flat_dist
4139 };
4140
4141 let sort = SortExec::new(
4143 [
4144 PhysicalSortExpr {
4145 expr: expressions::col(DIST_COL, knn_plan.schema().as_ref())?,
4146 options: SortOptions {
4147 descending: false,
4148 nulls_first: false,
4149 },
4150 },
4151 PhysicalSortExpr {
4152 expr: expressions::col(ROW_ID, knn_plan.schema().as_ref())?,
4153 options: SortOptions {
4154 descending: false,
4155 nulls_first: false,
4156 },
4157 },
4158 ]
4159 .into(),
4160 knn_plan,
4161 )
4162 .with_fetch(Some(q.k));
4163
4164 let logical_not_null = col(DIST_COL).is_not_null();
4165 let not_nulls = Arc::new(LanceFilterExec::try_new(logical_not_null, Arc::new(sort))?);
4166
4167 Ok(not_nulls)
4168 }
4169
4170 fn get_fragments_as_bitmap(&self) -> RoaringBitmap {
4171 if let Some(fragments) = &self.fragments {
4172 RoaringBitmap::from_iter(fragments.iter().map(|f| f.id as u32))
4173 } else {
4174 self.dataset.fragment_bitmap.as_ref().clone()
4175 }
4176 }
4177
4178 fn retain_target_fragments(&self, mut fragments: Vec<Fragment>) -> Vec<Fragment> {
4181 if let Some(target) = &self.fragments {
4182 let bitmap = RoaringBitmap::from_iter(target.iter().map(|f| f.id as u32));
4183 fragments.retain(|f| bitmap.contains(f.id as u32));
4184 }
4185 fragments
4186 }
4187
4188 fn get_indexed_frags(&self, index: &[IndexMetadata]) -> RoaringBitmap {
4189 let all_fragments = self.get_fragments_as_bitmap();
4190
4191 let mut all_indexed_frags = RoaringBitmap::new();
4192 for idx in index {
4193 if let Some(fragmap) = idx.fragment_bitmap.as_ref() {
4194 all_indexed_frags |= fragmap;
4195 } else {
4196 return all_fragments;
4199 }
4200 }
4201
4202 all_indexed_frags & all_fragments
4203 }
4204
4205 async fn ann(
4207 &self,
4208 q: &Query,
4209 index: &[IndexMetadata],
4210 filter_plan: &ExprFilterPlan,
4211 ) -> Result<Arc<dyn ExecutionPlan>> {
4212 let prefilter_source = self
4213 .prefilter_source(filter_plan, self.get_indexed_frags(index))
4214 .await?;
4215 let inner_fanout_search = new_knn_exec(self.dataset.clone(), index, q, prefilter_source)?;
4216 let sort_expr = PhysicalSortExpr {
4217 expr: expressions::col(DIST_COL, inner_fanout_search.schema().as_ref())?,
4218 options: SortOptions {
4219 descending: false,
4220 nulls_first: false,
4221 },
4222 };
4223 let sort_expr_row_id = PhysicalSortExpr {
4224 expr: expressions::col(ROW_ID, inner_fanout_search.schema().as_ref())?,
4225 options: SortOptions {
4226 descending: false,
4227 nulls_first: false,
4228 },
4229 };
4230 Ok(Arc::new(
4231 SortExec::new([sort_expr, sort_expr_row_id].into(), inner_fanout_search)
4232 .with_fetch(Some(q.k * q.refine_factor.unwrap_or(1) as usize)),
4233 ))
4234 }
4235
4236 async fn multivec_ann(
4238 &self,
4239 q: &Query,
4240 index: &[IndexMetadata],
4241 filter_plan: &ExprFilterPlan,
4242 ) -> Result<Arc<dyn ExecutionPlan>> {
4243 let over_fetch_factor = *DEFAULT_XTR_OVERFETCH;
4248
4249 let prefilter_source = self
4250 .prefilter_source(filter_plan, self.get_indexed_frags(index))
4251 .await?;
4252 let dim = get_vector_dim(self.dataset.schema(), &q.column)?;
4253
4254 let num_queries = q.key.len() / dim;
4255 let new_queries = (0..num_queries)
4256 .map(|i| q.key.slice(i * dim, dim))
4257 .map(|query_vec| {
4258 let mut new_query = q.clone();
4259 new_query.key = query_vec;
4260 new_query.refine_factor = Some(over_fetch_factor);
4264 new_query
4265 });
4266 let mut ann_nodes = Vec::with_capacity(new_queries.len());
4267 for query in new_queries {
4268 let ann_node = new_knn_exec(
4270 self.dataset.clone(),
4271 index,
4272 &query,
4273 prefilter_source.clone(),
4274 )?;
4275 let sort_expr = PhysicalSortExpr {
4276 expr: expressions::col(DIST_COL, ann_node.schema().as_ref())?,
4277 options: SortOptions {
4278 descending: false,
4279 nulls_first: false,
4280 },
4281 };
4282 let sort_expr_row_id = PhysicalSortExpr {
4283 expr: expressions::col(ROW_ID, ann_node.schema().as_ref())?,
4284 options: SortOptions {
4285 descending: false,
4286 nulls_first: false,
4287 },
4288 };
4289 let ann_node = Arc::new(
4290 SortExec::new([sort_expr, sort_expr_row_id].into(), ann_node)
4291 .with_fetch(Some(q.k * over_fetch_factor as usize)),
4292 );
4293 ann_nodes.push(ann_node as Arc<dyn ExecutionPlan>);
4294 }
4295
4296 let ann_node = Arc::new(MultivectorScoringExec::try_new(ann_nodes, q.clone())?);
4297
4298 let sort_expr = PhysicalSortExpr {
4299 expr: expressions::col(DIST_COL, ann_node.schema().as_ref())?,
4300 options: SortOptions {
4301 descending: false,
4302 nulls_first: false,
4303 },
4304 };
4305 let sort_expr_row_id = PhysicalSortExpr {
4306 expr: expressions::col(ROW_ID, ann_node.schema().as_ref())?,
4307 options: SortOptions {
4308 descending: false,
4309 nulls_first: false,
4310 },
4311 };
4312 let ann_node = Arc::new(
4313 SortExec::new([sort_expr, sort_expr_row_id].into(), ann_node)
4314 .with_fetch(Some(q.k * q.refine_factor.unwrap_or(1) as usize)),
4315 );
4316
4317 Ok(ann_node)
4318 }
4319
4320 async fn prefilter_source(
4325 &self,
4326 filter_plan: &ExprFilterPlan,
4327 required_frags: RoaringBitmap,
4328 ) -> Result<PreFilterSource> {
4329 if filter_plan.is_empty() && self.fragments.is_none() {
4330 log::trace!("no filter plan, no prefilter");
4331 return Ok(PreFilterSource::None);
4332 }
4333
4334 let fragments: Vec<Fragment> = self
4336 .dataset
4337 .manifest
4338 .fragments
4339 .iter()
4340 .filter(|f| required_frags.contains(f.id as u32))
4341 .cloned()
4342 .collect();
4343
4344 let fragments = Arc::new(self.retain_target_fragments(fragments));
4346
4347 if filter_plan.is_exact_index_search() && self.fragments.is_none() {
4353 let index_query = filter_plan.index_query.as_ref().expect_ok()?;
4354 let (_, missing_frags) = self
4355 .partition_frags_by_coverage(index_query, fragments.clone())
4356 .await?;
4357
4358 if missing_frags.is_empty() {
4359 log::trace!("prefilter entirely satisfied by exact index search");
4360 return Ok(PreFilterSource::ScalarIndexQuery(Arc::new(
4365 ScalarIndexExec::new(self.dataset.clone(), index_query.clone()),
4366 )));
4367 } else {
4368 log::trace!("exact index search did not cover all fragments");
4369 }
4370 }
4371
4372 log::trace!(
4374 "prefilter is a filtered read of {} fragments",
4375 fragments.len()
4376 );
4377 let PlannedFilteredScan { plan, .. } = self
4378 .filtered_read(
4379 filter_plan,
4380 self.dataset.empty_projection().with_row_id(),
4381 false,
4382 Some(fragments),
4383 None,
4384 true,
4385 )
4386 .await?;
4387 Ok(PreFilterSource::FilteredRowIds(plan))
4388 }
4389
4390 fn take(
4392 &self,
4393 input: Arc<dyn ExecutionPlan>,
4394 output_projection: Projection,
4395 ) -> Result<Arc<dyn ExecutionPlan>> {
4396 let coalesced = Arc::new(CoalesceBatchesExec::new(
4397 input.clone(),
4398 self.get_batch_size(),
4399 ));
4400 if let Some(take_plan) =
4401 TakeExec::try_new(self.dataset.clone(), coalesced, output_projection)?
4402 {
4403 Ok(Arc::new(take_plan))
4404 } else {
4405 Ok(input)
4407 }
4408 }
4409
4410 fn limit_node(&self, plan: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
4412 Arc::new(GlobalLimitExec::new(
4413 plan,
4414 *self.offset.as_ref().unwrap_or(&0) as usize,
4415 self.limit.map(|l| l as usize),
4416 ))
4417 }
4418
4419 #[instrument(level = "info", skip(self))]
4420 pub async fn analyze_plan(&self) -> Result<String> {
4421 let plan = self.create_plan().await?;
4422 analyze_plan(
4423 plan,
4424 LanceExecutionOptions {
4425 batch_size: self.batch_size,
4426 ..Default::default()
4427 },
4428 )
4429 .await
4430 }
4431
4432 #[instrument(level = "info", skip(self))]
4433 pub async fn explain_plan(&self, verbose: bool) -> Result<String> {
4434 let plan = self.create_plan().await?;
4435 let display = DisplayableExecutionPlan::new(plan.as_ref());
4436
4437 Ok(format!("{}", display.indent(verbose)))
4438 }
4439}
4440
4441async fn fts_indexed_columns(dataset: Arc<Dataset>) -> Result<Vec<String>> {
4444 let mut indexed_columns = Vec::new();
4445 for field in dataset.schema().fields_pre_order() {
4446 let is_string_field = match field.data_type() {
4448 DataType::Utf8 | DataType::LargeUtf8 => true,
4449 DataType::List(inner_field) | DataType::LargeList(inner_field) => {
4450 matches!(
4451 inner_field.data_type(),
4452 DataType::Utf8 | DataType::LargeUtf8
4453 )
4454 }
4455 _ => false,
4456 };
4457
4458 if is_string_field {
4459 let column_path =
4461 if let Some(ancestors) = dataset.schema().field_ancestry_by_id(field.id) {
4462 let field_refs: Vec<&str> = ancestors.iter().map(|f| f.name.as_str()).collect();
4463 format_field_path(&field_refs)
4464 } else {
4465 continue; };
4467
4468 let has_fts_index = dataset
4470 .load_scalar_index(
4471 IndexCriteria::default()
4472 .for_column(&column_path)
4473 .supports_fts(),
4474 )
4475 .await?
4476 .is_some();
4477
4478 if has_fts_index {
4479 indexed_columns.push(column_path);
4480 }
4481 }
4482 }
4483 Ok(indexed_columns)
4484}
4485
4486#[pin_project::pin_project]
4490pub struct DatasetRecordBatchStream {
4491 #[pin]
4492 exec_node: SendableRecordBatchStream,
4493 span: Span,
4494}
4495
4496impl DatasetRecordBatchStream {
4497 pub fn new(exec_node: SendableRecordBatchStream) -> Self {
4498 let schema = exec_node.schema();
4499 let adapter = SchemaAdapter::new(schema.clone());
4500 let exec_node = if SchemaAdapter::requires_logical_conversion(&schema) {
4501 adapter.to_logical_stream(exec_node)
4502 } else {
4503 exec_node
4504 };
4505
4506 let span = info_span!("DatasetRecordBatchStream");
4507 Self { exec_node, span }
4508 }
4509}
4510
4511impl RecordBatchStream for DatasetRecordBatchStream {
4512 fn schema(&self) -> SchemaRef {
4513 self.exec_node.schema()
4514 }
4515}
4516
4517impl Stream for DatasetRecordBatchStream {
4518 type Item = Result<RecordBatch>;
4519
4520 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
4521 let mut this = self.project();
4522 let _guard = this.span.enter();
4523 match this.exec_node.poll_next_unpin(cx) {
4524 Poll::Ready(result) => Poll::Ready(result.map(|r| Ok(r?))),
4525 Poll::Pending => Poll::Pending,
4526 }
4527 }
4528}
4529
4530impl From<DatasetRecordBatchStream> for SendableRecordBatchStream {
4531 fn from(stream: DatasetRecordBatchStream) -> Self {
4532 stream.exec_node
4533 }
4534}
4535
4536#[cfg(test)]
4537pub mod test_dataset {
4538
4539 use super::*;
4540
4541 use std::{collections::HashMap, vec};
4542
4543 use arrow_array::{
4544 ArrayRef, FixedSizeListArray, Int32Array, RecordBatch, RecordBatchIterator, StringArray,
4545 };
4546 use arrow_schema::{ArrowError, DataType};
4547 use lance_arrow::FixedSizeListArrayExt;
4548 use lance_core::utils::tempfile::TempStrDir;
4549 use lance_file::version::LanceFileVersion;
4550 use lance_index::{
4551 IndexType,
4552 scalar::{ScalarIndexParams, inverted::tokenizer::InvertedIndexParams},
4553 };
4554
4555 use crate::dataset::WriteParams;
4556 use crate::index::vector::VectorIndexParams;
4557
4558 pub struct TestVectorDataset {
4568 pub tmp_dir: TempStrDir,
4569 pub schema: Arc<ArrowSchema>,
4570 pub dataset: Dataset,
4571 dimension: u32,
4572 }
4573
4574 impl TestVectorDataset {
4575 pub async fn new(
4576 data_storage_version: LanceFileVersion,
4577 stable_row_ids: bool,
4578 ) -> Result<Self> {
4579 Self::new_with_dimension(data_storage_version, stable_row_ids, 32).await
4580 }
4581
4582 pub async fn new_with_dimension(
4583 data_storage_version: LanceFileVersion,
4584 stable_row_ids: bool,
4585 dimension: u32,
4586 ) -> Result<Self> {
4587 let path = TempStrDir::default();
4588
4589 let metadata: HashMap<String, String> =
4591 vec![("dataset".to_string(), "vector".to_string())]
4592 .into_iter()
4593 .collect();
4594
4595 let schema = Arc::new(ArrowSchema::new_with_metadata(
4596 vec![
4597 ArrowField::new("i", DataType::Int32, true),
4598 ArrowField::new("s", DataType::Utf8, true),
4599 ArrowField::new(
4600 "vec",
4601 DataType::FixedSizeList(
4602 Arc::new(ArrowField::new("item", DataType::Float32, true)),
4603 dimension as i32,
4604 ),
4605 true,
4606 ),
4607 ],
4608 metadata,
4609 ));
4610
4611 let batches: Vec<RecordBatch> = (0..5)
4612 .map(|i| {
4613 let vector_values: Float32Array =
4614 (0..dimension * 80).map(|v| v as f32).collect();
4615 let vectors =
4616 FixedSizeListArray::try_new_from_values(vector_values, dimension as i32)
4617 .unwrap();
4618 RecordBatch::try_new(
4619 schema.clone(),
4620 vec![
4621 Arc::new(Int32Array::from_iter_values(i * 80..(i + 1) * 80)),
4622 Arc::new(StringArray::from_iter_values(
4623 (i * 80..(i + 1) * 80).map(|v| format!("s-{}", v)),
4624 )),
4625 Arc::new(vectors),
4626 ],
4627 )
4628 })
4629 .collect::<std::result::Result<Vec<_>, ArrowError>>()?;
4630
4631 let params = WriteParams {
4632 max_rows_per_group: 10,
4633 max_rows_per_file: 200,
4634 data_storage_version: Some(data_storage_version),
4635 enable_stable_row_ids: stable_row_ids,
4636 ..Default::default()
4637 };
4638 let reader = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
4639
4640 let dataset = Dataset::write(reader, &path, Some(params)).await?;
4641
4642 Ok(Self {
4643 tmp_dir: path,
4644 schema,
4645 dataset,
4646 dimension,
4647 })
4648 }
4649
4650 pub async fn make_vector_index(&mut self) -> Result<()> {
4651 let params = VectorIndexParams::ivf_pq(2, 8, 2, MetricType::L2, 2);
4652 self.dataset
4653 .create_index(
4654 &["vec"],
4655 IndexType::Vector,
4656 Some("idx".to_string()),
4657 ¶ms,
4658 true,
4659 )
4660 .await?;
4661 Ok(())
4662 }
4663
4664 pub async fn make_scalar_index(&mut self) -> Result<()> {
4665 self.dataset
4666 .create_index(
4667 &["i"],
4668 IndexType::Scalar,
4669 None,
4670 &ScalarIndexParams::default(),
4671 true,
4672 )
4673 .await?;
4674 Ok(())
4675 }
4676
4677 pub async fn make_fts_index(&mut self) -> Result<()> {
4678 let params = InvertedIndexParams::default().with_position(true);
4679 self.dataset
4680 .create_index(&["s"], IndexType::Inverted, None, ¶ms, true)
4681 .await?;
4682 Ok(())
4683 }
4684
4685 pub async fn append_new_data(&mut self) -> Result<()> {
4686 self.append_data_with_range(400, 410).await
4687 }
4688
4689 pub async fn append_data_with_range(&mut self, start: i32, end: i32) -> Result<()> {
4690 let count = (end - start) as usize;
4691 let vector_values: Float32Array = (0..count)
4692 .flat_map(|i| vec![i as f32; self.dimension as usize].into_iter())
4693 .collect();
4694 let new_vectors =
4695 FixedSizeListArray::try_new_from_values(vector_values, self.dimension as i32)
4696 .unwrap();
4697 let new_data: Vec<ArrayRef> = vec![
4698 Arc::new(Int32Array::from_iter_values(start..end)),
4699 Arc::new(StringArray::from_iter_values(
4700 (start..end).map(|v| format!("s-{}", v)),
4701 )),
4702 Arc::new(new_vectors),
4703 ];
4704 let reader = RecordBatchIterator::new(
4705 vec![RecordBatch::try_new(self.schema.clone(), new_data).unwrap()]
4706 .into_iter()
4707 .map(Ok),
4708 self.schema.clone(),
4709 );
4710 self.dataset.append(reader, None).await?;
4711 Ok(())
4712 }
4713 }
4714}
4715
4716#[cfg(test)]
4717mod test {
4718
4719 use std::collections::BTreeSet;
4720 use std::time::{Duration, Instant};
4721 use std::vec;
4722
4723 use arrow::array::as_primitive_array;
4724 use arrow::datatypes::{Float64Type, Int32Type, Int64Type};
4725 use arrow_array::cast::AsArray;
4726 use arrow_array::types::{Float32Type, UInt64Type};
4727 use arrow_array::{
4728 ArrayRef, FixedSizeListArray, Float16Array, Int32Array, LargeStringArray, PrimitiveArray,
4729 RecordBatchIterator, StringArray, StructArray, UInt8Array,
4730 };
4731
4732 use arrow_ord::sort::sort_to_indices;
4733 use arrow_schema::Fields;
4734 use arrow_select::take;
4735 use datafusion::logical_expr::{col, lit};
4736 use half::f16;
4737 use lance_arrow::{FixedSizeListArrayExt, SchemaExt};
4738 use lance_core::utils::tempfile::TempStrDir;
4739 use lance_core::{ROW_CREATED_AT_VERSION, ROW_LAST_UPDATED_AT_VERSION};
4740 use lance_datagen::{
4741 ArrayGeneratorExt, BatchCount, ByteCount, Dimension, RowCount, array, gen_batch,
4742 };
4743 use lance_file::version::LanceFileVersion;
4744 use lance_index::optimize::OptimizeOptions;
4745 use lance_index::scalar::inverted::query::{MatchQuery, PhraseQuery};
4746 use lance_index::vector::hnsw::builder::HnswBuildParams;
4747 use lance_index::vector::ivf::IvfBuildParams;
4748 use lance_index::vector::pq::PQBuildParams;
4749 use lance_index::vector::sq::builder::SQBuildParams;
4750 use lance_index::{IndexType, scalar::ScalarIndexParams};
4751 use lance_io::assert_io_gt;
4752 use lance_io::object_store::ObjectStoreParams;
4753
4754 use lance_linalg::distance::DistanceType;
4755 use lance_testing::datagen::{BatchGenerator, IncrementingInt32, RandomVector};
4756 use object_store::throttle::ThrottleConfig;
4757 use rstest::rstest;
4758
4759 use super::*;
4760 use crate::dataset::WriteMode;
4761 use crate::dataset::WriteParams;
4762 use crate::dataset::optimize::{CompactionOptions, compact_files};
4763 use crate::dataset::scanner::test_dataset::TestVectorDataset;
4764 use crate::index::vector::{StageParams, VectorIndexParams};
4765 use crate::utils::test::{
4766 DatagenExt, FragmentCount, FragmentRowCount, ThrottledStoreWrapper, assert_plan_node_equals,
4767 };
4768
4769 #[test]
4770 fn test_env_var_parsing() {
4771 unsafe {
4775 std::env::set_var("LANCE_DEFAULT_BATCH_SIZE", "not_a_number");
4776 }
4777 let result = get_default_batch_size();
4778 assert_eq!(result, None, "Should return None for invalid batch size");
4779
4780 unsafe {
4782 std::env::set_var("LANCE_DEFAULT_BATCH_SIZE", "2048");
4783 }
4784 let result = get_default_batch_size();
4785 assert_eq!(result, Some(2048), "Should parse valid batch size");
4786
4787 unsafe {
4789 std::env::remove_var("LANCE_DEFAULT_BATCH_SIZE");
4790 }
4791 let result = get_default_batch_size();
4792 assert_eq!(result, None, "Should return None when env var is not set");
4793 }
4794
4795 #[test]
4796 fn test_parse_env_var() {
4797 let test_var = "LANCE_TEST_PARSE_ENV_VAR_USIZE";
4801
4802 unsafe {
4804 std::env::set_var(test_var, "12345");
4805 }
4806 let result: Option<usize> = parse_env_var(test_var, "Using default.");
4807 assert_eq!(result, Some(12345));
4808
4809 unsafe {
4811 std::env::set_var(test_var, "not_a_number");
4812 }
4813 let result: Option<usize> = parse_env_var(test_var, "Using default.");
4814 assert_eq!(result, None);
4815
4816 unsafe {
4818 std::env::remove_var(test_var);
4819 }
4820 let result: Option<usize> = parse_env_var(test_var, "Using default.");
4821 assert_eq!(result, None);
4822
4823 let test_var_u32 = "LANCE_TEST_PARSE_ENV_VAR_U32";
4825 unsafe {
4826 std::env::set_var(test_var_u32, "42");
4827 }
4828 let result: Option<u32> = parse_env_var(test_var_u32, "Using default value.");
4829 assert_eq!(result, Some(42));
4830
4831 unsafe {
4832 std::env::set_var(test_var_u32, "invalid");
4833 }
4834 let result: Option<u32> = parse_env_var(test_var_u32, "Using default value.");
4835 assert_eq!(result, None);
4836
4837 unsafe {
4838 std::env::remove_var(test_var_u32);
4839 }
4840
4841 let test_var_u64 = "LANCE_TEST_PARSE_ENV_VAR_U64";
4843 unsafe {
4844 std::env::set_var(test_var_u64, "9999999999");
4845 }
4846 let result: Option<u64> = parse_env_var(test_var_u64, "Using default value.");
4847 assert_eq!(result, Some(9999999999));
4848
4849 unsafe {
4850 std::env::set_var(test_var_u64, "-1");
4851 }
4852 let result: Option<u64> = parse_env_var(test_var_u64, "Using default value.");
4853 assert_eq!(result, None);
4854
4855 unsafe {
4856 std::env::remove_var(test_var_u64);
4857 }
4858 }
4859
4860 async fn make_binary_vector_dataset() -> Result<(TempStrDir, Dataset)> {
4861 let tmp_dir = TempStrDir::default();
4862 let dim = 4;
4863 let schema = Arc::new(ArrowSchema::new(vec![
4864 ArrowField::new("id", DataType::Int32, false),
4865 ArrowField::new(
4866 "bin",
4867 DataType::FixedSizeList(
4868 Arc::new(ArrowField::new("item", DataType::UInt8, true)),
4869 dim,
4870 ),
4871 false,
4872 ),
4873 ]));
4874
4875 let vectors = FixedSizeListArray::try_new_from_values(
4876 UInt8Array::from(vec![
4877 0b0000_1111u8,
4878 0,
4879 0,
4880 0, 0b0000_0011u8,
4882 0,
4883 0,
4884 0, 0u8,
4886 0,
4887 0,
4888 0,
4889 ]),
4890 dim,
4891 )?;
4892 let ids = Int32Array::from(vec![0, 1, 2]);
4893
4894 let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(ids), Arc::new(vectors)])?;
4895 let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
4896 Dataset::write(reader, &tmp_dir, None).await?;
4897 let dataset = Dataset::open(&tmp_dir).await?;
4898 Ok((tmp_dir, dataset))
4899 }
4900
4901 #[tokio::test]
4902 async fn test_batch_size() {
4903 let schema = Arc::new(ArrowSchema::new(vec![
4904 ArrowField::new("i", DataType::Int32, true),
4905 ArrowField::new("s", DataType::Utf8, true),
4906 ]));
4907
4908 let batches: Vec<RecordBatch> = (0..5)
4909 .map(|i| {
4910 RecordBatch::try_new(
4911 schema.clone(),
4912 vec![
4913 Arc::new(Int32Array::from_iter_values(i * 20..(i + 1) * 20)),
4914 Arc::new(StringArray::from_iter_values(
4915 (i * 20..(i + 1) * 20).map(|v| format!("s-{}", v)),
4916 )),
4917 ],
4918 )
4919 .unwrap()
4920 })
4921 .collect();
4922
4923 for use_filter in [false, true] {
4924 let test_dir = TempStrDir::default();
4925 let test_uri = &test_dir;
4926 let write_params = WriteParams {
4927 max_rows_per_file: 40,
4928 max_rows_per_group: 10,
4929 ..Default::default()
4930 };
4931 let batches =
4932 RecordBatchIterator::new(batches.clone().into_iter().map(Ok), schema.clone());
4933 Dataset::write(batches, test_uri, Some(write_params))
4934 .await
4935 .unwrap();
4936
4937 let dataset = Dataset::open(test_uri).await.unwrap();
4938 let mut builder = dataset.scan();
4939 builder.batch_size(8);
4940 if use_filter {
4941 builder.filter("i IS NOT NULL").unwrap();
4942 }
4943 let mut stream = builder.try_into_stream().await.unwrap();
4944 let mut rows_read = 0;
4945 while let Some(next) = stream.next().await {
4946 let next = next.unwrap();
4947 let expected = 8.min(100 - rows_read);
4948 assert_eq!(next.num_rows(), expected);
4949 rows_read += next.num_rows();
4950 }
4951 }
4952 }
4953
4954 #[tokio::test]
4955 async fn test_strict_batch_size() {
4956 let dataset = lance_datagen::gen_batch()
4957 .col("x", array::step::<Int32Type>())
4958 .anon_col(array::step::<Int64Type>())
4959 .into_ram_dataset(FragmentCount::from(7), FragmentRowCount::from(6))
4960 .await
4961 .unwrap();
4962
4963 let mut scan = dataset.scan();
4964 scan.batch_size(10)
4965 .strict_batch_size(true)
4966 .filter("x % 2 == 0")
4967 .unwrap();
4968
4969 let batches = scan
4970 .try_into_stream()
4971 .await
4972 .unwrap()
4973 .try_collect::<Vec<_>>()
4974 .await
4975 .unwrap();
4976
4977 let batch_sizes = batches.iter().map(|b| b.num_rows()).collect::<Vec<_>>();
4978 assert_eq!(batch_sizes, vec![10, 10, 1]);
4979 }
4980
4981 #[tokio::test]
4982 async fn test_column_not_exist() {
4983 let dataset = lance_datagen::gen_batch()
4984 .col("x", array::step::<Int32Type>())
4985 .into_ram_dataset(FragmentCount::from(7), FragmentRowCount::from(6))
4986 .await
4987 .unwrap();
4988
4989 let check_err_msg = |r: Result<DatasetRecordBatchStream>| {
4990 let Err(err) = r else {
4991 panic!(
4992 "Expected an error to be raised saying column y is not found but got no error"
4993 )
4994 };
4995
4996 assert!(
4997 err.to_string().contains("No field named y"),
4998 "Expected error to contain 'No field named y' but got {}",
4999 err
5000 );
5001 };
5002
5003 let mut scan = dataset.scan();
5004 scan.project(&["x", "y"]).unwrap();
5005 check_err_msg(scan.try_into_stream().await);
5006
5007 let mut scan = dataset.scan();
5008 scan.project(&["y"]).unwrap();
5009 check_err_msg(scan.try_into_stream().await);
5010
5011 let mut scan = dataset.scan();
5014 scan.project_with_transform(&[("foo", "1")]).unwrap();
5015 match scan.try_into_stream().await {
5016 Ok(_) => panic!("Expected an error to be raised saying not supported"),
5017 Err(e) => {
5018 assert!(
5019 e.to_string().contains("Received only dynamic expressions"),
5020 "Expected error to contain 'Received only dynamic expressions' but got {}",
5021 e
5022 );
5023 }
5024 }
5025 }
5026
5027 #[cfg(not(windows))]
5028 #[tokio::test]
5029 async fn test_local_object_store() {
5030 let schema = Arc::new(ArrowSchema::new(vec![
5031 ArrowField::new("i", DataType::Int32, true),
5032 ArrowField::new("s", DataType::Utf8, true),
5033 ]));
5034
5035 let batches: Vec<RecordBatch> = (0..5)
5036 .map(|i| {
5037 RecordBatch::try_new(
5038 schema.clone(),
5039 vec![
5040 Arc::new(Int32Array::from_iter_values(i * 20..(i + 1) * 20)),
5041 Arc::new(StringArray::from_iter_values(
5042 (i * 20..(i + 1) * 20).map(|v| format!("s-{}", v)),
5043 )),
5044 ],
5045 )
5046 .unwrap()
5047 })
5048 .collect();
5049
5050 let test_dir = TempStrDir::default();
5051 let test_uri = &test_dir;
5052 let write_params = WriteParams {
5053 max_rows_per_file: 40,
5054 max_rows_per_group: 10,
5055 ..Default::default()
5056 };
5057 let batches = RecordBatchIterator::new(batches.clone().into_iter().map(Ok), schema.clone());
5058 Dataset::write(batches, test_uri, Some(write_params))
5059 .await
5060 .unwrap();
5061
5062 let dataset = Dataset::open(&format!("file-object-store://{}", test_uri))
5063 .await
5064 .unwrap();
5065 let mut builder = dataset.scan();
5066 builder.batch_size(8);
5067 let mut stream = builder.try_into_stream().await.unwrap();
5068 let mut rows_read = 0;
5069 while let Some(next) = stream.next().await {
5070 let next = next.unwrap();
5071 let expected = 8.min(100 - rows_read);
5072 assert_eq!(next.num_rows(), expected);
5073 rows_read += next.num_rows();
5074 }
5075 }
5076
5077 #[tokio::test]
5078 async fn test_filter_parsing() -> Result<()> {
5079 let test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false).await?;
5080 let dataset = &test_ds.dataset;
5081
5082 let mut scan = dataset.scan();
5083 assert!(scan.filter.is_none());
5084
5085 scan.filter("i > 50")?;
5086 assert_eq!(scan.get_expr_filter().unwrap(), Some(col("i").gt(lit(50))));
5087
5088 for use_stats in [false, true] {
5089 let batches = scan
5090 .project(&["s"])?
5091 .use_stats(use_stats)
5092 .try_into_stream()
5093 .await?
5094 .try_collect::<Vec<_>>()
5095 .await?;
5096 let batch = concat_batches(&batches[0].schema(), &batches)?;
5097
5098 let expected_batch = RecordBatch::try_new(
5099 Arc::new(test_ds.schema.project(&[1])?),
5101 vec![Arc::new(StringArray::from_iter_values(
5102 (51..400).map(|v| format!("s-{}", v)),
5103 ))],
5104 )?;
5105 assert_eq!(batch, expected_batch);
5106 }
5107 Ok(())
5108 }
5109
5110 #[tokio::test]
5111 async fn test_scan_regexp_match_and_non_empty_captions() {
5112 let schema = Arc::new(ArrowSchema::new(vec![
5115 ArrowField::new("keywords", DataType::Utf8, true),
5116 ArrowField::new("natural_caption", DataType::Utf8, true),
5117 ArrowField::new("poetic_caption", DataType::Utf8, true),
5118 ]));
5119
5120 let batch = RecordBatch::try_new(
5121 schema.clone(),
5122 vec![
5123 Arc::new(StringArray::from(vec![
5124 Some("Liberty for all"),
5125 Some("peace"),
5126 Some("revolution now"),
5127 Some("Liberty"),
5128 Some("revolutionary"),
5129 Some("none"),
5130 ])) as ArrayRef,
5131 Arc::new(StringArray::from(vec![
5132 Some("a"),
5133 Some("b"),
5134 None,
5135 Some(""),
5136 Some("c"),
5137 Some("d"),
5138 ])) as ArrayRef,
5139 Arc::new(StringArray::from(vec![
5140 Some("x"),
5141 Some(""),
5142 Some("y"),
5143 Some("z"),
5144 None,
5145 Some("w"),
5146 ])) as ArrayRef,
5147 ],
5148 )
5149 .unwrap();
5150
5151 let reader = RecordBatchIterator::new(vec![Ok(batch.clone())], schema.clone());
5152 let dataset = Dataset::write(reader, "memory://", None).await.unwrap();
5153
5154 let mut scan = dataset.scan();
5155 scan.filter(
5156 "regexp_match(keywords, 'Liberty|revolution') AND \
5157 (natural_caption IS NOT NULL AND natural_caption <> '' AND \
5158 poetic_caption IS NOT NULL AND poetic_caption <> '')",
5159 )
5160 .unwrap();
5161
5162 let out = scan.try_into_batch().await.unwrap();
5163 assert_eq!(out.num_rows(), 1);
5164
5165 let out_keywords = out
5166 .column_by_name("keywords")
5167 .unwrap()
5168 .as_string::<i32>()
5169 .value(0);
5170 let out_nat = out
5171 .column_by_name("natural_caption")
5172 .unwrap()
5173 .as_string::<i32>()
5174 .value(0);
5175 let out_poetic = out
5176 .column_by_name("poetic_caption")
5177 .unwrap()
5178 .as_string::<i32>()
5179 .value(0);
5180
5181 assert_eq!(out_keywords, "Liberty for all");
5182 assert_eq!(out_nat, "a");
5183 assert_eq!(out_poetic, "x");
5184 }
5185
5186 #[tokio::test]
5187 async fn test_nested_projection() {
5188 let point_fields: Fields = vec![
5189 ArrowField::new("x", DataType::Float32, true),
5190 ArrowField::new("y", DataType::Float32, true),
5191 ]
5192 .into();
5193 let metadata_fields: Fields = vec![
5194 ArrowField::new("location", DataType::Struct(point_fields), true),
5195 ArrowField::new("age", DataType::Int32, true),
5196 ]
5197 .into();
5198 let metadata_field = ArrowField::new("metadata", DataType::Struct(metadata_fields), true);
5199 let schema = Arc::new(ArrowSchema::new(vec![
5200 metadata_field,
5201 ArrowField::new("idx", DataType::Int32, true),
5202 ]));
5203 let data = lance_datagen::rand(&schema)
5204 .into_ram_dataset(FragmentCount::from(7), FragmentRowCount::from(6))
5205 .await
5206 .unwrap();
5207
5208 let mut scan = data.scan();
5209 scan.project(&["metadata.location.x", "metadata.age"])
5210 .unwrap();
5211 let batch = scan.try_into_batch().await.unwrap();
5212
5213 assert_eq!(
5214 batch.schema().as_ref(),
5215 &ArrowSchema::new(vec![
5216 ArrowField::new("metadata.location.x", DataType::Float32, true),
5217 ArrowField::new("metadata.age", DataType::Int32, true),
5218 ])
5219 );
5220
5221 let take_schema = data.schema().project_by_ids(&[0, 2, 4], false);
5225
5226 let taken = data.take_rows(&[0, 5], take_schema).await.unwrap();
5227
5228 let part_point_fields = Fields::from(vec![ArrowField::new("x", DataType::Float32, true)]);
5230 let part_metadata_fields = Fields::from(vec![
5231 ArrowField::new("location", DataType::Struct(part_point_fields), true),
5232 ArrowField::new("age", DataType::Int32, true),
5233 ]);
5234 let part_schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
5235 "metadata",
5236 DataType::Struct(part_metadata_fields),
5237 true,
5238 )]));
5239
5240 assert_eq!(taken.schema(), part_schema);
5241 }
5242
5243 #[rstest]
5244 #[tokio::test]
5245 async fn test_limit(
5246 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5247 data_storage_version: LanceFileVersion,
5248 ) -> Result<()> {
5249 let test_ds = TestVectorDataset::new(data_storage_version, false).await?;
5250 let dataset = &test_ds.dataset;
5251
5252 let full_data = dataset.scan().try_into_batch().await?.slice(19, 2);
5253
5254 let actual = dataset
5255 .scan()
5256 .limit(Some(2), Some(19))?
5257 .try_into_batch()
5258 .await?;
5259
5260 assert_eq!(actual.num_rows(), 2);
5261 assert_eq!(actual, full_data);
5262 Ok(())
5263 }
5264
5265 #[test_log::test(tokio::test)]
5266 async fn test_limit_cancel() {
5267 let throttled = Arc::new(ThrottledStoreWrapper {
5276 config: ThrottleConfig {
5277 wait_get_per_call: Duration::from_secs(1),
5278 ..Default::default()
5279 },
5280 });
5281 let write_params = WriteParams {
5282 store_params: Some(ObjectStoreParams {
5283 object_store_wrapper: Some(throttled.clone()),
5284 ..Default::default()
5285 }),
5286 max_rows_per_file: 1,
5287 ..Default::default()
5288 };
5289
5290 let dataset = gen_batch()
5292 .col("i", array::step::<Int32Type>().with_random_nulls(0.1))
5293 .into_ram_dataset_with_params(
5294 FragmentCount::from(2000),
5295 FragmentRowCount::from(1),
5296 Some(write_params),
5297 )
5298 .await
5299 .unwrap();
5300
5301 let mut scan = dataset.scan();
5302 scan.filter("i IS NOT NULL").unwrap();
5303 scan.limit(Some(10), None).unwrap();
5304
5305 let start = Instant::now();
5306 scan.try_into_stream()
5307 .await
5308 .unwrap()
5309 .try_collect::<Vec<_>>()
5310 .await
5311 .unwrap();
5312 let duration = start.elapsed();
5313
5314 assert!(duration < Duration::from_secs(10));
5318 }
5319
5320 #[rstest]
5321 #[tokio::test]
5322 async fn test_knn_nodes(
5323 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5324 data_storage_version: LanceFileVersion,
5325 #[values(false, true)] stable_row_ids: bool,
5326 #[values(false, true)] build_index: bool,
5327 ) {
5328 let mut test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5329 .await
5330 .unwrap();
5331 if build_index {
5332 test_ds.make_vector_index().await.unwrap();
5333 }
5334 let dataset = &test_ds.dataset;
5335
5336 let mut scan = dataset.scan();
5337 let key: Float32Array = (32..64).map(|v| v as f32).collect();
5338 scan.nearest("vec", &key, 5).unwrap();
5339 scan.refine(5);
5340
5341 let batch = scan.try_into_batch().await.unwrap();
5342
5343 assert_eq!(batch.num_rows(), 5);
5344 assert_eq!(
5345 batch.schema().as_ref(),
5346 &ArrowSchema::new(vec![
5347 ArrowField::new("i", DataType::Int32, true),
5348 ArrowField::new("s", DataType::Utf8, true),
5349 ArrowField::new(
5350 "vec",
5351 DataType::FixedSizeList(
5352 Arc::new(ArrowField::new("item", DataType::Float32, true)),
5353 32,
5354 ),
5355 true,
5356 ),
5357 ArrowField::new(DIST_COL, DataType::Float32, true),
5358 ])
5359 .with_metadata([("dataset".into(), "vector".into())].into())
5360 );
5361
5362 let expected_i = BTreeSet::from_iter(vec![1, 81, 161, 241, 321]);
5363 let column_i = batch.column_by_name("i").unwrap();
5364 let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
5365 .values()
5366 .iter()
5367 .copied()
5368 .collect();
5369 assert_eq!(expected_i, actual_i);
5370 }
5371
5372 #[rstest]
5373 #[tokio::test]
5374 async fn test_can_project_distance() {
5375 let test_ds = TestVectorDataset::new(LanceFileVersion::Stable, true)
5376 .await
5377 .unwrap();
5378 let dataset = &test_ds.dataset;
5379
5380 let mut scan = dataset.scan();
5381 let key: Float32Array = (32..64).map(|v| v as f32).collect();
5382 scan.nearest("vec", &key, 5).unwrap();
5383 scan.refine(5);
5384 scan.project(&["_distance"]).unwrap();
5385
5386 let batch = scan.try_into_batch().await.unwrap();
5387
5388 assert_eq!(batch.num_rows(), 5);
5389 assert_eq!(batch.num_columns(), 1);
5390 }
5391
5392 #[rstest]
5393 #[tokio::test]
5394 async fn test_knn_with_new_data(
5395 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5396 data_storage_version: LanceFileVersion,
5397 #[values(false, true)] stable_row_ids: bool,
5398 ) {
5399 let mut test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5400 .await
5401 .unwrap();
5402 test_ds.make_vector_index().await.unwrap();
5403 test_ds.append_new_data().await.unwrap();
5404 let dataset = &test_ds.dataset;
5405
5406 let key: Float32Array = [0f32; 32].into_iter().collect();
5408 let k = 20;
5411
5412 #[derive(Debug)]
5413 struct TestCase {
5414 filter: Option<&'static str>,
5415 limit: Option<i64>,
5416 use_index: bool,
5417 }
5418
5419 let mut cases = vec![];
5420 for filter in [Some("i > 100"), None] {
5421 for limit in [None, Some(10)] {
5422 for use_index in [true, false] {
5423 cases.push(TestCase {
5424 filter,
5425 limit,
5426 use_index,
5427 });
5428 }
5429 }
5430 }
5431
5432 for case in cases {
5434 let mut scanner = dataset.scan();
5435 scanner
5436 .nearest("vec", &key, k)
5437 .unwrap()
5438 .limit(case.limit, None)
5439 .unwrap()
5440 .refine(3)
5441 .use_index(case.use_index);
5442 if let Some(filter) = case.filter {
5443 scanner.filter(filter).unwrap();
5444 }
5445
5446 let result = scanner
5447 .try_into_stream()
5448 .await
5449 .unwrap()
5450 .try_collect::<Vec<_>>()
5451 .await
5452 .unwrap();
5453 assert!(!result.is_empty());
5454 let result = concat_batches(&result[0].schema(), result.iter()).unwrap();
5455
5456 if case.filter.is_some() {
5457 let result_rows = result.num_rows();
5458 let expected_rows = case.limit.unwrap_or(k as i64) as usize;
5459 assert!(
5460 result_rows <= expected_rows,
5461 "Expected less than {} rows, got {}",
5462 expected_rows,
5463 result_rows
5464 );
5465 } else {
5466 assert_eq!(result.num_rows(), case.limit.unwrap_or(k as i64) as usize);
5468 }
5469
5470 assert_eq!(
5472 as_primitive_array::<Int32Type>(result.column(0).as_ref()).value(0),
5473 400
5474 );
5475 }
5476 }
5477
5478 #[rstest]
5479 #[tokio::test]
5480 async fn test_knn_with_prefilter(
5481 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5482 data_storage_version: LanceFileVersion,
5483 #[values(false, true)] stable_row_ids: bool,
5484 ) {
5485 let mut test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5486 .await
5487 .unwrap();
5488 test_ds.make_vector_index().await.unwrap();
5489 let dataset = &test_ds.dataset;
5490
5491 let mut scan = dataset.scan();
5492 let key: Float32Array = (32..64).map(|v| v as f32).collect();
5493 scan.filter("i > 100").unwrap();
5494 scan.prefilter(true);
5495 scan.project(&["i", "vec"]).unwrap();
5496 scan.nearest("vec", &key, 5).unwrap();
5497 scan.use_index(false);
5498
5499 let results = scan
5500 .try_into_stream()
5501 .await
5502 .unwrap()
5503 .try_collect::<Vec<_>>()
5504 .await
5505 .unwrap();
5506
5507 assert_eq!(results.len(), 1);
5508 let batch = &results[0];
5509
5510 assert_eq!(batch.num_rows(), 5);
5511 assert_eq!(
5512 batch.schema().as_ref(),
5513 &ArrowSchema::new(vec![
5514 ArrowField::new("i", DataType::Int32, true),
5515 ArrowField::new(
5516 "vec",
5517 DataType::FixedSizeList(
5518 Arc::new(ArrowField::new("item", DataType::Float32, true)),
5519 32,
5520 ),
5521 true,
5522 ),
5523 ArrowField::new(DIST_COL, DataType::Float32, true),
5524 ])
5525 .with_metadata([("dataset".into(), "vector".into())].into())
5526 );
5527
5528 let exact_i = BTreeSet::from_iter(vec![161, 241, 321]);
5530 let close_i = BTreeSet::from_iter(vec![161, 241, 321, 160, 162, 240, 242, 320, 322]);
5532 let column_i = batch.column_by_name("i").unwrap();
5533 let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
5534 .values()
5535 .iter()
5536 .copied()
5537 .collect();
5538 assert!(exact_i.is_subset(&actual_i));
5539 assert!(actual_i.is_subset(&close_i));
5540 }
5541
5542 #[rstest]
5543 #[tokio::test]
5544 async fn test_knn_filter_new_data(
5545 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5546 data_storage_version: LanceFileVersion,
5547 #[values(false, true)] stable_row_ids: bool,
5548 ) {
5549 let mut test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5553 .await
5554 .unwrap();
5555 test_ds.make_vector_index().await.unwrap();
5556 test_ds.append_new_data().await.unwrap();
5557 let dataset = &test_ds.dataset;
5558
5559 let key: Float32Array = [0f32; 32].into_iter().collect();
5561
5562 let mut query = dataset.scan();
5563 query.nearest("vec", &key, 20).unwrap();
5564
5565 let results = query
5567 .try_into_stream()
5568 .await
5569 .unwrap()
5570 .try_collect::<Vec<_>>()
5571 .await
5572 .unwrap();
5573
5574 let results_i = results[0]["i"]
5575 .as_primitive::<Int32Type>()
5576 .values()
5577 .iter()
5578 .copied()
5579 .collect::<BTreeSet<_>>();
5580
5581 assert!(results_i.contains(&400));
5582
5583 for prefilter in [false, true] {
5585 let mut query = dataset.scan();
5586 query
5587 .filter("i != 400")
5588 .unwrap()
5589 .prefilter(prefilter)
5590 .nearest("vec", &key, 20)
5591 .unwrap();
5592
5593 let results = query
5594 .try_into_stream()
5595 .await
5596 .unwrap()
5597 .try_collect::<Vec<_>>()
5598 .await
5599 .unwrap();
5600
5601 let results_i = results[0]["i"]
5602 .as_primitive::<Int32Type>()
5603 .values()
5604 .iter()
5605 .copied()
5606 .collect::<BTreeSet<_>>();
5607
5608 assert!(!results_i.contains(&400));
5609 }
5610 }
5611
5612 #[rstest]
5613 #[tokio::test]
5614 async fn test_knn_with_filter(
5615 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5616 data_storage_version: LanceFileVersion,
5617 #[values(false, true)] stable_row_ids: bool,
5618 ) {
5619 let test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5620 .await
5621 .unwrap();
5622 let dataset = &test_ds.dataset;
5623
5624 let mut scan = dataset.scan();
5625 let key: Float32Array = (32..64).map(|v| v as f32).collect();
5626 scan.nearest("vec", &key, 5).unwrap();
5627 scan.filter("i > 100").unwrap();
5628 scan.project(&["i", "vec"]).unwrap();
5629 scan.refine(5);
5630
5631 let results = scan
5632 .try_into_stream()
5633 .await
5634 .unwrap()
5635 .try_collect::<Vec<_>>()
5636 .await
5637 .unwrap();
5638
5639 assert_eq!(results.len(), 1);
5640 let batch = &results[0];
5641
5642 assert_eq!(batch.num_rows(), 3);
5643 assert_eq!(
5644 batch.schema().as_ref(),
5645 &ArrowSchema::new(vec![
5646 ArrowField::new("i", DataType::Int32, true),
5647 ArrowField::new(
5648 "vec",
5649 DataType::FixedSizeList(
5650 Arc::new(ArrowField::new("item", DataType::Float32, true)),
5651 32,
5652 ),
5653 true,
5654 ),
5655 ArrowField::new(DIST_COL, DataType::Float32, true),
5656 ])
5657 .with_metadata([("dataset".into(), "vector".into())].into())
5658 );
5659
5660 let expected_i = BTreeSet::from_iter(vec![161, 241, 321]);
5661 let column_i = batch.column_by_name("i").unwrap();
5662 let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
5663 .values()
5664 .iter()
5665 .copied()
5666 .collect();
5667 assert_eq!(expected_i, actual_i);
5668 }
5669
5670 #[rstest]
5671 #[tokio::test]
5672 async fn test_refine_factor(
5673 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5674 data_storage_version: LanceFileVersion,
5675 #[values(false, true)] stable_row_ids: bool,
5676 ) {
5677 let test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5678 .await
5679 .unwrap();
5680 let dataset = &test_ds.dataset;
5681
5682 let mut scan = dataset.scan();
5683 let key: Float32Array = (32..64).map(|v| v as f32).collect();
5684 scan.nearest("vec", &key, 5).unwrap();
5685 scan.refine(5);
5686
5687 let results = scan
5688 .try_into_stream()
5689 .await
5690 .unwrap()
5691 .try_collect::<Vec<_>>()
5692 .await
5693 .unwrap();
5694
5695 assert_eq!(results.len(), 1);
5696 let batch = &results[0];
5697
5698 assert_eq!(batch.num_rows(), 5);
5699 assert_eq!(
5700 batch.schema().as_ref(),
5701 &ArrowSchema::new(vec![
5702 ArrowField::new("i", DataType::Int32, true),
5703 ArrowField::new("s", DataType::Utf8, true),
5704 ArrowField::new(
5705 "vec",
5706 DataType::FixedSizeList(
5707 Arc::new(ArrowField::new("item", DataType::Float32, true)),
5708 32,
5709 ),
5710 true,
5711 ),
5712 ArrowField::new(DIST_COL, DataType::Float32, true),
5713 ])
5714 .with_metadata([("dataset".into(), "vector".into())].into())
5715 );
5716
5717 let expected_i = BTreeSet::from_iter(vec![1, 81, 161, 241, 321]);
5718 let column_i = batch.column_by_name("i").unwrap();
5719 let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
5720 .values()
5721 .iter()
5722 .copied()
5723 .collect();
5724 assert_eq!(expected_i, actual_i);
5725 }
5726
5727 #[tokio::test]
5728 async fn test_binary_vectors_default_to_hamming() {
5729 let (_tmp_dir, dataset) = make_binary_vector_dataset().await.unwrap();
5730 let query = UInt8Array::from(vec![0b0000_1111u8, 0, 0, 0]);
5731
5732 let mut scan = dataset.scan();
5733 scan.nearest("bin", &query, 3).unwrap();
5734
5735 assert_eq!(scan.nearest.as_ref().unwrap().metric_type, None);
5737
5738 let batch = scan.try_into_batch().await.unwrap();
5739 let ids = batch
5740 .column_by_name("id")
5741 .unwrap()
5742 .as_primitive::<Int32Type>()
5743 .values();
5744 assert_eq!(ids, &[0, 1, 2]);
5745 let distances = batch
5746 .column_by_name(DIST_COL)
5747 .unwrap()
5748 .as_primitive::<Float32Type>()
5749 .values();
5750 assert_eq!(distances, &[0.0, 2.0, 4.0]);
5751 }
5752
5753 #[tokio::test]
5754 async fn test_binary_vectors_invalid_distance_error() {
5755 let (_tmp_dir, dataset) = make_binary_vector_dataset().await.unwrap();
5756 let query = UInt8Array::from(vec![0b0000_1111u8, 0, 0, 0]);
5757
5758 let mut scan = dataset.scan();
5759 scan.nearest("bin", &query, 1).unwrap();
5760 scan.distance_metric(DistanceType::L2);
5761
5762 let err = scan.try_into_batch().await.unwrap_err();
5763 assert!(matches!(err, Error::InvalidInput { .. }));
5764 let message = err.to_string();
5765 assert!(
5766 message.contains("l2") && message.contains("UInt8"),
5767 "unexpected message: {message}"
5768 );
5769 }
5770
5771 #[tokio::test]
5775 async fn test_knn_metric_mismatch_falls_back_to_flat_search() {
5776 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, true)
5777 .await
5778 .unwrap();
5779 test_ds.make_vector_index().await.unwrap();
5781
5782 let dataset = &test_ds.dataset;
5783 let key: Float32Array = (32..64).map(|v| v as f32).collect();
5784
5785 let mut scan = dataset.scan();
5787 scan.nearest("vec", &key, 5).unwrap();
5788 scan.distance_metric(DistanceType::Dot);
5789
5790 let plan = scan.explain_plan(false).await.unwrap();
5792 assert!(
5793 !plan.contains("ANNSubIndex"),
5794 "Expected flat search, but got ANN index in plan:\n{}",
5795 plan
5796 );
5797 assert!(
5799 plan.contains("KNNVectorDistance") && plan.to_lowercase().contains("dot"),
5800 "Expected flat KNN with Dot metric in plan:\n{}",
5801 plan
5802 );
5803
5804 let dot_batch = dataset
5806 .scan()
5807 .nearest("vec", &key, 5)
5808 .unwrap()
5809 .distance_metric(DistanceType::Dot)
5810 .try_into_batch()
5811 .await
5812 .unwrap();
5813
5814 let l2_batch = dataset
5815 .scan()
5816 .nearest("vec", &key, 5)
5817 .unwrap()
5818 .distance_metric(DistanceType::L2)
5819 .try_into_batch()
5820 .await
5821 .unwrap();
5822
5823 let dot_distances: Vec<f32> = dot_batch
5824 .column_by_name(DIST_COL)
5825 .unwrap()
5826 .as_primitive::<Float32Type>()
5827 .values()
5828 .to_vec();
5829 let l2_distances: Vec<f32> = l2_batch
5830 .column_by_name(DIST_COL)
5831 .unwrap()
5832 .as_primitive::<Float32Type>()
5833 .values()
5834 .to_vec();
5835
5836 assert_ne!(dot_distances, l2_distances);
5838 }
5839
5840 #[tokio::test]
5843 async fn test_knn_no_metric_uses_index_metric() {
5844 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, true)
5845 .await
5846 .unwrap();
5847 test_ds.make_vector_index().await.unwrap();
5849
5850 let dataset = &test_ds.dataset;
5851 let key: Float32Array = (32..64).map(|v| v as f32).collect();
5852
5853 let mut scan = dataset.scan();
5855 scan.nearest("vec", &key, 5).unwrap();
5856 let plan = scan.explain_plan(false).await.unwrap();
5860 assert!(
5861 plan.contains("ANNSubIndex") && plan.to_lowercase().contains("l2"),
5862 "Expected ANN index with L2 metric in plan:\n{}",
5863 plan
5864 );
5865 }
5866
5867 #[rstest]
5868 #[tokio::test]
5869 async fn test_only_row_id(
5870 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5871 data_storage_version: LanceFileVersion,
5872 ) {
5873 let test_ds = TestVectorDataset::new(data_storage_version, false)
5874 .await
5875 .unwrap();
5876 let dataset = &test_ds.dataset;
5877
5878 let mut scan = dataset.scan();
5879 scan.project::<&str>(&[]).unwrap().with_row_id();
5880
5881 let batch = scan.try_into_batch().await.unwrap();
5882
5883 assert_eq!(batch.num_columns(), 1);
5884 assert_eq!(batch.num_rows(), 400);
5885 let expected_schema =
5886 ArrowSchema::new(vec![ArrowField::new(ROW_ID, DataType::UInt64, true)])
5887 .with_metadata(dataset.schema().metadata.clone());
5888 assert_eq!(batch.schema().as_ref(), &expected_schema,);
5889
5890 let expected_row_ids: Vec<u64> = (0..200_u64).chain((1 << 32)..((1 << 32) + 200)).collect();
5891 let actual_row_ids: Vec<u64> = as_primitive_array::<UInt64Type>(batch.column(0).as_ref())
5892 .values()
5893 .iter()
5894 .copied()
5895 .collect();
5896 assert_eq!(expected_row_ids, actual_row_ids);
5897 }
5898
5899 #[tokio::test]
5900 async fn test_scan_unordered_with_row_id() {
5901 let test_ds = TestVectorDataset::new(LanceFileVersion::Legacy, false)
5903 .await
5904 .unwrap();
5905 let dataset = &test_ds.dataset;
5906
5907 let mut scan = dataset.scan();
5908 scan.with_row_id();
5909
5910 let ordered_batches = scan
5911 .try_into_stream()
5912 .await
5913 .unwrap()
5914 .try_collect::<Vec<RecordBatch>>()
5915 .await
5916 .unwrap();
5917 assert!(ordered_batches.len() > 2);
5918 let ordered_batch =
5919 concat_batches(&ordered_batches[0].schema(), ordered_batches.iter()).unwrap();
5920
5921 scan.scan_in_order(false);
5923 for _ in 0..10 {
5924 let unordered_batches = scan
5925 .try_into_stream()
5926 .await
5927 .unwrap()
5928 .try_collect::<Vec<RecordBatch>>()
5929 .await
5930 .unwrap();
5931 let unordered_batch =
5932 concat_batches(&unordered_batches[0].schema(), unordered_batches.iter()).unwrap();
5933
5934 assert_eq!(ordered_batch.num_rows(), unordered_batch.num_rows());
5935
5936 if ordered_batch != unordered_batch {
5938 let sort_indices = sort_to_indices(&unordered_batch[ROW_ID], None, None).unwrap();
5939
5940 let ordered_i = ordered_batch["i"].clone();
5941 let sorted_i = take::take(&unordered_batch["i"], &sort_indices, None).unwrap();
5942
5943 assert_eq!(&ordered_i, &sorted_i);
5944
5945 break;
5946 }
5947 }
5948 }
5949
5950 #[tokio::test]
5951 async fn test_scan_with_wildcard() {
5952 let data = gen_batch()
5953 .col("x", array::step::<Float64Type>())
5954 .col("y", array::step::<Float64Type>())
5955 .into_ram_dataset(FragmentCount::from(1), FragmentRowCount::from(100))
5956 .await
5957 .unwrap();
5958
5959 let check_cols = async |projection: &[&str], expected_cols: &[&str]| {
5960 let mut scan = data.scan();
5961 scan.project(projection).unwrap();
5962 let stream = scan.try_into_stream().await.unwrap();
5963 let schema = stream.schema();
5964 let field_names = schema.field_names();
5965 assert_eq!(field_names, expected_cols);
5966 };
5967
5968 check_cols(&["*"], &["x", "y"]).await;
5969 check_cols(&["x", "y"], &["x", "y"]).await;
5970 check_cols(&["x"], &["x"]).await;
5971 check_cols(&["_rowid", "*"], &["_rowid", "x", "y"]).await;
5972 check_cols(&["*", "_rowid"], &["x", "y", "_rowid"]).await;
5973 check_cols(
5974 &["_rowid", "*", "_rowoffset"],
5975 &["_rowid", "x", "y", "_rowoffset"],
5976 )
5977 .await;
5978
5979 let check_exprs = async |exprs: &[&str], expected_cols: &[&str]| {
5980 let mut scan = data.scan();
5981 let projection = exprs
5982 .iter()
5983 .map(|e| (e.to_string(), e.to_string()))
5984 .collect::<Vec<_>>();
5985 scan.project_with_transform(&projection).unwrap();
5986 let stream = scan.try_into_stream().await.unwrap();
5987 let schema = stream.schema();
5988 let field_names = schema.field_names();
5989 assert_eq!(field_names, expected_cols);
5990 };
5991
5992 check_exprs(&["_rowid", "*", "x * 2"], &["_rowid", "x", "y", "x * 2"]).await;
5994
5995 let check_fails = |projection: &[&str]| {
5996 let mut scan = data.scan();
5997 assert!(scan.project(projection).is_err());
5998 };
5999
6000 check_fails(&["x", "*"]);
6002 check_fails(&["_rowid", "_rowid"]);
6003 }
6004
6005 #[rstest]
6006 #[tokio::test]
6007 async fn test_scan_order(
6008 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6009 data_storage_version: LanceFileVersion,
6010 ) {
6011 let test_dir = TempStrDir::default();
6012 let test_uri = &test_dir;
6013
6014 let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
6015 "i",
6016 DataType::Int32,
6017 true,
6018 )]));
6019
6020 let batch1 = RecordBatch::try_new(
6021 schema.clone(),
6022 vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5]))],
6023 )
6024 .unwrap();
6025
6026 let batch2 = RecordBatch::try_new(
6027 schema.clone(),
6028 vec![Arc::new(Int32Array::from(vec![6, 7, 8]))],
6029 )
6030 .unwrap();
6031
6032 let params = WriteParams {
6033 mode: WriteMode::Append,
6034 data_storage_version: Some(data_storage_version),
6035 ..Default::default()
6036 };
6037
6038 let write_batch = |batch: RecordBatch| async {
6039 let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
6040 Dataset::write(reader, test_uri, Some(params)).await
6041 };
6042
6043 write_batch.clone()(batch1.clone()).await.unwrap();
6044 write_batch(batch2.clone()).await.unwrap();
6045
6046 let dataset = Arc::new(Dataset::open(test_uri).await.unwrap());
6047 let fragment1 = dataset.get_fragment(0).unwrap().metadata().clone();
6048 let fragment2 = dataset.get_fragment(1).unwrap().metadata().clone();
6049
6050 let mut scanner = dataset.scan();
6052 scanner.with_fragments(vec![fragment1.clone(), fragment2.clone()]);
6053 let output = scanner
6054 .try_into_stream()
6055 .await
6056 .unwrap()
6057 .try_collect::<Vec<_>>()
6058 .await
6059 .unwrap();
6060 assert_eq!(output.len(), 2);
6061 assert_eq!(output[0], batch1);
6062 assert_eq!(output[1], batch2);
6063
6064 let mut scanner = dataset.scan();
6066 scanner.with_fragments(vec![fragment2, fragment1]);
6067 let output = scanner
6068 .try_into_stream()
6069 .await
6070 .unwrap()
6071 .try_collect::<Vec<_>>()
6072 .await
6073 .unwrap();
6074 assert_eq!(output.len(), 2);
6075 assert_eq!(output[0], batch2);
6076 assert_eq!(output[1], batch1);
6077 }
6078
6079 #[rstest]
6080 #[tokio::test]
6081 async fn test_scan_sort(
6082 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6083 data_storage_version: LanceFileVersion,
6084 ) {
6085 let test_dir = TempStrDir::default();
6086 let test_uri = &test_dir;
6087
6088 let data = gen_batch()
6089 .col("int", array::cycle::<Int32Type>(vec![5, 4, 1, 2, 3]))
6090 .col(
6091 "str",
6092 array::cycle_utf8_literals(&["a", "b", "c", "e", "d"]),
6093 );
6094
6095 let sorted_by_int = gen_batch()
6096 .col("int", array::cycle::<Int32Type>(vec![1, 2, 3, 4, 5]))
6097 .col(
6098 "str",
6099 array::cycle_utf8_literals(&["c", "e", "d", "b", "a"]),
6100 )
6101 .into_batch_rows(RowCount::from(5))
6102 .unwrap();
6103
6104 let sorted_by_str = gen_batch()
6105 .col("int", array::cycle::<Int32Type>(vec![5, 4, 1, 3, 2]))
6106 .col(
6107 "str",
6108 array::cycle_utf8_literals(&["a", "b", "c", "d", "e"]),
6109 )
6110 .into_batch_rows(RowCount::from(5))
6111 .unwrap();
6112
6113 Dataset::write(
6114 data.into_reader_rows(RowCount::from(5), BatchCount::from(1)),
6115 test_uri,
6116 Some(WriteParams {
6117 data_storage_version: Some(data_storage_version),
6118 ..Default::default()
6119 }),
6120 )
6121 .await
6122 .unwrap();
6123
6124 let dataset = Arc::new(Dataset::open(test_uri).await.unwrap());
6125
6126 let batches_by_int = dataset
6127 .scan()
6128 .order_by(Some(vec![ColumnOrdering::asc_nulls_first(
6129 "int".to_string(),
6130 )]))
6131 .unwrap()
6132 .try_into_stream()
6133 .await
6134 .unwrap()
6135 .try_collect::<Vec<_>>()
6136 .await
6137 .unwrap();
6138
6139 assert_eq!(batches_by_int[0], sorted_by_int);
6140
6141 let batches_by_str = dataset
6142 .scan()
6143 .order_by(Some(vec![ColumnOrdering::asc_nulls_first(
6144 "str".to_string(),
6145 )]))
6146 .unwrap()
6147 .try_into_stream()
6148 .await
6149 .unwrap()
6150 .try_collect::<Vec<_>>()
6151 .await
6152 .unwrap();
6153
6154 assert_eq!(batches_by_str[0], sorted_by_str);
6155
6156 dataset
6158 .scan()
6159 .order_by(Some(vec![]))
6160 .unwrap()
6161 .try_into_stream()
6162 .await
6163 .unwrap()
6164 .try_collect::<Vec<_>>()
6165 .await
6166 .unwrap();
6167 }
6168
6169 #[rstest]
6170 #[tokio::test]
6171 async fn test_sort_multi_columns(
6172 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6173 data_storage_version: LanceFileVersion,
6174 ) {
6175 let test_dir = TempStrDir::default();
6176 let test_uri = &test_dir;
6177
6178 let data = gen_batch()
6179 .col("int", array::cycle::<Int32Type>(vec![5, 5, 1, 1, 3]))
6180 .col(
6181 "float",
6182 array::cycle::<Float32Type>(vec![7.3, -f32::NAN, f32::NAN, 4.3, f32::INFINITY]),
6183 );
6184
6185 let sorted_by_int_then_float = gen_batch()
6186 .col("int", array::cycle::<Int32Type>(vec![1, 1, 3, 5, 5]))
6187 .col(
6188 "float",
6189 array::cycle::<Float32Type>(vec![4.3, f32::NAN, f32::INFINITY, -f32::NAN, 7.3]),
6191 )
6192 .into_batch_rows(RowCount::from(5))
6193 .unwrap();
6194
6195 Dataset::write(
6196 data.into_reader_rows(RowCount::from(5), BatchCount::from(1)),
6197 test_uri,
6198 Some(WriteParams {
6199 data_storage_version: Some(data_storage_version),
6200 ..Default::default()
6201 }),
6202 )
6203 .await
6204 .unwrap();
6205
6206 let dataset = Arc::new(Dataset::open(test_uri).await.unwrap());
6207
6208 let batches_by_int_then_float = dataset
6209 .scan()
6210 .order_by(Some(vec![
6211 ColumnOrdering::asc_nulls_first("int".to_string()),
6212 ColumnOrdering::asc_nulls_first("float".to_string()),
6213 ]))
6214 .unwrap()
6215 .try_into_stream()
6216 .await
6217 .unwrap()
6218 .try_collect::<Vec<_>>()
6219 .await
6220 .unwrap();
6221
6222 assert_eq!(batches_by_int_then_float[0], sorted_by_int_then_float);
6223 }
6224
6225 #[rstest]
6226 #[tokio::test]
6227 async fn test_ann_prefilter(
6228 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6229 data_storage_version: LanceFileVersion,
6230 #[values(false, true)] stable_row_ids: bool,
6231 #[values(
6232 VectorIndexParams::ivf_pq(2, 8, 2, MetricType::L2, 2),
6233 VectorIndexParams::with_ivf_hnsw_sq_params(
6234 MetricType::L2,
6235 IvfBuildParams::new(2),
6236 HnswBuildParams::default(),
6237 SQBuildParams::default()
6238 )
6239 )]
6240 index_params: VectorIndexParams,
6241 ) {
6242 use lance_arrow::{FixedSizeListArrayExt, fixed_size_list_type};
6243
6244 let test_dir = TempStrDir::default();
6245 let test_uri = &test_dir;
6246
6247 let schema = Arc::new(ArrowSchema::new(vec![
6248 ArrowField::new("filterable", DataType::Int32, true),
6249 ArrowField::new("vector", fixed_size_list_type(2, DataType::Float32), true),
6250 ]));
6251
6252 let vector_values = Float32Array::from_iter_values((0..600).map(|x| x as f32));
6253
6254 let batches = vec![
6255 RecordBatch::try_new(
6256 schema.clone(),
6257 vec![
6258 Arc::new(Int32Array::from_iter_values(0..300)),
6259 Arc::new(FixedSizeListArray::try_new_from_values(vector_values, 2).unwrap()),
6260 ],
6261 )
6262 .unwrap(),
6263 ];
6264
6265 let write_params = WriteParams {
6266 data_storage_version: Some(data_storage_version),
6267 max_rows_per_file: 300, enable_stable_row_ids: stable_row_ids,
6269 ..Default::default()
6270 };
6271 let batches = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6272 let mut dataset = Dataset::write(batches, test_uri, Some(write_params))
6273 .await
6274 .unwrap();
6275
6276 dataset
6277 .create_index(&["vector"], IndexType::Vector, None, &index_params, false)
6278 .await
6279 .unwrap();
6280
6281 let query_key = Arc::new(Float32Array::from_iter_values((0..2).map(|x| x as f32)));
6282 let mut scan = dataset.scan();
6283 scan.filter("filterable > 5").unwrap();
6284 scan.nearest("vector", query_key.as_ref(), 1).unwrap();
6285 scan.minimum_nprobes(100);
6286 scan.ef(100);
6287 scan.with_row_id();
6288
6289 let batches = scan
6290 .try_into_stream()
6291 .await
6292 .unwrap()
6293 .try_collect::<Vec<_>>()
6294 .await
6295 .unwrap();
6296
6297 assert_eq!(batches.len(), 0);
6298
6299 scan.prefilter(true);
6300
6301 let batches = scan
6302 .try_into_stream()
6303 .await
6304 .unwrap()
6305 .try_collect::<Vec<_>>()
6306 .await
6307 .unwrap();
6308 assert_eq!(batches.len(), 1);
6309
6310 let first_match = batches[0][ROW_ID].as_primitive::<UInt64Type>().values()[0];
6311
6312 assert_eq!(6, first_match);
6313 }
6314
6315 #[rstest]
6316 #[tokio::test]
6317 async fn test_filter_on_large_utf8(
6318 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6319 data_storage_version: LanceFileVersion,
6320 ) {
6321 let test_dir = TempStrDir::default();
6322 let test_uri = &test_dir;
6323
6324 let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
6325 "ls",
6326 DataType::LargeUtf8,
6327 true,
6328 )]));
6329
6330 let batches = vec![
6331 RecordBatch::try_new(
6332 schema.clone(),
6333 vec![Arc::new(LargeStringArray::from_iter_values(
6334 (0..10).map(|v| format!("s-{}", v)),
6335 ))],
6336 )
6337 .unwrap(),
6338 ];
6339
6340 let write_params = WriteParams {
6341 data_storage_version: Some(data_storage_version),
6342 ..Default::default()
6343 };
6344 let batches = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6345 Dataset::write(batches, test_uri, Some(write_params))
6346 .await
6347 .unwrap();
6348
6349 let dataset = Dataset::open(test_uri).await.unwrap();
6350 let mut scan = dataset.scan();
6351 scan.filter("ls = 's-8'").unwrap();
6352
6353 let batches = scan
6354 .try_into_stream()
6355 .await
6356 .unwrap()
6357 .try_collect::<Vec<_>>()
6358 .await
6359 .unwrap();
6360 let batch = &batches[0];
6361
6362 let expected = RecordBatch::try_new(
6363 schema.clone(),
6364 vec![Arc::new(LargeStringArray::from_iter_values(
6365 (8..9).map(|v| format!("s-{}", v)),
6366 ))],
6367 )
6368 .unwrap();
6369
6370 assert_eq!(batch, &expected);
6371 }
6372
6373 #[rstest]
6374 #[tokio::test]
6375 async fn test_filter_with_regex(
6376 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6377 data_storage_version: LanceFileVersion,
6378 ) {
6379 let test_dir = TempStrDir::default();
6380 let test_uri = &test_dir;
6381
6382 let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
6383 "ls",
6384 DataType::Utf8,
6385 true,
6386 )]));
6387
6388 let batches = vec![
6389 RecordBatch::try_new(
6390 schema.clone(),
6391 vec![Arc::new(StringArray::from_iter_values(
6392 (0..20).map(|v| format!("s-{}", v)),
6393 ))],
6394 )
6395 .unwrap(),
6396 ];
6397
6398 let write_params = WriteParams {
6399 data_storage_version: Some(data_storage_version),
6400 ..Default::default()
6401 };
6402 let batches = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6403 Dataset::write(batches, test_uri, Some(write_params))
6404 .await
6405 .unwrap();
6406
6407 let dataset = Dataset::open(test_uri).await.unwrap();
6408 let mut scan = dataset.scan();
6409 scan.filter("regexp_match(ls, 's-1.')").unwrap();
6410
6411 let stream = scan.try_into_stream().await.unwrap();
6412 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
6413 let batch = &batches[0];
6414
6415 let expected = RecordBatch::try_new(
6416 schema.clone(),
6417 vec![Arc::new(StringArray::from_iter_values(
6418 (10..=19).map(|v| format!("s-{}", v)),
6419 ))],
6420 )
6421 .unwrap();
6422
6423 assert_eq!(batch, &expected);
6424 }
6425
6426 #[tokio::test]
6427 async fn test_filter_proj_bug() {
6428 let struct_i_field = ArrowField::new("i", DataType::Int32, true);
6429 let struct_o_field = ArrowField::new("o", DataType::Utf8, true);
6430 let schema = Arc::new(ArrowSchema::new(vec![
6431 ArrowField::new(
6432 "struct",
6433 DataType::Struct(vec![struct_i_field.clone(), struct_o_field.clone()].into()),
6434 true,
6435 ),
6436 ArrowField::new("s", DataType::Utf8, true),
6437 ]));
6438
6439 let input_batches: Vec<RecordBatch> = (0..5)
6440 .map(|i| {
6441 let struct_i_arr: Arc<Int32Array> =
6442 Arc::new(Int32Array::from_iter_values(i * 20..(i + 1) * 20));
6443 let struct_o_arr: Arc<StringArray> = Arc::new(StringArray::from_iter_values(
6444 (i * 20..(i + 1) * 20).map(|v| format!("o-{:02}", v)),
6445 ));
6446 RecordBatch::try_new(
6447 schema.clone(),
6448 vec![
6449 Arc::new(StructArray::from(vec![
6450 (Arc::new(struct_i_field.clone()), struct_i_arr as ArrayRef),
6451 (Arc::new(struct_o_field.clone()), struct_o_arr as ArrayRef),
6452 ])),
6453 Arc::new(StringArray::from_iter_values(
6454 (i * 20..(i + 1) * 20).map(|v| format!("s-{}", v)),
6455 )),
6456 ],
6457 )
6458 .unwrap()
6459 })
6460 .collect();
6461 let batches =
6462 RecordBatchIterator::new(input_batches.clone().into_iter().map(Ok), schema.clone());
6463 let test_dir = TempStrDir::default();
6464 let test_uri = &test_dir;
6465 let write_params = WriteParams {
6466 max_rows_per_file: 40,
6467 max_rows_per_group: 10,
6468 data_storage_version: Some(LanceFileVersion::Legacy),
6469 ..Default::default()
6470 };
6471 Dataset::write(batches, test_uri, Some(write_params))
6472 .await
6473 .unwrap();
6474
6475 let dataset = Dataset::open(test_uri).await.unwrap();
6476 let batches = dataset
6477 .scan()
6478 .filter("struct.i >= 20")
6479 .unwrap()
6480 .try_into_stream()
6481 .await
6482 .unwrap()
6483 .try_collect::<Vec<_>>()
6484 .await
6485 .unwrap();
6486 let batch = concat_batches(&batches[0].schema(), &batches).unwrap();
6487
6488 let expected_batch = concat_batches(&schema, &input_batches.as_slice()[1..]).unwrap();
6489 assert_eq!(batch, expected_batch);
6490
6491 let batches = dataset
6493 .scan()
6494 .filter("struct.o >= 'o-20'")
6495 .unwrap()
6496 .try_into_stream()
6497 .await
6498 .unwrap()
6499 .try_collect::<Vec<_>>()
6500 .await
6501 .unwrap();
6502 let batch = concat_batches(&batches[0].schema(), &batches).unwrap();
6503 assert_eq!(batch, expected_batch);
6504
6505 let batches = dataset
6507 .scan()
6508 .project(vec!["struct"].as_slice())
6509 .unwrap()
6510 .try_into_stream()
6511 .await
6512 .unwrap()
6513 .try_collect::<Vec<_>>()
6514 .await
6515 .unwrap();
6516 concat_batches(&batches[0].schema(), &batches).unwrap();
6517 }
6518
6519 #[rstest]
6520 #[tokio::test]
6521 async fn test_ann_with_deletion(
6522 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6523 data_storage_version: LanceFileVersion,
6524 #[values(false, true)] stable_row_ids: bool,
6525 ) {
6526 let vec_params = vec![
6527 VectorIndexParams::ivf_pq(4, 8, 2, MetricType::L2, 2),
6530 ];
6531 for params in vec_params {
6532 use lance_arrow::FixedSizeListArrayExt;
6533
6534 let test_dir = TempStrDir::default();
6535 let test_uri = &test_dir;
6536
6537 let schema = Arc::new(ArrowSchema::new(vec![
6539 ArrowField::new("i", DataType::Int32, true),
6540 ArrowField::new(
6541 "vec",
6542 DataType::FixedSizeList(
6543 Arc::new(ArrowField::new("item", DataType::Float32, true)),
6544 32,
6545 ),
6546 true,
6547 ),
6548 ]));
6549
6550 let vector_values: Float32Array =
6552 (0..32 * 512).map(|v| (v / 32) as f32 + 1.0).collect();
6553 let vectors = FixedSizeListArray::try_new_from_values(vector_values, 32).unwrap();
6554
6555 let batches = vec![
6556 RecordBatch::try_new(
6557 schema.clone(),
6558 vec![
6559 Arc::new(Int32Array::from_iter_values(0..512)),
6560 Arc::new(vectors.clone()),
6561 ],
6562 )
6563 .unwrap(),
6564 ];
6565
6566 let reader = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6567 let mut dataset = Dataset::write(
6568 reader,
6569 test_uri,
6570 Some(WriteParams {
6571 data_storage_version: Some(data_storage_version),
6572 enable_stable_row_ids: stable_row_ids,
6573 ..Default::default()
6574 }),
6575 )
6576 .await
6577 .unwrap();
6578
6579 assert_eq!(dataset.index_cache_entry_count().await, 0);
6580 dataset
6581 .create_index(
6582 &["vec"],
6583 IndexType::Vector,
6584 Some("idx".to_string()),
6585 ¶ms,
6586 true,
6587 )
6588 .await
6589 .unwrap();
6590
6591 let mut scan = dataset.scan();
6592 let key: Float32Array = (0..32).map(|_v| 1.0_f32).collect();
6594 scan.nearest("vec", &key, 5).unwrap();
6595 scan.refine(100);
6596 scan.minimum_nprobes(100);
6597
6598 assert_eq!(
6599 dataset.index_cache_entry_count().await,
6600 2, );
6602 let results = scan
6603 .try_into_stream()
6604 .await
6605 .unwrap()
6606 .try_collect::<Vec<_>>()
6607 .await
6608 .unwrap();
6609
6610 assert_eq!(
6611 dataset.index_cache_entry_count().await,
6612 5 + dataset.versions().await.unwrap().len()
6613 );
6614 assert_eq!(results.len(), 1);
6615 let batch = &results[0];
6616
6617 let expected_i = BTreeSet::from_iter(vec![0, 1, 2, 3, 4]);
6618 let column_i = batch.column_by_name("i").unwrap();
6619 let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
6620 .values()
6621 .iter()
6622 .copied()
6623 .collect();
6624 assert_eq!(expected_i, actual_i);
6625
6626 dataset.delete("i = 1").await.unwrap();
6629 let mut scan = dataset.scan();
6630 scan.nearest("vec", &key, 5).unwrap();
6631 scan.refine(100);
6632 scan.minimum_nprobes(100);
6633
6634 let results = scan
6635 .try_into_stream()
6636 .await
6637 .unwrap()
6638 .try_collect::<Vec<_>>()
6639 .await
6640 .unwrap();
6641
6642 assert_eq!(results.len(), 1);
6643 let batch = &results[0];
6644
6645 let expected_i = BTreeSet::from_iter(vec![0, 2, 3, 4, 5]);
6647 let column_i = batch.column_by_name("i").unwrap();
6648 let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
6649 .values()
6650 .iter()
6651 .copied()
6652 .collect();
6653 assert_eq!(expected_i, actual_i);
6654
6655 let batches = vec![
6658 RecordBatch::try_new(
6659 schema.clone(),
6660 vec![
6661 Arc::new(Int32Array::from_iter_values(512..1024)),
6662 Arc::new(vectors),
6663 ],
6664 )
6665 .unwrap(),
6666 ];
6667
6668 let reader = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6669 let mut dataset = Dataset::write(
6670 reader,
6671 test_uri,
6672 Some(WriteParams {
6673 mode: WriteMode::Append,
6674 data_storage_version: Some(data_storage_version),
6675 ..Default::default()
6676 }),
6677 )
6678 .await
6679 .unwrap();
6680 dataset
6681 .create_index(
6682 &["vec"],
6683 IndexType::Vector,
6684 Some("idx".to_string()),
6685 ¶ms,
6686 true,
6687 )
6688 .await
6689 .unwrap();
6690
6691 dataset.delete("i < 512").await.unwrap();
6692
6693 let mut scan = dataset.scan();
6694 scan.nearest("vec", &key, 5).unwrap();
6695 scan.refine(100);
6696 scan.minimum_nprobes(100);
6697
6698 let results = scan
6699 .try_into_stream()
6700 .await
6701 .unwrap()
6702 .try_collect::<Vec<_>>()
6703 .await
6704 .unwrap();
6705
6706 assert_eq!(results.len(), 1);
6707 let batch = &results[0];
6708
6709 let expected_i = BTreeSet::from_iter(vec![512, 513, 514, 515, 516]);
6711 let column_i = batch.column_by_name("i").unwrap();
6712 let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
6713 .values()
6714 .iter()
6715 .copied()
6716 .collect();
6717 assert_eq!(expected_i, actual_i);
6718 }
6719 }
6720
6721 #[tokio::test]
6722 async fn test_projection_order() {
6723 let vec_params = VectorIndexParams::ivf_pq(4, 8, 2, MetricType::L2, 2);
6724 let mut data = gen_batch()
6725 .col("vec", array::rand_vec::<Float32Type>(Dimension::from(4)))
6726 .col("text", array::rand_utf8(ByteCount::from(10), false))
6727 .into_ram_dataset(FragmentCount::from(3), FragmentRowCount::from(100))
6728 .await
6729 .unwrap();
6730 data.create_index(&["vec"], IndexType::Vector, None, &vec_params, true)
6731 .await
6732 .unwrap();
6733
6734 let mut scan = data.scan();
6735 scan.nearest("vec", &Float32Array::from(vec![1.0, 1.0, 1.0, 1.0]), 5)
6736 .unwrap();
6737 scan.with_row_id().project(&["text"]).unwrap();
6738
6739 let results = scan
6740 .try_into_stream()
6741 .await
6742 .unwrap()
6743 .try_collect::<Vec<_>>()
6744 .await
6745 .unwrap();
6746
6747 assert_eq!(
6748 results[0].schema().field_names(),
6749 vec!["text", "_distance", "_rowid"]
6750 );
6751 }
6752
6753 #[rstest]
6754 #[tokio::test]
6755 async fn test_count_rows_with_filter(
6756 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6757 data_storage_version: LanceFileVersion,
6758 ) {
6759 let test_dir = TempStrDir::default();
6760 let test_uri = &test_dir;
6761 let mut data_gen = BatchGenerator::new().col(Box::new(
6762 IncrementingInt32::new().named("Filter_me".to_owned()),
6763 ));
6764 Dataset::write(
6765 data_gen.batch(32),
6766 test_uri,
6767 Some(WriteParams {
6768 data_storage_version: Some(data_storage_version),
6769 ..Default::default()
6770 }),
6771 )
6772 .await
6773 .unwrap();
6774
6775 let dataset = Dataset::open(test_uri).await.unwrap();
6776 assert_eq!(32, dataset.count_rows(None).await.unwrap());
6777 assert_eq!(
6778 16,
6779 dataset
6780 .count_rows(Some("`Filter_me` > 15".to_string()))
6781 .await
6782 .unwrap()
6783 );
6784 }
6785
6786 #[rstest]
6787 #[tokio::test]
6788 async fn test_dynamic_projection(
6789 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6790 data_storage_version: LanceFileVersion,
6791 ) {
6792 let test_dir = TempStrDir::default();
6793 let test_uri = &test_dir;
6794 let mut data_gen =
6795 BatchGenerator::new().col(Box::new(IncrementingInt32::new().named("i".to_owned())));
6796 Dataset::write(
6797 data_gen.batch(32),
6798 test_uri,
6799 Some(WriteParams {
6800 data_storage_version: Some(data_storage_version),
6801 ..Default::default()
6802 }),
6803 )
6804 .await
6805 .unwrap();
6806
6807 let dataset = Dataset::open(test_uri).await.unwrap();
6808 assert_eq!(dataset.count_rows(None).await.unwrap(), 32);
6809
6810 let mut scanner = dataset.scan();
6811
6812 let scan_res = scanner
6813 .project_with_transform(&[("bool", "i > 15")])
6814 .unwrap()
6815 .try_into_batch()
6816 .await
6817 .unwrap();
6818
6819 assert_eq!(1, scan_res.num_columns());
6820
6821 let bool_col = scan_res
6822 .column_by_name("bool")
6823 .expect("bool column should exist");
6824 let bool_arr = bool_col.as_boolean();
6825 for i in 0..32 {
6826 if i > 15 {
6827 assert!(bool_arr.value(i));
6828 } else {
6829 assert!(!bool_arr.value(i));
6830 }
6831 }
6832 }
6833
6834 #[rstest]
6835 #[tokio::test]
6836 async fn test_column_casting_function(
6837 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6838 data_storage_version: LanceFileVersion,
6839 ) {
6840 let test_dir = TempStrDir::default();
6841 let test_uri = &test_dir;
6842 let mut data_gen =
6843 BatchGenerator::new().col(Box::new(RandomVector::new().named("vec".to_owned())));
6844 Dataset::write(
6845 data_gen.batch(32),
6846 test_uri,
6847 Some(WriteParams {
6848 data_storage_version: Some(data_storage_version),
6849 ..Default::default()
6850 }),
6851 )
6852 .await
6853 .unwrap();
6854
6855 let dataset = Dataset::open(test_uri).await.unwrap();
6856 assert_eq!(dataset.count_rows(None).await.unwrap(), 32);
6857
6858 let mut scanner = dataset.scan();
6859
6860 let scan_res = scanner
6861 .project_with_transform(&[("f16", "_cast_list_f16(vec)")])
6862 .unwrap()
6863 .try_into_batch()
6864 .await
6865 .unwrap();
6866
6867 assert_eq!(1, scan_res.num_columns());
6868 assert_eq!(32, scan_res.num_rows());
6869 assert_eq!("f16", scan_res.schema().field(0).name());
6870
6871 let mut scanner = dataset.scan();
6872 let scan_res_original = scanner
6873 .project(&["vec"])
6874 .unwrap()
6875 .try_into_batch()
6876 .await
6877 .unwrap();
6878
6879 let f32_col: &Float32Array = scan_res_original
6880 .column_by_name("vec")
6881 .unwrap()
6882 .as_fixed_size_list()
6883 .values()
6884 .as_primitive();
6885 let f16_col: &Float16Array = scan_res
6886 .column_by_name("f16")
6887 .unwrap()
6888 .as_fixed_size_list()
6889 .values()
6890 .as_primitive();
6891
6892 for (f32_val, f16_val) in f32_col.iter().zip(f16_col.iter()) {
6893 let f32_val = f32_val.unwrap();
6894 let f16_val = f16_val.unwrap();
6895 assert_eq!(f16::from_f32(f32_val), f16_val);
6896 }
6897 }
6898
6899 struct ScalarIndexTestFixture {
6900 _test_dir: TempStrDir,
6901 dataset: Dataset,
6902 sample_query: Arc<dyn Array>,
6903 delete_query: Arc<dyn Array>,
6904 original_version: u64,
6906 compact_version: u64,
6908 append_version: u64,
6910 updated_version: u64,
6912 delete_version: u64,
6914 append_then_delete_version: u64,
6916 }
6917
6918 #[derive(Debug, PartialEq)]
6919 struct ScalarTestParams {
6920 use_index: bool,
6921 use_projection: bool,
6922 use_deleted_data: bool,
6923 use_new_data: bool,
6924 with_row_id: bool,
6925 use_compaction: bool,
6926 use_updated: bool,
6927 }
6928
6929 impl ScalarIndexTestFixture {
6930 async fn new(data_storage_version: LanceFileVersion, use_stable_row_ids: bool) -> Self {
6931 let test_dir = TempStrDir::default();
6932 let test_uri = &test_dir;
6933
6934 let data = gen_batch()
6940 .col(
6941 "vector",
6942 array::rand_vec::<Float32Type>(Dimension::from(32)),
6943 )
6944 .col("indexed", array::step::<Int32Type>())
6945 .col("not_indexed", array::step::<Int32Type>())
6946 .into_batch_rows(RowCount::from(1000))
6947 .unwrap();
6948
6949 let mut dataset = Dataset::write(
6951 RecordBatchIterator::new(vec![Ok(data.clone())], data.schema().clone()),
6952 test_uri,
6953 Some(WriteParams {
6954 max_rows_per_file: 500,
6955 data_storage_version: Some(data_storage_version),
6956 enable_stable_row_ids: use_stable_row_ids,
6957 ..Default::default()
6958 }),
6959 )
6960 .await
6961 .unwrap();
6962
6963 dataset
6964 .create_index(
6965 &["vector"],
6966 IndexType::Vector,
6967 None,
6968 &VectorIndexParams::ivf_pq(2, 8, 2, MetricType::L2, 2),
6969 false,
6970 )
6971 .await
6972 .unwrap();
6973
6974 dataset
6975 .create_index(
6976 &["indexed"],
6977 IndexType::Scalar,
6978 None,
6979 &ScalarIndexParams::default(),
6980 false,
6981 )
6982 .await
6983 .unwrap();
6984
6985 let original_version = dataset.version().version;
6986 let sample_query = data["vector"].as_fixed_size_list().value(50);
6987 let delete_query = data["vector"].as_fixed_size_list().value(75);
6988
6989 let new_indexed =
6994 arrow_arith::numeric::add(&data["indexed"], &Int32Array::new_scalar(1000)).unwrap();
6995 let new_not_indexed =
6996 arrow_arith::numeric::add(&data["indexed"], &Int32Array::new_scalar(1000)).unwrap();
6997 let append_data = RecordBatch::try_new(
6998 data.schema(),
6999 vec![data["vector"].clone(), new_indexed, new_not_indexed],
7000 )
7001 .unwrap();
7002
7003 dataset
7004 .append(
7005 RecordBatchIterator::new(vec![Ok(append_data)], data.schema()),
7006 Some(WriteParams {
7007 data_storage_version: Some(data_storage_version),
7008 ..Default::default()
7009 }),
7010 )
7011 .await
7012 .unwrap();
7013
7014 let append_version = dataset.version().version;
7015
7016 dataset
7019 .optimize_indices(&OptimizeOptions::merge(1))
7020 .await
7021 .unwrap();
7022 let updated_version = dataset.version().version;
7023
7024 dataset.checkout_version(append_version).await.unwrap();
7027 dataset.restore().await.unwrap();
7028
7029 dataset.delete("not_indexed = 75").await.unwrap();
7030
7031 let append_then_delete_version = dataset.version().version;
7032
7033 let mut dataset = dataset.checkout_version(original_version).await.unwrap();
7036 dataset.restore().await.unwrap();
7037
7038 dataset.delete("not_indexed = 75").await.unwrap();
7039
7040 let delete_version = dataset.version().version;
7041
7042 compact_files(&mut dataset, CompactionOptions::default(), None)
7044 .await
7045 .unwrap();
7046 let compact_version = dataset.version().version;
7047 dataset.checkout_version(original_version).await.unwrap();
7048 dataset.restore().await.unwrap();
7049
7050 Self {
7051 _test_dir: test_dir,
7052 dataset,
7053 sample_query,
7054 delete_query,
7055 original_version,
7056 compact_version,
7057 append_version,
7058 updated_version,
7059 delete_version,
7060 append_then_delete_version,
7061 }
7062 }
7063
7064 fn sample_query(&self) -> &PrimitiveArray<Float32Type> {
7065 self.sample_query.as_primitive::<Float32Type>()
7066 }
7067
7068 fn delete_query(&self) -> &PrimitiveArray<Float32Type> {
7069 self.delete_query.as_primitive::<Float32Type>()
7070 }
7071
7072 async fn get_dataset(&self, params: &ScalarTestParams) -> Dataset {
7073 let version = if params.use_compaction {
7074 if params.use_deleted_data || params.use_new_data || params.use_updated {
7076 panic!(
7077 "There is no test data combining new/deleted/updated data with compaction"
7078 );
7079 } else {
7080 self.compact_version
7081 }
7082 } else if params.use_updated {
7083 if params.use_deleted_data || params.use_new_data || params.use_compaction {
7085 panic!(
7086 "There is no test data combining updated data with new/deleted/compaction"
7087 );
7088 } else {
7089 self.updated_version
7090 }
7091 } else {
7092 match (params.use_new_data, params.use_deleted_data) {
7093 (false, false) => self.original_version,
7094 (false, true) => self.delete_version,
7095 (true, false) => self.append_version,
7096 (true, true) => self.append_then_delete_version,
7097 }
7098 };
7099 self.dataset.checkout_version(version).await.unwrap()
7100 }
7101
7102 async fn run_query(
7103 &self,
7104 query: &str,
7105 vector: Option<&PrimitiveArray<Float32Type>>,
7106 params: &ScalarTestParams,
7107 ) -> (String, RecordBatch) {
7108 let dataset = self.get_dataset(params).await;
7109 let mut scan = dataset.scan();
7110 if let Some(vector) = vector {
7111 scan.nearest("vector", vector, 10).unwrap();
7112 }
7113 if params.use_projection {
7114 scan.project(&["indexed"]).unwrap();
7115 }
7116 if params.with_row_id {
7117 scan.with_row_id();
7118 }
7119 scan.scan_in_order(true);
7120 scan.use_index(params.use_index);
7121 scan.filter(query).unwrap();
7122 scan.prefilter(true);
7123
7124 let plan = scan.explain_plan(true).await.unwrap();
7125 let batch = scan.try_into_batch().await.unwrap();
7126
7127 if params.use_projection {
7128 let mut expected_columns = 1;
7130 if vector.is_some() {
7131 expected_columns += 1;
7133 }
7134 if params.with_row_id {
7135 expected_columns += 1;
7136 }
7137 assert_eq!(batch.num_columns(), expected_columns);
7138 } else {
7139 let mut expected_columns = 3;
7140 if vector.is_some() {
7141 expected_columns += 1;
7143 }
7144 if params.with_row_id {
7145 expected_columns += 1;
7146 }
7147 assert_eq!(batch.num_columns(), expected_columns);
7149 }
7150
7151 (plan, batch)
7152 }
7153
7154 fn assert_none<F: Fn(i32) -> bool>(
7155 &self,
7156 batch: &RecordBatch,
7157 predicate: F,
7158 message: &str,
7159 ) {
7160 let indexed = batch["indexed"].as_primitive::<Int32Type>();
7161 if indexed.iter().map(|val| val.unwrap()).any(predicate) {
7162 panic!("{}", message);
7163 }
7164 }
7165
7166 fn assert_one<F: Fn(i32) -> bool>(&self, batch: &RecordBatch, predicate: F, message: &str) {
7167 let indexed = batch["indexed"].as_primitive::<Int32Type>();
7168 if !indexed.iter().map(|val| val.unwrap()).any(predicate) {
7169 panic!("{}", message);
7170 }
7171 }
7172
7173 async fn check_vector_scalar_indexed_and_refine(&self, params: &ScalarTestParams) {
7174 let (query_plan, batch) = self
7175 .run_query(
7176 "indexed != 50 AND ((not_indexed < 100) OR (not_indexed >= 1000 AND not_indexed < 1100))",
7177 Some(self.sample_query()),
7178 params,
7179 )
7180 .await;
7181 if self.dataset.is_legacy_storage() {
7183 assert!(query_plan.contains("MaterializeIndex"));
7184 }
7185 self.assert_none(
7187 &batch,
7188 |val| val == 50,
7189 "The query contained 50 even though it was filtered",
7190 );
7191 if !params.use_new_data {
7192 self.assert_none(
7194 &batch,
7195 |val| (100..1000).contains(&val) || (val >= 1100),
7196 "The non-indexed refine filter was not applied",
7197 );
7198 }
7199
7200 if params.use_new_data || params.use_updated {
7202 self.assert_one(
7203 &batch,
7204 |val| val == 1050,
7205 "The query did not contain 1050 from the new data",
7206 );
7207 }
7208 }
7209
7210 async fn check_vector_scalar_indexed_only(&self, params: &ScalarTestParams) {
7211 let (query_plan, batch) = self
7212 .run_query("indexed != 50", Some(self.sample_query()), params)
7213 .await;
7214 if self.dataset.is_legacy_storage() {
7215 if params.use_index {
7216 assert!(query_plan.contains("ScalarIndexQuery"));
7219 } else {
7220 assert!(query_plan.contains("MaterializeIndex"));
7222 }
7223 }
7224 self.assert_none(
7226 &batch,
7227 |val| val == 50,
7228 "The query contained 50 even though it was filtered",
7229 );
7230 if params.use_new_data {
7232 self.assert_one(
7233 &batch,
7234 |val| val == 1050,
7235 "The query did not contain 1050 from the new data",
7236 );
7237 if !params.use_new_data {
7238 let (_, batch) = self
7240 .run_query("indexed == 1050", Some(self.sample_query()), params)
7241 .await;
7242 assert_eq!(batch.num_rows(), 1);
7243 }
7244 }
7245 if params.use_deleted_data {
7246 let (_, batch) = self
7247 .run_query("indexed == 75", Some(self.delete_query()), params)
7248 .await;
7249 if !params.use_new_data {
7250 assert_eq!(batch.num_rows(), 0);
7251 }
7252 }
7253 }
7254
7255 async fn check_vector_queries(&self, params: &ScalarTestParams) {
7256 self.check_vector_scalar_indexed_only(params).await;
7257 self.check_vector_scalar_indexed_and_refine(params).await;
7258 }
7259
7260 async fn check_simple_indexed_only(&self, params: &ScalarTestParams) {
7261 let (query_plan, batch) = self.run_query("indexed != 50", None, params).await;
7262 if self.dataset.is_legacy_storage() {
7264 assert!(query_plan.contains("MaterializeIndex"));
7265 } else {
7266 assert!(query_plan.contains("LanceRead"));
7267 }
7268 self.assert_none(
7270 &batch,
7271 |val| val == 50,
7272 "The query contained 50 even though it was filtered",
7273 );
7274 let mut expected_num_rows = if params.use_new_data || params.use_updated {
7275 1999
7276 } else {
7277 999
7278 };
7279 if params.use_deleted_data || params.use_compaction {
7280 expected_num_rows -= 1;
7281 }
7282 assert_eq!(batch.num_rows(), expected_num_rows);
7283
7284 if params.use_new_data || params.use_updated {
7286 let (_, batch) = self.run_query("indexed == 1050", None, params).await;
7287 assert_eq!(batch.num_rows(), 1);
7288 }
7289
7290 if params.use_deleted_data || params.use_compaction {
7292 let (_, batch) = self.run_query("indexed == 75", None, params).await;
7293 assert_eq!(batch.num_rows(), 0);
7294 }
7295 }
7296
7297 async fn check_simple_indexed_and_refine(&self, params: &ScalarTestParams) {
7298 let (query_plan, batch) = self.run_query(
7299 "indexed != 50 AND ((not_indexed < 100) OR (not_indexed >= 1000 AND not_indexed < 1100))",
7300 None,
7301 params
7302 ).await;
7303 if self.dataset.is_legacy_storage() {
7305 assert!(query_plan.contains("MaterializeIndex"));
7306 } else {
7307 assert!(query_plan.contains("LanceRead"));
7308 }
7309 self.assert_none(
7311 &batch,
7312 |val| val == 50,
7313 "The query contained 50 even though it was filtered",
7314 );
7315 self.assert_none(
7317 &batch,
7318 |val| (100..1000).contains(&val) || (val >= 1100),
7319 "The non-indexed refine filter was not applied",
7320 );
7321
7322 let mut expected_num_rows = if params.use_new_data || params.use_updated {
7323 199
7324 } else {
7325 99
7326 };
7327 if params.use_deleted_data || params.use_compaction {
7328 expected_num_rows -= 1;
7329 }
7330 assert_eq!(batch.num_rows(), expected_num_rows);
7331 }
7332
7333 async fn check_simple_queries(&self, params: &ScalarTestParams) {
7334 self.check_simple_indexed_only(params).await;
7335 self.check_simple_indexed_and_refine(params).await;
7336 }
7337 }
7338
7339 #[rstest]
7343 #[tokio::test]
7344 async fn test_secondary_index_scans(
7345 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
7346 data_storage_version: LanceFileVersion,
7347 #[values(false, true)] use_stable_row_ids: bool,
7348 ) {
7349 let fixture = Box::pin(ScalarIndexTestFixture::new(
7350 data_storage_version,
7351 use_stable_row_ids,
7352 ))
7353 .await;
7354
7355 for use_index in [false, true] {
7356 for use_projection in [false, true] {
7357 for use_deleted_data in [false, true] {
7358 for use_new_data in [false, true] {
7359 let compaction_choices =
7364 if use_deleted_data || use_new_data || use_stable_row_ids {
7365 vec![false]
7366 } else {
7367 vec![false, true]
7368 };
7369 for use_compaction in compaction_choices {
7370 let updated_choices =
7371 if use_deleted_data || use_new_data || use_compaction {
7372 vec![false]
7373 } else {
7374 vec![false, true]
7375 };
7376 for use_updated in updated_choices {
7377 for with_row_id in [false, true] {
7378 let params = ScalarTestParams {
7379 use_index,
7380 use_projection,
7381 use_deleted_data,
7382 use_new_data,
7383 with_row_id,
7384 use_compaction,
7385 use_updated,
7386 };
7387 fixture.check_vector_queries(¶ms).await;
7388 fixture.check_simple_queries(¶ms).await;
7389 }
7390 }
7391 }
7392 }
7393 }
7394 }
7395 }
7396 }
7397
7398 #[tokio::test]
7399 async fn can_filter_row_id() {
7400 let dataset = lance_datagen::gen_batch()
7401 .col("x", array::step::<Int32Type>())
7402 .into_ram_dataset(FragmentCount::from(1), FragmentRowCount::from(1000))
7403 .await
7404 .unwrap();
7405
7406 let mut scan = dataset.scan();
7407 scan.with_row_id();
7408 scan.project::<&str>(&[]).unwrap();
7409 scan.filter("_rowid == 50").unwrap();
7410 let batch = scan.try_into_batch().await.unwrap();
7411 assert_eq!(batch.num_rows(), 1);
7412 assert_eq!(batch.column(0).as_primitive::<UInt64Type>().values()[0], 50);
7413 }
7414
7415 #[rstest]
7416 #[tokio::test]
7417 async fn test_index_take_batch_size() {
7418 let fixture = Box::pin(ScalarIndexTestFixture::new(LanceFileVersion::Stable, false)).await;
7419 let stream = fixture
7420 .dataset
7421 .scan()
7422 .filter("indexed > 0")
7423 .unwrap()
7424 .batch_size(16)
7425 .try_into_stream()
7426 .await
7427 .unwrap();
7428 let batches = stream.collect::<Vec<_>>().await;
7429 assert_eq!(batches.len(), 1000_usize.div_ceil(16));
7430 }
7431
7432 async fn assert_plan_equals(
7436 dataset: &Dataset,
7437 plan: impl Fn(&mut Scanner) -> Result<&mut Scanner>,
7438 expected: &str,
7439 ) -> Result<()> {
7440 let mut scan = dataset.scan();
7441 plan(&mut scan)?;
7442 let exec_plan = scan.create_plan().await?;
7443 assert_plan_node_equals(exec_plan, expected).await
7444 }
7445
7446 #[tokio::test]
7447 async fn test_inexact_scalar_index_plans() {
7448 let data = gen_batch()
7449 .col("ngram", array::rand_utf8(ByteCount::from(5), false))
7450 .col("exact", array::rand_type(&DataType::UInt32))
7451 .col("no_index", array::rand_type(&DataType::UInt32))
7452 .into_reader_rows(RowCount::from(1000), BatchCount::from(5));
7453
7454 let mut dataset = Dataset::write(data, "memory://test", None).await.unwrap();
7455 dataset
7456 .create_index(
7457 &["ngram"],
7458 IndexType::NGram,
7459 None,
7460 &ScalarIndexParams::default(),
7461 true,
7462 )
7463 .await
7464 .unwrap();
7465 dataset
7466 .create_index(
7467 &["exact"],
7468 IndexType::BTree,
7469 None,
7470 &ScalarIndexParams::default(),
7471 true,
7472 )
7473 .await
7474 .unwrap();
7475
7476 assert_plan_equals(
7478 &dataset,
7479 |scanner| scanner.filter("contains(ngram, 'test string')"),
7480 "LanceRead: uri=..., projection=[ngram, exact, no_index], num_fragments=1, \
7481 range_before=None, range_after=None, row_id=false, row_addr=false, \
7482 full_filter=contains(ngram, Utf8(\"test string\")), refine_filter=--
7483 ScalarIndexQuery: query=[contains(ngram, Utf8(\"test string\"))]@ngram_idx",
7484 )
7485 .await
7486 .unwrap();
7487
7488 assert_plan_equals(
7490 &dataset,
7491 |scanner| scanner.filter("contains(ngram, 'test string') and exact < 50"),
7492 "LanceRead: uri=..., projection=[ngram, exact, no_index], num_fragments=1, \
7493 range_before=None, range_after=None, row_id=false, row_addr=false, \
7494 full_filter=contains(ngram, Utf8(\"test string\")) AND exact < UInt32(50), \
7495 refine_filter=--
7496 ScalarIndexQuery: query=AND([contains(ngram, Utf8(\"test string\"))]@ngram_idx,[exact < 50]@exact_idx)",
7497 )
7498 .await
7499 .unwrap();
7500
7501 assert_plan_equals(
7503 &dataset,
7504 |scanner| {
7505 scanner.filter("contains(ngram, 'test string') and exact < 50 AND no_index > 100")
7506 },
7507 "ProjectionExec: expr=[ngram@0 as ngram, exact@1 as exact, no_index@2 as no_index]
7508 LanceRead: uri=..., projection=[ngram, exact, no_index], num_fragments=1, range_before=None, \
7509 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), \
7510 refine_filter=no_index > UInt32(100)
7511 ScalarIndexQuery: query=AND([contains(ngram, Utf8(\"test string\"))]@ngram_idx,[exact < 50]@exact_idx)",
7512 )
7513 .await
7514 .unwrap();
7515 }
7516
7517 #[tokio::test]
7518 async fn test_like_prefix_with_btree_index() {
7519 let data = gen_batch()
7522 .col(
7523 "name",
7524 array::cycle_utf8_literals(&[
7525 "apple",
7526 "application",
7527 "app",
7528 "banana",
7529 "band",
7530 "testns1",
7531 "testns2",
7532 "test",
7533 "testing",
7534 "zoo",
7535 ]),
7536 )
7537 .col("id", array::step::<Int32Type>())
7538 .into_reader_rows(RowCount::from(100), BatchCount::from(1));
7539
7540 let mut dataset = Dataset::write(data, "memory://test_like", None)
7541 .await
7542 .unwrap();
7543
7544 dataset
7546 .create_index(
7547 &["name"],
7548 IndexType::BTree,
7549 None,
7550 &ScalarIndexParams::default(),
7551 true,
7552 )
7553 .await
7554 .unwrap();
7555
7556 assert_plan_equals(
7558 &dataset,
7559 |scanner| scanner.filter("name LIKE 'app%'"),
7560 "LanceRead: uri=..., projection=[name, id], num_fragments=1, \
7561 range_before=None, range_after=None, row_id=false, row_addr=false, \
7562 full_filter=name LIKE Utf8(\"app%\"), refine_filter=--
7563 ScalarIndexQuery: query=[name LIKE 'app%']@name_idx",
7564 )
7565 .await
7566 .unwrap();
7567
7568 let results = dataset
7570 .scan()
7571 .filter("name LIKE 'app%'")
7572 .unwrap()
7573 .try_into_batch()
7574 .await
7575 .unwrap();
7576 let names: Vec<&str> = results
7577 .column_by_name("name")
7578 .unwrap()
7579 .as_any()
7580 .downcast_ref::<StringArray>()
7581 .unwrap()
7582 .iter()
7583 .map(|s| s.unwrap())
7584 .collect();
7585 assert!(names.iter().all(|n| n.starts_with("app")));
7587 assert!(!names.is_empty());
7588
7589 assert_plan_equals(
7592 &dataset,
7593 |scanner| scanner.filter("starts_with(name, 'ban')"),
7594 "LanceRead: uri=..., projection=[name, id], num_fragments=1, \
7595 range_before=None, range_after=None, row_id=false, row_addr=false, \
7596 full_filter=name LIKE Utf8(\"ban%\"), refine_filter=--
7597 ScalarIndexQuery: query=[name LIKE 'ban%']@name_idx",
7598 )
7599 .await
7600 .unwrap();
7601
7602 let results = dataset
7604 .scan()
7605 .filter("starts_with(name, 'ban')")
7606 .unwrap()
7607 .try_into_batch()
7608 .await
7609 .unwrap();
7610 let names: Vec<&str> = results
7611 .column_by_name("name")
7612 .unwrap()
7613 .as_any()
7614 .downcast_ref::<StringArray>()
7615 .unwrap()
7616 .iter()
7617 .map(|s| s.unwrap())
7618 .collect();
7619 assert!(names.iter().all(|n| n.starts_with("ban")));
7621 assert!(!names.is_empty());
7622
7623 assert_plan_equals(
7625 &dataset,
7626 |scanner| scanner.filter("name LIKE 'test%2'"),
7627 "ProjectionExec: expr=[name@0 as name, id@1 as id]
7628 LanceRead: uri=..., projection=[name, id], num_fragments=1, \
7629range_before=None, range_after=None, row_id=true, row_addr=false, \
7630full_filter=name LIKE Utf8(\"test%2\"), refine_filter=name LIKE Utf8(\"test%2\")
7631 ScalarIndexQuery: query=[name LIKE 'test%']@name_idx",
7632 )
7633 .await
7634 .unwrap();
7635
7636 let results = dataset
7638 .scan()
7639 .filter("name LIKE 'test%2'")
7640 .unwrap()
7641 .try_into_batch()
7642 .await
7643 .unwrap();
7644 let names: Vec<&str> = results
7645 .column_by_name("name")
7646 .unwrap()
7647 .as_any()
7648 .downcast_ref::<StringArray>()
7649 .unwrap()
7650 .iter()
7651 .map(|s| s.unwrap())
7652 .collect();
7653 assert!(
7655 names
7656 .iter()
7657 .all(|n| n.starts_with("test") && n.ends_with("2"))
7658 );
7659
7660 let mut scanner = dataset.scan();
7663 scanner.filter("name LIKE '%app%'").unwrap();
7664 let plan = scanner.create_plan().await.unwrap();
7665 let plan_str = format!("{:?}", plan);
7666 assert!(
7667 !plan_str.contains("ScalarIndexQuery"),
7668 "LIKE '%app%' should not use scalar index, but got: {}",
7669 plan_str
7670 );
7671
7672 let results = dataset
7674 .scan()
7675 .filter("name LIKE '%app%'")
7676 .unwrap()
7677 .try_into_batch()
7678 .await
7679 .unwrap();
7680 let names: Vec<&str> = results
7681 .column_by_name("name")
7682 .unwrap()
7683 .as_any()
7684 .downcast_ref::<StringArray>()
7685 .unwrap()
7686 .iter()
7687 .map(|s| s.unwrap())
7688 .collect();
7689 assert!(names.iter().all(|n| n.contains("app")));
7691
7692 let mut scanner = dataset.scan();
7694 scanner.filter("name NOT LIKE 'app%'").unwrap();
7695 let plan = scanner.create_plan().await.unwrap();
7696 let plan_str = format!("{:?}", plan);
7697 assert!(
7698 !plan_str.contains("ScalarIndexQuery"),
7699 "NOT LIKE should not use scalar index, but got: {}",
7700 plan_str
7701 );
7702 }
7703
7704 #[tokio::test]
7705 async fn test_like_prefix_correctness_with_btree_index() {
7706 let names: Vec<&str> = vec![
7708 "alpha", "alphabet", "beta", "gamma", "delta", "epsilon", "eta", "theta", "iota",
7709 "kappa",
7710 ];
7711 let data = RecordBatch::try_new(
7712 Arc::new(ArrowSchema::new(vec![
7713 ArrowField::new("name", DataType::Utf8, false),
7714 ArrowField::new("id", DataType::Int32, false),
7715 ])),
7716 vec![
7717 Arc::new(StringArray::from(names.clone())),
7718 Arc::new(Int32Array::from_iter_values(0..10)),
7719 ],
7720 )
7721 .unwrap();
7722
7723 let reader = RecordBatchIterator::new(
7724 vec![Ok(data)],
7725 Arc::new(ArrowSchema::new(vec![
7726 ArrowField::new("name", DataType::Utf8, false),
7727 ArrowField::new("id", DataType::Int32, false),
7728 ])),
7729 );
7730
7731 let mut dataset = Dataset::write(reader, "memory://test_like_correctness", None)
7732 .await
7733 .unwrap();
7734
7735 dataset
7737 .create_index(
7738 &["name"],
7739 IndexType::BTree,
7740 None,
7741 &ScalarIndexParams::default(),
7742 true,
7743 )
7744 .await
7745 .unwrap();
7746
7747 let with_index = dataset
7749 .scan()
7750 .filter("name LIKE 'alpha%'")
7751 .unwrap()
7752 .try_into_batch()
7753 .await
7754 .unwrap();
7755
7756 let without_index = dataset
7758 .scan()
7759 .use_scalar_index(false)
7760 .filter("name LIKE 'alpha%'")
7761 .unwrap()
7762 .try_into_batch()
7763 .await
7764 .unwrap();
7765
7766 assert_eq!(with_index.num_rows(), without_index.num_rows());
7768 assert_eq!(with_index.num_rows(), 2);
7769
7770 let with_index_names: BTreeSet<String> = with_index
7771 .column_by_name("name")
7772 .unwrap()
7773 .as_any()
7774 .downcast_ref::<StringArray>()
7775 .unwrap()
7776 .iter()
7777 .map(|s| s.unwrap().to_string())
7778 .collect();
7779
7780 let without_index_names: BTreeSet<String> = without_index
7781 .column_by_name("name")
7782 .unwrap()
7783 .as_any()
7784 .downcast_ref::<StringArray>()
7785 .unwrap()
7786 .iter()
7787 .map(|s| s.unwrap().to_string())
7788 .collect();
7789
7790 assert_eq!(with_index_names, without_index_names);
7791 assert_eq!(
7792 with_index_names,
7793 BTreeSet::from(["alpha".to_string(), "alphabet".to_string()])
7794 );
7795
7796 let starts_with_result = dataset
7798 .scan()
7799 .filter("starts_with(name, 'e')")
7800 .unwrap()
7801 .try_into_batch()
7802 .await
7803 .unwrap();
7804
7805 let starts_with_names: BTreeSet<String> = starts_with_result
7806 .column_by_name("name")
7807 .unwrap()
7808 .as_any()
7809 .downcast_ref::<StringArray>()
7810 .unwrap()
7811 .iter()
7812 .map(|s| s.unwrap().to_string())
7813 .collect();
7814
7815 assert_eq!(
7817 starts_with_names,
7818 BTreeSet::from(["epsilon".to_string(), "eta".to_string()])
7819 );
7820 }
7821
7822 #[tokio::test]
7823 async fn test_like_prefix_with_zone_map() {
7824 use lance_index::scalar::BuiltinIndexType;
7825
7826 let data = gen_batch()
7828 .col(
7829 "name",
7830 array::cycle_utf8_literals(&[
7831 "apple",
7832 "application",
7833 "app",
7834 "banana",
7835 "band",
7836 "testns1",
7837 "testns2",
7838 "test",
7839 "testing",
7840 "zoo",
7841 ]),
7842 )
7843 .col("id", array::step::<Int32Type>())
7844 .into_reader_rows(RowCount::from(100), BatchCount::from(1));
7845
7846 let mut dataset = Dataset::write(data, "memory://test_like_zonemap", None)
7847 .await
7848 .unwrap();
7849
7850 let params = ScalarIndexParams::for_builtin(BuiltinIndexType::ZoneMap);
7852 dataset
7853 .create_index(
7854 &["name"],
7855 IndexType::Scalar,
7856 Some("name_zonemap".to_string()),
7857 ¶ms,
7858 true,
7859 )
7860 .await
7861 .unwrap();
7862
7863 let mut scanner = dataset.scan();
7865 scanner.filter("name LIKE 'app%'").unwrap();
7866 let plan = scanner.create_plan().await.unwrap();
7867 let plan_str = format!("{:?}", plan);
7868 assert!(
7870 plan_str.contains("ScalarIndexExec") && plan_str.contains("LikePrefix"),
7871 "LIKE 'app%' should use zone map index with LikePrefix, but got: {}",
7872 plan_str
7873 );
7874
7875 let results = dataset
7877 .scan()
7878 .filter("name LIKE 'app%'")
7879 .unwrap()
7880 .try_into_batch()
7881 .await
7882 .unwrap();
7883 let names: Vec<&str> = results
7884 .column_by_name("name")
7885 .unwrap()
7886 .as_any()
7887 .downcast_ref::<StringArray>()
7888 .unwrap()
7889 .iter()
7890 .map(|s| s.unwrap())
7891 .collect();
7892 assert!(names.iter().all(|n| n.starts_with("app")));
7893 assert!(!names.is_empty());
7894
7895 let mut scanner = dataset.scan();
7897 scanner.filter("starts_with(name, 'ban')").unwrap();
7898 let plan = scanner.create_plan().await.unwrap();
7899 let plan_str = format!("{:?}", plan);
7900 assert!(
7901 plan_str.contains("ScalarIndexExec") && plan_str.contains("LikePrefix"),
7902 "starts_with should use zone map index with LikePrefix, but got: {}",
7903 plan_str
7904 );
7905
7906 let results = dataset
7908 .scan()
7909 .filter("starts_with(name, 'ban')")
7910 .unwrap()
7911 .try_into_batch()
7912 .await
7913 .unwrap();
7914 let names: Vec<&str> = results
7915 .column_by_name("name")
7916 .unwrap()
7917 .as_any()
7918 .downcast_ref::<StringArray>()
7919 .unwrap()
7920 .iter()
7921 .map(|s| s.unwrap())
7922 .collect();
7923 assert!(names.iter().all(|n| n.starts_with("ban")));
7924
7925 let mut scanner = dataset.scan();
7927 scanner.filter("name LIKE 'test%2'").unwrap();
7928 let plan = scanner.create_plan().await.unwrap();
7929 let plan_str = format!("{:?}", plan);
7930 assert!(
7931 plan_str.contains("ScalarIndexExec") && plan_str.contains("LikePrefix"),
7932 "LIKE 'test%2' should use zone map index for prefix, but got: {}",
7933 plan_str
7934 );
7935
7936 let mut scanner = dataset.scan();
7938 scanner.filter("name LIKE '%app%'").unwrap();
7939 let plan = scanner.create_plan().await.unwrap();
7940 let plan_str = format!("{:?}", plan);
7941 assert!(
7942 !plan_str.contains("LikePrefix"),
7943 "LIKE '%app%' should not use LikePrefix index, but got: {}",
7944 plan_str
7945 );
7946 }
7947
7948 #[tokio::test]
7949 async fn test_like_prefix_correctness_with_zone_map() {
7950 use lance_index::scalar::BuiltinIndexType;
7951
7952 let names: Vec<&str> = vec![
7954 "alpha", "alphabet", "beta", "gamma", "delta", "epsilon", "eta", "theta", "iota",
7955 "kappa",
7956 ];
7957 let data = RecordBatch::try_new(
7958 Arc::new(ArrowSchema::new(vec![
7959 ArrowField::new("name", DataType::Utf8, false),
7960 ArrowField::new("id", DataType::Int32, false),
7961 ])),
7962 vec![
7963 Arc::new(StringArray::from(names.clone())),
7964 Arc::new(Int32Array::from_iter_values(0..10)),
7965 ],
7966 )
7967 .unwrap();
7968
7969 let reader = RecordBatchIterator::new(
7970 vec![Ok(data)],
7971 Arc::new(ArrowSchema::new(vec![
7972 ArrowField::new("name", DataType::Utf8, false),
7973 ArrowField::new("id", DataType::Int32, false),
7974 ])),
7975 );
7976
7977 let mut dataset = Dataset::write(reader, "memory://test_like_correctness_zonemap", None)
7978 .await
7979 .unwrap();
7980
7981 let params = ScalarIndexParams::for_builtin(BuiltinIndexType::ZoneMap);
7983 dataset
7984 .create_index(
7985 &["name"],
7986 IndexType::Scalar,
7987 Some("name_zonemap".to_string()),
7988 ¶ms,
7989 true,
7990 )
7991 .await
7992 .unwrap();
7993
7994 let with_index = dataset
7996 .scan()
7997 .filter("name LIKE 'alpha%'")
7998 .unwrap()
7999 .try_into_batch()
8000 .await
8001 .unwrap();
8002
8003 let without_index = dataset
8005 .scan()
8006 .use_scalar_index(false)
8007 .filter("name LIKE 'alpha%'")
8008 .unwrap()
8009 .try_into_batch()
8010 .await
8011 .unwrap();
8012
8013 assert_eq!(with_index.num_rows(), without_index.num_rows());
8015 assert_eq!(with_index.num_rows(), 2);
8016
8017 let with_index_names: BTreeSet<String> = with_index
8018 .column_by_name("name")
8019 .unwrap()
8020 .as_any()
8021 .downcast_ref::<StringArray>()
8022 .unwrap()
8023 .iter()
8024 .map(|s| s.unwrap().to_string())
8025 .collect();
8026
8027 let without_index_names: BTreeSet<String> = without_index
8028 .column_by_name("name")
8029 .unwrap()
8030 .as_any()
8031 .downcast_ref::<StringArray>()
8032 .unwrap()
8033 .iter()
8034 .map(|s| s.unwrap().to_string())
8035 .collect();
8036
8037 assert_eq!(with_index_names, without_index_names);
8038 assert_eq!(
8039 with_index_names,
8040 BTreeSet::from(["alpha".to_string(), "alphabet".to_string()])
8041 );
8042
8043 let starts_with_result = dataset
8045 .scan()
8046 .filter("starts_with(name, 'e')")
8047 .unwrap()
8048 .try_into_batch()
8049 .await
8050 .unwrap();
8051
8052 let starts_with_names: BTreeSet<String> = starts_with_result
8053 .column_by_name("name")
8054 .unwrap()
8055 .as_any()
8056 .downcast_ref::<StringArray>()
8057 .unwrap()
8058 .iter()
8059 .map(|s| s.unwrap().to_string())
8060 .collect();
8061
8062 assert_eq!(
8064 starts_with_names,
8065 BTreeSet::from(["epsilon".to_string(), "eta".to_string()])
8066 );
8067 }
8068
8069 #[rstest]
8070 #[tokio::test]
8071 async fn test_late_materialization(
8072 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
8073 data_storage_version: LanceFileVersion,
8074 ) {
8075 use lance_io::assert_io_lt;
8076 use lance_table::io::commit::RenameCommitHandler;
8079 let data = gen_batch()
8080 .col(
8081 "vector",
8082 array::rand_vec::<Float32Type>(Dimension::from(32)),
8083 )
8084 .col("indexed", array::step::<Int32Type>())
8085 .col("not_indexed", array::step::<Int32Type>())
8086 .into_reader_rows(RowCount::from(1000), BatchCount::from(20));
8087
8088 let mut dataset = Dataset::write(
8089 data,
8090 "memory://test",
8091 Some(WriteParams {
8092 commit_handler: Some(Arc::new(RenameCommitHandler)),
8093 data_storage_version: Some(data_storage_version),
8094 ..Default::default()
8095 }),
8096 )
8097 .await
8098 .unwrap();
8099 dataset
8100 .create_index(
8101 &["indexed"],
8102 IndexType::Scalar,
8103 None,
8104 &ScalarIndexParams::default(),
8105 false,
8106 )
8107 .await
8108 .unwrap();
8109
8110 let _ = dataset.object_store().io_stats_incremental(); dataset.scan().try_into_batch().await.unwrap();
8113 let io_stats = dataset.object_store().io_stats_incremental();
8114 let full_scan_bytes = io_stats.read_bytes;
8115
8116 dataset
8118 .scan()
8119 .use_stats(false)
8120 .filter("not_indexed = 50")
8121 .unwrap()
8122 .try_into_batch()
8123 .await
8124 .unwrap();
8125 let io_stats = dataset.object_store().io_stats_incremental();
8126 assert_io_lt!(io_stats, read_bytes, full_scan_bytes);
8127 let filtered_scan_bytes = io_stats.read_bytes;
8128
8129 if data_storage_version == LanceFileVersion::Legacy {
8132 dataset
8133 .scan()
8134 .filter("not_indexed = 50")
8135 .unwrap()
8136 .try_into_batch()
8137 .await
8138 .unwrap();
8139 let io_stats = dataset.object_store().io_stats_incremental();
8140 assert_io_lt!(io_stats, read_bytes, filtered_scan_bytes);
8141 }
8142
8143 dataset
8147 .scan()
8148 .filter("indexed = 50")
8149 .unwrap()
8150 .try_into_batch()
8151 .await
8152 .unwrap();
8153 let io_stats = dataset.object_store().io_stats_incremental();
8154 assert_io_lt!(io_stats, read_bytes, full_scan_bytes);
8155 let index_scan_bytes = io_stats.read_bytes;
8156
8157 dataset
8160 .scan()
8161 .filter("indexed = 50")
8162 .unwrap()
8163 .try_into_batch()
8164 .await
8165 .unwrap();
8166 let io_stats = dataset.object_store().io_stats_incremental();
8167 assert_io_lt!(io_stats, read_bytes, index_scan_bytes);
8168 }
8169
8170 #[rstest]
8171 #[tokio::test]
8172 async fn test_project_nested(
8173 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
8174 data_storage_version: LanceFileVersion,
8175 ) -> Result<()> {
8176 let struct_i_field = ArrowField::new("i", DataType::Int32, true);
8177 let struct_o_field = ArrowField::new("o", DataType::Utf8, true);
8178 let schema = Arc::new(ArrowSchema::new(vec![
8179 ArrowField::new(
8180 "struct",
8181 DataType::Struct(vec![struct_i_field.clone(), struct_o_field.clone()].into()),
8182 true,
8183 ),
8184 ArrowField::new("s", DataType::Utf8, true),
8185 ]));
8186
8187 let input_batches: Vec<RecordBatch> = (0..5)
8188 .map(|i| {
8189 let struct_i_arr: Arc<Int32Array> =
8190 Arc::new(Int32Array::from_iter_values(i * 20..(i + 1) * 20));
8191 let struct_o_arr: Arc<StringArray> = Arc::new(StringArray::from_iter_values(
8192 (i * 20..(i + 1) * 20).map(|v| format!("o-{:02}", v)),
8193 ));
8194 RecordBatch::try_new(
8195 schema.clone(),
8196 vec![
8197 Arc::new(StructArray::from(vec![
8198 (Arc::new(struct_i_field.clone()), struct_i_arr as ArrayRef),
8199 (Arc::new(struct_o_field.clone()), struct_o_arr as ArrayRef),
8200 ])),
8201 Arc::new(StringArray::from_iter_values(
8202 (i * 20..(i + 1) * 20).map(|v| format!("s-{}", v)),
8203 )),
8204 ],
8205 )
8206 .unwrap()
8207 })
8208 .collect();
8209 let batches =
8210 RecordBatchIterator::new(input_batches.clone().into_iter().map(Ok), schema.clone());
8211 let test_dir = TempStrDir::default();
8212 let test_uri = &test_dir;
8213 let write_params = WriteParams {
8214 max_rows_per_file: 40,
8215 max_rows_per_group: 10,
8216 data_storage_version: Some(data_storage_version),
8217 ..Default::default()
8218 };
8219 Dataset::write(batches, test_uri, Some(write_params))
8220 .await
8221 .unwrap();
8222
8223 let dataset = Dataset::open(test_uri).await.unwrap();
8224
8225 let batches = dataset
8226 .scan()
8227 .project(&["struct.i"])
8228 .unwrap()
8229 .try_into_stream()
8230 .await
8231 .unwrap()
8232 .try_collect::<Vec<_>>()
8233 .await
8234 .unwrap();
8235 let batch = concat_batches(&batches[0].schema(), &batches).unwrap();
8236 assert!(batch.column_by_name("struct.i").is_some());
8237 Ok(())
8238 }
8239
8240 #[rstest]
8241 #[tokio::test]
8242 async fn test_plans(
8243 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
8244 data_storage_version: LanceFileVersion,
8245 #[values(false, true)] stable_row_id: bool,
8246 ) -> Result<()> {
8247 use lance_index::scalar::inverted::query::BoostQuery;
8250 let dim = 256;
8251 let mut dataset =
8252 TestVectorDataset::new_with_dimension(data_storage_version, stable_row_id, dim).await?;
8253 let lance_schema = dataset.dataset.schema();
8254
8255 if data_storage_version == LanceFileVersion::Legacy {
8259 log::info!("Test case: Pushdown scan");
8260 assert_plan_equals(
8261 &dataset.dataset,
8262 |scan| scan.project(&["s"])?.filter("i > 10 and i < 20"),
8263 "LancePushdownScan: uri=..., projection=[s], predicate=i > Int32(10) AND i < Int32(20), row_id=false, row_addr=false, ordered=true"
8264 ).await?;
8265 }
8266
8267 log::info!("Test case: Project and filter");
8268 let expected = if data_storage_version == LanceFileVersion::Legacy {
8269 "ProjectionExec: expr=[s@2 as s]
8270 Take: columns=\"i, _rowid, (s)\"
8271 CoalesceBatchesExec: target_batch_size=8192
8272 FilterExec: i@0 > 10 AND i@0 < 20
8273 LanceScan: uri..., projection=[i], row_id=true, row_addr=false, ordered=true, range=None"
8274 } else {
8275 "ProjectionExec: expr=[s@2 as s]
8276 Take: columns=\"i, _rowid, (s)\"
8277 CoalesceBatchesExec: target_batch_size=8192
8278 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)"
8279 };
8280 assert_plan_equals(
8281 &dataset.dataset,
8282 |scan| {
8283 scan.use_stats(false)
8284 .project(&["s"])?
8285 .filter("i > 10 and i < 20")
8286 },
8287 expected,
8288 )
8289 .await?;
8290
8291 log::info!("Test case: Late materialization");
8294 let expected = if data_storage_version == LanceFileVersion::Legacy {
8295 "ProjectionExec: expr=[i@0 as i, s@1 as s, vec@3 as vec]
8296 Take: columns=\"i, s, _rowid, (vec)\"
8297 CoalesceBatchesExec: target_batch_size=8192
8298 FilterExec: s@1 IS NOT NULL
8299 LanceScan: uri..., projection=[i, s], row_id=true, row_addr=false, ordered=true, range=None"
8300 } else {
8301 "ProjectionExec: expr=[i@0 as i, s@1 as s, vec@3 as vec]
8302 Take: columns=\"i, s, _rowid, (vec)\"
8303 CoalesceBatchesExec: target_batch_size=8192
8304 LanceRead: uri=..., projection=[i, s], num_fragments=2, range_before=None, range_after=None, \
8305 row_id=true, row_addr=false, full_filter=s IS NOT NULL, refine_filter=s IS NOT NULL"
8306 };
8307 assert_plan_equals(
8308 &dataset.dataset,
8309 |scan| scan.use_stats(false).filter("s IS NOT NULL"),
8310 expected,
8311 )
8312 .await?;
8313
8314 log::info!("Test case: Custom materialization (all early)");
8316 let expected = if data_storage_version == LanceFileVersion::Legacy {
8317 "ProjectionExec: expr=[i@0 as i, s@1 as s, vec@2 as vec]
8318 FilterExec: s@1 IS NOT NULL
8319 LanceScan: uri..., projection=[i, s, vec], row_id=true, row_addr=false, ordered=true, range=None"
8320 } else {
8321 "ProjectionExec: expr=[i@0 as i, s@1 as s, vec@2 as vec]
8322 LanceRead: uri=..., projection=[i, s, vec], num_fragments=2, range_before=None, \
8323 range_after=None, row_id=true, row_addr=false, full_filter=s IS NOT NULL, refine_filter=s IS NOT NULL"
8324 };
8325 assert_plan_equals(
8326 &dataset.dataset,
8327 |scan| {
8328 scan.use_stats(false)
8329 .materialization_style(MaterializationStyle::AllEarly)
8330 .filter("s IS NOT NULL")
8331 },
8332 expected,
8333 )
8334 .await?;
8335
8336 log::info!("Test case: Custom materialization 2 (all late)");
8337 let expected = if data_storage_version == LanceFileVersion::Legacy {
8338 "ProjectionExec: expr=[i@2 as i, s@0 as s, vec@3 as vec]
8339 Take: columns=\"s, _rowid, (i), (vec)\"
8340 CoalesceBatchesExec: target_batch_size=8192
8341 FilterExec: s@0 IS NOT NULL
8342 LanceScan: uri..., projection=[s], row_id=true, row_addr=false, ordered=true, range=None"
8343 } else {
8344 "ProjectionExec: expr=[i@2 as i, s@0 as s, vec@3 as vec]
8345 Take: columns=\"s, _rowid, (i), (vec)\"
8346 CoalesceBatchesExec: target_batch_size=8192
8347 LanceRead: uri=..., projection=[s], num_fragments=2, range_before=None, \
8348 range_after=None, row_id=true, row_addr=false, full_filter=s IS NOT NULL, refine_filter=s IS NOT NULL"
8349 };
8350 assert_plan_equals(
8351 &dataset.dataset,
8352 |scan| {
8353 scan.use_stats(false)
8354 .materialization_style(MaterializationStyle::AllLate)
8355 .filter("s IS NOT NULL")
8356 },
8357 expected,
8358 )
8359 .await?;
8360
8361 log::info!("Test case: Custom materialization 3 (mixed)");
8362 let expected = if data_storage_version == LanceFileVersion::Legacy {
8363 "ProjectionExec: expr=[i@3 as i, s@0 as s, vec@1 as vec]
8364 Take: columns=\"s, vec, _rowid, (i)\"
8365 CoalesceBatchesExec: target_batch_size=8192
8366 FilterExec: s@0 IS NOT NULL
8367 LanceScan: uri..., projection=[s, vec], row_id=true, row_addr=false, ordered=true, range=None"
8368 } else {
8369 "ProjectionExec: expr=[i@3 as i, s@0 as s, vec@1 as vec]
8370 Take: columns=\"s, vec, _rowid, (i)\"
8371 CoalesceBatchesExec: target_batch_size=8192
8372 LanceRead: uri=..., projection=[s, vec], num_fragments=2, range_before=None, range_after=None, \
8373 row_id=true, row_addr=false, full_filter=s IS NOT NULL, refine_filter=s IS NOT NULL"
8374 };
8375 assert_plan_equals(
8376 &dataset.dataset,
8377 |scan| {
8378 scan.use_stats(false)
8379 .materialization_style(
8380 MaterializationStyle::all_early_except(&["i"], lance_schema).unwrap(),
8381 )
8382 .filter("s IS NOT NULL")
8383 },
8384 expected,
8385 )
8386 .await?;
8387
8388 log::info!("Test case: Scan out of order");
8389 let expected = if data_storage_version == LanceFileVersion::Legacy {
8390 "LanceScan: uri=..., projection=[s], row_id=true, row_addr=false, ordered=false, range=None"
8391 } else {
8392 "LanceRead: uri=..., projection=[s], num_fragments=2, range_before=None, range_after=None, row_id=true, \
8393 row_addr=false, full_filter=--, refine_filter=--"
8394 };
8395 assert_plan_equals(
8396 &dataset.dataset,
8397 |scan| Ok(scan.project(&["s"])?.with_row_id().scan_in_order(false)),
8398 expected,
8399 )
8400 .await?;
8401
8402 let q: Float32Array = (32..32 + dim).map(|v| v as f32).collect();
8405 log::info!("Test case: Basic KNN");
8406 let expected = if data_storage_version == LanceFileVersion::Legacy {
8407 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8408 Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8409 CoalesceBatchesExec: target_batch_size=8192
8410 FilterExec: _distance@2 IS NOT NULL
8411 SortExec: TopK(fetch=5), expr=...
8412 KNNVectorDistance: metric=l2
8413 LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None"
8414 } else {
8415 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8416 Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8417 CoalesceBatchesExec: target_batch_size=8192
8418 FilterExec: _distance@2 IS NOT NULL
8419 SortExec: TopK(fetch=5), expr=...
8420 KNNVectorDistance: metric=l2
8421 LanceRead: uri=..., projection=[vec], num_fragments=2, range_before=None, range_after=None, \
8422 row_id=true, row_addr=false, full_filter=--, refine_filter=--"
8423 };
8424 assert_plan_equals(
8425 &dataset.dataset,
8426 |scan| scan.nearest("vec", &q, 5),
8427 expected,
8428 )
8429 .await?;
8430
8431 let q: Float32Array = (32..32 + dim).map(|v| v as f32).collect();
8434 log::info!("Test case: KNN with extraneous limit");
8435 let expected = if data_storage_version == LanceFileVersion::Legacy {
8436 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8437 Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8438 CoalesceBatchesExec: target_batch_size=8192
8439 GlobalLimitExec: skip=0, fetch=1
8440 FilterExec: _distance@2 IS NOT NULL
8441 SortExec: TopK(fetch=5), expr=...
8442 KNNVectorDistance: metric=l2
8443 LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None"
8444 } else {
8445 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8446 Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8447 CoalesceBatchesExec: target_batch_size=8192
8448 GlobalLimitExec: skip=0, fetch=1
8449 FilterExec: _distance@2 IS NOT NULL
8450 SortExec: TopK(fetch=5), expr=...
8451 KNNVectorDistance: metric=l2
8452 LanceRead: uri=..., projection=[vec], num_fragments=2, range_before=None, range_after=None, \
8453 row_id=true, row_addr=false, full_filter=--, refine_filter=--"
8454 };
8455 assert_plan_equals(
8456 &dataset.dataset,
8457 |scan| scan.nearest("vec", &q, 5)?.limit(Some(1), None),
8458 expected,
8459 )
8460 .await?;
8461
8462 dataset.make_vector_index().await?;
8465 log::info!("Test case: Basic ANN");
8466 let expected =
8467 "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8468 Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8469 CoalesceBatchesExec: target_batch_size=8192
8470 SortExec: TopK(fetch=42), expr=...
8471 ANNSubIndex: name=..., k=42, deltas=1, metric=L2
8472 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
8473 assert_plan_equals(
8474 &dataset.dataset,
8475 |scan| scan.nearest("vec", &q, 42),
8476 expected,
8477 )
8478 .await?;
8479
8480 log::info!("Test case: ANN with refine");
8481 let expected =
8482 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8483 Take: columns=\"_rowid, vec, _distance, (i), (s)\"
8484 CoalesceBatchesExec: target_batch_size=8192
8485 FilterExec: _distance@... IS NOT NULL
8486 SortExec: TopK(fetch=10), expr=...
8487 KNNVectorDistance: metric=l2
8488 Take: columns=\"_distance, _rowid, (vec)\"
8489 CoalesceBatchesExec: target_batch_size=8192
8490 SortExec: TopK(fetch=40), expr=...
8491 ANNSubIndex: name=..., k=40, deltas=1, metric=L2
8492 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
8493 assert_plan_equals(
8494 &dataset.dataset,
8495 |scan| Ok(scan.nearest("vec", &q, 10)?.refine(4)),
8496 expected,
8497 )
8498 .await?;
8499
8500 log::info!("Test case: ANN with index disabled");
8502 let expected = if data_storage_version == LanceFileVersion::Legacy {
8503 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8504 Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8505 CoalesceBatchesExec: target_batch_size=8192
8506 FilterExec: _distance@... IS NOT NULL
8507 SortExec: TopK(fetch=13), expr=...
8508 KNNVectorDistance: metric=l2
8509 LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None"
8510 } else {
8511 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8512 Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8513 CoalesceBatchesExec: target_batch_size=8192
8514 FilterExec: _distance@... IS NOT NULL
8515 SortExec: TopK(fetch=13), expr=...
8516 KNNVectorDistance: metric=l2
8517 LanceRead: uri=..., projection=[vec], num_fragments=2, range_before=None, range_after=None, \
8518 row_id=true, row_addr=false, full_filter=--, refine_filter=--"
8519 };
8520 assert_plan_equals(
8521 &dataset.dataset,
8522 |scan| Ok(scan.nearest("vec", &q, 13)?.use_index(false)),
8523 expected,
8524 )
8525 .await?;
8526
8527 log::info!("Test case: ANN with postfilter");
8528 let expected = "ProjectionExec: expr=[s@3 as s, vec@4 as vec, _distance@0 as _distance, _rowid@1 as _rowid]
8529 Take: columns=\"_distance, _rowid, i, (s), (vec)\"
8530 CoalesceBatchesExec: target_batch_size=8192
8531 FilterExec: i@2 > 10
8532 Take: columns=\"_distance, _rowid, (i)\"
8533 CoalesceBatchesExec: target_batch_size=8192
8534 SortExec: TopK(fetch=17), expr=...
8535 ANNSubIndex: name=..., k=17, deltas=1, metric=L2
8536 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
8537 assert_plan_equals(
8538 &dataset.dataset,
8539 |scan| {
8540 Ok(scan
8541 .nearest("vec", &q, 17)?
8542 .filter("i > 10")?
8543 .project(&["s", "vec"])?
8544 .with_row_id())
8545 },
8546 expected,
8547 )
8548 .await?;
8549
8550 log::info!("Test case: ANN with prefilter");
8551 let expected = if data_storage_version == LanceFileVersion::Legacy {
8552 "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8553 Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8554 CoalesceBatchesExec: target_batch_size=8192
8555 SortExec: TopK(fetch=17), expr=...
8556 ANNSubIndex: name=..., k=17, deltas=1, metric=L2
8557 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8558 FilterExec: i@0 > 10
8559 LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None"
8560 } else {
8561 "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8562 Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8563 CoalesceBatchesExec: target_batch_size=8192
8564 SortExec: TopK(fetch=17), expr=...
8565 ANNSubIndex: name=..., k=17, deltas=1, metric=L2
8566 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8567 LanceRead: uri=..., projection=[], num_fragments=2, range_before=None, range_after=None, \
8568 row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=i > Int32(10)
8569"
8570 };
8571 assert_plan_equals(
8572 &dataset.dataset,
8573 |scan| {
8574 Ok(scan
8575 .nearest("vec", &q, 17)?
8576 .filter("i > 10")?
8577 .prefilter(true))
8578 },
8579 expected,
8580 )
8581 .await?;
8582
8583 dataset.append_new_data().await?;
8584 log::info!("Test case: Combined KNN/ANN");
8585 let expected = "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8586 Take: columns=\"_rowid, vec, _distance, (i), (s)\"
8587 CoalesceBatchesExec: target_batch_size=8192
8588 FilterExec: _distance@... IS NOT NULL
8589 SortExec: TopK(fetch=6), expr=...
8590 KNNVectorDistance: metric=l2
8591 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8592 UnionExec
8593 ProjectionExec: expr=[_distance@2 as _distance, _rowid@1 as _rowid, vec@0 as vec]
8594 FilterExec: _distance@... IS NOT NULL
8595 SortExec: TopK(fetch=6), expr=...
8596 KNNVectorDistance: metric=l2
8597 LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None
8598 Take: columns=\"_distance, _rowid, (vec)\"
8599 CoalesceBatchesExec: target_batch_size=8192
8600 SortExec: TopK(fetch=6), expr=...
8601 ANNSubIndex: name=..., k=6, deltas=1, metric=L2
8602 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
8603 assert_plan_equals(
8604 &dataset.dataset,
8605 |scan| scan.nearest("vec", &q, 6),
8606 expected,
8609 )
8610 .await?;
8611
8612 log::info!("Test case: Combined KNN/ANN with postfilter");
8614 let expected = "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8615 Take: columns=\"_rowid, vec, _distance, i, (s)\"
8616 CoalesceBatchesExec: target_batch_size=8192
8617 FilterExec: i@3 > 10
8618 Take: columns=\"_rowid, vec, _distance, (i)\"
8619 CoalesceBatchesExec: target_batch_size=8192
8620 FilterExec: _distance@... IS NOT NULL
8621 SortExec: TopK(fetch=15), expr=...
8622 KNNVectorDistance: metric=l2
8623 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8624 UnionExec
8625 ProjectionExec: expr=[_distance@2 as _distance, _rowid@1 as _rowid, vec@0 as vec]
8626 FilterExec: _distance@... IS NOT NULL
8627 SortExec: TopK(fetch=15), expr=...
8628 KNNVectorDistance: metric=l2
8629 LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None
8630 Take: columns=\"_distance, _rowid, (vec)\"
8631 CoalesceBatchesExec: target_batch_size=8192
8632 SortExec: TopK(fetch=15), expr=...
8633 ANNSubIndex: name=..., k=15, deltas=1, metric=L2
8634 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
8635 assert_plan_equals(
8636 &dataset.dataset,
8637 |scan| scan.nearest("vec", &q, 15)?.filter("i > 10"),
8638 expected,
8639 )
8640 .await?;
8641
8642 log::info!("Test case: Combined KNN/ANN with prefilter");
8644 let expected = if data_storage_version == LanceFileVersion::Legacy {
8645 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8646 Take: columns=\"_rowid, vec, _distance, (i), (s)\"
8647 CoalesceBatchesExec: target_batch_size=8192
8648 FilterExec: _distance@... IS NOT NULL
8649 SortExec: TopK(fetch=5), expr=...
8650 KNNVectorDistance: metric=l2
8651 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8652 UnionExec
8653 ProjectionExec: expr=[_distance@3 as _distance, _rowid@2 as _rowid, vec@0 as vec]
8654 FilterExec: _distance@... IS NOT NULL
8655 SortExec: TopK(fetch=5), expr=...
8656 KNNVectorDistance: metric=l2
8657 FilterExec: i@1 > 10
8658 LanceScan: uri=..., projection=[vec, i], row_id=true, row_addr=false, ordered=false, range=None
8659 Take: columns=\"_distance, _rowid, (vec)\"
8660 CoalesceBatchesExec: target_batch_size=8192
8661 SortExec: TopK(fetch=5), expr=...
8662 ANNSubIndex: name=..., k=5, deltas=1, metric=L2
8663 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8664 FilterExec: i@0 > 10
8665 LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None"
8666 } else {
8667 "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8668 Take: columns=\"_rowid, vec, _distance, (i), (s)\"
8669 CoalesceBatchesExec: target_batch_size=8192
8670 FilterExec: _distance@... IS NOT NULL
8671 SortExec: TopK(fetch=5), expr=...
8672 KNNVectorDistance: metric=l2
8673 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8674 UnionExec
8675 ProjectionExec: expr=[_distance@3 as _distance, _rowid@2 as _rowid, vec@0 as vec]
8676 FilterExec: _distance@... IS NOT NULL
8677 SortExec: TopK(fetch=5), expr=...
8678 KNNVectorDistance: metric=l2
8679 FilterExec: i@1 > 10
8680 LanceScan: uri=..., projection=[vec, i], row_id=true, row_addr=false, ordered=false, range=None
8681 Take: columns=\"_distance, _rowid, (vec)\"
8682 CoalesceBatchesExec: target_batch_size=8192
8683 SortExec: TopK(fetch=5), expr=...
8684 ANNSubIndex: name=..., k=5, deltas=1, metric=L2
8685 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8686 LanceRead: uri=..., projection=[], num_fragments=2, range_before=None, range_after=None, \
8687 row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=i > Int32(10)"
8688 };
8689 assert_plan_equals(
8690 &dataset.dataset,
8691 |scan| {
8692 Ok(scan
8693 .nearest("vec", &q, 5)?
8694 .filter("i > 10")?
8695 .prefilter(true))
8696 },
8697 expected,
8700 )
8701 .await?;
8702
8703 dataset.make_vector_index().await?;
8707 dataset.make_scalar_index().await?;
8708
8709 log::info!("Test case: ANN with scalar index");
8710 let expected =
8711 "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8712 Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8713 CoalesceBatchesExec: target_batch_size=8192
8714 SortExec: TopK(fetch=5), expr=...
8715 ANNSubIndex: name=..., k=5, deltas=1, metric=L2
8716 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8717 ScalarIndexQuery: query=[i > 10]@i_idx";
8718 assert_plan_equals(
8719 &dataset.dataset,
8720 |scan| {
8721 Ok(scan
8722 .nearest("vec", &q, 5)?
8723 .filter("i > 10")?
8724 .prefilter(true))
8725 },
8726 expected,
8727 )
8728 .await?;
8729
8730 log::info!("Test case: ANN with scalar index disabled");
8731 let expected = if data_storage_version == LanceFileVersion::Legacy {
8732 "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8733 Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8734 CoalesceBatchesExec: target_batch_size=8192
8735 SortExec: TopK(fetch=5), expr=...
8736 ANNSubIndex: name=..., k=5, deltas=1, metric=L2
8737 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8738 FilterExec: i@0 > 10
8739 LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None"
8740 } else {
8741 "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8742 Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8743 CoalesceBatchesExec: target_batch_size=8192
8744 SortExec: TopK(fetch=5), expr=...
8745 ANNSubIndex: name=..., k=5, deltas=1, metric=L2
8746 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8747 LanceRead: uri=..., projection=[], num_fragments=3, range_before=None, \
8748 range_after=None, row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=i > Int32(10)"
8749 };
8750 assert_plan_equals(
8751 &dataset.dataset,
8752 |scan| {
8753 Ok(scan
8754 .nearest("vec", &q, 5)?
8755 .use_scalar_index(false)
8756 .filter("i > 10")?
8757 .prefilter(true))
8758 },
8759 expected,
8760 )
8761 .await?;
8762
8763 dataset.append_new_data().await?;
8764
8765 log::info!("Test case: Combined KNN/ANN with scalar index");
8766 let expected = "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8767 Take: columns=\"_rowid, vec, _distance, (i), (s)\"
8768 CoalesceBatchesExec: target_batch_size=8192
8769 FilterExec: _distance@... IS NOT NULL
8770 SortExec: TopK(fetch=8), expr=...
8771 KNNVectorDistance: metric=l2
8772 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8773 UnionExec
8774 ProjectionExec: expr=[_distance@3 as _distance, _rowid@2 as _rowid, vec@0 as vec]
8775 FilterExec: _distance@... IS NOT NULL
8776 SortExec: TopK(fetch=8), expr=...
8777 KNNVectorDistance: metric=l2
8778 FilterExec: i@1 > 10
8779 LanceScan: uri=..., projection=[vec, i], row_id=true, row_addr=false, ordered=false, range=None
8780 Take: columns=\"_distance, _rowid, (vec)\"
8781 CoalesceBatchesExec: target_batch_size=8192
8782 SortExec: TopK(fetch=8), expr=...
8783 ANNSubIndex: name=..., k=8, deltas=1, metric=L2
8784 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8785 ScalarIndexQuery: query=[i > 10]@i_idx";
8786 assert_plan_equals(
8787 &dataset.dataset,
8788 |scan| {
8789 Ok(scan
8790 .nearest("vec", &q, 8)?
8791 .filter("i > 10")?
8792 .prefilter(true))
8793 },
8794 expected,
8795 )
8796 .await?;
8797
8798 log::info!(
8800 "Test case: Combined KNN/ANN with updated scalar index and outdated vector index"
8801 );
8802 let expected = "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8803 Take: columns=\"_rowid, vec, _distance, (i), (s)\"
8804 CoalesceBatchesExec: target_batch_size=8192
8805 FilterExec: _distance@... IS NOT NULL
8806 SortExec: TopK(fetch=11), expr=...
8807 KNNVectorDistance: metric=l2
8808 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8809 UnionExec
8810 ProjectionExec: expr=[_distance@3 as _distance, _rowid@2 as _rowid, vec@0 as vec]
8811 FilterExec: _distance@... IS NOT NULL
8812 SortExec: TopK(fetch=11), expr=...
8813 KNNVectorDistance: metric=l2
8814 FilterExec: i@1 > 10
8815 LanceScan: uri=..., projection=[vec, i], row_id=true, row_addr=false, ordered=false, range=None
8816 Take: columns=\"_distance, _rowid, (vec)\"
8817 CoalesceBatchesExec: target_batch_size=8192
8818 SortExec: TopK(fetch=11), expr=...
8819 ANNSubIndex: name=..., k=11, deltas=1, metric=L2
8820 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8821 ScalarIndexQuery: query=[i > 10]@i_idx";
8822 dataset.make_scalar_index().await?;
8823 assert_plan_equals(
8824 &dataset.dataset,
8825 |scan| {
8826 Ok(scan
8827 .nearest("vec", &q, 11)?
8828 .filter("i > 10")?
8829 .prefilter(true))
8830 },
8831 expected,
8832 )
8833 .await?;
8834
8835 log::info!("Test case: Filtered read with scalar index");
8838 let expected = if data_storage_version == LanceFileVersion::Legacy {
8839 "ProjectionExec: expr=[s@1 as s]
8840 Take: columns=\"_rowid, (s)\"
8841 CoalesceBatchesExec: target_batch_size=8192
8842 MaterializeIndex: query=[i > 10]@i_idx"
8843 } else {
8844 "LanceRead: uri=..., projection=[s], num_fragments=4, range_before=None, \
8845 range_after=None, row_id=false, row_addr=false, full_filter=i > Int32(10), refine_filter=--
8846 ScalarIndexQuery: query=[i > 10]@i_idx"
8847 };
8848 assert_plan_equals(
8849 &dataset.dataset,
8850 |scan| scan.project(&["s"])?.filter("i > 10"),
8851 expected,
8852 )
8853 .await?;
8854
8855 if data_storage_version != LanceFileVersion::Legacy {
8856 log::info!(
8857 "Test case: Filtered read with scalar index disabled (late materialization)"
8858 );
8859 assert_plan_equals(
8860 &dataset.dataset,
8861 |scan| {
8862 scan.project(&["s"])?
8863 .use_scalar_index(false)
8864 .filter("i > 10")
8865 },
8866 "ProjectionExec: expr=[s@2 as s]
8867 Take: columns=\"i, _rowid, (s)\"
8868 CoalesceBatchesExec: target_batch_size=8192
8869 LanceRead: uri=..., projection=[i], num_fragments=4, range_before=None, \
8870 range_after=None, row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=i > Int32(10)",
8871 )
8872 .await?;
8873 }
8874
8875 log::info!("Test case: Empty projection");
8876 let expected = if data_storage_version == LanceFileVersion::Legacy {
8877 "ProjectionExec: expr=[_rowaddr@0 as _rowaddr]
8878 AddRowAddrExec
8879 MaterializeIndex: query=[i > 10]@i_idx"
8880 } else {
8881 "LanceRead: uri=..., projection=[], num_fragments=4, range_before=None, \
8882 range_after=None, row_id=false, row_addr=true, full_filter=i > Int32(10), refine_filter=--
8883 ScalarIndexQuery: query=[i > 10]@i_idx"
8884 };
8885 assert_plan_equals(
8886 &dataset.dataset,
8887 |scan| {
8888 scan.filter("i > 10")
8889 .unwrap()
8890 .with_row_address()
8891 .project::<&str>(&[])
8892 },
8893 expected,
8894 )
8895 .await?;
8896
8897 dataset.append_new_data().await?;
8898 log::info!("Test case: Combined Scalar/non-scalar filtered read");
8899 let expected = if data_storage_version == LanceFileVersion::Legacy {
8900 "ProjectionExec: expr=[s@1 as s]
8901 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8902 UnionExec
8903 Take: columns=\"_rowid, (s)\"
8904 CoalesceBatchesExec: target_batch_size=8192
8905 MaterializeIndex: query=[i > 10]@i_idx
8906 ProjectionExec: expr=[_rowid@2 as _rowid, s@1 as s]
8907 FilterExec: i@0 > 10
8908 LanceScan: uri=..., projection=[i, s], row_id=true, row_addr=false, ordered=false, range=None"
8909 } else {
8910 "LanceRead: uri=..., projection=[s], num_fragments=5, range_before=None, \
8911 range_after=None, row_id=false, row_addr=false, full_filter=i > Int32(10), refine_filter=--
8912 ScalarIndexQuery: query=[i > 10]@i_idx"
8913 };
8914 assert_plan_equals(
8915 &dataset.dataset,
8916 |scan| scan.project(&["s"])?.filter("i > 10"),
8917 expected,
8918 )
8919 .await?;
8920
8921 log::info!("Test case: Combined Scalar/non-scalar filtered read with empty projection");
8922 let expected = if data_storage_version == LanceFileVersion::Legacy {
8923 "ProjectionExec: expr=[_rowaddr@0 as _rowaddr]
8924 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8925 UnionExec
8926 AddRowAddrExec
8927 MaterializeIndex: query=[i > 10]@i_idx
8928 ProjectionExec: expr=[_rowaddr@2 as _rowaddr, _rowid@1 as _rowid]
8929 FilterExec: i@0 > 10
8930 LanceScan: uri=..., projection=[i], row_id=true, row_addr=true, ordered=false, range=None"
8931 } else {
8932 "LanceRead: uri=..., projection=[], num_fragments=5, range_before=None, \
8933 range_after=None, row_id=false, row_addr=true, full_filter=i > Int32(10), refine_filter=--
8934 ScalarIndexQuery: query=[i > 10]@i_idx"
8935 };
8936 assert_plan_equals(
8937 &dataset.dataset,
8938 |scan| {
8939 scan.filter("i > 10")
8940 .unwrap()
8941 .with_row_address()
8942 .project::<&str>(&[])
8943 },
8944 expected,
8945 )
8946 .await?;
8947
8948 log::info!("Test case: Dynamic projection");
8951 let expected = if data_storage_version == LanceFileVersion::Legacy {
8952 "ProjectionExec: expr=[regexp_match(s@1, .*) as matches]
8953 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8954 UnionExec
8955 Take: columns=\"_rowid, (s)\"
8956 CoalesceBatchesExec: target_batch_size=8192
8957 MaterializeIndex: query=[i > 10]@i_idx
8958 ProjectionExec: expr=[_rowid@2 as _rowid, s@1 as s]
8959 FilterExec: i@0 > 10
8960 LanceScan: uri=..., row_id=true, row_addr=false, ordered=false, range=None"
8961 } else {
8962 "ProjectionExec: expr=[regexp_match(s@0, .*) as matches]
8963 LanceRead: uri=..., projection=[s], num_fragments=5, range_before=None, \
8964 range_after=None, row_id=false, row_addr=false, full_filter=i > Int32(10), refine_filter=--
8965 ScalarIndexQuery: query=[i > 10]@i_idx"
8966 };
8967 assert_plan_equals(
8968 &dataset.dataset,
8969 |scan| {
8970 scan.project_with_transform(&[("matches", "regexp_match(s, \".*\")")])?
8971 .filter("i > 10")
8972 },
8973 expected,
8974 )
8975 .await?;
8976
8977 dataset.make_fts_index().await?;
8981 log::info!("Test case: Full text search (match query)");
8982 let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
8983 Take: columns="_rowid, _score, (s)"
8984 CoalesceBatchesExec: target_batch_size=8192
8985 MatchQuery: column=s, query=hello"#;
8986 assert_plan_equals(
8987 &dataset.dataset,
8988 |scan| {
8989 scan.project(&["s"])?
8990 .with_row_id()
8991 .full_text_search(FullTextSearchQuery::new("hello".to_owned()))
8992 },
8993 expected,
8994 )
8995 .await?;
8996
8997 log::info!("Test case: Full text search (phrase query)");
8998 let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
8999 Take: columns="_rowid, _score, (s)"
9000 CoalesceBatchesExec: target_batch_size=8192
9001 PhraseQuery: column=s, query=hello world"#;
9002 assert_plan_equals(
9003 &dataset.dataset,
9004 |scan| {
9005 let query = PhraseQuery::new("hello world".to_owned());
9006 scan.project(&["s"])?
9007 .with_row_id()
9008 .full_text_search(FullTextSearchQuery::new_query(query.into()))
9009 },
9010 expected,
9011 )
9012 .await?;
9013
9014 log::info!("Test case: Full text search (boost query)");
9015 let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9016 Take: columns="_rowid, _score, (s)"
9017 CoalesceBatchesExec: target_batch_size=8192
9018 BoostQuery: negative_boost=1
9019 MatchQuery: column=s, query=hello
9020 MatchQuery: column=s, query=world"#;
9021 assert_plan_equals(
9022 &dataset.dataset,
9023 |scan| {
9024 let positive =
9025 MatchQuery::new("hello".to_owned()).with_column(Some("s".to_owned()));
9026 let negative =
9027 MatchQuery::new("world".to_owned()).with_column(Some("s".to_owned()));
9028 let query = BoostQuery::new(positive.into(), negative.into(), Some(1.0));
9029 scan.project(&["s"])?
9030 .with_row_id()
9031 .full_text_search(FullTextSearchQuery::new_query(query.into()))
9032 },
9033 expected,
9034 )
9035 .await?;
9036
9037 log::info!("Test case: Full text search with prefilter");
9038 let expected = if data_storage_version == LanceFileVersion::Legacy {
9039 r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9040 Take: columns="_rowid, _score, (s)"
9041 CoalesceBatchesExec: target_batch_size=8192
9042 MatchQuery: column=s, query=hello
9043 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9044 UnionExec
9045 MaterializeIndex: query=[i > 10]@i_idx
9046 ProjectionExec: expr=[_rowid@1 as _rowid]
9047 FilterExec: i@0 > 10
9048 LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None"#
9049 } else {
9050 r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9051 Take: columns="_rowid, _score, (s)"
9052 CoalesceBatchesExec: target_batch_size=8192
9053 MatchQuery: column=s, query=hello
9054 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=--
9055 ScalarIndexQuery: query=[i > 10]@i_idx"#
9056 };
9057 assert_plan_equals(
9058 &dataset.dataset,
9059 |scan| {
9060 scan.project(&["s"])?
9061 .with_row_id()
9062 .filter("i > 10")?
9063 .prefilter(true)
9064 .full_text_search(FullTextSearchQuery::new("hello".to_owned()))
9065 },
9066 expected,
9067 )
9068 .await?;
9069
9070 log::info!("Test case: Full text search with unindexed rows");
9071 let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9072 Take: columns="_rowid, _score, (s)"
9073 CoalesceBatchesExec: target_batch_size=8192
9074 SortExec: expr=[_score@1 DESC NULLS LAST], preserve_partitioning=[false]
9075 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9076 UnionExec
9077 MatchQuery: column=s, query=hello
9078 FlatMatchQuery: column=s, query=hello
9079 LanceScan: uri=..., projection=[s], row_id=true, row_addr=false, ordered=false, range=None"#;
9080 dataset.append_new_data().await?;
9081 assert_plan_equals(
9082 &dataset.dataset,
9083 |scan| {
9084 scan.project(&["s"])?
9085 .with_row_id()
9086 .full_text_search(FullTextSearchQuery::new("hello".to_owned()))
9087 },
9088 expected,
9089 )
9090 .await?;
9091
9092 log::info!("Test case: Full text search with unindexed rows and fast_search");
9093 let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9094 Take: columns="_rowid, _score, (s)"
9095 CoalesceBatchesExec: target_batch_size=8192
9096 MatchQuery: column=s, query=hello"#;
9097 assert_plan_equals(
9098 &dataset.dataset,
9099 |scan| {
9100 let scan = scan
9101 .project(&["s"])?
9102 .with_row_id()
9103 .full_text_search(FullTextSearchQuery::new("hello".to_owned()))?;
9104 scan.fast_search();
9105 Ok(scan)
9106 },
9107 expected,
9108 )
9109 .await?;
9110
9111 log::info!("Test case: Full text search with unindexed rows and prefilter");
9112 let expected = if data_storage_version == LanceFileVersion::Legacy {
9113 r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9114 Take: columns="_rowid, _score, (s)"
9115 CoalesceBatchesExec: target_batch_size=8192
9116 SortExec: expr=[_score@1 DESC NULLS LAST], preserve_partitioning=[false]
9117 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9118 UnionExec
9119 MatchQuery: column=s, query=hello
9120 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9121 UnionExec
9122 MaterializeIndex: query=[i > 10]@i_idx
9123 ProjectionExec: expr=[_rowid@1 as _rowid]
9124 FilterExec: i@0 > 10
9125 LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None
9126 FlatMatchQuery: column=s, query=hello
9127 FilterExec: i@1 > 10
9128 LanceScan: uri=..., projection=[s, i], row_id=true, row_addr=false, ordered=false, range=None"#
9129 } else {
9130 r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9131 Take: columns="_rowid, _score, (s)"
9132 CoalesceBatchesExec: target_batch_size=8192
9133 SortExec: expr=[_score@1 DESC NULLS LAST], preserve_partitioning=[false]
9134 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9135 UnionExec
9136 MatchQuery: column=s, query=hello
9137 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=--
9138 ScalarIndexQuery: query=[i > 10]@i_idx
9139 FlatMatchQuery: column=s, query=hello
9140 FilterExec: i@1 > 10
9141 LanceScan: uri=..., projection=[s, i], row_id=true, row_addr=false, ordered=false, range=None"#
9142 };
9143 assert_plan_equals(
9144 &dataset.dataset,
9145 |scan| {
9146 scan.project(&["s"])?
9147 .with_row_id()
9148 .filter("i > 10")?
9149 .prefilter(true)
9150 .full_text_search(FullTextSearchQuery::new("hello".to_owned()))
9151 },
9152 expected,
9153 )
9154 .await?;
9155
9156 Ok(())
9157 }
9158
9159 #[tokio::test]
9160 async fn test_fast_search_plan() {
9161 let mut dataset = TestVectorDataset::new(LanceFileVersion::Stable, true)
9163 .await
9164 .unwrap();
9165 dataset.make_vector_index().await.unwrap();
9166 dataset.append_new_data().await.unwrap();
9167
9168 let q: Float32Array = (32..64).map(|v| v as f32).collect();
9169
9170 assert_plan_equals(
9171 &dataset.dataset,
9172 |scan| {
9173 scan.nearest("vec", &q, 32)?
9174 .fast_search()
9175 .project(&["_distance", "_rowid"])
9176 },
9177 "SortExec: TopK(fetch=32), expr=[_distance@0 ASC NULLS LAST, _rowid@1 ASC NULLS LAST]...
9178 ANNSubIndex: name=idx, k=32, deltas=1, metric=L2
9179 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1",
9180 )
9181 .await
9182 .unwrap();
9183
9184 assert_plan_equals(
9185 &dataset.dataset,
9186 |scan| {
9187 scan.nearest("vec", &q, 33)?
9188 .fast_search()
9189 .with_row_id()
9190 .project(&["_distance", "_rowid"])
9191 },
9192 "SortExec: TopK(fetch=33), expr=[_distance@0 ASC NULLS LAST, _rowid@1 ASC NULLS LAST]...
9193 ANNSubIndex: name=idx, k=33, deltas=1, metric=L2
9194 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1",
9195 )
9196 .await
9197 .unwrap();
9198
9199 assert_plan_equals(
9201 &dataset.dataset,
9202 |scan| {
9203 scan.nearest("vec", &q, 34)?
9204 .with_row_id()
9205 .project(&["_distance", "_rowid"])
9206 },
9207 "ProjectionExec: expr=[_distance@2 as _distance, _rowid@0 as _rowid]
9208 FilterExec: _distance@2 IS NOT NULL
9209 SortExec: TopK(fetch=34), expr=[_distance@2 ASC NULLS LAST, _rowid@0 ASC NULLS LAST]...
9210 KNNVectorDistance: metric=l2
9211 RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9212 UnionExec
9213 ProjectionExec: expr=[_distance@2 as _distance, _rowid@1 as _rowid, vec@0 as vec]
9214 FilterExec: _distance@2 IS NOT NULL
9215 SortExec: TopK(fetch=34), expr=[_distance@2 ASC NULLS LAST, _rowid@1 ASC NULLS LAST]...
9216 KNNVectorDistance: metric=l2
9217 LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None
9218 Take: columns=\"_distance, _rowid, (vec)\"
9219 CoalesceBatchesExec: target_batch_size=8192
9220 SortExec: TopK(fetch=34), expr=[_distance@0 ASC NULLS LAST, _rowid@1 ASC NULLS LAST]...
9221 ANNSubIndex: name=idx, k=34, deltas=1, metric=L2
9222 ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1",
9223 )
9224 .await
9225 .unwrap();
9226 }
9227
9228 #[tokio::test]
9229 async fn test_fast_search_without_vector_index_returns_empty() {
9230 let dataset = TestVectorDataset::new(LanceFileVersion::Stable, true)
9231 .await
9232 .unwrap();
9233 let q: Float32Array = (32..64).map(|v| v as f32).collect();
9234
9235 let mut scanner = dataset.dataset.scan();
9236 scanner.nearest("vec", &q, 10).unwrap();
9237 let normal_rows = scanner.try_into_batch().await.unwrap().num_rows();
9238
9239 let mut scanner = dataset.dataset.scan();
9240 scanner.nearest("vec", &q, 10).unwrap().fast_search();
9241 let fast_rows = scanner.try_into_batch().await.unwrap().num_rows();
9242
9243 assert_eq!(normal_rows, 10);
9244 assert_eq!(fast_rows, 0);
9245 }
9246
9247 #[rstest]
9248 #[tokio::test]
9249 pub async fn test_scan_planning_io(
9250 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
9251 data_storage_version: LanceFileVersion,
9252 ) {
9253 use lance_index::scalar::inverted::tokenizer::InvertedIndexParams;
9257 use lance_io::assert_io_eq;
9258 let data = gen_batch()
9259 .col(
9260 "vector",
9261 array::rand_vec::<Float32Type>(Dimension::from(32)),
9262 )
9263 .col("text", array::rand_utf8(ByteCount::from(4), false))
9264 .col("indexed", array::step::<Int32Type>())
9265 .col("not_indexed", array::step::<Int32Type>())
9266 .into_reader_rows(RowCount::from(100), BatchCount::from(5));
9267
9268 let mut dataset = Dataset::write(
9269 data,
9270 "memory://test",
9271 Some(WriteParams {
9272 data_storage_version: Some(data_storage_version),
9273 ..Default::default()
9274 }),
9275 )
9276 .await
9277 .unwrap();
9278 dataset
9279 .create_index(
9280 &["indexed"],
9281 IndexType::Scalar,
9282 None,
9283 &ScalarIndexParams::default(),
9284 false,
9285 )
9286 .await
9287 .unwrap();
9288 dataset
9289 .create_index(
9290 &["text"],
9291 IndexType::Inverted,
9292 None,
9293 &InvertedIndexParams::default(),
9294 false,
9295 )
9296 .await
9297 .unwrap();
9298 dataset
9299 .create_index(
9300 &["vector"],
9301 IndexType::Vector,
9302 None,
9303 &VectorIndexParams {
9304 metric_type: DistanceType::L2,
9305 stages: vec![
9306 StageParams::Ivf(IvfBuildParams {
9307 max_iters: 2,
9308 num_partitions: Some(2),
9309 sample_rate: 2,
9310 ..Default::default()
9311 }),
9312 StageParams::PQ(PQBuildParams {
9313 max_iters: 2,
9314 num_sub_vectors: 2,
9315 ..Default::default()
9316 }),
9317 ],
9318 version: crate::index::vector::IndexFileVersion::Legacy,
9319 skip_transpose: false,
9320 },
9321 false,
9322 )
9323 .await
9324 .unwrap();
9325
9326 dataset
9328 .scan()
9329 .prefilter(true)
9330 .filter("indexed > 10")
9331 .unwrap()
9332 .explain_plan(true)
9333 .await
9334 .unwrap();
9335
9336 let io_stats = dataset.object_store().io_stats_incremental();
9338 assert_io_gt!(io_stats, read_iops, 0);
9339
9340 dataset
9342 .scan()
9343 .prefilter(true)
9344 .filter("indexed > 10")
9345 .unwrap()
9346 .explain_plan(true)
9347 .await
9348 .unwrap();
9349
9350 let io_stats = dataset.object_store().io_stats_incremental();
9351 assert_io_eq!(io_stats, read_iops, 0);
9352
9353 dataset
9354 .scan()
9355 .prefilter(true)
9356 .filter("true")
9357 .unwrap()
9358 .explain_plan(true)
9359 .await
9360 .unwrap();
9361
9362 let io_stats = dataset.object_store().io_stats_incremental();
9363 assert_io_eq!(io_stats, read_iops, 0);
9364
9365 dataset
9366 .scan()
9367 .prefilter(true)
9368 .materialization_style(MaterializationStyle::AllEarly)
9369 .filter("true")
9370 .unwrap()
9371 .explain_plan(true)
9372 .await
9373 .unwrap();
9374
9375 let io_stats = dataset.object_store().io_stats_incremental();
9376 assert_io_eq!(io_stats, read_iops, 0);
9377
9378 dataset
9379 .scan()
9380 .prefilter(true)
9381 .materialization_style(MaterializationStyle::AllLate)
9382 .filter("true")
9383 .unwrap()
9384 .explain_plan(true)
9385 .await
9386 .unwrap();
9387
9388 let io_stats = dataset.object_store().io_stats_incremental();
9389 assert_io_eq!(io_stats, read_iops, 0);
9390 }
9391
9392 #[rstest]
9393 #[tokio::test]
9394 pub async fn test_row_meta_columns(
9395 #[values(
9396 (true, false), (false, true), (true, true) )]
9400 columns: (bool, bool),
9401 ) {
9402 let (with_row_id, with_row_address) = columns;
9403 let test_dir = TempStrDir::default();
9404 let uri = &test_dir;
9405
9406 let schema = Arc::new(arrow_schema::Schema::new(vec![
9407 arrow_schema::Field::new("data_item_id", arrow_schema::DataType::Int32, false),
9408 arrow_schema::Field::new("a", arrow_schema::DataType::Int32, false),
9409 ]));
9410
9411 let data = RecordBatch::try_new(
9412 schema.clone(),
9413 vec![
9414 Arc::new(Int32Array::from(vec![1001, 1002, 1003])),
9415 Arc::new(Int32Array::from(vec![1, 2, 3])),
9416 ],
9417 )
9418 .unwrap();
9419
9420 let dataset = Dataset::write(
9421 RecordBatchIterator::new(vec![Ok(data)], schema.clone()),
9422 uri,
9423 None,
9424 )
9425 .await
9426 .unwrap();
9427
9428 let mut scanner = dataset.scan();
9430
9431 let mut projection = vec!["data_item_id".to_string()];
9432 if with_row_id {
9433 scanner.with_row_id();
9434 projection.push(ROW_ID.to_string());
9435 }
9436 if with_row_address {
9437 scanner.with_row_address();
9438 projection.push(ROW_ADDR.to_string());
9439 }
9440
9441 scanner.project(&projection).unwrap();
9442 let stream = scanner.try_into_stream().await.unwrap();
9443 let batch = stream.try_collect::<Vec<_>>().await.unwrap().pop().unwrap();
9444
9445 if with_row_id {
9447 let column = batch.column_by_name(ROW_ID).unwrap();
9448 assert_eq!(column.data_type(), &DataType::UInt64);
9449 }
9450 if with_row_address {
9451 let column = batch.column_by_name(ROW_ADDR).unwrap();
9452 assert_eq!(column.data_type(), &DataType::UInt64);
9453 }
9454
9455 let mut scanner = dataset.scan();
9457 if with_row_id {
9458 scanner.with_row_id();
9459 }
9460 if with_row_address {
9461 scanner.with_row_address();
9462 }
9463 scanner.project(&["data_item_id"]).unwrap();
9464 let stream = scanner.try_into_stream().await.unwrap();
9465 let batch = stream.try_collect::<Vec<_>>().await.unwrap().pop().unwrap();
9466 let meta_column = batch.column_by_name(if with_row_id { ROW_ID } else { ROW_ADDR });
9467 assert!(meta_column.is_some());
9468
9469 let mut scanner = dataset.scan();
9471 if with_row_id {
9472 scanner.project(&[ROW_ID]).unwrap();
9473 } else {
9474 scanner.project(&[ROW_ADDR]).unwrap();
9475 };
9476 let stream = scanner.try_into_stream().await.unwrap();
9477 assert_eq!(stream.schema().fields().len(), 1);
9478 if with_row_id {
9479 assert!(stream.schema().field_with_name(ROW_ID).is_ok());
9480 } else {
9481 assert!(stream.schema().field_with_name(ROW_ADDR).is_ok());
9482 }
9483 }
9484
9485 async fn limit_offset_equivalency_test(scanner: &Scanner) {
9486 async fn test_one(
9487 scanner: &Scanner,
9488 full_result: &RecordBatch,
9489 limit: Option<i64>,
9490 offset: Option<i64>,
9491 ) {
9492 let mut new_scanner = scanner.clone();
9493 new_scanner.limit(limit, offset).unwrap();
9494 if let Some(nearest) = new_scanner.nearest_mut() {
9495 nearest.k = offset.unwrap_or(0).saturating_add(limit.unwrap_or(10_000)) as usize;
9496 }
9497 let result = new_scanner.try_into_batch().await.unwrap();
9498
9499 let resolved_offset = offset.unwrap_or(0).min(full_result.num_rows() as i64);
9500 let resolved_length = limit
9501 .unwrap_or(i64::MAX)
9502 .min(full_result.num_rows() as i64 - resolved_offset);
9503
9504 let expected = full_result.slice(resolved_offset as usize, resolved_length as usize);
9505
9506 if expected != result {
9507 let plan = new_scanner.analyze_plan().await.unwrap();
9508 assert_eq!(
9509 &expected, &result,
9510 "Limit: {:?}, Offset: {:?}, Plan: \n{}",
9511 limit, offset, plan
9512 );
9513 }
9514 }
9515
9516 let mut scanner_full = scanner.clone();
9517 if let Some(nearest) = scanner_full.nearest_mut() {
9518 nearest.k = 500;
9519 }
9520 let full_results = scanner_full.try_into_batch().await.unwrap();
9521
9522 test_one(scanner, &full_results, Some(1), None).await;
9523 test_one(scanner, &full_results, Some(1), Some(1)).await;
9524 test_one(scanner, &full_results, Some(1), Some(2)).await;
9525 test_one(scanner, &full_results, Some(1), Some(10)).await;
9526
9527 test_one(scanner, &full_results, Some(3), None).await;
9528 test_one(scanner, &full_results, Some(3), Some(2)).await;
9529 test_one(scanner, &full_results, Some(3), Some(4)).await;
9530
9531 test_one(scanner, &full_results, None, Some(3)).await;
9532 test_one(scanner, &full_results, None, Some(10)).await;
9533 }
9534
9535 #[tokio::test]
9536 async fn test_scan_limit_offset() {
9537 let test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9538 .await
9539 .unwrap();
9540 let scanner = test_ds.dataset.scan();
9541 limit_offset_equivalency_test(&scanner).await;
9542 }
9543
9544 #[tokio::test]
9545 async fn test_knn_limit_offset() {
9546 let test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9547 .await
9548 .unwrap();
9549 let query_vector = Float32Array::from(vec![0.0; 32]);
9550 let mut scanner = test_ds.dataset.scan();
9551 scanner
9552 .nearest("vec", &query_vector, 5)
9553 .unwrap()
9554 .project(&["i"])
9555 .unwrap();
9556 limit_offset_equivalency_test(&scanner).await;
9557 }
9558
9559 #[tokio::test]
9560 async fn test_ivf_pq_limit_offset() {
9561 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9562 .await
9563 .unwrap();
9564 test_ds.make_vector_index().await.unwrap();
9565 test_ds.append_new_data().await.unwrap();
9566 let query_vector = Float32Array::from(vec![0.0; 32]);
9567 let mut scanner = test_ds.dataset.scan();
9568 scanner.nearest("vec", &query_vector, 500).unwrap();
9569 limit_offset_equivalency_test(&scanner).await;
9570 }
9571
9572 #[tokio::test]
9573 async fn test_fts_limit_offset() {
9574 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9575 .await
9576 .unwrap();
9577 test_ds.make_fts_index().await.unwrap();
9578 test_ds.append_new_data().await.unwrap();
9579 let mut scanner = test_ds.dataset.scan();
9580 scanner
9581 .full_text_search(FullTextSearchQuery::new("4".into()))
9582 .unwrap();
9583 limit_offset_equivalency_test(&scanner).await;
9584 }
9585
9586 #[tokio::test]
9587 async fn test_fts_fast_search_excludes_unindexed_rows() {
9588 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9589 .await
9590 .unwrap();
9591 test_ds.make_fts_index().await.unwrap();
9592 test_ds.append_data_with_range(10, 20).await.unwrap();
9594
9595 let mut scanner = test_ds.dataset.scan();
9596 scanner
9597 .full_text_search(FullTextSearchQuery::new_query(
9598 MatchQuery::new("15".to_owned())
9599 .with_column(Some("s".to_owned()))
9600 .into(),
9601 ))
9602 .unwrap();
9603 let normal_rows = scanner.try_into_batch().await.unwrap().num_rows();
9604
9605 let mut scanner = test_ds.dataset.scan();
9606 scanner
9607 .full_text_search(FullTextSearchQuery::new_query(
9608 MatchQuery::new("15".to_owned())
9609 .with_column(Some("s".to_owned()))
9610 .into(),
9611 ))
9612 .unwrap()
9613 .fast_search();
9614 let fast_rows = scanner.try_into_batch().await.unwrap().num_rows();
9615
9616 assert_eq!(normal_rows, 2);
9617 assert_eq!(fast_rows, 1);
9618 }
9619
9620 async fn test_row_offset_read_helper(
9621 ds: &Dataset,
9622 scan_builder: impl FnOnce(&mut Scanner) -> &mut Scanner,
9623 expected_cols: &[&str],
9624 expected_row_offsets: &[u64],
9625 ) {
9626 let mut scanner = ds.scan();
9627 let scanner = scan_builder(&mut scanner);
9628 let stream = scanner.try_into_stream().await.unwrap();
9629
9630 let schema = stream.schema();
9631 let actual_cols = schema
9632 .fields()
9633 .iter()
9634 .map(|f| f.name().as_str())
9635 .collect::<Vec<_>>();
9636 assert_eq!(&actual_cols, expected_cols);
9637
9638 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
9639 let batch = arrow_select::concat::concat_batches(&schema, &batches).unwrap();
9640
9641 let row_offsets = batch
9642 .column_by_name(ROW_OFFSET)
9643 .unwrap()
9644 .as_primitive::<UInt64Type>()
9645 .values();
9646 assert_eq!(row_offsets.as_ref(), expected_row_offsets);
9647 }
9648
9649 #[tokio::test]
9650 async fn test_row_offset_read() {
9651 let mut ds = lance_datagen::gen_batch()
9652 .col("idx", array::step::<Int32Type>())
9653 .into_ram_dataset(FragmentCount::from(3), FragmentRowCount::from(3))
9654 .await
9655 .unwrap();
9656 ds.delete("idx >= 2 AND idx <= 6").await.unwrap();
9660
9661 test_row_offset_read_helper(
9663 &ds,
9664 |scanner| scanner.project(&["idx", ROW_OFFSET]).unwrap(),
9665 &["idx", ROW_OFFSET],
9666 &[0, 1, 2, 3],
9667 )
9668 .await;
9669
9670 test_row_offset_read_helper(
9672 &ds,
9673 |scanner| scanner.project(&[ROW_OFFSET]).unwrap(),
9674 &[ROW_OFFSET],
9675 &[0, 1, 2, 3],
9676 )
9677 .await;
9678
9679 test_row_offset_read_helper(
9681 &ds,
9682 |scanner| {
9683 scanner
9684 .filter("idx > 1")
9685 .unwrap()
9686 .project(&[ROW_OFFSET])
9687 .unwrap()
9688 },
9689 &[ROW_OFFSET],
9690 &[2, 3],
9691 )
9692 .await;
9693 }
9694
9695 #[tokio::test]
9696 async fn test_filter_to_take() {
9697 let mut ds = lance_datagen::gen_batch()
9698 .col("idx", array::step::<Int32Type>())
9699 .into_ram_dataset(FragmentCount::from(3), FragmentRowCount::from(100))
9700 .await
9701 .unwrap();
9702
9703 let row_ids = ds
9704 .scan()
9705 .project(&Vec::<&str>::default())
9706 .unwrap()
9707 .with_row_id()
9708 .try_into_stream()
9709 .await
9710 .unwrap()
9711 .try_collect::<Vec<_>>()
9712 .await
9713 .unwrap();
9714 let schema = row_ids[0].schema();
9715 let row_ids = concat_batches(&schema, row_ids.iter()).unwrap();
9716 let row_ids = row_ids.column(0).as_primitive::<UInt64Type>().clone();
9717
9718 let row_addrs = ds
9719 .scan()
9720 .project(&Vec::<&str>::default())
9721 .unwrap()
9722 .with_row_address()
9723 .try_into_stream()
9724 .await
9725 .unwrap()
9726 .try_collect::<Vec<_>>()
9727 .await
9728 .unwrap();
9729 let schema = row_addrs[0].schema();
9730 let row_addrs = concat_batches(&schema, row_addrs.iter()).unwrap();
9731 let row_addrs = row_addrs.column(0).as_primitive::<UInt64Type>().clone();
9732
9733 ds.delete("idx >= 190 AND idx < 210").await.unwrap();
9734
9735 let ds_copy = ds.clone();
9736 let do_check = async move |filt: &str, expected_idx: &[i32], applies_optimization: bool| {
9737 let mut scanner = ds_copy.scan();
9738 scanner.filter(filt).unwrap();
9739 let plan = scanner.explain_plan(true).await.unwrap();
9741 if applies_optimization {
9742 assert!(
9743 plan.contains("OneShotStream"),
9744 "expected take optimization to be applied. Filter: '{}'. Plan:\n{}",
9745 filt,
9746 plan
9747 );
9748 } else {
9749 assert!(
9750 !plan.contains("OneShotStream"),
9751 "expected take optimization to not be applied. Filter: '{}'. Plan:\n{}",
9752 filt,
9753 plan
9754 );
9755 }
9756
9757 let stream = scanner.try_into_stream().await.unwrap();
9759 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
9760 let idx = batches
9761 .iter()
9762 .map(|b| b.column_by_name("idx").unwrap().as_ref())
9763 .collect::<Vec<_>>();
9764
9765 if idx.is_empty() {
9766 assert!(expected_idx.is_empty());
9767 return;
9768 }
9769
9770 let idx = arrow::compute::concat(&idx).unwrap();
9771 assert_eq!(idx.as_primitive::<Int32Type>().values(), expected_idx);
9772 };
9773 let check =
9774 async |filt: &str, expected_idx: &[i32]| do_check(filt, expected_idx, true).await;
9775 let check_no_opt = async |filt: &str, expected_idx: &[i32]| {
9776 do_check(filt, expected_idx, false).await;
9777 };
9778
9779 check("_rowid = 50", &[50]).await;
9781 check("_rowaddr = 50", &[50]).await;
9782 check("_rowoffset = 50", &[50]).await;
9783
9784 check(
9785 "_rowid = 50 OR _rowid = 51 OR _rowid = 52 OR _rowid = 49",
9786 &[49, 50, 51, 52],
9787 )
9788 .await;
9789 check(
9790 "_rowaddr = 50 OR _rowaddr = 51 OR _rowaddr = 52 OR _rowaddr = 49",
9791 &[49, 50, 51, 52],
9792 )
9793 .await;
9794 check(
9795 "_rowoffset = 50 OR _rowoffset = 51 OR _rowoffset = 52 OR _rowoffset = 49",
9796 &[49, 50, 51, 52],
9797 )
9798 .await;
9799
9800 check("_rowid IN (52, 51, 50, 17)", &[17, 50, 51, 52]).await;
9801 check("_rowaddr IN (52, 51, 50, 17)", &[17, 50, 51, 52]).await;
9802 check("_rowoffset IN (52, 51, 50, 17)", &[17, 50, 51, 52]).await;
9803
9804 check(&format!("_rowid = {}", row_ids.value(190)), &[]).await;
9808 check(&format!("_rowaddr = {}", row_addrs.value(190)), &[]).await;
9809 check("_rowoffset = 190", &[210]).await;
9812
9813 check(&format!("_rowid = {}", row_ids.value(250)), &[250]).await;
9815 check(&format!("_rowaddr = {}", row_addrs.value(250)), &[250]).await;
9816 check("_rowoffset = 250", &[270]).await;
9817
9818 check("_rowoffset = 1000", &[]).await;
9820
9821 check("_rowid IN (5, 10, 15) AND idx > 10", &[15]).await;
9823 check("_rowaddr IN (5, 10, 15) AND idx > 10", &[15]).await;
9824 check("_rowoffset IN (5, 10, 15) AND idx > 10", &[15]).await;
9825 check("idx > 10 AND _rowid IN (5, 10, 15)", &[15]).await;
9826 check("idx > 10 AND _rowaddr IN (5, 10, 15)", &[15]).await;
9827 check("idx > 10 AND _rowoffset IN (5, 10, 15)", &[15]).await;
9828 check("_rowid = 50 AND _rowid = 50", &[50]).await;
9830
9831 check_no_opt("_rowid = 50 AND _rowid = 51", &[]).await;
9833 check_no_opt("(_rowid = 50 AND idx < 100) OR _rowid = 51", &[50, 51]).await;
9834
9835 let mut scanner = ds.scan();
9837 scanner.filter("_rowoffset = 77").unwrap();
9838 scanner
9839 .project_with_transform(&[("foo", "idx * 2")])
9840 .unwrap();
9841 let stream = scanner.try_into_stream().await.unwrap();
9842 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
9843 assert_eq!(batches[0].schema().field(0).name(), "foo");
9844 let val = batches[0].column(0).as_primitive::<Int32Type>().values()[0];
9845 assert_eq!(val, 154);
9846 }
9847
9848 #[tokio::test]
9849 async fn test_nested_field_ordering() {
9850 use arrow_array::StructArray;
9851
9852 let id_array = Int32Array::from(vec![3, 1, 2]);
9854 let nested_values = Int32Array::from(vec![30, 10, 20]);
9855 let nested_struct = StructArray::from(vec![(
9856 Arc::new(ArrowField::new("value", DataType::Int32, false)),
9857 Arc::new(nested_values) as ArrayRef,
9858 )]);
9859
9860 let schema = Arc::new(ArrowSchema::new(vec![
9861 ArrowField::new("id", DataType::Int32, false),
9862 ArrowField::new(
9863 "nested",
9864 DataType::Struct(vec![ArrowField::new("value", DataType::Int32, false)].into()),
9865 false,
9866 ),
9867 ]));
9868
9869 let batch = RecordBatch::try_new(
9870 schema.clone(),
9871 vec![Arc::new(id_array), Arc::new(nested_struct)],
9872 )
9873 .unwrap();
9874
9875 let test_dir = TempStrDir::default();
9876 let test_uri = &test_dir;
9877 let reader = RecordBatchIterator::new(vec![Ok(batch)].into_iter(), schema.clone());
9878
9879 let dataset = Dataset::write(reader, test_uri, None).await.unwrap();
9880
9881 let mut scanner = dataset.scan();
9883 scanner
9884 .order_by(Some(vec![ColumnOrdering {
9885 column_name: "nested.value".to_string(),
9886 ascending: true,
9887 nulls_first: true,
9888 }]))
9889 .unwrap(); let stream = scanner.try_into_stream().await.unwrap();
9892 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
9893
9894 let sorted_ids = batches[0].column(0).as_primitive::<Int32Type>().values();
9896 assert_eq!(sorted_ids[0], 1); assert_eq!(sorted_ids[1], 2); assert_eq!(sorted_ids[2], 3); }
9900
9901 #[tokio::test]
9902 async fn test_limit_with_ordering_not_pushed_down() {
9903 let id_array = Int32Array::from(vec![5, 2, 8, 1, 3, 7, 4, 6]);
9909 let value_array = Int32Array::from(vec![50, 20, 80, 10, 30, 70, 40, 60]);
9910
9911 let schema = Arc::new(ArrowSchema::new(vec![
9912 ArrowField::new("id", DataType::Int32, false),
9913 ArrowField::new("value", DataType::Int32, false),
9914 ]));
9915
9916 let batch = RecordBatch::try_new(
9917 schema.clone(),
9918 vec![Arc::new(id_array), Arc::new(value_array)],
9919 )
9920 .unwrap();
9921
9922 let test_dir = TempStrDir::default();
9923 let test_uri = &test_dir;
9924 let reader = RecordBatchIterator::new(vec![Ok(batch)].into_iter(), schema.clone());
9925
9926 let dataset = Dataset::write(reader, test_uri, None).await.unwrap();
9927
9928 let mut scanner = dataset.scan();
9930 scanner
9931 .order_by(Some(vec![ColumnOrdering {
9932 column_name: "value".to_string(),
9933 ascending: true,
9934 nulls_first: true,
9935 }]))
9936 .unwrap();
9937 scanner.limit(Some(3), None).unwrap();
9938
9939 let stream = scanner.try_into_stream().await.unwrap();
9940 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
9941
9942 let sorted_ids = batches[0].column(0).as_primitive::<Int32Type>().values();
9944 let sorted_values = batches[0].column(1).as_primitive::<Int32Type>().values();
9945 assert_eq!(batches[0].num_rows(), 3);
9946 assert_eq!(sorted_ids[0], 1); assert_eq!(sorted_ids[1], 2); assert_eq!(sorted_ids[2], 3); assert_eq!(sorted_values[0], 10);
9950 assert_eq!(sorted_values[1], 20);
9951 assert_eq!(sorted_values[2], 30);
9952
9953 let mut scanner = dataset.scan();
9955 scanner
9956 .order_by(Some(vec![ColumnOrdering {
9957 column_name: "value".to_string(),
9958 ascending: true,
9959 nulls_first: true,
9960 }]))
9961 .unwrap();
9962 scanner.limit(Some(3), Some(2)).unwrap(); let stream = scanner.try_into_stream().await.unwrap();
9965 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
9966
9967 let sorted_ids = batches[0].column(0).as_primitive::<Int32Type>().values();
9968 let sorted_values = batches[0].column(1).as_primitive::<Int32Type>().values();
9969 assert_eq!(batches[0].num_rows(), 3);
9970 assert_eq!(sorted_ids[0], 3); assert_eq!(sorted_ids[1], 4); assert_eq!(sorted_ids[2], 5); assert_eq!(sorted_values[0], 30);
9974 assert_eq!(sorted_values[1], 40);
9975 assert_eq!(sorted_values[2], 50);
9976
9977 let mut scanner = dataset.scan();
9979 scanner.limit(Some(3), None).unwrap();
9980
9981 let stream = scanner.try_into_stream().await.unwrap();
9982 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
9983
9984 assert_eq!(batches[0].num_rows(), 3);
9986 let unsorted_values = batches[0].column(1).as_primitive::<Int32Type>().values();
9987 assert_eq!(unsorted_values[0], 50);
9989 assert_eq!(unsorted_values[1], 20);
9990 assert_eq!(unsorted_values[2], 80);
9991 }
9992
9993 #[tokio::test]
9994 async fn test_scan_with_version_columns() {
9995 use arrow_array::{Int32Array, RecordBatch, RecordBatchIterator};
9996 use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
9997
9998 let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
10000 "id",
10001 DataType::Int32,
10002 false,
10003 )]));
10004
10005 let batch = RecordBatch::try_new(
10006 schema.clone(),
10007 vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
10008 )
10009 .unwrap();
10010
10011 let test_dir = lance_core::utils::tempfile::TempStrDir::default();
10012 let test_uri = test_dir.as_str();
10013
10014 let reader = RecordBatchIterator::new(vec![Ok(batch)], schema);
10015 let write_params = WriteParams {
10016 enable_stable_row_ids: true,
10017 ..Default::default()
10018 };
10019 Dataset::write(reader, test_uri, Some(write_params))
10020 .await
10021 .unwrap();
10022
10023 let dataset = Dataset::open(test_uri).await.unwrap();
10024 let mut scanner = dataset.scan();
10025
10026 scanner
10027 .project(&[ROW_CREATED_AT_VERSION, ROW_LAST_UPDATED_AT_VERSION])
10028 .unwrap();
10029
10030 let output_schema = scanner.schema().await.unwrap();
10032 assert!(
10033 output_schema
10034 .column_with_name("_row_last_updated_at_version")
10035 .is_some(),
10036 "Schema should include _row_last_updated_at_version"
10037 );
10038 assert!(
10039 output_schema
10040 .column_with_name("_row_created_at_version")
10041 .is_some(),
10042 "Schema should include _row_created_at_version"
10043 );
10044
10045 let batches = scanner
10047 .try_into_stream()
10048 .await
10049 .unwrap()
10050 .try_collect::<Vec<_>>()
10051 .await
10052 .unwrap();
10053
10054 assert_eq!(batches.len(), 1);
10055 let batch = &batches[0];
10056
10057 let last_updated = batch
10059 .column_by_name("_row_last_updated_at_version")
10060 .expect("Should have _row_last_updated_at_version column");
10061 let created_at = batch
10062 .column_by_name("_row_created_at_version")
10063 .expect("Should have _row_created_at_version column");
10064
10065 let last_updated_array = last_updated
10067 .as_any()
10068 .downcast_ref::<arrow_array::UInt64Array>()
10069 .unwrap();
10070 let created_at_array = created_at
10071 .as_any()
10072 .downcast_ref::<arrow_array::UInt64Array>()
10073 .unwrap();
10074
10075 for i in 0..batch.num_rows() {
10076 assert_eq!(
10077 last_updated_array.value(i),
10078 1,
10079 "All rows last updated at version 1"
10080 );
10081 assert_eq!(
10082 created_at_array.value(i),
10083 1,
10084 "All rows created at version 1"
10085 );
10086 }
10087 }
10088
10089 #[test_log::test(test)]
10090 fn test_scan_finishes_all_tasks() {
10091 let runtime = tokio::runtime::Builder::new_multi_thread()
10093 .enable_time()
10094 .build()
10095 .unwrap();
10096
10097 runtime.block_on(async move {
10098 let ds = lance_datagen::gen_batch()
10099 .col("id", lance_datagen::array::step::<Int32Type>())
10100 .into_ram_dataset(FragmentCount::from(1000), FragmentRowCount::from(10))
10101 .await
10102 .unwrap();
10103
10104 let mut stream = ds
10112 .scan()
10113 .fragment_readahead(1000)
10114 .batch_size(1)
10115 .io_buffer_size(1)
10116 .batch_readahead(1)
10117 .try_into_stream()
10118 .await
10119 .unwrap();
10120 stream.next().await.unwrap().unwrap();
10121 });
10122
10123 let start = Instant::now();
10124 while start.elapsed() < Duration::from_secs(10) {
10125 if runtime.handle().metrics().num_alive_tasks() == 0 {
10126 break;
10127 }
10128 std::thread::sleep(Duration::from_millis(100));
10129 }
10130
10131 assert!(
10132 runtime.handle().metrics().num_alive_tasks() == 0,
10133 "Tasks should have finished within 10 seconds but there are still {} tasks running",
10134 runtime.handle().metrics().num_alive_tasks()
10135 );
10136 }
10137
10138 fn assert_values_in_range(array: &Int32Array, range: std::ops::Range<i32>, msg: &str) {
10139 assert!(!array.is_empty(), "Expected some results but got none");
10140 assert!(
10141 array
10142 .iter()
10143 .all(|v| v.is_some_and(|val| range.contains(&val))),
10144 "{msg} (expected range {range:?})"
10145 );
10146 }
10147
10148 fn assert_has_all_fragments(array: &Int32Array) {
10150 assert!(
10151 array
10152 .iter()
10153 .any(|v| v.is_some_and(|val| (0..200).contains(&val)))
10154 && array
10155 .iter()
10156 .any(|v| v.is_some_and(|val| (200..400).contains(&val)))
10157 && array
10158 .iter()
10159 .any(|v| v.is_some_and(|val| (400..410).contains(&val)))
10160 && array
10161 .iter()
10162 .any(|v| v.is_some_and(|val| (410..420).contains(&val))),
10163 "Expected results from all fragments"
10164 );
10165 }
10166
10167 async fn test_fragment_list_filtering(
10169 test_ds: &TestVectorDataset,
10170 fragments: &[Fragment],
10171 mut build_scanner: impl FnMut(&Dataset) -> Scanner,
10172 ) {
10173 let batch = build_scanner(&test_ds.dataset)
10175 .try_into_batch()
10176 .await
10177 .unwrap();
10178 let i_array = batch
10179 .column_by_name("i")
10180 .unwrap()
10181 .as_any()
10182 .downcast_ref::<Int32Array>()
10183 .unwrap();
10184 assert_has_all_fragments(i_array);
10185
10186 let mut scanner = build_scanner(&test_ds.dataset);
10188 scanner.with_fragments(vec![fragments[2].clone()]);
10189 let batch = scanner.try_into_batch().await.unwrap();
10190 let i_array = batch
10191 .column_by_name("i")
10192 .unwrap()
10193 .as_any()
10194 .downcast_ref::<Int32Array>()
10195 .unwrap();
10196 assert_values_in_range(i_array, 400..410, "Should only get results from fragment 2");
10197
10198 let mut scanner = build_scanner(&test_ds.dataset);
10200 scanner.with_fragments(vec![fragments[0].clone()]);
10201 let batch = scanner.try_into_batch().await.unwrap();
10202 let i_array = batch
10203 .column_by_name("i")
10204 .unwrap()
10205 .as_any()
10206 .downcast_ref::<Int32Array>()
10207 .unwrap();
10208 assert_values_in_range(i_array, 0..200, "Should only get results from fragment 0");
10209
10210 let mut scanner = build_scanner(&test_ds.dataset);
10212 scanner.with_fragments(vec![
10213 fragments[0].clone(),
10214 fragments[1].clone(),
10215 fragments[2].clone(),
10216 ]);
10217 let batch = scanner.try_into_batch().await.unwrap();
10218 let i_array = batch
10219 .column_by_name("i")
10220 .unwrap()
10221 .as_any()
10222 .downcast_ref::<Int32Array>()
10223 .unwrap();
10224 assert_values_in_range(
10225 i_array,
10226 0..410,
10227 "Should get results from fragments 0, 1, and 2, excluding fragment 3",
10228 );
10229
10230 let mut scanner = build_scanner(&test_ds.dataset);
10232 scanner.with_fragments(vec![fragments[0].clone(), fragments[2].clone()]);
10233 let batch = scanner.try_into_batch().await.unwrap();
10234 let i_array = batch
10235 .column_by_name("i")
10236 .unwrap()
10237 .as_any()
10238 .downcast_ref::<Int32Array>()
10239 .unwrap();
10240 assert!(
10241 i_array
10242 .iter()
10243 .all(|v| v.is_some_and(|val| (0..200).contains(&val) || (400..410).contains(&val)))
10244 && i_array
10245 .iter()
10246 .any(|v| v.is_some_and(|val| (0..200).contains(&val)))
10247 && i_array
10248 .iter()
10249 .any(|v| v.is_some_and(|val| (400..410).contains(&val))),
10250 "Should only get results from fragment 0 (indexed) and fragment 2 (unindexed)"
10251 );
10252 }
10253
10254 #[tokio::test]
10255 async fn test_vector_search_respects_fragment_list() {
10256 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10257 .await
10258 .unwrap();
10259
10260 test_ds.make_vector_index().await.unwrap();
10262
10263 let query: Float32Array = (0..32).map(|v| v as f32).collect();
10264
10265 test_ds.append_data_with_range(400, 410).await.unwrap();
10267 test_ds.append_data_with_range(410, 420).await.unwrap();
10268
10269 let fragments = test_ds.dataset.fragments();
10272 assert_eq!(fragments.len(), 4);
10273
10274 test_fragment_list_filtering(&test_ds, fragments, |dataset| {
10275 let mut scanner = dataset.scan();
10276 scanner.nearest("vec", &query, 420).unwrap();
10277 scanner
10278 })
10279 .await;
10280 }
10281
10282 #[tokio::test]
10283 async fn test_fts_respects_fragment_list() {
10284 let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10285 .await
10286 .unwrap();
10287
10288 test_ds.make_fts_index().await.unwrap();
10290
10291 test_ds.append_data_with_range(400, 410).await.unwrap();
10293 test_ds.append_data_with_range(410, 420).await.unwrap();
10294
10295 let fragments = test_ds.dataset.fragments();
10298 assert_eq!(fragments.len(), 4);
10299
10300 test_fragment_list_filtering(&test_ds, fragments, |dataset| {
10302 let mut scanner = dataset.scan();
10303 scanner
10304 .full_text_search(FullTextSearchQuery::new("s-5".into()))
10305 .unwrap();
10306 scanner
10307 })
10308 .await;
10309 }
10310}