1use lance_core::utils::row_addr_remap::RowAddrRemap;
5use std::any::Any;
6use std::collections::BTreeMap;
7use std::iter::once;
8use std::pin::Pin;
9use std::time::Instant;
10use std::{
11 collections::{HashMap, HashSet},
12 sync::Arc,
13};
14
15use super::lance_format::LanceIndexStore;
16use super::{
17 AnyQuery, BuiltinIndexType, IndexFile, IndexReader, IndexStore, IndexWriter, MetricsCollector,
18 ScalarIndex, ScalarIndexParams, SearchResult, TextQuery,
19};
20use crate::frag_reuse::FragReuseIndex;
21use crate::metrics::NoOpMetricsCollector;
22use crate::pbold;
23use crate::scalar::expression::{ScalarQueryParser, TextQueryParser};
24use crate::scalar::registry::{
25 BasicTrainer, DefaultTrainingRequest, ScalarIndexPlugin, TrainingCriteria, TrainingOrdering,
26 TrainingRequest, VALUE_COLUMN_NAME,
27};
28use crate::scalar::{CreatedIndex, RowIdRemapper, UpdateCriteria};
29use crate::{Index, IndexType};
30use arrow::array::{AsArray, UInt32Builder};
31use arrow::buffer::{Buffer, OffsetBuffer, ScalarBuffer};
32use arrow::datatypes::{UInt32Type, UInt64Type};
33use arrow_array::{BinaryArray, RecordBatch, UInt32Array};
34use arrow_schema::{DataType, Field, Schema, SchemaRef};
35use async_trait::async_trait;
36use datafusion::execution::SendableRecordBatchStream;
37use futures::{FutureExt, Stream, StreamExt, TryStreamExt, stream};
38use lance_arrow::iter_str_array;
39use lance_core::cache::{CacheKey, CacheKeySchema, KeyBuilder, LanceCache, WeakLanceCache};
40use lance_core::deepsize::DeepSizeOf;
41use lance_core::error::LanceOptionExt;
42use lance_core::utils::address::RowAddress;
43use lance_core::utils::tempfile::TempDir;
44use lance_core::utils::tokio::get_num_compute_intensive_cpus;
45use lance_core::utils::tracing::{IO_TYPE_LOAD_SCALAR_PART, TRACE_IO_EVENTS};
46use lance_core::{Error, ROW_ID, Result};
47use lance_io::object_store::ObjectStore;
48use lance_select::{RowAddrTreeMap, RowSetOps};
49use lance_tokenizer::{
50 AlphaNumOnlyFilter, AsciiFoldingFilter, LowerCaser, NgramTokenizer, RawTokenizer, TextAnalyzer,
51};
52use log::info;
53use roaring::{RoaringBitmap, RoaringTreemap};
54use serde::Serialize;
55use tracing::instrument;
56
57mod ngram_regex;
58pub(crate) use ngram_regex::regex_can_use_index;
59
60const TOKENS_COL: &str = "tokens";
61const POSTING_LIST_COL: &str = "posting_list";
62pub const POSTINGS_FILENAME: &str = "ngram_postings.lance";
63const NGRAM_INDEX_VERSION: u32 = 0;
64
65const MAX_POSTING_LIST_BATCH_BYTES: usize = i32::MAX as usize - 1024 * 1024;
69const POSTING_LIST_STREAM_BATCH_ROWS: usize = 64;
70
71use std::sync::LazyLock;
72
73pub static TOKENS_FIELD: LazyLock<Field> =
74 LazyLock::new(|| Field::new(TOKENS_COL, DataType::UInt32, true));
75pub static POSTINGS_FIELD: LazyLock<Field> = LazyLock::new(|| {
76 Field::new(POSTING_LIST_COL, DataType::Binary, false).with_metadata(HashMap::from([(
77 lance_encoding::constants::COMPRESSION_META_KEY.to_string(),
78 "none".to_string(),
79 )]))
80});
81pub static POSTINGS_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(|| {
82 Arc::new(Schema::new(vec![
83 TOKENS_FIELD.clone(),
84 POSTINGS_FIELD.clone(),
85 ]))
86});
87pub static TEXT_PREPPER: LazyLock<TextAnalyzer> = LazyLock::new(|| {
88 TextAnalyzer::builder(RawTokenizer::default())
89 .filter(LowerCaser)
90 .filter(AsciiFoldingFilter)
91 .build()
92});
93pub static NGRAM_TOKENIZER: LazyLock<TextAnalyzer> = LazyLock::new(|| {
95 TextAnalyzer::builder(NgramTokenizer::all_ngrams(3, 3).unwrap())
96 .filter(AlphaNumOnlyFilter)
97 .build()
98});
99
100fn tokenize_visitor(tokenizer: &TextAnalyzer, text: &str, mut visitor: impl FnMut(&String)) {
102 let mut prepper = TEXT_PREPPER.clone();
110 let mut tokenizer = tokenizer.clone();
111 let mut raw_stream = prepper.token_stream(text);
112 while raw_stream.advance() {
113 let mut token_stream = tokenizer.token_stream(&raw_stream.token().text);
114 while token_stream.advance() {
115 visitor(&token_stream.token().text);
116 }
117 }
118}
119
120const ALPHA_SPAN: usize = 37;
121const MAX_TOKEN: usize = ALPHA_SPAN.pow(2) + ALPHA_SPAN;
122const MIN_TOKEN: usize = 0;
123const NGRAM_N: usize = 3;
124
125fn ngram_to_token(ngram: &str, ngram_length: usize) -> u32 {
146 let mut token = 0;
147 for (idx, byte) in ngram.bytes().enumerate() {
149 let pos = if byte <= b'9' {
150 byte - b'0'
151 } else if byte <= b'z' {
152 byte - b'a' + 10
153 } else {
154 unreachable!()
155 } + 1;
156 debug_assert!(pos < ALPHA_SPAN as u8);
157 let mult = ALPHA_SPAN.pow(ngram_length as u32 - idx as u32 - 1) as u32;
158 token += pos as u32 * mult;
159 }
160 token
161}
162
163#[derive(Serialize)]
165struct NGramStatistics {
166 num_ngrams: usize,
167}
168
169#[derive(Debug)]
171pub struct NGramPostingList {
172 bitmap: RoaringTreemap,
173}
174
175impl DeepSizeOf for NGramPostingList {
176 fn deep_size_of_children(&self, _: &mut lance_core::deepsize::Context) -> usize {
177 self.bitmap.serialized_size()
178 }
179}
180
181#[derive(Debug, Clone)]
183pub struct NGramPostingListKey {
184 pub row_offset: u32,
185}
186
187impl CacheKey for NGramPostingListKey {
188 type ValueType = NGramPostingList;
189
190 fn key(&self) -> std::borrow::Cow<'_, str> {
191 format!("posting-list-{}", self.row_offset).into()
192 }
193
194 fn type_name() -> &'static str {
195 "NGramPostingList"
196 }
197
198 fn schema() -> CacheKeySchema {
199 CacheKeySchema::new("lance.scalar.ngram-posting-list-key", 1)
200 }
201
202 fn write_key(&self, builder: &mut KeyBuilder) {
203 builder.write_u32(self.row_offset);
204 }
205}
206
207impl NGramPostingList {
208 fn try_from_batch(
209 batch: RecordBatch,
210 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
211 ) -> Result<Self> {
212 let bitmap_bytes = batch.column(0).as_binary::<i32>().value(0);
213 let mut bitmap = RoaringTreemap::deserialize_from(bitmap_bytes)
214 .map_err(|e| Error::internal(format!("Error deserializing ngram list: {}", e)))?;
215 if let Some(frag_reuse_index_ref) = frag_reuse_index.as_ref() {
216 bitmap = frag_reuse_index_ref.remap_row_ids_roaring_tree_map(&bitmap);
217 }
218 Ok(Self { bitmap })
219 }
220
221 fn intersect<'a>(lists: impl IntoIterator<Item = &'a Self>) -> RoaringTreemap {
222 let mut iter = lists.into_iter();
223 let mut result = iter
224 .next()
225 .map(|list| list.bitmap.clone())
226 .unwrap_or_default();
227 for list in iter {
228 result &= &list.bitmap;
229 }
230 result
231 }
232}
233
234struct NGramPostingListReader {
236 reader: Arc<dyn IndexReader>,
237 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
238 index_cache: WeakLanceCache,
239}
240
241impl DeepSizeOf for NGramPostingListReader {
242 fn deep_size_of_children(&self, _: &mut lance_core::deepsize::Context) -> usize {
243 0
244 }
245}
246
247impl std::fmt::Debug for NGramPostingListReader {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 f.debug_struct("NGramListReader").finish()
250 }
251}
252
253impl NGramPostingListReader {
254 #[instrument(level = "debug", skip(self, metrics))]
255 pub async fn ngram_list(
256 &self,
257 row_offset: u32,
258 metrics: &dyn MetricsCollector,
259 ) -> Result<Arc<NGramPostingList>> {
260 let result = self.index_cache.get_or_insert_with_key_hit(NGramPostingListKey { row_offset }, || async move {
261 metrics.record_part_load();
262 tracing::info!(target: TRACE_IO_EVENTS, r#type=IO_TYPE_LOAD_SCALAR_PART, index_type="ngram", part_id=row_offset);
263 let batch = self
264 .reader
265 .read_range(
266 row_offset as usize..row_offset as usize + 1,
267 Some(&[POSTING_LIST_COL]),
268 )
269 .await?;
270 NGramPostingList::try_from_batch(batch, self.frag_reuse_index.clone())
271 }).await;
272 match &result {
273 Ok((_, true)) => metrics.record_index_cache_hit(),
274 _ => metrics.record_index_cache_miss(),
275 }
276 result.map(|(v, _)| v)
277 }
278}
279
280pub struct NGramIndex {
295 tokens: HashMap<u32, u32>,
297 list_reader: Arc<NGramPostingListReader>,
299 tokenizer: TextAnalyzer,
304 io_parallelism: usize,
305 store: Arc<dyn IndexStore>,
307}
308
309impl std::fmt::Debug for NGramIndex {
310 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311 f.debug_struct("NGramIndex")
312 .field("tokens", &self.tokens)
313 .field("list_reader", &self.list_reader)
314 .finish()
315 }
316}
317
318impl DeepSizeOf for NGramIndex {
319 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
320 self.tokens.deep_size_of_children(context)
321 }
322}
323
324impl NGramIndex {
325 async fn from_store(
326 store: Arc<dyn IndexStore>,
327 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
328 index_cache: &LanceCache,
329 ) -> Result<Self> {
330 let tokens = store.open_index_file(POSTINGS_FILENAME).await?;
331 let tokens = tokens
332 .read_range(0..tokens.num_rows(), Some(&[TOKENS_COL]))
333 .await?;
334
335 let tokens_map = HashMap::from_iter(
336 tokens
337 .column(0)
338 .as_primitive::<UInt32Type>()
339 .values()
340 .iter()
341 .copied()
342 .enumerate()
343 .map(|(idx, token)| (token, idx as u32)),
344 );
345
346 let posting_reader = Arc::new(NGramPostingListReader {
347 reader: store.open_index_file(POSTINGS_FILENAME).await?,
348 frag_reuse_index,
349 index_cache: WeakLanceCache::from(index_cache),
350 });
351
352 Ok(Self {
353 io_parallelism: store.io_parallelism(),
354 tokens: tokens_map,
355 list_reader: posting_reader,
356 tokenizer: NGRAM_TOKENIZER.clone(),
357 store,
358 })
359 }
360
361 fn remap_state(
362 &self,
363 state: NGramIndexSpillState,
364 mapping: &RowAddrRemap,
365 ) -> Result<Vec<RecordBatch>> {
366 let bitmaps = state
367 .bitmaps
368 .into_iter()
369 .map(|posting_list| {
370 RoaringTreemap::from_iter(posting_list.into_iter().filter_map(|row_id| {
371 match mapping.get(row_id) {
372 Some(Some(new_row_id)) => Some(new_row_id),
373 Some(None) => None,
374 None => Some(row_id),
375 }
376 }))
377 })
378 .collect();
379
380 NGramIndexSpillState {
381 tokens: state.tokens,
382 bitmaps,
383 }
384 .try_into_batches()
385 }
386
387 async fn load(
388 store: Arc<dyn IndexStore>,
389 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
390 index_cache: &LanceCache,
391 ) -> Result<Arc<Self>>
392 where
393 Self: Sized,
394 {
395 Ok(Arc::new(
396 Self::from_store(store, frag_reuse_index, index_cache).await?,
397 ))
398 }
399
400 pub async fn merge_segments(
404 segment_stores: &[Arc<dyn IndexStore>],
405 new_data: Option<SendableRecordBatchStream>,
406 dest_store: &dyn IndexStore,
407 old_data_filters: &[Option<super::OldIndexDataFilter>],
408 frag_reuse_index: Option<Arc<FragReuseIndex>>,
409 ) -> Result<CreatedIndex> {
410 let mut builder = NGramIndexBuilder::try_new(NGramIndexBuilderOptions::default())?;
411 let new_data_spills = match new_data {
414 Some(new_data) => builder.train(new_data).await?,
415 None => Vec::new(),
416 };
417 let file = builder
418 .merge_indices(
419 new_data_spills,
420 segment_stores,
421 old_data_filters,
422 frag_reuse_index,
423 dest_store,
424 )
425 .await?;
426 Ok(CreatedIndex {
427 index_details: prost_types::Any::from_msg(&pbold::NGramIndexDetails::default())?,
428 index_version: NGRAM_INDEX_VERSION,
429 files: vec![file],
430 })
431 }
432}
433
434#[async_trait]
435impl Index for NGramIndex {
436 fn as_any(&self) -> &dyn Any {
437 self
438 }
439
440 fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
441 self
442 }
443
444 fn statistics(&self) -> Result<serde_json::Value> {
445 let ngram_stats = NGramStatistics {
446 num_ngrams: self.tokens.len(),
447 };
448 serde_json::to_value(ngram_stats)
449 .map_err(|e| Error::internal(format!("Error serializing statistics: {}", e)))
450 }
451
452 async fn prewarm(&self) -> Result<()> {
453 Ok(())
455 }
456
457 fn index_type(&self) -> IndexType {
458 IndexType::NGram
459 }
460
461 async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
462 let mut frag_ids = RoaringBitmap::new();
463 for row_offset in self.tokens.values() {
464 let list = self
465 .list_reader
466 .ngram_list(*row_offset, &NoOpMetricsCollector)
467 .await?;
468 frag_ids.extend(
469 list.bitmap
470 .iter()
471 .map(|row_addr| RowAddress::from(row_addr).fragment_id()),
472 );
473 }
474 Ok(frag_ids)
475 }
476}
477
478#[async_trait]
479impl ScalarIndex for NGramIndex {
480 async fn search(
481 &self,
482 query: &dyn AnyQuery,
483 metrics: &dyn MetricsCollector,
484 ) -> Result<SearchResult> {
485 let query = query
486 .as_any()
487 .downcast_ref::<TextQuery>()
488 .ok_or_else(|| Error::invalid_input_source("Query is not a TextQuery".into()))?;
489 match query {
490 TextQuery::StringContains(substr) => {
491 if substr.len() < NGRAM_N {
492 return Ok(SearchResult::at_least(RowAddrTreeMap::new()));
494 }
495
496 let mut row_offsets = Vec::with_capacity(substr.len() * 3);
497 let mut missing = false;
498 tokenize_visitor(&self.tokenizer, substr, |ngram| {
499 let token = ngram_to_token(ngram, NGRAM_N);
500 if let Some(row_offset) = self.tokens.get(&token) {
501 row_offsets.push(*row_offset);
502 } else {
503 missing = true;
504 }
505 });
506 if missing {
508 return Ok(SearchResult::exact(RowAddrTreeMap::new()));
509 }
510 let posting_lists = futures::stream::iter(
511 row_offsets
512 .into_iter()
513 .map(|row_offset| self.list_reader.ngram_list(row_offset, metrics)),
514 )
515 .buffer_unordered(self.io_parallelism)
516 .try_collect::<Vec<_>>()
517 .await?;
518 metrics.record_comparisons(posting_lists.len());
519 let list_refs = posting_lists.iter().map(|list| list.as_ref());
520 let row_ids = NGramPostingList::intersect(list_refs);
521 Ok(SearchResult::at_most(RowAddrTreeMap::from(row_ids)))
522 }
523 TextQuery::Regex(pattern) => {
524 let trigram_query = ngram_regex::regex_to_trigram_query(pattern);
525 match &trigram_query {
526 ngram_regex::TrigramQuery::All => {
529 Ok(SearchResult::at_least(RowAddrTreeMap::new()))
530 }
531 ngram_regex::TrigramQuery::None => {
533 Ok(SearchResult::exact(RowAddrTreeMap::new()))
534 }
535 _ => {
536 let mut tokens = HashSet::new();
537 ngram_regex::collect_tokens(&trigram_query, &mut tokens);
538 let present = tokens.into_iter().filter_map(|token| {
542 self.tokens.get(&token).map(|offset| (token, *offset))
543 });
544 let lists = futures::stream::iter(present.map(|(token, offset)| {
545 self.list_reader
546 .ngram_list(offset, metrics)
547 .map(move |result| result.map(|list| (token, list)))
548 }))
549 .buffer_unordered(self.io_parallelism)
550 .try_collect::<Vec<(u32, Arc<NGramPostingList>)>>()
551 .await?;
552 metrics.record_comparisons(lists.len());
553 let bitmaps: HashMap<u32, RoaringTreemap> = lists
554 .into_iter()
555 .map(|(token, list)| (token, list.bitmap.clone()))
556 .collect();
557 let row_ids = ngram_regex::eval_trigram_query(&trigram_query, &bitmaps);
558 Ok(SearchResult::at_most(RowAddrTreeMap::from(row_ids)))
559 }
560 }
561 }
562 }
563 }
564
565 fn can_remap(&self) -> bool {
566 true
567 }
568
569 async fn remap(
570 &self,
571 mapping: &RowAddrRemap,
572 dest_store: &dyn IndexStore,
573 ) -> Result<CreatedIndex> {
574 let reader = self.store.open_index_file(POSTINGS_FILENAME).await?;
575 let mut writer = dest_store
576 .new_index_file(POSTINGS_FILENAME, POSTINGS_SCHEMA.clone())
577 .await?;
578
579 let mut spill_stream =
580 NGramIndexBuilder::stream_spill_reader(reader, MAX_POSTING_LIST_BATCH_BYTES)?;
581 while let Some(state) = spill_stream.try_next().await? {
582 for batch in self.remap_state(state, mapping)? {
583 writer.write_record_batch(batch).await?;
584 }
585 }
586
587 let file = writer.finish().await?;
588
589 Ok(CreatedIndex {
590 index_details: prost_types::Any::from_msg(&pbold::NGramIndexDetails::default())?,
591 index_version: NGRAM_INDEX_VERSION,
592 files: vec![file],
593 })
594 }
595
596 async fn update(
597 &self,
598 new_data: SendableRecordBatchStream,
599 dest_store: &dyn IndexStore,
600 _old_data_filter: Option<super::OldIndexDataFilter>,
601 ) -> Result<CreatedIndex> {
602 let mut builder = NGramIndexBuilder::try_new(NGramIndexBuilderOptions::default())?;
603 let spill_files = builder.train(new_data).await?;
604
605 let file = builder
606 .write_index(dest_store, spill_files, Some(self.store.clone()))
607 .await?;
608
609 Ok(CreatedIndex {
610 index_details: prost_types::Any::from_msg(&pbold::NGramIndexDetails::default())?,
611 index_version: NGRAM_INDEX_VERSION,
612 files: vec![file],
613 })
614 }
615
616 fn update_criteria(&self) -> UpdateCriteria {
617 UpdateCriteria::only_new_data(TrainingCriteria::new(TrainingOrdering::None).with_row_id())
618 }
619
620 fn derive_index_params(&self) -> Result<ScalarIndexParams> {
621 Ok(ScalarIndexParams::for_builtin(BuiltinIndexType::NGram))
622 }
623}
624
625#[derive(Debug, Clone)]
626pub struct NGramIndexBuilderOptions {
627 tokens_per_spill: usize,
628}
629
630static DEFAULT_TOKENS_PER_SPILL: LazyLock<usize> = LazyLock::new(|| {
632 std::env::var("LANCE_NGRAM_TOKENS_PER_SPILL")
633 .unwrap_or_else(|_| "1000000000".to_string())
634 .parse()
635 .expect("failed to parse LANCE_NGRAM_TOKENS_PER_SPILL")
636});
637static DEFAULT_NUM_PARTITIONS: LazyLock<usize> = LazyLock::new(|| {
643 std::env::var("LANCE_NGRAM_NUM_PARTITIONS")
644 .map(|s| s.parse().expect("failed to parse LANCE_NGRAM_PARALLELISM"))
645 .unwrap_or((get_num_compute_intensive_cpus() * 4).max(128))
646});
647static DEFAULT_TOKENIZE_PARALLELISM: LazyLock<usize> = LazyLock::new(|| {
649 std::env::var("LANCE_NGRAM_TOKENIZE_PARALLELISM")
650 .map(|s| {
651 s.parse()
652 .expect("failed to parse LANCE_NGRAM_TOKENIZE_PARALLELISM")
653 })
654 .unwrap_or(8)
655});
656
657impl Default for NGramIndexBuilderOptions {
658 fn default() -> Self {
659 Self {
660 tokens_per_spill: *DEFAULT_TOKENS_PER_SPILL,
661 }
662 }
663}
664
665struct NGramIndexSpillState {
669 tokens: UInt32Array,
670 bitmaps: Vec<RoaringTreemap>,
671}
672
673struct NGramIndexSpillStateBuilder {
674 tokens: UInt32Builder,
675 bitmaps: Vec<RoaringTreemap>,
676 serialized_bytes: usize,
677}
678
679impl NGramIndexSpillStateBuilder {
680 fn new() -> Self {
681 Self {
682 tokens: UInt32Builder::with_capacity(0),
683 bitmaps: Vec::new(),
684 serialized_bytes: 0,
685 }
686 }
687
688 fn is_empty(&self) -> bool {
689 self.bitmaps.is_empty()
690 }
691
692 fn len(&self) -> usize {
693 self.bitmaps.len()
694 }
695
696 fn push(
697 &mut self,
698 token: u32,
699 bitmap: RoaringTreemap,
700 max_batch_bytes: usize,
701 ) -> Result<Option<NGramIndexSpillState>> {
702 let posting_size = bitmap.serialized_size();
703 if posting_size > max_batch_bytes {
704 return Err(Error::invalid_input(format!(
705 "posting list for ngram token {} serializes to {} bytes, which exceeds the {} bytes that fit in a single binary array",
706 token, posting_size, max_batch_bytes,
707 )));
708 }
709
710 let new_size = self
711 .serialized_bytes
712 .checked_add(posting_size)
713 .ok_or_else(|| {
714 Error::invalid_input(format!(
715 "posting list byte size overflowed while adding ngram token {}",
716 token
717 ))
718 })?;
719 let full_state = if !self.is_empty() && new_size > max_batch_bytes {
720 Some(self.finish())
721 } else {
722 None
723 };
724
725 self.tokens.append_value(token);
726 self.bitmaps.push(bitmap);
727 self.serialized_bytes = posting_size
728 .checked_add(if full_state.is_some() {
729 0
730 } else {
731 self.serialized_bytes
732 })
733 .ok_or_else(|| {
734 Error::invalid_input(format!(
735 "posting list byte size overflowed while adding ngram token {}",
736 token
737 ))
738 })?;
739
740 Ok(full_state)
741 }
742
743 fn finish(&mut self) -> NGramIndexSpillState {
744 self.serialized_bytes = 0;
745 NGramIndexSpillState {
746 tokens: std::mem::replace(&mut self.tokens, UInt32Builder::with_capacity(0)).finish(),
747 bitmaps: std::mem::take(&mut self.bitmaps),
748 }
749 }
750}
751
752impl NGramIndexSpillState {
753 fn try_from_batch(batch: RecordBatch) -> Result<Self> {
754 let tokens = batch
755 .column_by_name(TOKENS_COL)
756 .expect_ok()?
757 .as_primitive::<UInt32Type>()
758 .clone();
759 let postings = batch
760 .column_by_name(POSTING_LIST_COL)
761 .expect_ok()?
762 .as_binary::<i32>();
763
764 let bitmaps = postings
765 .into_iter()
766 .map(|bytes| {
767 RoaringTreemap::deserialize_from(bytes.expect_ok()?)
768 .map_err(|e| Error::internal(format!("Error deserializing ngram list: {}", e)))
769 })
770 .collect::<Result<Vec<_>>>()?;
771
772 Ok(Self { tokens, bitmaps })
773 }
774
775 fn try_into_batches(self) -> Result<Vec<RecordBatch>> {
776 self.try_into_batches_impl(MAX_POSTING_LIST_BATCH_BYTES)
777 }
778
779 fn try_into_batches_impl(self, max_batch_bytes: usize) -> Result<Vec<RecordBatch>> {
784 debug_assert_eq!(self.tokens.len(), self.bitmaps.len());
785 debug_assert!(max_batch_bytes <= i32::MAX as usize);
786 let make_batch =
787 |tokens: UInt32Array, values: Vec<u8>, offsets: Vec<i32>| -> Result<RecordBatch> {
788 let posting_array = BinaryArray::new(
789 OffsetBuffer::new(ScalarBuffer::from(offsets)),
790 Buffer::from_vec(values),
791 None,
792 );
793 Ok(RecordBatch::try_new(
794 POSTINGS_SCHEMA.clone(),
795 vec![Arc::new(tokens), Arc::new(posting_array)],
796 )?)
797 };
798
799 let mut batches = Vec::new();
800 let mut values: Vec<u8> = Vec::new();
801 let mut offsets: Vec<i32> = vec![0];
802 let mut batch_start = 0;
803 for (idx, bitmap) in self.bitmaps.into_iter().enumerate() {
804 let posting_size = bitmap.serialized_size();
805 if posting_size > max_batch_bytes {
806 return Err(Error::invalid_input(format!(
807 "posting list for ngram token {} serializes to {} bytes, which exceeds the {} bytes that fit in a single binary array",
808 self.tokens.value(idx),
809 posting_size,
810 max_batch_bytes,
811 )));
812 }
813 if values.len() + posting_size > max_batch_bytes {
814 batches.push(make_batch(
815 self.tokens.slice(batch_start, idx - batch_start),
816 std::mem::take(&mut values),
817 std::mem::replace(&mut offsets, vec![0]),
818 )?);
819 batch_start = idx;
820 }
821 bitmap.serialize_into(&mut values)?;
822 offsets.push(values.len() as i32);
823 }
824 if offsets.len() > 1 || batches.is_empty() {
825 batches.push(make_batch(
826 self.tokens.slice(batch_start, offsets.len() - 1),
827 values,
828 offsets,
829 )?);
830 }
831 Ok(batches)
832 }
833
834 fn remap_and_filter_rows(
835 self,
836 frag_reuse_index: Option<&Arc<FragReuseIndex>>,
837 filter: Option<&super::OldIndexDataFilter>,
838 ) -> Self {
839 if let Some(fri) = frag_reuse_index {
840 return self.remap_then_keep(fri, filter);
841 }
842 match filter {
843 None => self,
844 Some(filter) => self.retain_rows(filter),
845 }
846 }
847
848 fn remap_then_keep(
851 self,
852 fri: &Arc<FragReuseIndex>,
853 filter: Option<&super::OldIndexDataFilter>,
854 ) -> Self {
855 let mut tokens = UInt32Builder::with_capacity(self.tokens.len());
856 let mut bitmaps = Vec::with_capacity(self.bitmaps.len());
857 for (token, bitmap) in self.tokens.values().iter().zip(self.bitmaps) {
858 let remapped = fri.remap_row_ids_roaring_tree_map(&bitmap);
859 let kept = match filter {
860 Some(filter) => RoaringTreemap::from_iter(
861 remapped
862 .into_iter()
863 .filter(|row_id| old_filter_keeps(filter, *row_id)),
864 ),
865 None => remapped,
866 };
867 if !kept.is_empty() {
868 tokens.append_value(*token);
869 bitmaps.push(kept);
870 }
871 }
872 Self {
873 tokens: tokens.finish(),
874 bitmaps,
875 }
876 }
877
878 fn retain_rows(self, filter: &super::OldIndexDataFilter) -> Self {
879 if let super::OldIndexDataFilter::Fragments { to_keep, .. } = filter
880 && self.bitmaps.iter().all(|bitmap| {
881 bitmap
882 .bitmaps()
883 .all(|(fragment, _)| to_keep.contains(fragment))
884 })
885 {
886 return self;
887 }
888 let mut tokens = UInt32Builder::with_capacity(self.tokens.len());
889 let mut bitmaps = Vec::with_capacity(self.bitmaps.len());
890 for (token, bitmap) in self.tokens.values().iter().zip(self.bitmaps) {
891 let kept = RoaringTreemap::from_iter(
892 bitmap
893 .into_iter()
894 .filter(|row_id| old_filter_keeps(filter, *row_id)),
895 );
896 if !kept.is_empty() {
897 tokens.append_value(*token);
898 bitmaps.push(kept);
899 }
900 }
901 Self {
902 tokens: tokens.finish(),
903 bitmaps,
904 }
905 }
906}
907
908fn old_filter_keeps(filter: &super::OldIndexDataFilter, row_id: u64) -> bool {
909 match filter {
910 super::OldIndexDataFilter::Fragments { to_keep, .. } => {
911 to_keep.contains((row_id >> 32) as u32)
912 }
913 super::OldIndexDataFilter::RowIds(valid) => valid.contains(row_id),
914 }
915}
916
917struct NGramIndexBuildState {
920 tokens_map: BTreeMap<u32, RoaringTreemap>,
921}
922
923impl NGramIndexBuildState {
924 fn starting() -> Self {
925 Self {
926 tokens_map: BTreeMap::new(),
927 }
928 }
929
930 fn take(&mut self) -> Self {
931 let mut taken = Self::starting();
932 std::mem::swap(&mut self.tokens_map, &mut taken.tokens_map);
933 taken
934 }
935
936 fn into_spill(self) -> NGramIndexSpillState {
937 let tokens = UInt32Array::from_iter_values(self.tokens_map.keys().copied());
939 let bitmaps = Vec::from_iter(self.tokens_map.into_values());
940
941 NGramIndexSpillState { bitmaps, tokens }
942 }
943}
944
945pub struct NGramIndexBuilder {
958 tokenizer: TextAnalyzer,
959 options: NGramIndexBuilderOptions,
960 tmpdir: Arc<TempDir>,
961 spill_store: Arc<dyn IndexStore>,
962
963 tokens_seen: usize,
964 worker_number: usize,
965 has_flushed: bool,
966
967 state: NGramIndexBuildState,
968}
969
970impl NGramIndexBuilder {
971 pub fn try_new(options: NGramIndexBuilderOptions) -> Result<Self> {
972 Self::from_state(NGramIndexBuildState::starting(), options)
973 }
974
975 fn clone_worker(&self, worker_number: usize) -> Self {
976 let mut bitmaps = Vec::with_capacity(36 * 36 * 36 + 1);
977 bitmaps.push(RoaringTreemap::new());
979 Self {
980 tokenizer: self.tokenizer.clone(),
981 state: NGramIndexBuildState::starting(),
982 tmpdir: self.tmpdir.clone(),
983 spill_store: self.spill_store.clone(),
984 options: self.options.clone(),
985 tokens_seen: 0,
986 worker_number,
987 has_flushed: false,
988 }
989 }
990
991 fn from_state(state: NGramIndexBuildState, options: NGramIndexBuilderOptions) -> Result<Self> {
992 let tokenizer = NGRAM_TOKENIZER.clone();
993
994 let tmpdir = Arc::new(TempDir::default());
995 let spill_store = Arc::new(LanceIndexStore::new(
996 Arc::new(ObjectStore::local()),
997 tmpdir.obj_path(),
998 Arc::new(LanceCache::no_cache()),
999 ));
1000
1001 Ok(Self {
1002 tokenizer,
1003 state,
1004 tmpdir,
1005 spill_store,
1006 options,
1007 tokens_seen: 0,
1008 worker_number: 0,
1009 has_flushed: false,
1010 })
1011 }
1012
1013 fn validate_schema(schema: &Schema) -> Result<()> {
1014 if schema.fields().len() != 2 {
1015 return Err(Error::invalid_input_source(
1016 "Ngram index schema must have exactly two fields".into(),
1017 ));
1018 }
1019 let values_field = schema.field_with_name(VALUE_COLUMN_NAME)?;
1020 if *values_field.data_type() != DataType::Utf8
1021 && *values_field.data_type() != DataType::LargeUtf8
1022 {
1023 return Err(Error::invalid_input_source(
1024 "First field in ngram index schema must be of type Utf8/LargeUtf8".into(),
1025 ));
1026 }
1027 let row_id_field = schema.field_with_name(ROW_ID)?;
1028 if *row_id_field.data_type() != DataType::UInt64 {
1029 return Err(Error::invalid_input_source(
1030 "Second field in ngram index schema must be of type UInt64".into(),
1031 ));
1032 }
1033 Ok(())
1034 }
1035
1036 async fn process_batch(&mut self, tokens_and_ids: Vec<(u32, u64)>) -> Result<()> {
1037 let mut tokens_seen = 0;
1038 for (token, row_id) in tokens_and_ids {
1039 tokens_seen += 1;
1040 self.state
1045 .tokens_map
1046 .entry(token)
1047 .or_default()
1048 .insert(row_id);
1049 }
1050 self.tokens_seen += tokens_seen;
1051 if self.tokens_seen >= self.options.tokens_per_spill {
1052 let state = self.state.take();
1053 self.flush(state).await?;
1054 }
1055 Ok(())
1056 }
1057
1058 fn spill_filename(id: usize) -> String {
1059 format!("spill-{}.lance", id)
1060 }
1061
1062 fn tmp_spill_filename(id: usize) -> String {
1063 format!("spill-{}.lance.tmp", id)
1064 }
1065
1066 async fn flush(&mut self, state: NGramIndexBuildState) -> Result<bool> {
1067 if self.tokens_seen == 0 {
1068 assert!(state.tokens_map.is_empty());
1069 return Ok(self.has_flushed);
1070 }
1071 self.tokens_seen = 0;
1072 let spill_state = state.into_spill();
1073 let flush_start = Instant::now();
1074 debug_assert_ne!(self.worker_number, 0);
1076 if self.has_flushed {
1077 info!("Merging flush for worker {}", self.worker_number);
1078 let mut writer = self
1080 .spill_store
1081 .new_index_file(
1082 &Self::tmp_spill_filename(self.worker_number),
1083 POSTINGS_SCHEMA.clone(),
1084 )
1085 .await?;
1086
1087 let left_stream = stream::once(std::future::ready(Ok(spill_state)));
1088 let right_stream =
1089 Self::stream_spill(self.spill_store.clone(), self.worker_number).await?;
1090 Self::merge_spill_streams(left_stream, right_stream, writer.as_mut()).await?;
1091 drop(writer);
1092 self.spill_store
1093 .rename_index_file(
1094 &Self::tmp_spill_filename(self.worker_number),
1095 &Self::spill_filename(self.worker_number),
1096 )
1097 .await?;
1098 } else {
1099 info!("Initial flush for worker {}", self.worker_number);
1101 self.has_flushed = true;
1102 let writer = self
1103 .spill_store
1104 .new_index_file(
1105 &Self::spill_filename(self.worker_number),
1106 POSTINGS_SCHEMA.clone(),
1107 )
1108 .await?;
1109 self.write(writer, spill_state).await?;
1110 }
1111 let flush_time = flush_start.elapsed();
1112 info!(
1113 "Flushed worker {} in {}ms",
1114 self.worker_number,
1115 flush_time.as_millis()
1116 );
1117 Ok(true)
1118 }
1119
1120 fn tokenize_and_partition(
1121 tokenizer: &TextAnalyzer,
1122 batch: RecordBatch,
1123 num_workers: usize,
1124 ) -> Result<Vec<Vec<(u32, u64)>>> {
1125 let text_iter = iter_str_array(batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?);
1126 let row_id_col = batch
1127 .column_by_name(ROW_ID)
1128 .expect_ok()?
1129 .as_primitive::<UInt64Type>();
1130 let mut partitions = vec![Vec::with_capacity(batch.num_rows() * 1000); num_workers];
1132 let divisor = (MAX_TOKEN - MIN_TOKEN) / num_workers;
1133 for (text, row_id) in text_iter.zip(row_id_col.values()) {
1134 if let Some(text) = text {
1135 tokenize_visitor(tokenizer, text, |token| {
1136 let token = ngram_to_token(token, NGRAM_N);
1137 let partition_id = (token as usize).saturating_sub(MIN_TOKEN) / divisor;
1138 partitions[partition_id % num_workers].push((token, *row_id));
1139 });
1140 } else {
1141 partitions[0].push((0, *row_id));
1142 }
1143 }
1144 Ok(partitions)
1145 }
1146
1147 pub async fn train(&mut self, data: SendableRecordBatchStream) -> Result<Vec<usize>> {
1148 let schema = data.schema();
1149 Self::validate_schema(schema.as_ref())?;
1150
1151 let num_workers = *DEFAULT_NUM_PARTITIONS;
1152 let mut senders = Vec::with_capacity(num_workers);
1153 let mut builders = Vec::with_capacity(num_workers);
1154 for worker_idx in 0..num_workers {
1155 let (send, mut recv) = tokio::sync::mpsc::channel(2);
1156 senders.push(send);
1157
1158 let mut builder = self.clone_worker(worker_idx + 1);
1159 let future = tokio::spawn(async move {
1160 while let Some(partition) = recv.recv().await {
1161 builder.process_batch(partition).await?;
1162 }
1163 Result::Ok(builder)
1164 });
1165 builders.push(future);
1166 }
1167
1168 let mut partitions_stream = data
1169 .and_then(|batch| {
1170 let tokenizer = self.tokenizer.clone();
1171 std::future::ready(Ok(tokio::task::spawn(async move {
1172 Ok(Self::tokenize_and_partition(
1173 &tokenizer,
1174 batch,
1175 num_workers,
1176 )?)
1177 })
1178 .map(|res| res.unwrap())))
1179 })
1180 .try_buffer_unordered(*DEFAULT_TOKENIZE_PARALLELISM);
1181
1182 while let Some(partitions) = partitions_stream.try_next().await? {
1183 for (part_idx, partition) in partitions.into_iter().enumerate() {
1184 senders[part_idx].send(partition).await.unwrap();
1185 }
1186 }
1187
1188 std::mem::drop(senders);
1189 let builders = futures::future::try_join_all(builders).await?;
1190
1191 let mut to_spill = Vec::with_capacity(builders.len());
1195
1196 for builder in builders {
1197 let mut builder = builder?;
1198 let state = builder.state.take();
1199 if builder.flush(state).await? {
1200 to_spill.push(builder.worker_number);
1201 }
1202 }
1203
1204 Ok(to_spill)
1205 }
1206
1207 async fn write(
1208 &mut self,
1209 mut writer: Box<dyn IndexWriter>,
1210 state: NGramIndexSpillState,
1211 ) -> Result<()> {
1212 Self::write_state(writer.as_mut(), state).await?;
1213 writer.finish().await?;
1214
1215 Ok(())
1216 }
1217
1218 async fn write_state(writer: &mut dyn IndexWriter, state: NGramIndexSpillState) -> Result<()> {
1219 for batch in state.try_into_batches()? {
1220 writer.write_record_batch(batch).await?;
1221 }
1222 Ok(())
1223 }
1224
1225 fn stream_spill_reader(
1226 reader: Arc<dyn IndexReader>,
1227 max_batch_bytes: usize,
1228 ) -> Result<impl Stream<Item = Result<NGramIndexSpillState>>> {
1229 let num_rows = reader.num_rows();
1230
1231 Ok(stream::try_unfold(
1232 (0, NGramIndexSpillStateBuilder::new()),
1233 move |(mut offset, mut builder)| {
1234 let reader = reader.clone();
1235 async move {
1236 loop {
1237 if offset >= num_rows {
1238 return if builder.is_empty() {
1239 Ok(None)
1240 } else {
1241 let state = builder.finish();
1242 Ok(Some((state, (offset, builder))))
1243 };
1244 }
1245
1246 let state = NGramIndexSpillState::try_from_batch(
1247 reader.read_range(offset..offset + 1, None).await?,
1248 )?;
1249 offset += 1;
1250 for (token, bitmap) in
1251 state.tokens.values().iter().copied().zip(state.bitmaps)
1252 {
1253 if let Some(full) = builder.push(token, bitmap, max_batch_bytes)? {
1254 return Ok(Some((full, (offset, builder))));
1255 }
1256 if builder.len() >= POSTING_LIST_STREAM_BATCH_ROWS {
1257 let state = builder.finish();
1258 return Ok(Some((state, (offset, builder))));
1259 }
1260 }
1261 }
1262 }
1263 .boxed()
1264 },
1265 ))
1266 }
1267
1268 async fn stream_spill(
1269 spill_store: Arc<dyn IndexStore>,
1270 id: usize,
1271 ) -> Result<impl Stream<Item = Result<NGramIndexSpillState>>> {
1272 let reader = spill_store
1273 .open_index_file(&Self::spill_filename(id))
1274 .await?;
1275 Self::stream_spill_reader(reader, MAX_POSTING_LIST_BATCH_BYTES)
1276 }
1277
1278 fn merge_spill_states(
1279 left_opt: &mut Option<NGramIndexSpillState>,
1280 right_opt: &mut Option<NGramIndexSpillState>,
1281 ) -> NGramIndexSpillState {
1282 let left = left_opt.take().unwrap();
1283 let right = right_opt.take().unwrap();
1284
1285 let item_capacity = left.tokens.len() + right.tokens.len();
1286 let mut merged_tokens = UInt32Builder::with_capacity(item_capacity);
1287 let mut merged_bitmaps = Vec::with_capacity(left.bitmaps.len() + right.bitmaps.len());
1288
1289 let mut left_tokens = left.tokens.values().iter().copied();
1290 let mut left_bitmaps = left.bitmaps.into_iter();
1291 let mut right_tokens = right.tokens.values().iter().copied();
1292 let mut right_bitmaps = right.bitmaps.into_iter();
1293
1294 let mut left_token = left_tokens.next();
1295 let mut left_bitmap = left_bitmaps.next();
1296 let mut right_token = right_tokens.next();
1297 let mut right_bitmap = right_bitmaps.next();
1298
1299 while left_token.is_some() && right_token.is_some() {
1300 let left_token_val = left_token.unwrap();
1301 let right_token_val = right_token.unwrap();
1302 match left_token_val.cmp(&right_token_val) {
1303 std::cmp::Ordering::Less => {
1304 merged_tokens.append_value(left_token_val);
1305 merged_bitmaps.push(left_bitmap.unwrap());
1306 left_token = left_tokens.next();
1307 left_bitmap = left_bitmaps.next();
1308 }
1309 std::cmp::Ordering::Greater => {
1310 merged_tokens.append_value(right_token_val);
1311 merged_bitmaps.push(right_bitmap.unwrap());
1312 right_token = right_tokens.next();
1313 right_bitmap = right_bitmaps.next();
1314 }
1315 std::cmp::Ordering::Equal => {
1316 merged_tokens.append_value(left_token_val);
1317 merged_bitmaps.push(left_bitmap.unwrap() | &right_bitmap.unwrap());
1318 left_token = left_tokens.next();
1319 left_bitmap = left_bitmaps.next();
1320 right_token = right_tokens.next();
1321 right_bitmap = right_bitmaps.next();
1322 }
1323 }
1324 }
1325
1326 let collect_remaining = |cur_token, tokens, cur_bitmap, bitmaps| {
1327 let tokens = UInt32Array::from_iter_values(once(cur_token).chain(tokens));
1328 let bitmaps = once(cur_bitmap).chain(bitmaps).collect::<Vec<_>>();
1329 NGramIndexSpillState { tokens, bitmaps }
1330 };
1331
1332 if let Some(left_token) = left_token {
1333 *left_opt = Some(collect_remaining(
1334 left_token,
1335 left_tokens,
1336 left_bitmap.unwrap(),
1337 left_bitmaps,
1338 ));
1339 } else {
1340 *left_opt = None;
1341 }
1342 if let Some(right_token) = right_token {
1343 *right_opt = Some(collect_remaining(
1344 right_token,
1345 right_tokens,
1346 right_bitmap.unwrap(),
1347 right_bitmaps,
1348 ));
1349 } else {
1350 *right_opt = None;
1351 }
1352
1353 NGramIndexSpillState {
1354 tokens: merged_tokens.finish(),
1355 bitmaps: merged_bitmaps,
1356 }
1357 }
1358
1359 async fn merge_spill_streams(
1360 mut left_stream: impl Stream<Item = Result<NGramIndexSpillState>> + Unpin,
1361 mut right_stream: impl Stream<Item = Result<NGramIndexSpillState>> + Unpin,
1362 writer: &mut dyn IndexWriter,
1363 ) -> Result<IndexFile> {
1364 let mut left_state = left_stream.try_next().await?;
1365 let mut right_state = right_stream.try_next().await?;
1366
1367 while left_state.is_some() || right_state.is_some() {
1368 if left_state.is_none() {
1369 let state = right_state.take().expect_ok()?;
1371 Self::write_state(writer, state).await?;
1372 while let Some(state) = right_stream.try_next().await? {
1373 Self::write_state(writer, state).await?;
1374 }
1375 } else if right_state.is_none() {
1376 let state = left_state.take().expect_ok()?;
1378 Self::write_state(writer, state).await?;
1379 while let Some(state) = left_stream.try_next().await? {
1380 Self::write_state(writer, state).await?;
1381 }
1382 } else {
1383 let merged = Self::merge_spill_states(&mut left_state, &mut right_state);
1385 Self::write_state(writer, merged).await?;
1386 if left_state.is_none() {
1387 left_state = left_stream.try_next().await?;
1388 }
1389 if right_state.is_none() {
1390 right_state = right_stream.try_next().await?;
1391 }
1392 }
1393 }
1394
1395 writer.finish().await
1396 }
1397
1398 async fn merge_spill_files(
1399 spill_store: Arc<dyn IndexStore>,
1400 index_of_left: usize,
1401 index_of_right: usize,
1402 output_index: usize,
1403 ) -> Result<()> {
1404 info!(
1406 "Merge spill files {} and {} into {}",
1407 index_of_left, index_of_right, output_index
1408 );
1409
1410 let mut writer = spill_store
1411 .new_index_file(&Self::spill_filename(output_index), POSTINGS_SCHEMA.clone())
1412 .await?;
1413
1414 let (left_stream, right_stream) = futures::try_join!(
1415 Self::stream_spill(spill_store.clone(), index_of_left),
1416 Self::stream_spill(spill_store.clone(), index_of_right)
1417 )?;
1418
1419 Self::merge_spill_streams(left_stream, right_stream, writer.as_mut()).await?;
1420
1421 spill_store
1422 .delete_index_file(&Self::spill_filename(index_of_left))
1423 .await?;
1424 spill_store
1425 .delete_index_file(&Self::spill_filename(index_of_right))
1426 .await?;
1427
1428 Ok(())
1429 }
1430
1431 async fn merge_spills(&mut self, mut spill_files: Vec<usize>) -> Result<usize> {
1438 info!(
1439 "Merging {} index files into one combined index",
1440 spill_files.len()
1441 );
1442
1443 let mut spill_counter = spill_files.iter().max().expect_ok()? + 1;
1444 while spill_files.len() > 1 {
1445 let mut new_spills = Vec::with_capacity(spill_files.len() / 2);
1446 while spill_files.len() >= 2 {
1447 let left = spill_files.pop().expect_ok()?;
1448 let right = spill_files.pop().expect_ok()?;
1449 new_spills.push(tokio::spawn(Self::merge_spill_files(
1450 self.spill_store.clone(),
1451 left,
1452 right,
1453 spill_counter + new_spills.len(),
1454 )));
1455 }
1456 for i in 0..new_spills.len() {
1457 spill_files.push(spill_counter + i);
1458 }
1459 spill_counter += new_spills.len();
1460 for result in futures::future::try_join_all(new_spills).await? {
1461 result?;
1462 }
1463 }
1464
1465 spill_files.pop().expect_ok()
1466 }
1467
1468 async fn merge_old_index(
1469 &mut self,
1470 new_data_num: usize,
1471 old_index: Arc<dyn IndexStore>,
1472 ) -> Result<usize> {
1473 info!("Merging old index into new index");
1474 let final_num = new_data_num + 1;
1475
1476 let mut writer = self
1477 .spill_store
1478 .new_index_file(&Self::spill_filename(final_num), POSTINGS_SCHEMA.clone())
1479 .await?;
1480
1481 let left_stream = Self::stream_spill(self.spill_store.clone(), new_data_num).await?;
1482 let old_reader = old_index.open_index_file(POSTINGS_FILENAME).await?;
1483 let right_stream = Self::stream_spill_reader(old_reader, MAX_POSTING_LIST_BATCH_BYTES)?;
1484
1485 Self::merge_spill_streams(left_stream, right_stream, writer.as_mut()).await?;
1486
1487 self.spill_store
1488 .delete_index_file(&Self::spill_filename(new_data_num))
1489 .await?;
1490
1491 Ok(final_num)
1492 }
1493
1494 pub async fn write_index(
1495 mut self,
1496 store: &dyn IndexStore,
1497 spill_files: Vec<usize>,
1498 old_index: Option<Arc<dyn IndexStore>>,
1499 ) -> Result<IndexFile> {
1500 let mut writer = store
1501 .new_index_file(POSTINGS_FILENAME, POSTINGS_SCHEMA.clone())
1502 .await?;
1503
1504 if spill_files.is_empty() {
1505 if let Some(old_index) = old_index {
1506 return old_index.copy_index_file(POSTINGS_FILENAME, store).await;
1508 } else {
1509 let mut writer = store
1511 .new_index_file(POSTINGS_FILENAME, POSTINGS_SCHEMA.clone())
1512 .await?;
1513 return writer.finish().await;
1514 }
1515 }
1516
1517 let mut index_to_copy = self.merge_spills(spill_files).await?;
1518
1519 if let Some(old_index) = old_index {
1520 index_to_copy = self.merge_old_index(index_to_copy, old_index).await?;
1521 }
1522
1523 let reader = self
1524 .spill_store
1525 .open_index_file(&Self::spill_filename(index_to_copy))
1526 .await?;
1527
1528 let mut spill_stream = Self::stream_spill_reader(reader, MAX_POSTING_LIST_BATCH_BYTES)?;
1529 while let Some(state) = spill_stream.try_next().await? {
1530 Self::write_state(writer.as_mut(), state).await?;
1531 }
1532
1533 writer.finish().await
1534 }
1535
1536 async fn open_segment_stream(
1537 store: Arc<dyn IndexStore>,
1538 frag_reuse_index: Option<Arc<FragReuseIndex>>,
1539 filter: Option<super::OldIndexDataFilter>,
1540 ) -> Result<Pin<Box<dyn Stream<Item = Result<NGramIndexSpillState>> + Send>>> {
1541 let reader = store.open_index_file(POSTINGS_FILENAME).await?;
1542 let stream =
1543 Self::stream_spill_reader(reader, MAX_POSTING_LIST_BATCH_BYTES)?.map(move |res| {
1544 res.map(|state| {
1545 state.remap_and_filter_rows(frag_reuse_index.as_ref(), filter.as_ref())
1546 })
1547 });
1548 Ok(Box::pin(stream))
1549 }
1550
1551 async fn merge_indices(
1554 mut self,
1555 new_data_spills: Vec<usize>,
1556 segment_stores: &[Arc<dyn IndexStore>],
1557 old_data_filters: &[Option<super::OldIndexDataFilter>],
1558 frag_reuse_index: Option<Arc<FragReuseIndex>>,
1559 dest_store: &dyn IndexStore,
1560 ) -> Result<IndexFile> {
1561 if old_data_filters.len() != segment_stores.len() {
1562 return Err(Error::invalid_input(format!(
1563 "NGram merge: expected one old-data filter per source segment \
1564 ({} segments) but got {}",
1565 segment_stores.len(),
1566 old_data_filters.len()
1567 )));
1568 }
1569
1570 let mut writer = dest_store
1571 .new_index_file(POSTINGS_FILENAME, POSTINGS_SCHEMA.clone())
1572 .await?;
1573
1574 if segment_stores.is_empty() && new_data_spills.is_empty() {
1576 return writer.finish().await;
1577 }
1578
1579 if segment_stores.len() == 1 {
1581 let seg_stream = Self::open_segment_stream(
1582 segment_stores[0].clone(),
1583 frag_reuse_index,
1584 old_data_filters[0].clone(),
1585 )
1586 .await?;
1587 let new_data_stream: Pin<Box<dyn Stream<Item = Result<NGramIndexSpillState>> + Send>> =
1588 if new_data_spills.is_empty() {
1589 Box::pin(stream::empty::<Result<NGramIndexSpillState>>())
1590 } else {
1591 let new_data = self.merge_spills(new_data_spills).await?;
1592 Box::pin(Self::stream_spill(self.spill_store.clone(), new_data).await?)
1593 };
1594 return Self::merge_spill_streams(seg_stream, new_data_stream, writer.as_mut()).await;
1595 }
1596
1597 let mut spills = new_data_spills;
1598 let next_spill = spills.iter().copied().max().map_or(0, |id| id + 1);
1599 let mut tasks = Vec::new();
1600 for (offset, (stores, filters)) in segment_stores
1601 .chunks(2)
1602 .zip(old_data_filters.chunks(2))
1603 .enumerate()
1604 {
1605 let out = next_spill + offset;
1606 spills.push(out);
1607 let spill_store = self.spill_store.clone();
1608 let fri = frag_reuse_index.clone();
1609 let s0 = stores[0].clone();
1610 let f0 = filters[0].clone();
1611 let second = (stores.len() == 2).then(|| (stores[1].clone(), filters[1].clone()));
1612 tasks.push(tokio::spawn(async move {
1613 let mut writer = spill_store
1614 .new_index_file(&Self::spill_filename(out), POSTINGS_SCHEMA.clone())
1615 .await?;
1616 let mut left = Self::open_segment_stream(s0, fri.clone(), f0).await?;
1617 match second {
1618 Some((s1, f1)) => {
1619 let right = Self::open_segment_stream(s1, fri, f1).await?;
1620 Self::merge_spill_streams(left, right, writer.as_mut()).await?;
1622 }
1623 None => {
1624 while let Some(state) = left.try_next().await? {
1626 Self::write_state(writer.as_mut(), state).await?;
1627 }
1628 writer.finish().await?;
1629 }
1630 }
1631 Result::Ok(())
1632 }));
1633 }
1634 for result in futures::future::try_join_all(tasks).await? {
1635 result?;
1636 }
1637
1638 let final_spill = self.merge_spills(spills).await?;
1640 let reader = self
1641 .spill_store
1642 .open_index_file(&Self::spill_filename(final_spill))
1643 .await?;
1644 let mut spill_stream = Self::stream_spill_reader(reader, MAX_POSTING_LIST_BATCH_BYTES)?;
1645 while let Some(state) = spill_stream.try_next().await? {
1646 Self::write_state(writer.as_mut(), state).await?;
1647 }
1648 writer.finish().await
1649 }
1650}
1651
1652#[derive(Debug, Default)]
1653pub struct NGramIndexPlugin;
1654
1655impl NGramIndexPlugin {
1656 pub async fn train_ngram_index(
1657 batches_source: SendableRecordBatchStream,
1658 index_store: &dyn IndexStore,
1659 ) -> Result<IndexFile> {
1660 let mut builder = NGramIndexBuilder::try_new(NGramIndexBuilderOptions::default())?;
1661
1662 let spill_files = builder.train(batches_source).await?;
1663
1664 builder.write_index(index_store, spill_files, None).await
1665 }
1666}
1667
1668#[async_trait]
1669impl BasicTrainer for NGramIndexPlugin {
1670 fn new_training_request(
1671 &self,
1672 _params: &str,
1673 field: &Field,
1674 ) -> Result<Box<dyn TrainingRequest>> {
1675 if !matches!(field.data_type(), DataType::Utf8 | DataType::LargeUtf8) {
1676 return Err(Error::invalid_input_source(format!(
1677 "A ngram index can only be created on a Utf8 or LargeUtf8 field. Column has type {:?}",
1678 field.data_type()
1679 )
1680 .into()));
1681 }
1682 Ok(Box::new(DefaultTrainingRequest::new(
1683 TrainingCriteria::new(TrainingOrdering::None).with_row_id(),
1684 )))
1685 }
1686
1687 async fn train_index(
1688 &self,
1689 data: SendableRecordBatchStream,
1690 index_store: &dyn IndexStore,
1691 _request: Box<dyn TrainingRequest>,
1692 _fragment_ids: Option<Vec<u32>>,
1693 _progress: Arc<dyn crate::progress::IndexBuildProgress>,
1694 ) -> Result<CreatedIndex> {
1695 let file = Self::train_ngram_index(data, index_store).await?;
1698 Ok(CreatedIndex {
1699 index_details: prost_types::Any::from_msg(&pbold::NGramIndexDetails::default())?,
1700 index_version: NGRAM_INDEX_VERSION,
1701 files: vec![file],
1702 })
1703 }
1704}
1705
1706#[async_trait]
1707impl ScalarIndexPlugin for NGramIndexPlugin {
1708 fn basic_trainer(&self) -> Option<&dyn BasicTrainer> {
1709 Some(self)
1710 }
1711
1712 fn name(&self) -> &str {
1713 "NGram"
1714 }
1715
1716 fn provides_exact_answer(&self) -> bool {
1717 false
1718 }
1719
1720 fn version(&self) -> u32 {
1721 NGRAM_INDEX_VERSION
1722 }
1723
1724 fn new_query_parser(
1725 &self,
1726 index_name: String,
1727 _index_details: &prost_types::Any,
1728 ) -> Option<Box<dyn ScalarQueryParser>> {
1729 Some(Box::new(TextQueryParser::new(
1730 index_name,
1731 self.name().to_string(),
1732 true,
1734 true,
1736 )))
1737 }
1738
1739 async fn load_index(
1740 &self,
1741 index_store: Arc<dyn IndexStore>,
1742 _index_details: &prost_types::Any,
1743 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1744 cache: &LanceCache,
1745 ) -> Result<Arc<dyn ScalarIndex>> {
1746 Ok(NGramIndex::load(index_store, frag_reuse_index, cache).await? as Arc<dyn ScalarIndex>)
1747 }
1748}
1749
1750#[cfg(test)]
1751mod tests {
1752 use lance_core::utils::row_addr_remap::RowAddrRemap;
1753 use rstest::rstest;
1754 use std::{
1755 collections::{HashMap, HashSet},
1756 sync::Arc,
1757 };
1758
1759 use arrow::array::AsArray;
1760 use arrow::datatypes::{UInt32Type, UInt64Type};
1761 use arrow_array::{Array, RecordBatch, StringArray, UInt32Array, UInt64Array};
1762 use arrow_schema::{DataType, Field, Schema};
1763 use async_trait::async_trait;
1764 use datafusion::{
1765 execution::SendableRecordBatchStream, physical_plan::stream::RecordBatchStreamAdapter,
1766 };
1767 use datafusion_common::DataFusionError;
1768 use futures::{TryStreamExt, stream};
1769 use itertools::Itertools;
1770 use lance_core::{Error, ROW_ID, Result, cache::LanceCache, utils::tempfile::TempDir};
1771 use lance_datagen::{BatchCount, ByteCount, RowCount};
1772 use lance_io::object_store::ObjectStore;
1773 use lance_select::RowAddrTreeMap;
1774 use lance_tokenizer::TextAnalyzer;
1775 use roaring::RoaringTreemap;
1776
1777 use crate::scalar::{
1778 IndexReader, IndexStore, ScalarIndex, SearchResult, TextQuery,
1779 lance_format::LanceIndexStore,
1780 ngram::{NGramIndex, NGramIndexBuilder, NGramIndexBuilderOptions},
1781 };
1782 use crate::{metrics::NoOpMetricsCollector, scalar::registry::VALUE_COLUMN_NAME};
1783
1784 use super::{
1785 NGRAM_TOKENIZER, NGramIndexSpillState, POSTINGS_FILENAME, POSTINGS_SCHEMA, ngram_to_token,
1786 tokenize_visitor,
1787 };
1788
1789 struct MaxReadRangeReader {
1790 inner: Arc<dyn IndexReader>,
1791 max_rows: usize,
1792 }
1793
1794 #[async_trait]
1795 impl IndexReader for MaxReadRangeReader {
1796 async fn read_record_batch(&self, n: u64, batch_size: u64) -> Result<RecordBatch> {
1797 self.inner.read_record_batch(n, batch_size).await
1798 }
1799
1800 async fn read_range(
1801 &self,
1802 range: std::ops::Range<usize>,
1803 projection: Option<&[&str]>,
1804 ) -> Result<RecordBatch> {
1805 let rows = range.end - range.start;
1806 if rows > self.max_rows {
1807 return Err(Error::invalid_input(format!(
1808 "read_range requested {} rows, max is {}",
1809 rows, self.max_rows,
1810 )));
1811 }
1812 self.inner.read_range(range, projection).await
1813 }
1814
1815 async fn num_batches(&self, batch_size: u64) -> u32 {
1816 self.inner.num_batches(batch_size).await
1817 }
1818
1819 fn num_rows(&self) -> usize {
1820 self.inner.num_rows()
1821 }
1822
1823 fn schema(&self) -> &lance_core::datatypes::Schema {
1824 self.inner.schema()
1825 }
1826
1827 fn file_size_bytes(&self) -> Option<u64> {
1828 self.inner.file_size_bytes()
1829 }
1830 }
1831
1832 fn collect_tokens(analyzer: &TextAnalyzer, text: &str) -> Vec<String> {
1833 let mut tokens = Vec::with_capacity(text.len() * 3);
1834 tokenize_visitor(analyzer, text, |token| tokens.push(token.to_owned()));
1835 tokens
1836 }
1837
1838 #[test]
1839 fn test_tokenizer() {
1840 let tokenizer = NGRAM_TOKENIZER.clone();
1841
1842 let tokens = collect_tokens(&tokenizer, "café");
1844 assert_eq!(
1845 tokens,
1846 vec!["caf", "afe"] );
1848
1849 let tokens = collect_tokens(&tokenizer, "a1b2");
1851 assert_eq!(tokens, vec!["a1b", "1b2"]);
1852
1853 let tokens = collect_tokens(&tokenizer, "abc👍b!c24");
1855
1856 assert_eq!(tokens, vec!["abc", "c24"]);
1857
1858 let tokens = collect_tokens(&tokenizer, "anstoß");
1859
1860 assert_eq!(tokens, vec!["ans", "nst", "sto", "tos", "oss"]);
1861
1862 let tokens = collect_tokens(&tokenizer, "ABC");
1864 assert_eq!(tokens, vec!["abc"]);
1865
1866 let tokens = collect_tokens(&tokenizer, "ababab");
1868 assert_eq!(
1871 tokens,
1872 vec!["aba", "bab", "aba", "bab"] );
1874 }
1875
1876 async fn do_train(
1877 mut builder: NGramIndexBuilder,
1878 data: SendableRecordBatchStream,
1879 ) -> (NGramIndex, Arc<TempDir>) {
1880 let spill_files = builder.train(data).await.unwrap();
1881
1882 let tmpdir = Arc::new(TempDir::default());
1883 let test_store = LanceIndexStore::new(
1884 Arc::new(ObjectStore::local()),
1885 tmpdir.obj_path(),
1886 Arc::new(LanceCache::no_cache()),
1887 );
1888
1889 builder
1890 .write_index(&test_store, spill_files, None)
1891 .await
1892 .unwrap();
1893
1894 (
1895 NGramIndex::from_store(Arc::new(test_store), None, &LanceCache::no_cache())
1896 .await
1897 .unwrap(),
1898 tmpdir,
1899 )
1900 }
1901
1902 async fn get_posting_list_for_trigram(index: &NGramIndex, trigram: &str) -> Vec<u64> {
1903 let token = ngram_to_token(trigram, 3);
1904 let row_offset = index.tokens[&token];
1905 let list = index
1906 .list_reader
1907 .ngram_list(row_offset, &NoOpMetricsCollector)
1908 .await
1909 .unwrap();
1910 list.bitmap.iter().sorted().collect()
1911 }
1912
1913 async fn get_null_posting_list(index: &NGramIndex) -> Vec<u64> {
1914 let row_offset = index.tokens[&0];
1915 let list = index
1916 .list_reader
1917 .ngram_list(row_offset, &NoOpMetricsCollector)
1918 .await
1919 .unwrap();
1920 list.bitmap.iter().sorted().collect()
1921 }
1922
1923 #[test_log::test(tokio::test)]
1924 async fn test_basic_ngram_index() {
1925 let data = StringArray::from_iter_values([
1926 "cat",
1927 "dog",
1928 "cat dog",
1929 "dog cat",
1930 "elephant",
1931 "mouse",
1932 "rhino",
1933 "giraffe",
1934 "rhinos nose",
1935 ]);
1936 let row_ids = UInt64Array::from_iter_values((0..data.len()).map(|i| i as u64));
1937 let schema = Arc::new(Schema::new(vec![
1938 Field::new(VALUE_COLUMN_NAME, DataType::Utf8, false),
1939 Field::new(ROW_ID, DataType::UInt64, false),
1940 ]));
1941 let data =
1942 RecordBatch::try_new(schema.clone(), vec![Arc::new(data), Arc::new(row_ids)]).unwrap();
1943 let data = Box::pin(RecordBatchStreamAdapter::new(
1944 schema,
1945 stream::once(std::future::ready(Ok(data))),
1946 ));
1947
1948 let builder = NGramIndexBuilder::try_new(NGramIndexBuilderOptions::default()).unwrap();
1949
1950 let (index, _tmpdir) = do_train(builder, data).await;
1951 assert_eq!(index.tokens.len(), 21);
1952
1953 let res = index
1955 .search(
1956 &TextQuery::StringContains("cat".to_string()),
1957 &NoOpMetricsCollector,
1958 )
1959 .await
1960 .unwrap();
1961
1962 let expected = SearchResult::at_most(RowAddrTreeMap::from_iter([0, 2, 3]));
1963
1964 assert_eq!(expected, res);
1965
1966 let res = index
1968 .search(
1969 &TextQuery::StringContains("nos nos".to_string()),
1970 &NoOpMetricsCollector,
1971 )
1972 .await
1973 .unwrap();
1974 let expected = SearchResult::at_most(RowAddrTreeMap::from_iter([8]));
1975 assert_eq!(expected, res);
1976
1977 let res = index
1979 .search(
1980 &TextQuery::StringContains("tdo".to_string()),
1981 &NoOpMetricsCollector,
1982 )
1983 .await
1984 .unwrap();
1985 let expected = SearchResult::exact(RowAddrTreeMap::new());
1986 assert_eq!(expected, res);
1987
1988 let res = index
1990 .search(
1991 &TextQuery::StringContains("inose".to_string()),
1992 &NoOpMetricsCollector,
1993 )
1994 .await
1995 .unwrap();
1996 let expected = SearchResult::at_most(RowAddrTreeMap::from_iter([8]));
1997 assert_eq!(expected, res);
1998
1999 let res = index
2001 .search(
2002 &TextQuery::StringContains("ab".to_string()),
2003 &NoOpMetricsCollector,
2004 )
2005 .await
2006 .unwrap();
2007 let expected = SearchResult::at_least(RowAddrTreeMap::new());
2008 assert_eq!(expected, res);
2009
2010 let res = index
2012 .search(
2013 &TextQuery::StringContains("no nos".to_string()),
2014 &NoOpMetricsCollector,
2015 )
2016 .await
2017 .unwrap();
2018 let expected = SearchResult::at_most(RowAddrTreeMap::from_iter([8]));
2019 assert_eq!(expected, res);
2020 }
2021
2022 #[test_log::test(tokio::test)]
2023 async fn test_ngram_regex_search() {
2024 let data = StringArray::from_iter_values([
2026 "cat", "dog", "cat dog", "dog cat", "elephant", "mouse", "rhino", "giraffe", "rhinos nose", ]);
2036 let row_ids = UInt64Array::from_iter_values((0..data.len()).map(|i| i as u64));
2037 let schema = Arc::new(Schema::new(vec![
2038 Field::new(VALUE_COLUMN_NAME, DataType::Utf8, false),
2039 Field::new(ROW_ID, DataType::UInt64, false),
2040 ]));
2041 let data =
2042 RecordBatch::try_new(schema.clone(), vec![Arc::new(data), Arc::new(row_ids)]).unwrap();
2043 let data = Box::pin(RecordBatchStreamAdapter::new(
2044 schema,
2045 stream::once(std::future::ready(Ok(data))),
2046 ));
2047
2048 let builder = NGramIndexBuilder::try_new(NGramIndexBuilderOptions::default()).unwrap();
2049 let (index, _tmpdir) = do_train(builder, data).await;
2050
2051 async fn search(index: &NGramIndex, pattern: &str) -> SearchResult {
2052 index
2053 .search(
2054 &TextQuery::Regex(pattern.to_string()),
2055 &NoOpMetricsCollector,
2056 )
2057 .await
2058 .unwrap()
2059 }
2060
2061 assert_eq!(
2063 search(&index, "cat").await,
2064 SearchResult::at_most(RowAddrTreeMap::from_iter([0, 2, 3]))
2065 );
2066
2067 assert_eq!(
2069 search(&index, "(cat|dog)").await,
2070 SearchResult::at_most(RowAddrTreeMap::from_iter([0, 1, 2, 3]))
2071 );
2072
2073 assert_eq!(
2076 search(&index, "rhino.*nose").await,
2077 SearchResult::at_most(RowAddrTreeMap::from_iter([8]))
2078 );
2079
2080 assert_eq!(
2082 search(&index, "a.b").await,
2083 SearchResult::at_least(RowAddrTreeMap::new())
2084 );
2085
2086 assert_eq!(
2088 search(&index, "zzz").await,
2089 SearchResult::at_most(RowAddrTreeMap::new())
2090 );
2091 }
2092
2093 #[test_log::test(tokio::test)]
2094 async fn test_ngram_regex_search_nulls() {
2095 let data = simple_data_with_nulls();
2097 let builder = NGramIndexBuilder::try_new(NGramIndexBuilderOptions::default()).unwrap();
2098 let (index, _tmpdir) = do_train(builder, data).await;
2099
2100 let res = index
2102 .search(&TextQuery::Regex("cat".to_string()), &NoOpMetricsCollector)
2103 .await
2104 .unwrap();
2105 assert_eq!(
2106 res,
2107 SearchResult::at_most(RowAddrTreeMap::from_iter([0, 4]))
2108 );
2109
2110 let res = index
2111 .search(
2112 &TextQuery::Regex("(cat|dog)".to_string()),
2113 &NoOpMetricsCollector,
2114 )
2115 .await
2116 .unwrap();
2117 assert_eq!(
2118 res,
2119 SearchResult::at_most(RowAddrTreeMap::from_iter([0, 1, 4]))
2120 );
2121 }
2122
2123 fn test_data_schema() -> Arc<Schema> {
2124 Arc::new(Schema::new(vec![
2125 Field::new(VALUE_COLUMN_NAME, DataType::Utf8, true),
2126 Field::new(ROW_ID, DataType::UInt64, false),
2127 ]))
2128 }
2129
2130 fn simple_data_with_nulls() -> SendableRecordBatchStream {
2131 let data = StringArray::from_iter(&[Some("cat"), Some("dog"), None, None, Some("cat dog")]);
2132 let row_ids = UInt64Array::from_iter_values((0..data.len()).map(|i| i as u64));
2133 let schema = test_data_schema();
2134 let data =
2135 RecordBatch::try_new(schema.clone(), vec![Arc::new(data), Arc::new(row_ids)]).unwrap();
2136 Box::pin(RecordBatchStreamAdapter::new(
2137 schema,
2138 stream::once(std::future::ready(Ok(data))),
2139 ))
2140 }
2141
2142 #[test_log::test(tokio::test)]
2143 async fn test_ngram_nulls() {
2144 let data = simple_data_with_nulls();
2145
2146 let builder = NGramIndexBuilder::try_new(NGramIndexBuilderOptions::default()).unwrap();
2147
2148 let (index, _tmpdir) = do_train(builder, data).await;
2149 assert_eq!(index.tokens.len(), 3);
2150
2151 let res = index
2152 .search(
2153 &TextQuery::StringContains("cat".to_string()),
2154 &NoOpMetricsCollector,
2155 )
2156 .await
2157 .unwrap();
2158 let expected = SearchResult::at_most(RowAddrTreeMap::from_iter([0, 4]));
2159 assert_eq!(expected, res);
2160
2161 let null_posting_list = get_null_posting_list(&index).await;
2162 assert_eq!(null_posting_list, vec![2, 3]);
2163
2164 }
2166
2167 fn empty_data() -> SendableRecordBatchStream {
2168 Box::pin(RecordBatchStreamAdapter::new(
2169 test_data_schema(),
2170 stream::empty::<lance_core::error::DataFusionResult<RecordBatch>>(),
2171 ))
2172 }
2173
2174 #[test_log::test(tokio::test)]
2175 async fn test_train_empty() {
2176 let builder = NGramIndexBuilder::try_new(NGramIndexBuilderOptions::default()).unwrap();
2177
2178 let (index, _tmpdir) = do_train(builder, empty_data()).await;
2179 assert_eq!(index.tokens.len(), 0);
2180 }
2181
2182 #[test_log::test(tokio::test)]
2183 async fn test_update_empty() {
2184 let data = simple_data_with_nulls();
2185
2186 let builder = NGramIndexBuilder::try_new(NGramIndexBuilderOptions::default()).unwrap();
2187 let (index, _tmpdir) = do_train(builder, empty_data()).await;
2188
2189 let new_tmpdir = Arc::new(TempDir::default());
2190 let test_store = Arc::new(LanceIndexStore::new(
2191 Arc::new(ObjectStore::local()),
2192 new_tmpdir.obj_path(),
2193 Arc::new(LanceCache::no_cache()),
2194 ));
2195
2196 index.update(data, test_store.as_ref(), None).await.unwrap();
2197
2198 let index = NGramIndex::from_store(test_store, None, &LanceCache::no_cache())
2199 .await
2200 .unwrap();
2201 assert_eq!(index.tokens.len(), 3);
2202 }
2203
2204 async fn row_ids_in_index(index: &NGramIndex) -> Vec<u64> {
2205 let mut row_ids = HashSet::new();
2206 for row_offset in index.tokens.values() {
2207 let list = index
2208 .list_reader
2209 .ngram_list(*row_offset, &NoOpMetricsCollector)
2210 .await
2211 .unwrap();
2212 row_ids.extend(list.bitmap.iter());
2213 }
2214 row_ids.into_iter().sorted().collect()
2215 }
2216
2217 #[test_log::test(tokio::test)]
2218 async fn test_ngram_index_remap() {
2219 let data = simple_data_with_nulls();
2220 let builder = NGramIndexBuilder::try_new(NGramIndexBuilderOptions::default()).unwrap();
2221 let (index, _tmpdir) = do_train(builder, data).await;
2222
2223 let row_ids = row_ids_in_index(&index).await;
2224 assert_eq!(row_ids, vec![0, 1, 2, 3, 4]);
2225
2226 let new_tmpdir = Arc::new(TempDir::default());
2227 let test_store = Arc::new(LanceIndexStore::new(
2228 Arc::new(ObjectStore::local()),
2229 new_tmpdir.obj_path(),
2230 Arc::new(LanceCache::no_cache()),
2231 ));
2232
2233 let remapping = HashMap::from([(2, Some(100)), (3, None), (4, Some(101))]);
2234 index
2235 .remap(&RowAddrRemap::direct(remapping), test_store.as_ref())
2236 .await
2237 .unwrap();
2238
2239 let index = NGramIndex::from_store(test_store, None, &LanceCache::no_cache())
2240 .await
2241 .unwrap();
2242 let row_ids = row_ids_in_index(&index).await;
2243 assert_eq!(row_ids, vec![0, 1, 100, 101]);
2244
2245 let null_posting_list = get_null_posting_list(&index).await;
2246 assert_eq!(null_posting_list, vec![100]);
2247 }
2248
2249 fn ngram_remap_compact() -> RowAddrRemap {
2252 use lance_core::utils::row_addr_remap::GroupInput;
2253 use roaring::RoaringTreemap;
2254 RowAddrRemap::compact([GroupInput {
2255 rewritten_old_row_addrs: RoaringTreemap::from_iter(0u64..4),
2256 old_frag_ids: vec![0],
2257 new_frags: vec![(10, 4)],
2258 }])
2259 .unwrap()
2260 }
2261
2262 fn ngram_remap_explicit() -> RowAddrRemap {
2263 RowAddrRemap::direct(
2264 (0u64..4)
2265 .map(|i| (i, Some((10u64 << 32) | i)))
2266 .chain(std::iter::once((4u64, None)))
2267 .collect(),
2268 )
2269 }
2270
2271 #[rstest]
2272 #[case(ngram_remap_compact())]
2273 #[case(ngram_remap_explicit())]
2274 #[tokio::test]
2275 async fn test_ngram_index_remap_compact(#[case] remap: RowAddrRemap) {
2276 let data = simple_data_with_nulls();
2277 let builder = NGramIndexBuilder::try_new(NGramIndexBuilderOptions::default()).unwrap();
2278 let (index, _tmpdir) = do_train(builder, data).await;
2279
2280 let row_ids = row_ids_in_index(&index).await;
2281 assert_eq!(row_ids, vec![0, 1, 2, 3, 4]);
2282
2283 let new_tmpdir = Arc::new(TempDir::default());
2284 let test_store = Arc::new(LanceIndexStore::new(
2285 Arc::new(ObjectStore::local()),
2286 new_tmpdir.obj_path(),
2287 Arc::new(LanceCache::no_cache()),
2288 ));
2289
2290 index.remap(&remap, test_store.as_ref()).await.unwrap();
2291
2292 let index = NGramIndex::from_store(test_store, None, &LanceCache::no_cache())
2293 .await
2294 .unwrap();
2295 let addr = |offset: u64| (10u64 << 32) | offset;
2296 let row_ids = row_ids_in_index(&index).await;
2297 assert_eq!(row_ids, vec![addr(0), addr(1), addr(2), addr(3)]);
2298
2299 let null_posting_list = get_null_posting_list(&index).await;
2301 assert_eq!(null_posting_list, vec![addr(2), addr(3)]);
2302 }
2303
2304 #[test_log::test(tokio::test)]
2305 async fn test_ngram_index_merge() {
2306 let data = simple_data_with_nulls();
2307 let builder = NGramIndexBuilder::try_new(NGramIndexBuilderOptions::default()).unwrap();
2308 let (index, _tmpdir) = do_train(builder, data).await;
2309
2310 let data = StringArray::from_iter(&[Some("giraffe"), Some("cat"), None]);
2311 let row_ids = UInt64Array::from_iter_values((0..data.len()).map(|i| i as u64 + 100));
2312 let schema = Arc::new(Schema::new(vec![
2313 Field::new(VALUE_COLUMN_NAME, DataType::Utf8, true),
2314 Field::new(ROW_ID, DataType::UInt64, false),
2315 ]));
2316 let data =
2317 RecordBatch::try_new(schema.clone(), vec![Arc::new(data), Arc::new(row_ids)]).unwrap();
2318 let data = Box::pin(RecordBatchStreamAdapter::new(
2319 schema,
2320 stream::once(std::future::ready(Ok(data))),
2321 ));
2322
2323 let posting_list = get_posting_list_for_trigram(&index, "cat").await;
2324 assert_eq!(posting_list, vec![0, 4]);
2325
2326 let new_tmpdir = Arc::new(TempDir::default());
2327 let test_store = Arc::new(LanceIndexStore::new(
2328 Arc::new(ObjectStore::local()),
2329 new_tmpdir.obj_path(),
2330 Arc::new(LanceCache::no_cache()),
2331 ));
2332
2333 index.update(data, test_store.as_ref(), None).await.unwrap();
2334
2335 let index = NGramIndex::from_store(test_store, None, &LanceCache::no_cache())
2336 .await
2337 .unwrap();
2338 let row_ids = row_ids_in_index(&index).await;
2339 assert_eq!(row_ids, vec![0, 1, 2, 3, 4, 100, 101, 102]);
2340
2341 let posting_list = get_posting_list_for_trigram(&index, "cat").await;
2342 assert_eq!(posting_list, vec![0, 4, 101]);
2343
2344 let posting_list = get_posting_list_for_trigram(&index, "ffe").await;
2345 assert_eq!(posting_list, vec![100]);
2346
2347 let posting_list = get_null_posting_list(&index).await;
2348 assert_eq!(posting_list, vec![2, 3, 102]);
2349 }
2350
2351 #[test]
2352 fn test_retain_row_ids_filters_by_allow_list() {
2353 use arrow_array::UInt32Array;
2354 use roaring::RoaringTreemap;
2355
2356 use super::NGramIndexSpillState;
2357 use crate::scalar::OldIndexDataFilter;
2358
2359 let addr = |frag: u32, local: u32| ((frag as u64) << 32) | local as u64;
2360
2361 let state = NGramIndexSpillState {
2363 tokens: UInt32Array::from(vec![10u32, 20, 30]),
2364 bitmaps: vec![
2365 RoaringTreemap::from_iter([addr(0, 0), addr(0, 1), addr(1, 0), addr(2, 0)]),
2366 RoaringTreemap::from_iter([addr(2, 5)]),
2367 RoaringTreemap::from_iter([addr(1, 0), addr(1, 1), addr(1, 2)]),
2368 ],
2369 };
2370
2371 let mut valid = RowAddrTreeMap::new();
2374 valid.insert_fragment(0);
2375 valid.insert(addr(1, 0));
2376
2377 let filtered = state.remap_and_filter_rows(None, Some(&OldIndexDataFilter::RowIds(valid)));
2378
2379 assert_eq!(filtered.tokens.values().to_vec(), vec![10u32, 30]);
2381 assert_eq!(
2384 filtered.bitmaps[0].iter().collect::<Vec<_>>(),
2385 vec![addr(0, 0), addr(0, 1), addr(1, 0)],
2386 );
2387 assert_eq!(
2389 filtered.bitmaps[1].iter().collect::<Vec<_>>(),
2390 vec![addr(1, 0)]
2391 );
2392 }
2393
2394 #[test]
2395 fn test_remap_then_keep_relocates_and_filters() {
2396 use arrow_array::UInt32Array;
2397 use roaring::{RoaringBitmap, RoaringTreemap};
2398 use uuid::Uuid;
2399
2400 use super::NGramIndexSpillState;
2401 use crate::frag_reuse::{FragReuseIndex, FragReuseIndexDetails};
2402 use crate::scalar::OldIndexDataFilter;
2403
2404 let addr = |frag: u32, local: u32| ((frag as u64) << 32) | local as u64;
2405
2406 let state = NGramIndexSpillState {
2408 tokens: UInt32Array::from(vec![10u32]),
2409 bitmaps: vec![RoaringTreemap::from_iter([
2410 addr(0, 0),
2411 addr(0, 1),
2412 addr(1, 0),
2413 ])],
2414 };
2415
2416 let fri = Arc::new(FragReuseIndex::new(
2420 Uuid::new_v4(),
2421 vec![HashMap::from([
2422 (addr(0, 0), Some(addr(2, 0))),
2423 (addr(0, 1), None),
2424 ])],
2425 FragReuseIndexDetails { versions: vec![] },
2426 ));
2427
2428 let filter = OldIndexDataFilter::Fragments {
2430 to_keep: RoaringBitmap::from_iter([2u32]),
2431 to_remove: RoaringBitmap::from_iter([1u32]),
2432 };
2433
2434 let filtered = state.remap_and_filter_rows(Some(&fri), Some(&filter));
2435
2436 assert_eq!(filtered.tokens.values().to_vec(), vec![10u32]);
2439 assert_eq!(
2440 filtered.bitmaps[0].iter().collect::<Vec<_>>(),
2441 vec![addr(2, 0)]
2442 );
2443 }
2444
2445 #[test_log::test(tokio::test)]
2446 async fn test_ngram_index_with_spill() {
2447 let (data, schema) = lance_datagen::gen_batch()
2448 .col(
2449 VALUE_COLUMN_NAME,
2450 lance_datagen::array::rand_utf8(ByteCount::from(50), false),
2451 )
2452 .col(ROW_ID, lance_datagen::array::step::<UInt64Type>())
2453 .into_reader_stream(RowCount::from(128), BatchCount::from(32));
2454
2455 let data = Box::pin(RecordBatchStreamAdapter::new(
2456 schema,
2457 data.map_err(|arrow_err| DataFusionError::ArrowError(Box::new(arrow_err), None)),
2458 ));
2459
2460 let builder = NGramIndexBuilder::try_new(NGramIndexBuilderOptions {
2461 tokens_per_spill: 100,
2462 })
2463 .unwrap();
2464
2465 let (index, _tmpdir) = do_train(builder, data).await;
2466
2467 assert_eq!(index.tokens.len(), 29012);
2468 }
2469
2470 #[test]
2471 fn test_spill_state_chunks_by_byte_size() {
2472 let bitmaps = (0..8u64)
2473 .map(|i| RoaringTreemap::from_iter(0..(i + 1) * 100))
2474 .collect::<Vec<_>>();
2475 let tokens = UInt32Array::from_iter_values(0..8);
2476 let state = NGramIndexSpillState {
2477 tokens: tokens.clone(),
2478 bitmaps: bitmaps.clone(),
2479 };
2480
2481 let max_batch_bytes = bitmaps.iter().map(|b| b.serialized_size()).max().unwrap() * 2;
2484 let batches = state.try_into_batches_impl(max_batch_bytes).unwrap();
2485 assert!(batches.len() > 1);
2486
2487 let mut row = 0;
2489 for batch in &batches {
2490 let batch_tokens = batch["tokens"].as_primitive::<UInt32Type>();
2491 let batch_postings = batch["posting_list"].as_binary::<i32>();
2492 let mut batch_bytes = 0;
2493 for i in 0..batch.num_rows() {
2494 assert_eq!(batch_tokens.value(i), tokens.value(row));
2495 let posting = batch_postings.value(i);
2496 batch_bytes += posting.len();
2497 assert_eq!(
2498 RoaringTreemap::deserialize_from(posting).unwrap(),
2499 bitmaps[row]
2500 );
2501 row += 1;
2502 }
2503 assert!(batch_bytes <= max_batch_bytes || batch.num_rows() == 1);
2504 }
2505 assert_eq!(row, 8);
2506 }
2507
2508 #[test_log::test(tokio::test)]
2509 async fn test_spill_reader_does_not_materialize_multirow_posting_batches() {
2510 let bitmaps = (0..8u64)
2511 .map(|i| RoaringTreemap::from_iter(0..(i + 1) * 100))
2512 .collect::<Vec<_>>();
2513 let tokens = UInt32Array::from_iter_values(0..8);
2514 let state = NGramIndexSpillState {
2515 tokens: tokens.clone(),
2516 bitmaps: bitmaps.clone(),
2517 };
2518 let max_batch_bytes = bitmaps.iter().map(|b| b.serialized_size()).max().unwrap() * 2;
2519
2520 let tmpdir = Arc::new(TempDir::default());
2521 let store = LanceIndexStore::new(
2522 Arc::new(ObjectStore::local()),
2523 tmpdir.obj_path(),
2524 Arc::new(LanceCache::no_cache()),
2525 );
2526 let mut writer = store
2527 .new_index_file(POSTINGS_FILENAME, POSTINGS_SCHEMA.clone())
2528 .await
2529 .unwrap();
2530 for batch in state.try_into_batches().unwrap() {
2531 writer.write_record_batch(batch).await.unwrap();
2532 }
2533 writer.finish().await.unwrap();
2534
2535 let reader = store.open_index_file(POSTINGS_FILENAME).await.unwrap();
2536 let reader = Arc::new(MaxReadRangeReader {
2537 inner: reader,
2538 max_rows: 1,
2539 });
2540 let states = NGramIndexBuilder::stream_spill_reader(reader, max_batch_bytes)
2541 .unwrap()
2542 .try_collect::<Vec<_>>()
2543 .await
2544 .unwrap();
2545 assert!(states.len() > 1);
2546
2547 let mut row = 0;
2548 for state in states {
2549 let batch_bytes = state
2550 .bitmaps
2551 .iter()
2552 .map(RoaringTreemap::serialized_size)
2553 .sum::<usize>();
2554 assert!(batch_bytes <= max_batch_bytes || state.bitmaps.len() == 1);
2555 for (token, bitmap) in state.tokens.values().iter().zip(state.bitmaps) {
2556 assert_eq!(*token, tokens.value(row));
2557 assert_eq!(bitmap, bitmaps[row]);
2558 row += 1;
2559 }
2560 }
2561 assert_eq!(row, 8);
2562 }
2563
2564 #[test]
2565 fn test_spill_state_rejects_oversized_posting() {
2566 let bitmap = RoaringTreemap::from_iter(0..1000u64);
2567 let too_small = bitmap.serialized_size() - 1;
2568 let state = NGramIndexSpillState {
2569 tokens: UInt32Array::from_iter_values([42]),
2570 bitmaps: vec![bitmap],
2571 };
2572 let err = state.try_into_batches_impl(too_small).unwrap_err();
2573 assert!(err.to_string().contains("token 42"), "{}", err);
2574 }
2575
2576 #[test]
2577 fn test_empty_spill_state_yields_one_empty_batch() {
2578 let state = NGramIndexSpillState {
2579 tokens: UInt32Array::from_iter_values([]),
2580 bitmaps: vec![],
2581 };
2582 let batches = state.try_into_batches().unwrap();
2583 assert_eq!(batches.len(), 1);
2584 assert_eq!(batches[0].num_rows(), 0);
2585 }
2586
2587 #[test]
2591 #[ignore = "needs ~8 GiB of RAM and a couple of minutes; run manually"]
2592 fn test_spill_state_over_i32_max_bytes() {
2593 let bitmap = RoaringTreemap::from_sorted_iter((0..225_000_000u64).map(|v| v * 16)).unwrap();
2597 assert!(bitmap.serialized_size() > 400 * 1024 * 1024);
2598 let bitmaps = vec![bitmap; 6];
2599 let tokens = UInt32Array::from_iter_values(0..6);
2600 let state = NGramIndexSpillState { tokens, bitmaps };
2601
2602 let batches = state.try_into_batches().unwrap();
2603 assert!(batches.len() > 1);
2604 assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 6);
2605 for batch in &batches {
2606 let postings = batch["posting_list"].as_binary::<i32>();
2607 assert!(postings.value_data().len() <= i32::MAX as usize);
2608 }
2609 }
2610}