1use super::{
5 InvertedIndexParams,
6 index::*,
7 merger::{Merger, PartitionSource, SizeBasedMerger},
8};
9use crate::scalar::IndexStore;
10use crate::scalar::inverted::json::JsonTextStream;
11use crate::scalar::inverted::lance_tokenizer::DocType;
12use crate::scalar::inverted::tokenizer::lance_tokenizer::LanceTokenizer;
13use crate::scalar::lance_format::LanceIndexStore;
14use crate::vector::graph::OrderedFloat;
15use crate::{progress::IndexBuildProgress, progress::noop_progress};
16use arrow::array::AsArray;
17use arrow::datatypes;
18use arrow_array::{Array, RecordBatch, UInt64Array};
19use arrow_schema::{DataType, Field, Schema, SchemaRef};
20use bitpacking::{BitPacker, BitPacker4x};
21use datafusion::execution::{RecordBatchStream, SendableRecordBatchStream};
22use deepsize::DeepSizeOf;
23use futures::{Stream, StreamExt, TryStreamExt};
24use lance_arrow::json::JSON_EXT_NAME;
25use lance_arrow::{ARROW_EXT_NAME_KEY, iter_str_array};
26use lance_core::cache::LanceCache;
27use lance_core::utils::tokio::{get_num_compute_intensive_cpus, spawn_cpu};
28use lance_core::{Error, ROW_ID, ROW_ID_FIELD, Result};
29use lance_core::{error::LanceOptionExt, utils::tempfile::TempDir};
30use lance_io::object_store::ObjectStore;
31use object_store::path::Path;
32use smallvec::SmallVec;
33use std::collections::HashMap;
34use std::pin::Pin;
35use std::str::FromStr;
36use std::sync::Arc;
37use std::sync::LazyLock;
38use std::task::{Context, Poll};
39use std::{fmt::Debug, sync::atomic::AtomicU64};
40use tracing::instrument;
41
42pub const BLOCK_SIZE: usize = BitPacker4x::BLOCK_LEN;
46
47pub static LANCE_FTS_NUM_SHARDS: LazyLock<usize> = LazyLock::new(|| {
52 std::env::var("LANCE_FTS_NUM_SHARDS")
53 .unwrap_or_else(|_| get_num_compute_intensive_cpus().to_string())
54 .parse()
55 .expect("failed to parse LANCE_FTS_NUM_SHARDS")
56});
57pub static LANCE_FTS_PARTITION_SIZE: LazyLock<u64> = LazyLock::new(|| {
60 std::env::var("LANCE_FTS_PARTITION_SIZE")
61 .unwrap_or_else(|_| "256".to_string())
62 .parse()
63 .expect("failed to parse LANCE_FTS_PARTITION_SIZE")
64});
65pub static LANCE_FTS_TARGET_SIZE: LazyLock<u64> = LazyLock::new(|| {
67 std::env::var("LANCE_FTS_TARGET_SIZE")
68 .unwrap_or_else(|_| "4096".to_string())
69 .parse()
70 .expect("failed to parse LANCE_FTS_TARGET_SIZE")
71});
72
73#[derive(Debug)]
74pub struct InvertedIndexBuilder {
75 params: InvertedIndexParams,
76 pub(crate) partitions: Vec<u64>,
77 new_partitions: Vec<u64>,
78 fragment_mask: Option<u64>,
79 token_set_format: TokenSetFormat,
80 _tmpdir: TempDir,
81 local_store: Arc<dyn IndexStore>,
82 src_store: Arc<dyn IndexStore>,
83 progress: Arc<dyn IndexBuildProgress>,
84}
85
86impl InvertedIndexBuilder {
87 pub fn new(params: InvertedIndexParams) -> Self {
88 Self::new_with_fragment_mask(params, None)
89 }
90
91 pub fn new_with_fragment_mask(params: InvertedIndexParams, fragment_mask: Option<u64>) -> Self {
92 Self::from_existing_index(
93 params,
94 None,
95 Vec::new(),
96 TokenSetFormat::default(),
97 fragment_mask,
98 )
99 }
100
101 pub fn from_existing_index(
108 params: InvertedIndexParams,
109 store: Option<Arc<dyn IndexStore>>,
110 partitions: Vec<u64>,
111 token_set_format: TokenSetFormat,
112 fragment_mask: Option<u64>,
113 ) -> Self {
114 let tmpdir = TempDir::default();
115 let local_store = Arc::new(LanceIndexStore::new(
116 ObjectStore::local().into(),
117 tmpdir.obj_path(),
118 Arc::new(LanceCache::no_cache()),
119 ));
120 let src_store = store.unwrap_or_else(|| local_store.clone());
121 Self {
122 params,
123 partitions,
124 new_partitions: Vec::new(),
125 _tmpdir: tmpdir,
126 local_store,
127 src_store,
128 token_set_format,
129 fragment_mask,
130 progress: noop_progress(),
131 }
132 }
133
134 pub fn with_progress(mut self, progress: Arc<dyn IndexBuildProgress>) -> Self {
135 self.progress = progress;
136 self
137 }
138
139 pub async fn update(
140 &mut self,
141 new_data: SendableRecordBatchStream,
142 dest_store: &dyn IndexStore,
143 ) -> Result<()> {
144 let schema = new_data.schema();
145 let doc_col = schema.field(0).name();
146
147 if self.params.lance_tokenizer.is_none() {
149 let schema = new_data.schema();
150 let field = schema.column_with_name(doc_col).expect_ok()?.1;
151 let doc_type = DocType::try_from(field)?;
152 self.params.lance_tokenizer = Some(doc_type.as_ref().to_string());
153 }
154
155 let new_data = document_input(new_data, doc_col)?;
156
157 self.progress
158 .stage_start("tokenize_docs", None, "rows")
159 .await?;
160 self.update_index(new_data).await?;
161 self.progress.stage_complete("tokenize_docs").await?;
162 self.write(dest_store).await?;
163 Ok(())
164 }
165
166 #[instrument(level = "debug", skip_all)]
167 async fn update_index(&mut self, stream: SendableRecordBatchStream) -> Result<()> {
168 let num_workers = *LANCE_FTS_NUM_SHARDS;
169 let tokenizer = self.params.build()?;
170 let with_position = self.params.with_position;
171 let next_id = self.partitions.iter().map(|id| id + 1).max().unwrap_or(0);
172 let id_alloc = Arc::new(AtomicU64::new(next_id));
173 let tokenized_count = Arc::new(AtomicU64::new(0));
174 let (sender, receiver) = async_channel::bounded(num_workers);
175 let mut index_tasks = Vec::with_capacity(num_workers);
176 for _ in 0..num_workers {
177 let store = self.local_store.clone();
178 let tokenizer = tokenizer.clone();
179 let receiver: async_channel::Receiver<RecordBatch> = receiver.clone();
180 let id_alloc = id_alloc.clone();
181 let progress = self.progress.clone();
182 let fragment_mask = self.fragment_mask;
183 let token_set_format = self.token_set_format;
184 let tokenized_count = tokenized_count.clone();
185 let task = tokio::task::spawn(async move {
186 let mut worker = IndexWorker::new(
187 store,
188 tokenizer,
189 with_position,
190 id_alloc,
191 fragment_mask,
192 token_set_format,
193 )
194 .await?;
195 while let Ok(batch) = receiver.recv().await {
196 let num_rows = batch.num_rows();
197 worker.process_batch(batch).await?;
198 let tokenized_count = tokenized_count
199 .fetch_add(num_rows as u64, std::sync::atomic::Ordering::Relaxed)
200 + num_rows as u64;
201 progress
202 .stage_progress("tokenize_docs", tokenized_count)
203 .await?;
204 }
205 let partitions = worker.finish().await?;
206 Result::Ok(partitions)
207 });
208 index_tasks.push(task);
209 }
210
211 let sender = Arc::new(sender);
212
213 let mut stream = Box::pin(stream.then({
214 |batch_result| {
215 let sender = sender.clone();
216 async move {
217 let sender = sender.clone();
218 let batch = batch_result?;
219 let num_rows = batch.num_rows();
220 sender.send(batch).await.expect("failed to send batch");
221 Result::Ok(num_rows)
222 }
223 }
224 }));
225 log::info!("indexing FTS with {} workers", num_workers);
226
227 let mut last_num_rows = 0;
228 let mut total_num_rows = 0;
229 let start = std::time::Instant::now();
230 while let Some(num_rows) = stream.try_next().await? {
231 total_num_rows += num_rows;
232 if total_num_rows >= last_num_rows + 1_000_000 {
233 log::debug!(
234 "indexed {} documents, elapsed: {:?}, speed: {}rows/s",
235 total_num_rows,
236 start.elapsed(),
237 total_num_rows as f32 / start.elapsed().as_secs_f32()
238 );
239 last_num_rows = total_num_rows;
240 }
241 }
242 drop(stream);
244 debug_assert_eq!(sender.sender_count(), 1);
245 drop(sender);
246 log::info!("dispatching elapsed: {:?}", start.elapsed());
247
248 let start = std::time::Instant::now();
250 for index_task in index_tasks {
251 self.new_partitions.extend(index_task.await??);
252 }
253 log::info!("wait workers indexing elapsed: {:?}", start.elapsed());
254 Ok(())
255 }
256
257 pub async fn remap(
258 &mut self,
259 mapping: &HashMap<u64, Option<u64>>,
260 src_store: Arc<dyn IndexStore>,
261 dest_store: &dyn IndexStore,
262 ) -> Result<()> {
263 for part in self.partitions.iter() {
264 let part = InvertedPartition::load(
265 src_store.clone(),
266 *part,
267 None,
268 &LanceCache::no_cache(),
269 self.token_set_format,
270 )
271 .await?;
272 let mut builder = part.into_builder().await?;
273 builder.remap(mapping).await?;
274 builder.write(dest_store).await?;
275 }
276 if self.fragment_mask.is_none() {
277 self.write_metadata(dest_store, &self.partitions).await?;
278 } else {
279 for &partition_id in &self.partitions {
281 self.write_part_metadata(dest_store, partition_id).await?;
282 }
283 }
284 Ok(())
285 }
286
287 async fn write_metadata(&self, dest_store: &dyn IndexStore, partitions: &[u64]) -> Result<()> {
288 let metadata = HashMap::from_iter(vec![
289 ("partitions".to_owned(), serde_json::to_string(&partitions)?),
290 ("params".to_owned(), serde_json::to_string(&self.params)?),
291 (
292 TOKEN_SET_FORMAT_KEY.to_owned(),
293 self.token_set_format.to_string(),
294 ),
295 ]);
296 let mut writer = dest_store
297 .new_index_file(METADATA_FILE, Arc::new(Schema::empty()))
298 .await?;
299 writer.finish_with_metadata(metadata).await?;
300 Ok(())
301 }
302
303 pub(crate) async fn write_part_metadata(
308 &self,
309 dest_store: &dyn IndexStore,
310 partition: u64, ) -> Result<()> {
312 let partitions = vec![partition];
313 let metadata = HashMap::from_iter(vec![
314 ("partitions".to_owned(), serde_json::to_string(&partitions)?),
315 ("params".to_owned(), serde_json::to_string(&self.params)?),
316 (
317 TOKEN_SET_FORMAT_KEY.to_owned(),
318 self.token_set_format.to_string(),
319 ),
320 ]);
321 let file_name = part_metadata_file_path(partition);
323 let mut writer = dest_store
324 .new_index_file(&file_name, Arc::new(Schema::empty()))
325 .await?;
326 writer.finish_with_metadata(metadata).await?;
327 Ok(())
328 }
329
330 async fn write_metadata_with_progress(
331 &self,
332 dest_store: &dyn IndexStore,
333 partitions: &[u64],
334 ) -> Result<()> {
335 let total = if self.fragment_mask.is_none() {
336 Some(1)
337 } else {
338 Some(partitions.len() as u64)
339 };
340 self.progress
341 .stage_start("write_metadata", total, "files")
342 .await?;
343 if self.fragment_mask.is_none() {
344 self.write_metadata(dest_store, partitions).await?;
345 self.progress.stage_progress("write_metadata", 1).await?;
346 } else {
347 let mut completed = 0;
348 for &partition_id in partitions {
349 self.write_part_metadata(dest_store, partition_id).await?;
350 completed += 1;
351 self.progress
352 .stage_progress("write_metadata", completed)
353 .await?;
354 }
355 }
356 self.progress.stage_complete("write_metadata").await?;
357 Ok(())
358 }
359
360 async fn write(&self, dest_store: &dyn IndexStore) -> Result<()> {
361 if self.params.skip_merge {
362 let mut partitions =
363 Vec::with_capacity(self.partitions.len() + self.new_partitions.len());
364 partitions.extend_from_slice(&self.partitions);
365 partitions.extend_from_slice(&self.new_partitions);
366 partitions.sort_unstable();
367
368 self.progress
369 .stage_start(
370 "copy_partitions",
371 Some(partitions.len() as u64),
372 "partitions",
373 )
374 .await?;
375 let mut copied = 0;
376 for part in self.partitions.iter() {
377 self.src_store
378 .copy_index_file(&token_file_path(*part), dest_store)
379 .await?;
380 self.src_store
381 .copy_index_file(&posting_file_path(*part), dest_store)
382 .await?;
383 self.src_store
384 .copy_index_file(&doc_file_path(*part), dest_store)
385 .await?;
386 copied += 1;
387 self.progress
388 .stage_progress("copy_partitions", copied)
389 .await?;
390 }
391 for part in self.new_partitions.iter() {
392 self.local_store
393 .copy_index_file(&token_file_path(*part), dest_store)
394 .await?;
395 self.local_store
396 .copy_index_file(&posting_file_path(*part), dest_store)
397 .await?;
398 self.local_store
399 .copy_index_file(&doc_file_path(*part), dest_store)
400 .await?;
401 copied += 1;
402 self.progress
403 .stage_progress("copy_partitions", copied)
404 .await?;
405 }
406 self.progress.stage_complete("copy_partitions").await?;
407
408 self.write_metadata_with_progress(dest_store, &partitions)
409 .await?;
410 return Ok(());
411 }
412
413 let partitions = self
414 .partitions
415 .iter()
416 .map(|part| PartitionSource::new(self.src_store.clone(), *part))
417 .chain(
418 self.new_partitions
419 .iter()
420 .map(|part| PartitionSource::new(self.local_store.clone(), *part)),
421 )
422 .collect::<Vec<_>>();
423 self.progress
424 .stage_start(
425 "merge_partitions",
426 Some(partitions.len() as u64),
427 "partitions",
428 )
429 .await?;
430 let mut merger = SizeBasedMerger::new(
431 dest_store,
432 partitions,
433 *LANCE_FTS_TARGET_SIZE << 20,
434 self.token_set_format,
435 self.progress.clone(),
436 );
437 let partitions = merger.merge().await?;
438 self.progress.stage_complete("merge_partitions").await?;
439
440 self.write_metadata_with_progress(dest_store, &partitions)
441 .await?;
442 Ok(())
443 }
444}
445
446impl Default for InvertedIndexBuilder {
447 fn default() -> Self {
448 let params = InvertedIndexParams::default();
449 Self::new(params)
450 }
451}
452
453#[derive(Debug)]
455pub struct InnerBuilder {
456 id: u64,
457 with_position: bool,
458 token_set_format: TokenSetFormat,
459 pub(crate) tokens: TokenSet,
460 pub(crate) posting_lists: Vec<PostingListBuilder>,
461 pub(crate) docs: DocSet,
462}
463
464impl InnerBuilder {
465 pub fn new(id: u64, with_position: bool, token_set_format: TokenSetFormat) -> Self {
466 Self {
467 id,
468 with_position,
469 token_set_format,
470 tokens: TokenSet::default(),
471 posting_lists: Vec::new(),
472 docs: DocSet::default(),
473 }
474 }
475
476 pub fn id(&self) -> u64 {
477 self.id
478 }
479
480 pub fn set_tokens(&mut self, tokens: TokenSet) {
482 self.tokens = tokens;
483 }
484
485 pub fn set_docs(&mut self, docs: DocSet) {
487 self.docs = docs;
488 }
489
490 pub fn set_posting_lists(&mut self, posting_lists: Vec<PostingListBuilder>) {
492 self.posting_lists = posting_lists;
493 }
494
495 pub async fn remap(&mut self, mapping: &HashMap<u64, Option<u64>>) -> Result<()> {
496 let removed = self.docs.remap(mapping);
499
500 let mut token_id = 0;
504 let mut removed_token_ids = Vec::new();
505 self.posting_lists.retain_mut(|posting_list| {
506 posting_list.remap(&removed);
507 let keep = !posting_list.is_empty();
508 if !keep {
509 removed_token_ids.push(token_id as u32);
510 }
511 token_id += 1;
512 keep
513 });
514
515 self.tokens.remap(&removed_token_ids);
517
518 Ok(())
519 }
520
521 pub async fn write(&mut self, store: &dyn IndexStore) -> Result<()> {
522 let docs = Arc::new(std::mem::take(&mut self.docs));
523 self.write_posting_lists(store, docs.clone()).await?;
524 self.write_tokens(store).await?;
525 self.write_docs(store, docs).await?;
526 Ok(())
527 }
528
529 #[instrument(level = "debug", skip_all)]
530 async fn write_posting_lists(
531 &mut self,
532 store: &dyn IndexStore,
533 docs: Arc<DocSet>,
534 ) -> Result<()> {
535 let id = self.id;
536 let mut writer = store
537 .new_index_file(
538 &posting_file_path(self.id),
539 inverted_list_schema(self.with_position),
540 )
541 .await?;
542 let posting_lists = std::mem::take(&mut self.posting_lists);
543
544 log::info!(
545 "writing {} posting lists of partition {}, with position {}",
546 posting_lists.len(),
547 id,
548 self.with_position
549 );
550 let schema = inverted_list_schema(self.with_position);
551 let docs_for_batches = docs.clone();
552 let schema_for_batches = schema.clone();
553 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
554 let producer = spawn_cpu(move || {
555 for posting_list in posting_lists {
556 let batch = posting_list
557 .to_batch_with_docs(&docs_for_batches, schema_for_batches.clone())?;
558 if let Err(err) = tx.send(batch) {
559 return Err(Error::execution(format!(
560 "failed to send posting list batch to writer: {err}"
561 )));
562 }
563 }
564 Result::Ok(())
565 });
566
567 let mut write_duration = std::time::Duration::ZERO;
568 let mut num_posting_lists = 0;
569 while let Some(batch) = rx.recv().await {
570 num_posting_lists += 1;
571 let start = std::time::Instant::now();
572 if let Err(err) = writer.write_record_batch(batch).await {
573 drop(rx);
574 let _ = producer.await;
576 return Err(err);
577 }
578 write_duration += start.elapsed();
579
580 if num_posting_lists % 500_000 == 0 {
581 log::info!(
582 "wrote {} posting lists of partition {}, writing elapsed: {:?}",
583 num_posting_lists,
584 id,
585 write_duration,
586 );
587 }
588 }
589 drop(rx);
590 producer.await?;
591
592 writer.finish().await?;
593 Ok(())
594 }
595
596 #[instrument(level = "debug", skip_all)]
597 async fn write_tokens(&mut self, store: &dyn IndexStore) -> Result<()> {
598 log::info!("writing tokens of partition {}", self.id);
599 let tokens = std::mem::take(&mut self.tokens);
600 let batch = tokens.to_batch(self.token_set_format)?;
601 let mut writer = store
602 .new_index_file(&token_file_path(self.id), batch.schema())
603 .await?;
604 writer.write_record_batch(batch).await?;
605 writer.finish().await?;
606 Ok(())
607 }
608
609 #[instrument(level = "debug", skip_all)]
610 async fn write_docs(&mut self, store: &dyn IndexStore, docs: Arc<DocSet>) -> Result<()> {
611 log::info!("writing docs of partition {}", self.id);
612 let batch = docs.to_batch()?;
613 let mut writer = store
614 .new_index_file(&doc_file_path(self.id), batch.schema())
615 .await?;
616 writer.write_record_batch(batch).await?;
617 writer.finish().await?;
618 Ok(())
619 }
620}
621
622struct IndexWorker {
623 store: Arc<dyn IndexStore>,
624 tokenizer: Box<dyn LanceTokenizer>,
625 id_alloc: Arc<AtomicU64>,
626 builder: InnerBuilder,
627 partitions: Vec<u64>,
628 schema: SchemaRef,
629 estimated_size: u64,
630 total_doc_length: usize,
631 fragment_mask: Option<u64>,
632 token_set_format: TokenSetFormat,
633 token_occurrences: HashMap<u32, PositionRecorder>,
634 token_ids: Vec<u32>,
635 last_token_count: usize,
636 last_unique_token_count: usize,
637}
638
639impl IndexWorker {
640 async fn new(
641 store: Arc<dyn IndexStore>,
642 tokenizer: Box<dyn LanceTokenizer>,
643 with_position: bool,
644 id_alloc: Arc<AtomicU64>,
645 fragment_mask: Option<u64>,
646 token_set_format: TokenSetFormat,
647 ) -> Result<Self> {
648 let schema = inverted_list_schema(with_position);
649
650 Ok(Self {
651 store,
652 tokenizer,
653 builder: InnerBuilder::new(
654 id_alloc.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
655 | fragment_mask.unwrap_or(0),
656 with_position,
657 token_set_format,
658 ),
659 partitions: Vec::new(),
660 id_alloc,
661 schema,
662 estimated_size: 0,
663 total_doc_length: 0,
664 fragment_mask,
665 token_set_format,
666 token_occurrences: HashMap::new(),
667 token_ids: Vec::new(),
668 last_token_count: 0,
669 last_unique_token_count: 0,
670 })
671 }
672
673 fn has_position(&self) -> bool {
674 self.schema.column_with_name(POSITION_COL).is_some()
675 }
676
677 async fn process_batch(&mut self, batch: RecordBatch) -> Result<()> {
678 let doc_col = batch.column(0);
679 let doc_iter = iter_str_array(doc_col);
680 let row_id_col = batch[ROW_ID].as_primitive::<datatypes::UInt64Type>();
681 let docs = doc_iter
682 .zip(row_id_col.values().iter())
683 .filter_map(|(doc, row_id)| doc.map(|doc| (doc, *row_id)));
684
685 let with_position = self.has_position();
686 for (doc, row_id) in docs {
687 let mut token_num: u32 = 0;
688 if with_position {
689 if self.token_occurrences.capacity() < self.last_unique_token_count {
690 self.token_occurrences
691 .reserve(self.last_unique_token_count - self.token_occurrences.capacity());
692 }
693 self.token_occurrences.clear();
694
695 let mut token_stream = self.tokenizer.token_stream_for_doc(doc);
696 while token_stream.advance() {
697 let token = token_stream.token_mut();
698 let token_text = std::mem::take(&mut token.text);
699 let token_id = self.builder.tokens.add(token_text);
700 self.token_occurrences
701 .entry(token_id)
702 .or_insert_with(|| PositionRecorder::new(true))
703 .push(token.position as u32);
704 token_num += 1;
705 }
706 } else {
707 if self.token_ids.capacity() < self.last_token_count {
708 self.token_ids
709 .reserve(self.last_token_count - self.token_ids.capacity());
710 }
711 self.token_ids.clear();
712
713 let mut token_stream = self.tokenizer.token_stream_for_doc(doc);
714 while token_stream.advance() {
715 let token = token_stream.token_mut();
716 let token_text = std::mem::take(&mut token.text);
717 let token_id = self.builder.tokens.add(token_text);
718 self.token_ids.push(token_id);
719 token_num += 1;
720 }
721 }
722 self.builder
723 .posting_lists
724 .resize_with(self.builder.tokens.len(), || {
725 PostingListBuilder::new(with_position)
726 });
727 let doc_id = self.builder.docs.append(row_id, token_num);
728 self.total_doc_length += doc.len();
729
730 if with_position {
731 let unique_tokens = self.token_occurrences.len();
732 for (token_id, term_positions) in self.token_occurrences.drain() {
733 let posting_list = &mut self.builder.posting_lists[token_id as usize];
734
735 let old_size = posting_list.size();
736 posting_list.add(doc_id, term_positions);
737 let new_size = posting_list.size();
738 self.estimated_size += new_size - old_size;
739 }
740 self.last_unique_token_count = unique_tokens;
741 } else if token_num > 0 {
742 self.token_ids.sort_unstable();
743 let mut iter = self.token_ids.iter();
744 let mut current = *iter.next().unwrap();
745 let mut count = 1u32;
746 for &token_id in iter {
747 if token_id == current {
748 count += 1;
749 continue;
750 }
751
752 let posting_list = &mut self.builder.posting_lists[current as usize];
753 let old_size = posting_list.size();
754 posting_list.add(doc_id, PositionRecorder::Count(count));
755 let new_size = posting_list.size();
756 self.estimated_size += new_size - old_size;
757
758 current = token_id;
759 count = 1;
760 }
761 let posting_list = &mut self.builder.posting_lists[current as usize];
762 let old_size = posting_list.size();
763 posting_list.add(doc_id, PositionRecorder::Count(count));
764 let new_size = posting_list.size();
765 self.estimated_size += new_size - old_size;
766 }
767 self.last_token_count = token_num as usize;
768
769 if self.builder.docs.len() as u32 == u32::MAX
770 || self.estimated_size >= *LANCE_FTS_PARTITION_SIZE << 20
771 {
772 self.flush().await?;
773 }
774 }
775
776 Ok(())
777 }
778
779 #[instrument(level = "debug", skip_all)]
780 async fn flush(&mut self) -> Result<()> {
781 if self.builder.tokens.is_empty() {
782 return Ok(());
783 }
784
785 log::info!(
786 "flushing posting lists, estimated size: {} MiB",
787 self.estimated_size / (1024 * 1024)
788 );
789 self.estimated_size = 0;
790 let with_position = self.has_position();
791 let mut builder = std::mem::replace(
792 &mut self.builder,
793 InnerBuilder::new(
794 self.id_alloc
795 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
796 | self.fragment_mask.unwrap_or(0),
797 with_position,
798 self.token_set_format,
799 ),
800 );
801 builder.write(self.store.as_ref()).await?;
802 self.partitions.push(builder.id());
803 Ok(())
804 }
805
806 async fn finish(mut self) -> Result<Vec<u64>> {
807 if !self.builder.tokens.is_empty() {
808 self.flush().await?;
809 }
810 Ok(self.partitions)
811 }
812}
813
814#[derive(Debug, Clone)]
815pub enum PositionRecorder {
816 Position(SmallVec<[u32; 4]>),
817 Count(u32),
818}
819
820impl PositionRecorder {
821 fn new(with_position: bool) -> Self {
822 if with_position {
823 Self::Position(SmallVec::new())
824 } else {
825 Self::Count(0)
826 }
827 }
828
829 fn push(&mut self, position: u32) {
830 match self {
831 Self::Position(positions) => positions.push(position),
832 Self::Count(count) => *count += 1,
833 }
834 }
835
836 pub fn len(&self) -> u32 {
837 match self {
838 Self::Position(positions) => positions.len() as u32,
839 Self::Count(count) => *count,
840 }
841 }
842
843 pub fn is_empty(&self) -> bool {
844 self.len() == 0
845 }
846
847 pub fn into_vec(self) -> Vec<u32> {
848 match self {
849 Self::Position(positions) => positions.into_vec(),
850 Self::Count(_) => vec![0],
851 }
852 }
853}
854
855#[derive(Debug, Eq, PartialEq, Clone, DeepSizeOf)]
856pub struct ScoredDoc {
857 pub row_id: u64,
858 pub score: OrderedFloat,
859}
860
861impl ScoredDoc {
862 pub fn new(row_id: u64, score: f32) -> Self {
863 Self {
864 row_id,
865 score: OrderedFloat(score),
866 }
867 }
868}
869
870impl PartialOrd for ScoredDoc {
871 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
872 Some(self.cmp(other))
873 }
874}
875
876impl Ord for ScoredDoc {
877 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
878 self.score.cmp(&other.score)
879 }
880}
881
882pub fn legacy_inverted_list_schema(with_position: bool) -> SchemaRef {
883 let mut fields = vec![
884 arrow_schema::Field::new(ROW_ID, arrow_schema::DataType::UInt64, false),
885 arrow_schema::Field::new(FREQUENCY_COL, arrow_schema::DataType::Float32, false),
886 ];
887 if with_position {
888 fields.push(arrow_schema::Field::new(
889 POSITION_COL,
890 arrow_schema::DataType::List(Arc::new(arrow_schema::Field::new(
891 "item",
892 arrow_schema::DataType::Int32,
893 true,
894 ))),
895 false,
896 ));
897 }
898 Arc::new(arrow_schema::Schema::new(fields))
899}
900
901pub fn inverted_list_schema(with_position: bool) -> SchemaRef {
902 let mut fields = vec![
903 arrow_schema::Field::new(
906 POSTING_COL,
907 datatypes::DataType::List(Arc::new(Field::new(
908 "item",
909 datatypes::DataType::LargeBinary,
910 true,
911 ))),
912 false,
913 ),
914 arrow_schema::Field::new(MAX_SCORE_COL, datatypes::DataType::Float32, false),
915 arrow_schema::Field::new(LENGTH_COL, datatypes::DataType::UInt32, false),
916 ];
917 if with_position {
918 fields.push(arrow_schema::Field::new(
919 POSITION_COL,
920 arrow_schema::DataType::List(Arc::new(arrow_schema::Field::new(
921 "item",
922 arrow_schema::DataType::List(Arc::new(arrow_schema::Field::new(
923 "item",
924 arrow_schema::DataType::LargeBinary,
925 true,
926 ))),
927 true,
928 ))),
929 false,
930 ));
931 }
932 Arc::new(arrow_schema::Schema::new(fields))
933}
934
935pub struct FlattenStream {
937 inner: SendableRecordBatchStream,
941 field_type: DataType,
942 data_type: DataType,
943}
944
945impl FlattenStream {
946 pub fn new(input: SendableRecordBatchStream) -> Self {
947 let schema = input.schema();
948 let field = schema.field(0);
949 let data_type = match field.data_type() {
950 DataType::List(f) if matches!(f.data_type(), DataType::Utf8) => DataType::Utf8,
951 DataType::List(f) if matches!(f.data_type(), DataType::LargeUtf8) => {
952 DataType::LargeUtf8
953 }
954 DataType::LargeList(f) if matches!(f.data_type(), DataType::Utf8) => DataType::Utf8,
955 DataType::LargeList(f) if matches!(f.data_type(), DataType::LargeUtf8) => {
956 DataType::LargeUtf8
957 }
958 _ => panic!(
959 "expect data type List(Utf8) or List(LargeUtf8) but got {:?}",
960 field.data_type()
961 ),
962 };
963 Self {
964 inner: input,
965 field_type: field.data_type().clone(),
966 data_type,
967 }
968 }
969}
970
971impl Stream for FlattenStream {
972 type Item = datafusion_common::Result<RecordBatch>;
973
974 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
975 match Pin::new(&mut self.inner).poll_next(cx) {
976 Poll::Ready(Some(Ok(batch))) => {
977 let doc_col = batch.column(0);
978 let batch = match self.field_type {
979 DataType::List(_) => flatten_string_list::<i32>(&batch, doc_col).map_err(|e| {
980 datafusion_common::error::DataFusionError::Execution(format!(
981 "flatten string list error: {}",
982 e
983 ))
984 }),
985 DataType::LargeList(_) => {
986 flatten_string_list::<i64>(&batch, doc_col).map_err(|e| {
987 datafusion_common::error::DataFusionError::Execution(format!(
988 "flatten string list error: {}",
989 e
990 ))
991 })
992 }
993 _ => unreachable!(
994 "expect data type List or LargeList but got {:?}",
995 self.field_type
996 ),
997 };
998 Poll::Ready(Some(batch))
999 }
1000 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
1001 Poll::Ready(None) => Poll::Ready(None),
1002 Poll::Pending => Poll::Pending,
1003 }
1004 }
1005}
1006
1007impl RecordBatchStream for FlattenStream {
1008 fn schema(&self) -> SchemaRef {
1009 let schema = Schema::new(vec![
1010 Field::new(
1011 self.inner.schema().field(0).name(),
1012 self.data_type.clone(),
1013 true,
1014 ),
1015 ROW_ID_FIELD.clone(),
1016 ]);
1017
1018 Arc::new(schema)
1019 }
1020}
1021
1022fn flatten_string_list<Offset: arrow::array::OffsetSizeTrait>(
1023 batch: &RecordBatch,
1024 doc_col: &Arc<dyn Array>,
1025) -> Result<RecordBatch> {
1026 let docs = doc_col.as_list::<Offset>();
1027 let row_ids = batch[ROW_ID].as_primitive::<datatypes::UInt64Type>();
1028
1029 let row_ids = row_ids
1030 .values()
1031 .iter()
1032 .zip(docs.iter())
1033 .flat_map(|(row_id, doc)| std::iter::repeat_n(*row_id, doc.map(|d| d.len()).unwrap_or(0)));
1034
1035 let row_ids = Arc::new(UInt64Array::from_iter_values(row_ids));
1036 let docs = match docs.value_type() {
1037 datatypes::DataType::Utf8 | datatypes::DataType::LargeUtf8 => docs.values().clone(),
1038 _ => {
1039 return Err(Error::index(format!(
1040 "expect data type String or LargeString but got {}",
1041 docs.value_type()
1042 )));
1043 }
1044 };
1045
1046 let schema = Schema::new(vec![
1047 Field::new(
1048 batch.schema().field(0).name(),
1049 docs.data_type().clone(),
1050 true,
1051 ),
1052 ROW_ID_FIELD.clone(),
1053 ]);
1054 let batch = RecordBatch::try_new(Arc::new(schema), vec![docs, row_ids])?;
1055 Ok(batch)
1056}
1057
1058pub(crate) fn token_file_path(partition_id: u64) -> String {
1059 format!("part_{}_{}", partition_id, TOKENS_FILE)
1060}
1061
1062pub(crate) fn posting_file_path(partition_id: u64) -> String {
1063 format!("part_{}_{}", partition_id, INVERT_LIST_FILE)
1064}
1065
1066pub(crate) fn doc_file_path(partition_id: u64) -> String {
1067 format!("part_{}_{}", partition_id, DOCS_FILE)
1068}
1069
1070pub(crate) fn part_metadata_file_path(partition_id: u64) -> String {
1071 format!("part_{}_{}", partition_id, METADATA_FILE)
1072}
1073
1074pub async fn merge_index_files(
1075 object_store: &ObjectStore,
1076 index_dir: &Path,
1077 store: Arc<dyn IndexStore>,
1078) -> Result<()> {
1079 let part_metadata_files = list_metadata_files(object_store, index_dir).await?;
1081
1082 merge_metadata_files(store, &part_metadata_files).await
1084}
1085
1086async fn list_metadata_files(object_store: &ObjectStore, index_dir: &Path) -> Result<Vec<String>> {
1089 let mut part_metadata_files = Vec::new();
1091 let mut list_stream = object_store.list(Some(index_dir.clone()));
1092
1093 while let Some(item) = list_stream.next().await {
1094 match item {
1095 Ok(meta) => {
1096 let file_name = meta.location.filename().unwrap_or_default();
1097 if file_name.starts_with("part_") && file_name.ends_with("_metadata.lance") {
1099 part_metadata_files.push(file_name.to_string());
1100 }
1101 }
1102 Err(_) => continue,
1103 }
1104 }
1105
1106 if part_metadata_files.is_empty() {
1107 return Err(Error::invalid_input_source(
1108 format!(
1109 "No partition metadata files found in index directory: {}",
1110 index_dir
1111 )
1112 .into(),
1113 ));
1114 }
1115
1116 Ok(part_metadata_files)
1117}
1118
1119async fn merge_metadata_files(
1121 store: Arc<dyn IndexStore>,
1122 part_metadata_files: &[String],
1123) -> Result<()> {
1124 let mut all_partitions = Vec::new();
1126 let mut params = None;
1127 let mut token_set_format = None;
1128
1129 for file_name in part_metadata_files {
1130 let reader = store.open_index_file(file_name).await?;
1131 let metadata = &reader.schema().metadata;
1132
1133 let partitions_str = metadata.get("partitions").ok_or(Error::index(format!(
1134 "partitions not found in {}",
1135 file_name
1136 )))?;
1137
1138 let partition_ids: Vec<u64> = serde_json::from_str(partitions_str)
1139 .map_err(|e| Error::index(format!("Failed to parse partitions: {}", e)))?;
1140
1141 all_partitions.extend(partition_ids);
1142
1143 if params.is_none() {
1144 let params_str = metadata
1145 .get("params")
1146 .ok_or(Error::index(format!("params not found in {}", file_name)))?;
1147 params = Some(
1148 serde_json::from_str::<InvertedIndexParams>(params_str)
1149 .map_err(|e| Error::index(format!("Failed to parse params: {}", e)))?,
1150 );
1151 }
1152
1153 if token_set_format.is_none()
1154 && let Some(name) = metadata.get(TOKEN_SET_FORMAT_KEY)
1155 {
1156 token_set_format = Some(TokenSetFormat::from_str(name)?);
1157 }
1158 }
1159
1160 let mut sorted_ids = all_partitions.clone();
1162 sorted_ids.sort();
1163 sorted_ids.dedup();
1164
1165 let id_mapping: HashMap<u64, u64> = sorted_ids
1166 .iter()
1167 .enumerate()
1168 .map(|(new_id, &old_id)| (old_id, new_id as u64))
1169 .collect();
1170
1171 let timestamp = std::time::SystemTime::now()
1173 .duration_since(std::time::UNIX_EPOCH)
1174 .unwrap()
1175 .as_secs();
1176
1177 let mut temp_files: Vec<(String, String, String)> = Vec::new(); for (&old_id, &new_id) in &id_mapping {
1181 if old_id != new_id {
1182 for suffix in [TOKENS_FILE, INVERT_LIST_FILE, DOCS_FILE] {
1183 let old_path = format!("part_{}_{}", old_id, suffix);
1184 let new_path = format!("part_{}_{}", new_id, suffix);
1185 let temp_path = format!("temp_{}_{}", timestamp, old_path);
1186
1187 if let Err(e) = store.rename_index_file(&old_path, &temp_path).await {
1189 for (temp_name, old_name, _) in temp_files.iter().rev() {
1191 let _ = store.rename_index_file(temp_name, old_name).await;
1192 }
1193 return Err(Error::index(format!(
1194 "Failed to move {} to temp {}: {}",
1195 old_path, temp_path, e
1196 )));
1197 }
1198 temp_files.push((temp_path, old_path, new_path));
1199 }
1200 }
1201 }
1202
1203 let mut completed_renames: Vec<(String, String)> = Vec::new(); for (temp_path, _old_path, final_path) in &temp_files {
1207 if let Err(e) = store.rename_index_file(temp_path, final_path).await {
1208 for (final_name, temp_name) in completed_renames.iter().rev() {
1210 let _ = store.rename_index_file(final_name, temp_name).await;
1211 }
1212 for (temp_name, orig_name, _) in temp_files.iter() {
1214 if !completed_renames.iter().any(|(_, t)| t == temp_name) {
1215 let _ = store.rename_index_file(temp_name, orig_name).await;
1216 }
1217 }
1218 return Err(Error::index(format!(
1219 "Failed to rename {} to {}: {}",
1220 temp_path, final_path, e
1221 )));
1222 }
1223 completed_renames.push((final_path.clone(), temp_path.clone()));
1224 }
1225
1226 let remapped_partitions: Vec<u64> = (0..id_mapping.len() as u64).collect();
1228 let params = params.unwrap_or_default();
1229 let token_set_format = token_set_format.unwrap_or(TokenSetFormat::Arrow);
1230 let builder = InvertedIndexBuilder::from_existing_index(
1231 params,
1232 None,
1233 remapped_partitions.clone(),
1234 token_set_format,
1235 None,
1236 );
1237 builder
1238 .write_metadata(&*store, &remapped_partitions)
1239 .await?;
1240
1241 for file_name in part_metadata_files {
1243 if file_name.starts_with("part_") && file_name.ends_with("_metadata.lance") {
1244 let _ = store.delete_index_file(file_name).await;
1245 }
1246 }
1247
1248 Ok(())
1249}
1250
1251pub fn document_input(
1258 input: SendableRecordBatchStream,
1259 column: &str,
1260) -> Result<SendableRecordBatchStream> {
1261 let schema = input.schema();
1262 let field = schema.column_with_name(column).expect_ok()?.1;
1263 match field.data_type() {
1264 DataType::Utf8 | DataType::LargeUtf8 => Ok(input),
1265 DataType::List(field) | DataType::LargeList(field)
1266 if matches!(field.data_type(), DataType::Utf8 | DataType::LargeUtf8) =>
1267 {
1268 Ok(Box::pin(FlattenStream::new(input)))
1269 }
1270 DataType::LargeBinary => match field.metadata().get(ARROW_EXT_NAME_KEY) {
1271 Some(name) if name.as_str() == JSON_EXT_NAME => {
1272 Ok(Box::pin(JsonTextStream::new(input, column.to_string())))
1273 }
1274 _ => Err(Error::invalid_input_source(
1275 format!("column {} is not json", column).into(),
1276 )),
1277 },
1278 _ => Err(Error::invalid_input_source(
1279 format!(
1280 "column {} has type {}, is not utf8, large utf8 type/list, or large binary",
1281 column,
1282 field.data_type()
1283 )
1284 .into(),
1285 )),
1286 }
1287}
1288
1289#[cfg(test)]
1290mod tests {
1291 use super::*;
1292 use crate::metrics::NoOpMetricsCollector;
1293 use crate::progress::IndexBuildProgress;
1294 use crate::scalar::{IndexReader, IndexWriter};
1295 use arrow_array::{RecordBatch, StringArray, UInt64Array};
1296 use arrow_schema::{DataType, Field, Schema};
1297 use async_trait::async_trait;
1298 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
1299 use futures::stream;
1300 use lance_core::ROW_ID;
1301 use lance_core::cache::LanceCache;
1302 use lance_core::utils::tempfile::TempDir;
1303 use std::any::Any;
1304 use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
1305 use tokio::sync::Mutex;
1306
1307 fn make_doc_batch(doc: &str, row_id: u64) -> RecordBatch {
1308 let schema = Arc::new(Schema::new(vec![
1309 Field::new("doc", DataType::Utf8, true),
1310 Field::new(ROW_ID, DataType::UInt64, false),
1311 ]));
1312 let docs = Arc::new(StringArray::from(vec![Some(doc)]));
1313 let row_ids = Arc::new(UInt64Array::from(vec![row_id]));
1314 RecordBatch::try_new(schema, vec![docs, row_ids]).unwrap()
1315 }
1316
1317 #[derive(Debug, Default)]
1318 struct CountingStore {
1319 write_count: Arc<AtomicUsize>,
1320 }
1321
1322 impl CountingStore {
1323 fn new() -> Self {
1324 Self {
1325 write_count: Arc::new(AtomicUsize::new(0)),
1326 }
1327 }
1328
1329 fn write_count(&self) -> usize {
1330 self.write_count.load(Ordering::SeqCst)
1331 }
1332 }
1333
1334 impl DeepSizeOf for CountingStore {
1335 fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
1336 0
1337 }
1338 }
1339
1340 #[derive(Debug)]
1341 struct CountingWriter {
1342 write_count: Arc<AtomicUsize>,
1343 }
1344
1345 #[async_trait]
1346 impl IndexWriter for CountingWriter {
1347 async fn write_record_batch(&mut self, _batch: RecordBatch) -> Result<u64> {
1348 Ok(self.write_count.fetch_add(1, Ordering::SeqCst) as u64)
1349 }
1350
1351 async fn finish(&mut self) -> Result<()> {
1352 Ok(())
1353 }
1354
1355 async fn finish_with_metadata(&mut self, _metadata: HashMap<String, String>) -> Result<()> {
1356 Ok(())
1357 }
1358 }
1359
1360 #[async_trait]
1361 impl IndexStore for CountingStore {
1362 fn as_any(&self) -> &dyn Any {
1363 self
1364 }
1365
1366 fn io_parallelism(&self) -> usize {
1367 1
1368 }
1369
1370 async fn new_index_file(
1371 &self,
1372 _name: &str,
1373 _schema: Arc<Schema>,
1374 ) -> Result<Box<dyn IndexWriter>> {
1375 Ok(Box::new(CountingWriter {
1376 write_count: self.write_count.clone(),
1377 }))
1378 }
1379
1380 async fn open_index_file(&self, _name: &str) -> Result<Arc<dyn IndexReader>> {
1381 Err(Error::not_supported(
1382 "CountingStore does not support reading",
1383 ))
1384 }
1385
1386 async fn copy_index_file(&self, _name: &str, _dest_store: &dyn IndexStore) -> Result<()> {
1387 Err(Error::not_supported(
1388 "CountingStore does not support copying",
1389 ))
1390 }
1391
1392 async fn rename_index_file(&self, _name: &str, _new_name: &str) -> Result<()> {
1393 Err(Error::not_supported(
1394 "CountingStore does not support renaming",
1395 ))
1396 }
1397
1398 async fn delete_index_file(&self, _name: &str) -> Result<()> {
1399 Err(Error::not_supported(
1400 "CountingStore does not support deleting",
1401 ))
1402 }
1403 }
1404
1405 #[tokio::test]
1406 async fn test_write_posting_lists_writes_each_batch() -> Result<()> {
1407 let mut builder = InnerBuilder::new(0, false, TokenSetFormat::default());
1408 for doc_id in 0..3u64 {
1409 builder.docs.append(doc_id, 1);
1410 }
1411
1412 for doc_id in 0..3u32 {
1413 let mut posting_list = PostingListBuilder::new(false);
1414 posting_list.add(doc_id, PositionRecorder::Count(1));
1415 builder.posting_lists.push(posting_list);
1416 }
1417
1418 let store = CountingStore::new();
1419 let docs = Arc::new(std::mem::take(&mut builder.docs));
1420 builder.write_posting_lists(&store, docs).await?;
1421
1422 assert_eq!(store.write_count(), 3);
1423 Ok(())
1424 }
1425
1426 #[tokio::test]
1427 async fn test_skip_merge_writes_partitions_as_is() -> Result<()> {
1428 let src_dir = TempDir::default();
1429 let dest_dir = TempDir::default();
1430 let src_store = Arc::new(LanceIndexStore::new(
1431 ObjectStore::local().into(),
1432 src_dir.obj_path(),
1433 Arc::new(LanceCache::no_cache()),
1434 ));
1435 let dest_store = Arc::new(LanceIndexStore::new(
1436 ObjectStore::local().into(),
1437 dest_dir.obj_path(),
1438 Arc::new(LanceCache::no_cache()),
1439 ));
1440
1441 let params = InvertedIndexParams::default();
1442 let tokenizer = params.build()?;
1443 let token_set_format = TokenSetFormat::default();
1444 let id_alloc = Arc::new(AtomicU64::new(0));
1445
1446 let mut worker1 = IndexWorker::new(
1447 src_store.clone(),
1448 tokenizer.clone(),
1449 params.with_position,
1450 id_alloc.clone(),
1451 None,
1452 token_set_format,
1453 )
1454 .await?;
1455 worker1
1456 .process_batch(make_doc_batch("hello world", 0))
1457 .await?;
1458 let mut partitions = worker1.finish().await?;
1459
1460 let mut worker2 = IndexWorker::new(
1461 src_store.clone(),
1462 tokenizer.clone(),
1463 params.with_position,
1464 id_alloc.clone(),
1465 None,
1466 token_set_format,
1467 )
1468 .await?;
1469 worker2
1470 .process_batch(make_doc_batch("goodbye world", 1))
1471 .await?;
1472 partitions.extend(worker2.finish().await?);
1473 partitions.sort_unstable();
1474 assert_eq!(partitions.len(), 2);
1475 assert_ne!(partitions[0], partitions[1]);
1476
1477 let builder = InvertedIndexBuilder::from_existing_index(
1478 InvertedIndexParams::default().skip_merge(true),
1479 Some(src_store.clone()),
1480 partitions.clone(),
1481 token_set_format,
1482 None,
1483 );
1484 builder.write(dest_store.as_ref()).await?;
1485
1486 let metadata_reader = dest_store.open_index_file(METADATA_FILE).await?;
1487 let metadata = &metadata_reader.schema().metadata;
1488 let partitions_str = metadata
1489 .get("partitions")
1490 .expect("partitions missing from metadata");
1491 let written_partitions: Vec<u64> = serde_json::from_str(partitions_str).unwrap();
1492 assert_eq!(written_partitions, partitions);
1493
1494 for id in &partitions {
1495 dest_store.open_index_file(&token_file_path(*id)).await?;
1496 dest_store.open_index_file(&posting_file_path(*id)).await?;
1497 dest_store.open_index_file(&doc_file_path(*id)).await?;
1498 }
1499
1500 Ok(())
1501 }
1502
1503 #[tokio::test]
1504 async fn test_inverted_index_without_positions_tracks_frequency() -> Result<()> {
1505 let index_dir = TempDir::default();
1506 let store = Arc::new(LanceIndexStore::new(
1507 ObjectStore::local().into(),
1508 index_dir.obj_path(),
1509 Arc::new(LanceCache::no_cache()),
1510 ));
1511
1512 let schema = Arc::new(Schema::new(vec![
1513 Field::new("doc", DataType::Utf8, true),
1514 Field::new(ROW_ID, DataType::UInt64, false),
1515 ]));
1516 let docs = Arc::new(StringArray::from(vec![Some("hello hello world")]));
1517 let row_ids = Arc::new(UInt64Array::from(vec![0u64]));
1518 let batch = RecordBatch::try_new(schema.clone(), vec![docs, row_ids])?;
1519 let stream = RecordBatchStreamAdapter::new(schema, stream::iter(vec![Ok(batch)]));
1520 let stream = Box::pin(stream);
1521
1522 let params = InvertedIndexParams::new(
1523 "whitespace".to_string(),
1524 tantivy::tokenizer::Language::English,
1525 )
1526 .with_position(false)
1527 .remove_stop_words(false)
1528 .stem(false)
1529 .max_token_length(None);
1530
1531 let mut builder = InvertedIndexBuilder::new(params);
1532 builder.update(stream, store.as_ref()).await?;
1533
1534 let index = InvertedIndex::load(store, None, &LanceCache::no_cache()).await?;
1535 assert_eq!(index.partitions.len(), 1);
1536 let partition = &index.partitions[0];
1537 let token_id = partition.tokens.get("hello").unwrap();
1538 let posting = partition
1539 .inverted_list
1540 .posting_list(token_id, false, &NoOpMetricsCollector)
1541 .await?;
1542
1543 let mut iter = posting.iter();
1544 let (doc_id, freq, positions) = iter.next().unwrap();
1545 assert_eq!(doc_id, 0);
1546 assert_eq!(freq, 2);
1547 assert!(positions.is_none());
1548 assert!(iter.next().is_none());
1549
1550 Ok(())
1551 }
1552
1553 #[derive(Debug, Default)]
1554 struct RecordingProgress {
1555 events: Mutex<Vec<(String, String, u64)>>,
1556 }
1557
1558 #[async_trait]
1559 impl IndexBuildProgress for RecordingProgress {
1560 async fn stage_start(&self, stage: &str, total: Option<u64>, _unit: &str) -> Result<()> {
1561 self.events.lock().await.push((
1562 "start".to_string(),
1563 stage.to_string(),
1564 total.unwrap_or(0),
1565 ));
1566 Ok(())
1567 }
1568
1569 async fn stage_progress(&self, stage: &str, completed: u64) -> Result<()> {
1570 self.events
1571 .lock()
1572 .await
1573 .push(("progress".to_string(), stage.to_string(), completed));
1574 Ok(())
1575 }
1576
1577 async fn stage_complete(&self, stage: &str) -> Result<()> {
1578 self.events
1579 .lock()
1580 .await
1581 .push(("complete".to_string(), stage.to_string(), 0));
1582 Ok(())
1583 }
1584 }
1585
1586 #[tokio::test]
1587 async fn test_builder_reports_progress_stages() -> Result<()> {
1588 let index_dir = TempDir::default();
1589 let store = Arc::new(LanceIndexStore::new(
1590 ObjectStore::local().into(),
1591 index_dir.obj_path(),
1592 Arc::new(LanceCache::no_cache()),
1593 ));
1594
1595 let batch1 = make_doc_batch("hello world", 0);
1596 let batch2 = make_doc_batch("goodbye world", 1);
1597 let total_rows = 2u64;
1598 let stream = RecordBatchStreamAdapter::new(
1599 batch1.schema(),
1600 stream::iter(vec![Ok(batch1), Ok(batch2)]),
1601 );
1602 let stream = Box::pin(stream);
1603
1604 let progress = Arc::new(RecordingProgress::default());
1605 let mut builder =
1606 InvertedIndexBuilder::new(InvertedIndexParams::default().skip_merge(true))
1607 .with_progress(progress.clone());
1608 builder.update(stream, store.as_ref()).await?;
1609
1610 let events = progress.events.lock().await.clone();
1611 let tags = events
1612 .iter()
1613 .map(|(kind, stage, _)| format!("{kind}:{stage}"))
1614 .collect::<Vec<_>>();
1615 let tokenize_progress = events
1616 .iter()
1617 .filter_map(|(kind, stage, completed)| {
1618 if kind == "progress" && stage == "tokenize_docs" {
1619 Some(*completed)
1620 } else {
1621 None
1622 }
1623 })
1624 .collect::<Vec<_>>();
1625
1626 let tokenize_start = tags
1627 .iter()
1628 .position(|e| e == "start:tokenize_docs")
1629 .expect("missing tokenize_docs start");
1630 let tokenize_complete = tags
1631 .iter()
1632 .position(|e| e == "complete:tokenize_docs")
1633 .expect("missing tokenize_docs complete");
1634 let copy_start = tags
1635 .iter()
1636 .position(|e| e == "start:copy_partitions")
1637 .expect("missing copy_partitions start");
1638 let copy_complete = tags
1639 .iter()
1640 .position(|e| e == "complete:copy_partitions")
1641 .expect("missing copy_partitions complete");
1642 let metadata_start = tags
1643 .iter()
1644 .position(|e| e == "start:write_metadata")
1645 .expect("missing write_metadata start");
1646 let metadata_complete = tags
1647 .iter()
1648 .position(|e| e == "complete:write_metadata")
1649 .expect("missing write_metadata complete");
1650
1651 assert!(tokenize_start < tokenize_complete);
1652 assert!(tokenize_complete < copy_start);
1653 assert!(copy_start < copy_complete);
1654 assert!(copy_complete < metadata_start);
1655 assert!(metadata_start < metadata_complete);
1656
1657 assert!(
1658 tags.iter().any(|e| e == "progress:tokenize_docs"),
1659 "expected progress callback for tokenize_docs"
1660 );
1661 assert!(
1662 tokenize_progress.len() >= 2,
1663 "expected at least two progress callbacks for tokenize_docs, got {tokenize_progress:?}"
1664 );
1665 assert_eq!(
1666 tokenize_progress.iter().copied().max().unwrap_or_default(),
1667 total_rows,
1668 "expected tokenize_docs progress to reach all rows"
1669 );
1670 assert!(
1671 tags.iter().any(|e| e == "progress:copy_partitions"),
1672 "expected progress callback for copy_partitions"
1673 );
1674 assert!(
1675 tags.iter().any(|e| e == "progress:write_metadata"),
1676 "expected progress callback for write_metadata"
1677 );
1678 assert!(
1679 !tags.iter().any(|e| e == "start:merge_partitions"),
1680 "merge_partitions should not run in skip_merge mode"
1681 );
1682
1683 Ok(())
1684 }
1685}