1use std::cmp::Reverse;
24use std::collections::{BinaryHeap, HashMap};
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::sync::{Arc, OnceLock};
27
28use arrow_array::RecordBatch;
29use arrow_schema::{DataType, Field};
30use async_trait::async_trait;
31use datafusion::execution::SendableRecordBatchStream;
32use futures::{StreamExt, TryStreamExt};
33use lance_core::cache::LanceCache;
34use lance_core::deepsize::DeepSizeOf;
35use lance_core::utils::row_addr_remap::RowAddrRemap;
36use lance_core::utils::tokio::{get_num_compute_intensive_cpus, spawn_cpu};
37use lance_core::{Error, ROW_ADDR, Result};
38use roaring::RoaringBitmap;
39
40use crate::metrics::MetricsCollector;
41use crate::pb;
42use crate::scalar::expression::{ScalarQueryParser, TextQueryParser};
43use crate::scalar::registry::{
44 BasicTrainer, DefaultTrainingRequest, ScalarIndexPlugin, TrainingCriteria, TrainingOrdering,
45 TrainingRequest, VALUE_COLUMN_NAME,
46};
47use crate::scalar::{
48 AnyQuery, BuiltinIndexType, CreatedIndex, IndexFile, IndexStore, OldIndexDataFilter,
49 RowIdRemapper, ScalarIndex, ScalarIndexParams, SearchResult, TextQuery, UpdateCriteria,
50};
51use crate::{Index, IndexType};
52
53const FMINDEX_INDEX_VERSION: u32 = 10;
54const BLOCK_WORDS: usize = 4096;
55const PARTITION_SIZE: usize = 10_000;
56const DEFAULT_PARTITION_SIZE_BYTES: usize = 16 * 1024 * 1024;
57const DEFAULT_DEMAND_PAGE_TARGET_BYTES: usize = 512 * 1024;
58const DEFAULT_PREWARM_CHUNK_TARGET_BYTES: usize = 8 * 1024 * 1024;
59const FMINDEX_PARTITION_FINGERPRINT_KEY: &str = "partition_fingerprint";
60const SENTINEL_BYTE: u8 = 0xFF;
61
62const SA_SAMPLE_RATE: usize = 32;
64
65static LANCE_FMINDEX_NUM_WORKERS: std::sync::LazyLock<usize> = std::sync::LazyLock::new(|| {
66 std::env::var("LANCE_FMINDEX_NUM_WORKERS")
67 .unwrap_or_else(|_| get_num_compute_intensive_cpus().to_string())
68 .parse()
69 .expect("failed to parse LANCE_FMINDEX_NUM_WORKERS")
70});
71static LANCE_FMINDEX_PARTITION_ROWS: std::sync::LazyLock<usize> = std::sync::LazyLock::new(|| {
72 std::env::var("LANCE_FMINDEX_PARTITION_ROWS")
73 .unwrap_or_else(|_| PARTITION_SIZE.to_string())
74 .parse()
75 .expect("failed to parse LANCE_FMINDEX_PARTITION_ROWS")
76});
77static LANCE_FMINDEX_PARTITION_BYTES: std::sync::LazyLock<usize> = std::sync::LazyLock::new(|| {
78 std::env::var("LANCE_FMINDEX_PARTITION_BYTES")
79 .unwrap_or_else(|_| DEFAULT_PARTITION_SIZE_BYTES.to_string())
80 .parse()
81 .expect("failed to parse LANCE_FMINDEX_PARTITION_BYTES")
82});
83static LANCE_FMINDEX_WRITE_QUEUE_SIZE: std::sync::LazyLock<usize> =
84 std::sync::LazyLock::new(|| {
85 std::env::var("LANCE_FMINDEX_WRITE_QUEUE_SIZE")
86 .unwrap_or_else(|_| "1".to_string())
87 .parse()
88 .expect("failed to parse LANCE_FMINDEX_WRITE_QUEUE_SIZE")
89 });
90static LANCE_FMINDEX_RESUME_EXISTING_PARTITIONS: std::sync::LazyLock<bool> =
91 std::sync::LazyLock::new(|| {
92 std::env::var("LANCE_FMINDEX_RESUME_EXISTING_PARTITIONS")
93 .map(|value| {
94 matches!(
95 value.as_str(),
96 "1" | "true" | "TRUE" | "True" | "yes" | "YES"
97 )
98 })
99 .unwrap_or(false)
100 });
101static LANCE_FMINDEX_PREWARM_CHUNK_BYTES: std::sync::LazyLock<usize> =
102 std::sync::LazyLock::new(|| {
103 std::env::var("LANCE_FMINDEX_PREWARM_CHUNK_BYTES")
104 .unwrap_or_else(|_| DEFAULT_PREWARM_CHUNK_TARGET_BYTES.to_string())
105 .parse()
106 .expect("failed to parse LANCE_FMINDEX_PREWARM_CHUNK_BYTES")
107 });
108static LANCE_FMINDEX_DEMAND_PAGE_BYTES: std::sync::LazyLock<usize> =
109 std::sync::LazyLock::new(|| {
110 std::env::var("LANCE_FMINDEX_DEMAND_PAGE_BYTES")
111 .unwrap_or_else(|_| DEFAULT_DEMAND_PAGE_TARGET_BYTES.to_string())
112 .parse()
113 .expect("failed to parse LANCE_FMINDEX_DEMAND_PAGE_BYTES")
114 });
115static LANCE_FMINDEX_PREWARM_CHUNK_CONCURRENCY: std::sync::LazyLock<usize> =
116 std::sync::LazyLock::new(|| {
117 std::env::var("LANCE_FMINDEX_PREWARM_CHUNK_CONCURRENCY")
118 .unwrap_or_else(|_| "1".to_string())
119 .parse()
120 .expect("failed to parse LANCE_FMINDEX_PREWARM_CHUNK_CONCURRENCY")
121 });
122
123fn fmindex_partition_path(partition_id: u64) -> String {
124 format!("part_{partition_id}_fm.lance")
125}
126
127fn fmindex_partition_id_from_path(path: &str) -> Option<u64> {
128 path.strip_prefix("part_")
129 .and_then(|r| r.strip_suffix("_fm.lance"))
130 .and_then(|s| s.parse::<u64>().ok())
131}
132
133fn fmindex_num_workers() -> usize {
134 (*LANCE_FMINDEX_NUM_WORKERS).max(1)
135}
136
137fn fmindex_partition_rows() -> usize {
138 (*LANCE_FMINDEX_PARTITION_ROWS).max(1)
139}
140
141fn fmindex_partition_bytes() -> usize {
142 (*LANCE_FMINDEX_PARTITION_BYTES).max(1)
143}
144
145fn fmindex_write_queue_size() -> usize {
146 (*LANCE_FMINDEX_WRITE_QUEUE_SIZE).max(1)
147}
148
149fn fmindex_resume_existing_partitions() -> bool {
150 *LANCE_FMINDEX_RESUME_EXISTING_PARTITIONS
151}
152
153fn fmindex_prewarm_chunk_bytes() -> usize {
154 (*LANCE_FMINDEX_PREWARM_CHUNK_BYTES).max(BLOCK_WORDS * 8)
155}
156
157fn fmindex_demand_page_bytes() -> usize {
158 (*LANCE_FMINDEX_DEMAND_PAGE_BYTES).max(BLOCK_WORDS * 8)
159}
160
161fn fmindex_demand_page_rows() -> usize {
162 (fmindex_demand_page_bytes() / (BLOCK_WORDS * 8)).max(1)
163}
164
165fn fmindex_prewarm_chunk_concurrency() -> usize {
166 (*LANCE_FMINDEX_PREWARM_CHUNK_CONCURRENCY).max(1)
167}
168
169const SUPERBLOCK_BITS: usize = 512;
172const WORDS_PER_SUPERBLOCK: usize = SUPERBLOCK_BITS / 64;
173
174#[derive(Debug, Clone)]
175struct RankBitVec {
176 words: Vec<u64>,
177 superblocks: Vec<u32>,
178 len: usize,
179}
180
181impl RankBitVec {
182 fn new(len: usize) -> Self {
183 Self {
184 words: vec![0u64; len.div_ceil(64)],
185 superblocks: Vec::new(),
186 len,
187 }
188 }
189
190 #[inline]
191 fn set(&mut self, pos: usize) {
192 self.words[pos / 64] |= 1u64 << (pos % 64);
193 }
194
195 fn build_rank_index(&mut self) {
196 let num_sb = self.words.len().div_ceil(WORDS_PER_SUPERBLOCK) + 1;
197 self.superblocks = Vec::with_capacity(num_sb);
198 let mut cum = 0u32;
199 for (i, chunk) in self.words.chunks(WORDS_PER_SUPERBLOCK).enumerate() {
200 self.superblocks.push(if i == 0 { 0 } else { cum });
201 for &w in chunk {
202 cum += w.count_ones();
203 }
204 }
205 self.superblocks.push(cum);
206 }
207
208 fn deep_size(&self) -> usize {
209 self.words.len() * 8 + self.superblocks.len() * 4
210 }
211}
212
213#[derive(Debug, Clone, Default)]
216struct HuffmanCode {
217 bits: u32,
218 length: u8,
219 node_path: Vec<usize>,
220}
221
222#[derive(Debug, Clone)]
223enum WaveletChild {
224 Node(usize),
225 Leaf(u8),
226}
227
228#[derive(Debug, Clone)]
229struct HuffmanWaveletTree {
230 nodes: Vec<RankBitVec>,
231 codes: [HuffmanCode; 256],
232 children: Vec<(WaveletChild, WaveletChild)>,
233 len: usize,
234}
235
236#[derive(Debug)]
237enum HuffNode {
238 Leaf(u8),
239 Internal { left: Box<Self>, right: Box<Self> },
240}
241
242impl PartialEq for HuffNode {
243 fn eq(&self, _: &Self) -> bool {
244 true
245 }
246}
247impl Eq for HuffNode {}
248impl PartialOrd for HuffNode {
249 fn partial_cmp(&self, o: &Self) -> Option<std::cmp::Ordering> {
250 Some(self.cmp(o))
251 }
252}
253impl Ord for HuffNode {
254 fn cmp(&self, _: &Self) -> std::cmp::Ordering {
255 std::cmp::Ordering::Equal
256 }
257}
258
259impl HuffmanWaveletTree {
260 fn build(data: &[u8]) -> Self {
261 let n = data.len();
262 if n == 0 {
263 return Self {
264 nodes: Vec::new(),
265 codes: std::array::from_fn(|_| HuffmanCode::default()),
266 children: Vec::new(),
267 len: 0,
268 };
269 }
270
271 let mut freq = [0u64; 256];
272 for &b in data {
273 freq[b as usize] += 1;
274 }
275
276 let mut heap: BinaryHeap<(Reverse<u64>, Reverse<usize>, Box<HuffNode>)> = BinaryHeap::new();
277 let mut tie = 0;
278 for (v, &f) in freq.iter().enumerate() {
279 if f > 0 {
280 heap.push((Reverse(f), Reverse(tie), Box::new(HuffNode::Leaf(v as u8))));
281 tie += 1;
282 }
283 }
284 if heap.len() == 1 {
285 let (f, _, node) = heap.pop().unwrap();
286 heap.push((Reverse(0), Reverse(tie), Box::new(HuffNode::Leaf(255))));
287 tie += 1;
288 heap.push((f, Reverse(tie), node));
289 tie += 1;
290 }
291 while heap.len() > 1 {
292 let (Reverse(f1), _, l) = heap.pop().unwrap();
293 let (Reverse(f2), _, r) = heap.pop().unwrap();
294 heap.push((
295 Reverse(f1 + f2),
296 Reverse(tie),
297 Box::new(HuffNode::Internal { left: l, right: r }),
298 ));
299 tie += 1;
300 }
301 let root = heap.pop().unwrap().2;
302
303 let mut codes: [HuffmanCode; 256] = std::array::from_fn(|_| HuffmanCode::default());
304 let mut node_count = 0;
305 let mut children_map: Vec<(WaveletChild, WaveletChild)> = Vec::new();
306
307 fn assign(
308 node: &HuffNode,
309 bits: u32,
310 len: u8,
311 path: &mut Vec<usize>,
312 nid: &mut usize,
313 codes: &mut [HuffmanCode; 256],
314 cm: &mut Vec<(WaveletChild, WaveletChild)>,
315 ) -> WaveletChild {
316 match node {
317 HuffNode::Leaf(b) => {
318 codes[*b as usize] = HuffmanCode {
319 bits,
320 length: len,
321 node_path: path.clone(),
322 };
323 WaveletChild::Leaf(*b)
324 }
325 HuffNode::Internal { left, right } => {
326 let my = *nid;
327 *nid += 1;
328 path.push(my);
329 cm.push((WaveletChild::Leaf(0), WaveletChild::Leaf(0)));
330 let lc = assign(left, bits << 1, len + 1, path, nid, codes, cm);
331 let rc = assign(right, (bits << 1) | 1, len + 1, path, nid, codes, cm);
332 cm[my] = (lc, rc);
333 path.pop();
334 WaveletChild::Node(my)
335 }
336 }
337 }
338 assign(
339 &root,
340 0,
341 0,
342 &mut Vec::new(),
343 &mut node_count,
344 &mut codes,
345 &mut children_map,
346 );
347
348 let mut node_sizes = vec![0usize; node_count];
349 for &b in data {
350 for &nid in &codes[b as usize].node_path {
351 node_sizes[nid] += 1;
352 }
353 }
354 let mut nodes: Vec<RankBitVec> = node_sizes.iter().map(|&sz| RankBitVec::new(sz)).collect();
355 let mut cursors = vec![0usize; node_count];
356 for &b in data {
357 let code = &codes[b as usize];
358 for (level, &nid) in code.node_path.iter().enumerate() {
359 if (code.bits >> (code.length - 1 - level as u8)) & 1 == 1 {
360 nodes[nid].set(cursors[nid]);
361 }
362 cursors[nid] += 1;
363 }
364 }
365 for n in &mut nodes {
366 n.build_rank_index();
367 }
368 Self {
369 nodes,
370 codes,
371 children: children_map,
372 len: n,
373 }
374 }
375
376 fn deep_size(&self) -> usize {
377 self.nodes.iter().map(|n| n.deep_size()).sum::<usize>()
378 + self
379 .codes
380 .iter()
381 .map(|c| c.node_path.len() * 8)
382 .sum::<usize>()
383 + self.children.len() * 24
384 }
385}
386
387fn build_suffix_array(text: &[u8]) -> Vec<usize> {
390 let n = text.len();
391 if n == 0 {
392 return Vec::new();
393 }
394 if n > i32::MAX as usize {
395 let mut sa = vec![0i64; n];
396 assert_eq!(libsais_rs::libsais64(text, &mut sa, 0, None), 0);
397 sa.iter().map(|&x| x as usize).collect()
398 } else {
399 let mut sa = vec![0i32; n];
400 assert_eq!(libsais_rs::libsais(text, &mut sa, 0, None), 0);
401 sa.iter().map(|&x| x as usize).collect()
402 }
403}
404
405const BLOCK_BITS: usize = BLOCK_WORDS * 64;
408
409struct LazyRankBitVec {
410 prefix_ranks: Vec<u64>,
411 blocks: Vec<OnceLock<Vec<u64>>>,
412 reader: Arc<dyn crate::scalar::IndexReader>,
413 block_row_offset: usize,
414 len: usize,
415}
416
417impl std::fmt::Debug for LazyRankBitVec {
418 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
419 f.debug_struct("LazyRankBitVec")
420 .field("len", &self.len)
421 .finish()
422 }
423}
424
425impl LazyRankBitVec {
426 fn new(
427 prefix_ranks: Vec<u64>,
428 num_blocks: usize,
429 reader: Arc<dyn crate::scalar::IndexReader>,
430 offset: usize,
431 len: usize,
432 ) -> Self {
433 Self {
434 prefix_ranks,
435 blocks: (0..num_blocks).map(|_| OnceLock::new()).collect(),
436 reader,
437 block_row_offset: offset,
438 len,
439 }
440 }
441
442 async fn load_block_if_needed(&self, idx: usize) -> Result<()> {
443 if idx >= self.blocks.len() {
444 return Err(Error::index(format!(
445 "FM-Index block {idx} is out of range for {} blocks",
446 self.blocks.len()
447 )));
448 }
449 if self.blocks[idx].get().is_none() {
450 self.load_page_containing(idx).await?;
451 }
452 Ok(())
453 }
454
455 async fn load_page_containing(&self, idx: usize) -> Result<()> {
456 let page_rows = fmindex_demand_page_rows();
457 let start = (idx / page_rows) * page_rows;
458 let end = (start + page_rows).min(self.blocks.len());
459 if self.blocks[start..end]
460 .iter()
461 .all(|block| block.get().is_some())
462 {
463 return Ok(());
464 }
465
466 let batch = self
467 .reader
468 .read_range(
469 self.block_row_offset + start..self.block_row_offset + end,
470 Some(&["words"]),
471 )
472 .await?;
473 if batch.num_rows() != end - start {
474 return Err(Error::index(format!(
475 "expected {} FM-Index block rows, got {}",
476 end - start,
477 batch.num_rows()
478 )));
479 }
480
481 let col = Self::words_column(&batch)?;
482 for row in 0..batch.num_rows() {
483 let block_idx = start + row;
484 if self.blocks[block_idx].get().is_none() {
485 let words = Self::decode_words(col.value(row));
486 let _ = self.blocks[block_idx].set(words);
487 }
488 }
489 Ok(())
490 }
491
492 async fn load_block_for_rank(&self, pos: usize) -> Result<()> {
493 if pos > self.len {
494 return Err(Error::invalid_input(format!(
495 "FM-Index rank position {pos} exceeds bitvector length {}",
496 self.len
497 )));
498 }
499 if pos == 0 {
500 return Ok(());
501 }
502 if pos == self.len && pos.is_multiple_of(BLOCK_BITS) {
503 if let Some(last_idx) = self.blocks.len().checked_sub(1) {
504 return self.load_block_if_needed(last_idx).await;
505 }
506 return Ok(());
507 }
508 if pos.is_multiple_of(BLOCK_BITS) {
509 return Ok(());
510 }
511 self.load_block_if_needed(pos / BLOCK_BITS).await
512 }
513
514 async fn load_block_for_access(&self, pos: usize) -> Result<()> {
515 self.load_block_if_needed(pos / BLOCK_BITS).await
516 }
517
518 #[inline]
519 fn ensure_block(&self, idx: usize) -> &[u64] {
520 self.blocks[idx].get_or_init(|| {
521 tokio::task::block_in_place(|| {
522 tokio::runtime::Handle::current().block_on(self.load_block(idx))
523 })
524 .unwrap_or_else(|e| panic!("FM-Index block load failed: {e}"))
525 })
526 }
527
528 async fn load_block(&self, idx: usize) -> Result<Vec<u64>> {
529 let row = self.block_row_offset + idx;
530 let batch = self
531 .reader
532 .read_range(row..row + 1, Some(&["words"]))
533 .await?;
534 let col = Self::words_column(&batch)?;
535 Ok(Self::decode_words(col.value(0)))
536 }
537
538 fn words_column(batch: &RecordBatch) -> Result<&arrow_array::LargeBinaryArray> {
539 batch
540 .column(0)
541 .as_any()
542 .downcast_ref::<arrow_array::LargeBinaryArray>()
543 .ok_or_else(|| Error::invalid_input("expected LargeBinary words column"))
544 }
545
546 fn decode_words(raw: &[u8]) -> Vec<u64> {
547 raw.chunks_exact(8)
548 .map(|c| u64::from_le_bytes(c.try_into().unwrap()))
549 .collect()
550 }
551
552 fn terminal_rank1(&self) -> usize {
553 if self.len == 0 {
554 return 0;
555 }
556 let Some(last_idx) = self.blocks.len().checked_sub(1) else {
557 return 0;
558 };
559 let mut count = self.prefix_ranks.get(last_idx).copied().unwrap_or(0) as usize;
560 let block = self.ensure_block(last_idx);
561 let local = self.len - last_idx * BLOCK_BITS;
562 let full_words = local / 64;
563 let trailing_bits = local % 64;
564 for w in &block[..full_words] {
565 count += w.count_ones() as usize;
566 }
567 if trailing_bits > 0 {
568 count += (block[full_words] & ((1u64 << trailing_bits) - 1)).count_ones() as usize;
569 }
570 count
571 }
572
573 #[inline]
574 fn rank1(&self, pos: usize) -> usize {
575 debug_assert!(pos <= self.len);
576 if pos == 0 {
577 return 0;
578 }
579 let bi = pos / BLOCK_BITS;
580 let local = pos % BLOCK_BITS;
581 if local == 0 {
582 if let Some(prefix_rank) = self.prefix_ranks.get(bi) {
583 return *prefix_rank as usize;
584 }
585 if pos == self.len {
586 return self.terminal_rank1();
587 }
588 }
589 let mut count = self.prefix_ranks[bi] as usize;
590 let block = self.ensure_block(bi);
591 let wi = local / 64;
592 let bit = local % 64;
593 for w in &block[..wi] {
594 count += w.count_ones() as usize;
595 }
596 if bit > 0 {
597 count += (block[wi] & ((1u64 << bit) - 1)).count_ones() as usize;
598 }
599 count
600 }
601
602 #[inline]
603 fn rank0(&self, pos: usize) -> usize {
604 pos - self.rank1(pos)
605 }
606
607 #[inline]
608 fn get(&self, pos: usize) -> bool {
609 debug_assert!(pos < self.len);
610 let bi = pos / BLOCK_BITS;
611 let local = pos % BLOCK_BITS;
612 let block = self.ensure_block(bi);
613 (block[local / 64] >> (local % 64)) & 1 != 0
614 }
615
616 fn deep_size(&self) -> usize {
617 let loaded: usize = self
618 .blocks
619 .iter()
620 .filter_map(|b| b.get())
621 .map(|w| w.len() * 8)
622 .sum();
623 self.prefix_ranks.len() * 8 + loaded
624 }
625}
626
627struct LazyHuffmanWaveletTree {
628 nodes: Vec<LazyRankBitVec>,
629 codes: [HuffmanCode; 256],
630 children: Vec<(WaveletChild, WaveletChild)>,
631 len: usize,
632}
633
634impl std::fmt::Debug for LazyHuffmanWaveletTree {
635 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636 f.debug_struct("LazyHuffmanWaveletTree")
637 .field("len", &self.len)
638 .finish()
639 }
640}
641
642impl LazyHuffmanWaveletTree {
643 async fn load_all(&self) -> Result<()> {
645 if self.nodes.is_empty() {
646 return Ok(());
647 }
648
649 #[derive(Clone, Copy)]
650 struct RowRange {
651 start: usize,
652 end: usize,
653 node_idx: usize,
654 }
655
656 let mut ranges = self
657 .nodes
658 .iter()
659 .enumerate()
660 .filter(|(_, node)| !node.blocks.is_empty())
661 .map(|(node_idx, node)| RowRange {
662 start: node.block_row_offset,
663 end: node.block_row_offset + node.blocks.len(),
664 node_idx,
665 })
666 .collect::<Vec<_>>();
667 ranges.sort_by_key(|range| range.start);
668 let Some(total_rows) = ranges.iter().map(|range| range.end).max() else {
669 return Ok(());
670 };
671 let ranges = Arc::new(ranges);
672
673 let chunk_loaded = |start: usize, end: usize| {
674 ranges.iter().all(|range| {
675 let overlap_start = start.max(range.start);
676 let overlap_end = end.min(range.end);
677 if overlap_start >= overlap_end {
678 return true;
679 }
680 let node = &self.nodes[range.node_idx];
681 node.blocks[overlap_start - range.start..overlap_end - range.start]
682 .iter()
683 .all(|block| block.get().is_some())
684 })
685 };
686
687 let chunk_rows = (fmindex_prewarm_chunk_bytes() / (BLOCK_WORDS * 8)).max(1);
688 let reader = Arc::clone(&self.nodes[0].reader);
689 let chunks = (0..total_rows)
690 .step_by(chunk_rows)
691 .map(|start| {
692 let end = (start + chunk_rows).min(total_rows);
693 (start, end)
694 })
695 .filter(|(start, end)| !chunk_loaded(*start, *end))
696 .collect::<Vec<_>>();
697
698 futures::stream::iter(chunks)
699 .map(|(start, end)| {
700 let reader = Arc::clone(&reader);
701 async move {
702 let batch = reader.read_range(start..end, Some(&["words"])).await?;
703 Result::<_>::Ok((start, end, batch))
704 }
705 })
706 .buffer_unordered(fmindex_prewarm_chunk_concurrency())
707 .try_for_each(|(start, end, batch)| {
708 let ranges = Arc::clone(&ranges);
709 let nodes = &self.nodes;
710 async move {
711 if batch.num_rows() != end - start {
712 return Err(Error::index(format!(
713 "expected {} FM-Index block rows, got {}",
714 end - start,
715 batch.num_rows()
716 )));
717 }
718
719 let col = LazyRankBitVec::words_column(&batch)?;
720 let mut range_idx = ranges.partition_point(|range| range.end <= start);
721 for row in 0..batch.num_rows() {
722 let absolute_row = start + row;
723 while range_idx < ranges.len() && absolute_row >= ranges[range_idx].end {
724 range_idx += 1;
725 }
726 if range_idx == ranges.len() || absolute_row < ranges[range_idx].start {
727 continue;
728 }
729
730 let range = ranges[range_idx];
731 let block_idx = absolute_row - range.start;
732 let node = &nodes[range.node_idx];
733 if node.blocks[block_idx].get().is_none() {
734 let words = LazyRankBitVec::decode_words(col.value(row));
735 let _ = node.blocks[block_idx].set(words);
736 }
737 }
738
739 Ok(())
740 }
741 })
742 .await?;
743
744 Ok(())
745 }
746
747 #[inline]
748 fn access(&self, mut pos: usize) -> u8 {
749 if self.nodes.is_empty() {
750 return 0;
751 }
752 let mut node_idx = 0;
753 loop {
754 let bit = self.nodes[node_idx].get(pos);
755 let (ref left, ref right) = self.children[node_idx];
756 if bit {
757 pos = self.nodes[node_idx].rank1(pos);
758 match right {
759 WaveletChild::Leaf(b) => return *b,
760 WaveletChild::Node(next) => node_idx = *next,
761 }
762 } else {
763 pos = self.nodes[node_idx].rank0(pos);
764 match left {
765 WaveletChild::Leaf(b) => return *b,
766 WaveletChild::Node(next) => node_idx = *next,
767 }
768 }
769 }
770 }
771
772 async fn access_async(&self, mut pos: usize) -> Result<u8> {
773 if self.nodes.is_empty() {
774 return Ok(0);
775 }
776 let mut node_idx = 0;
777 loop {
778 self.nodes[node_idx].load_block_for_access(pos).await?;
779 let bit = self.nodes[node_idx].get(pos);
780 let (ref left, ref right) = self.children[node_idx];
781 if bit {
782 pos = self.nodes[node_idx].rank1(pos);
783 match right {
784 WaveletChild::Leaf(b) => return Ok(*b),
785 WaveletChild::Node(next) => node_idx = *next,
786 }
787 } else {
788 pos = self.nodes[node_idx].rank0(pos);
789 match left {
790 WaveletChild::Leaf(b) => return Ok(*b),
791 WaveletChild::Node(next) => node_idx = *next,
792 }
793 }
794 }
795 }
796
797 #[inline]
798 fn rank(&self, c: u8, pos: usize) -> usize {
799 let code = &self.codes[c as usize];
800 if code.length == 0 {
801 return 0;
802 }
803 let (mut lo, mut hi) = (0, pos);
804 for (level, &nid) in code.node_path.iter().enumerate() {
805 if (code.bits >> (code.length - 1 - level as u8)) & 1 == 0 {
806 lo = self.nodes[nid].rank0(lo);
807 hi = self.nodes[nid].rank0(hi);
808 } else {
809 lo = self.nodes[nid].rank1(lo);
810 hi = self.nodes[nid].rank1(hi);
811 }
812 }
813 hi - lo
814 }
815
816 async fn rank_async(&self, c: u8, pos: usize) -> Result<usize> {
817 let code = &self.codes[c as usize];
818 if code.length == 0 {
819 return Ok(0);
820 }
821 let (mut lo, mut hi) = (0, pos);
822 for (level, &nid) in code.node_path.iter().enumerate() {
823 self.nodes[nid].load_block_for_rank(lo).await?;
824 self.nodes[nid].load_block_for_rank(hi).await?;
825 if (code.bits >> (code.length - 1 - level as u8)) & 1 == 0 {
826 lo = self.nodes[nid].rank0(lo);
827 hi = self.nodes[nid].rank0(hi);
828 } else {
829 lo = self.nodes[nid].rank1(lo);
830 hi = self.nodes[nid].rank1(hi);
831 }
832 }
833 Ok(hi - lo)
834 }
835
836 #[inline]
837 fn rank_pair(&self, c: u8, lo: usize, hi: usize) -> (usize, usize) {
838 let code = &self.codes[c as usize];
839 if code.length == 0 {
840 return (0, 0);
841 }
842 let (mut s, mut l, mut h) = (0, lo, hi);
843 for (level, &nid) in code.node_path.iter().enumerate() {
844 if (code.bits >> (code.length - 1 - level as u8)) & 1 == 0 {
845 s = self.nodes[nid].rank0(s);
846 l = self.nodes[nid].rank0(l);
847 h = self.nodes[nid].rank0(h);
848 } else {
849 s = self.nodes[nid].rank1(s);
850 l = self.nodes[nid].rank1(l);
851 h = self.nodes[nid].rank1(h);
852 }
853 }
854 (l - s, h - s)
855 }
856
857 async fn rank_pair_async(&self, c: u8, lo: usize, hi: usize) -> Result<(usize, usize)> {
858 let code = &self.codes[c as usize];
859 if code.length == 0 {
860 return Ok((0, 0));
861 }
862 let (mut s, mut l, mut h) = (0, lo, hi);
863 for (level, &nid) in code.node_path.iter().enumerate() {
864 self.nodes[nid].load_block_for_rank(s).await?;
865 self.nodes[nid].load_block_for_rank(l).await?;
866 self.nodes[nid].load_block_for_rank(h).await?;
867 if (code.bits >> (code.length - 1 - level as u8)) & 1 == 0 {
868 s = self.nodes[nid].rank0(s);
869 l = self.nodes[nid].rank0(l);
870 h = self.nodes[nid].rank0(h);
871 } else {
872 s = self.nodes[nid].rank1(s);
873 l = self.nodes[nid].rank1(l);
874 h = self.nodes[nid].rank1(h);
875 }
876 }
877 Ok((l - s, h - s))
878 }
879
880 fn deep_size(&self) -> usize {
881 self.nodes.iter().map(|n| n.deep_size()).sum::<usize>()
882 + self
883 .codes
884 .iter()
885 .map(|c| c.node_path.len() * 8)
886 .sum::<usize>()
887 }
888}
889
890#[derive(Debug, Clone)]
893pub struct FMIndex {
894 wavelet: HuffmanWaveletTree,
895 row_ids: Vec<u64>,
896 sa_samples: Vec<u64>,
898 doc_start_positions: Vec<u64>,
900 c_table: Vec<usize>,
901 alphabet_size: usize,
902}
903
904impl DeepSizeOf for FMIndex {
905 fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
906 self.wavelet.deep_size()
907 + self.row_ids.len() * 8
908 + self.sa_samples.len() * 8
909 + self.doc_start_positions.len() * 8
910 + self.c_table.len() * std::mem::size_of::<usize>()
911 }
912}
913
914impl FMIndex {
915 fn build(texts: &[(u64, &[u8])]) -> Result<Self> {
916 if texts.is_empty() {
917 return Ok(Self {
918 wavelet: HuffmanWaveletTree {
919 nodes: Vec::new(),
920 codes: std::array::from_fn(|_| HuffmanCode::default()),
921 children: Vec::new(),
922 len: 0,
923 },
924 row_ids: Vec::new(),
925 sa_samples: Vec::new(),
926 doc_start_positions: Vec::new(),
927 c_table: vec![0; 257],
928 alphabet_size: 256,
929 });
930 }
931
932 let mut concat = Vec::new();
933 let mut doc_row_ids = Vec::new();
934 let mut doc_starts: Vec<u64> = Vec::new();
935 for (row_id, text) in texts {
936 doc_starts.push(concat.len() as u64);
937 doc_row_ids.push(*row_id);
938 concat.extend_from_slice(text);
939 concat.push(SENTINEL_BYTE); }
941 concat.push(0x00);
944 let n = concat.len();
945 let sa = build_suffix_array(&concat);
946
947 let bwt: Vec<u8> = sa
948 .iter()
949 .map(|&pos| {
950 if pos == 0 {
951 concat[n - 1]
952 } else {
953 concat[pos - 1]
954 }
955 })
956 .collect();
957
958 let mut counts = vec![0usize; 257];
959 for &b in &concat {
960 counts[b as usize + 1] += 1;
961 }
962 for i in 1..257 {
963 counts[i] += counts[i - 1];
964 }
965
966 let sa_samples: Vec<u64> = sa
968 .iter()
969 .step_by(SA_SAMPLE_RATE)
970 .map(|&pos| pos as u64)
971 .collect();
972
973 let wavelet = HuffmanWaveletTree::build(&bwt);
974
975 Ok(Self {
976 wavelet,
977 row_ids: doc_row_ids,
978 sa_samples,
979 doc_start_positions: doc_starts,
980 c_table: counts,
981 alphabet_size: 256,
982 })
983 }
984
985 fn serialize_huffman_codes(&self) -> Vec<u8> {
986 let mut buf = Vec::new();
987 for code in &self.wavelet.codes {
988 buf.extend_from_slice(&code.bits.to_le_bytes());
989 buf.push(code.length);
990 buf.extend_from_slice(&(code.node_path.len() as u16).to_le_bytes());
991 for &nid in &code.node_path {
992 buf.extend_from_slice(&(nid as u32).to_le_bytes());
993 }
994 }
995 buf
996 }
997
998 fn deserialize_huffman_codes(data: &[u8]) -> [HuffmanCode; 256] {
999 let mut codes: [HuffmanCode; 256] = std::array::from_fn(|_| HuffmanCode::default());
1000 let mut cur = 0;
1001 for code in &mut codes {
1002 let bits = u32::from_le_bytes(data[cur..cur + 4].try_into().unwrap());
1003 cur += 4;
1004 let length = data[cur];
1005 cur += 1;
1006 let plen = u16::from_le_bytes(data[cur..cur + 2].try_into().unwrap()) as usize;
1007 cur += 2;
1008 let mut node_path = Vec::with_capacity(plen);
1009 for _ in 0..plen {
1010 node_path.push(u32::from_le_bytes(data[cur..cur + 4].try_into().unwrap()) as usize);
1011 cur += 4;
1012 }
1013 *code = HuffmanCode {
1014 bits,
1015 length,
1016 node_path,
1017 };
1018 }
1019 codes
1020 }
1021
1022 fn serialize_tree_topology(&self) -> Vec<u8> {
1023 let mut buf = Vec::new();
1024 buf.extend_from_slice(&(self.wavelet.children.len() as u32).to_le_bytes());
1025 for (left, right) in &self.wavelet.children {
1026 for child in [left, right] {
1027 match child {
1028 WaveletChild::Node(id) => {
1029 buf.push(0);
1030 buf.extend_from_slice(&(*id as u32).to_le_bytes());
1031 }
1032 WaveletChild::Leaf(b) => {
1033 buf.push(1);
1034 buf.extend_from_slice(&(*b as u32).to_le_bytes());
1035 }
1036 }
1037 }
1038 }
1039 buf
1040 }
1041
1042 fn deserialize_tree_topology(data: &[u8]) -> Vec<(WaveletChild, WaveletChild)> {
1043 let mut cur = 0;
1044 let count = u32::from_le_bytes(data[cur..cur + 4].try_into().unwrap()) as usize;
1045 cur += 4;
1046 let mut children = Vec::with_capacity(count);
1047 for _ in 0..count {
1048 let mut read_child = || {
1049 let t = data[cur];
1050 cur += 1;
1051 let v = u32::from_le_bytes(data[cur..cur + 4].try_into().unwrap());
1052 cur += 4;
1053 if t == 0 {
1054 WaveletChild::Node(v as usize)
1055 } else {
1056 WaveletChild::Leaf(v as u8)
1057 }
1058 };
1059 let l = read_child();
1060 let r = read_child();
1061 children.push((l, r));
1062 }
1063 children
1064 }
1065
1066 fn serialize_c_table(&self) -> Vec<u8> {
1067 self.c_table
1068 .iter()
1069 .flat_map(|&v| (v as u64).to_le_bytes())
1070 .collect()
1071 }
1072
1073 fn deserialize_c_table(data: &[u8]) -> Vec<usize> {
1074 data.chunks_exact(8)
1075 .map(|c| u64::from_le_bytes(c.try_into().unwrap()) as usize)
1076 .collect()
1077 }
1078
1079 fn u64_to_bytes(data: &[u64]) -> Vec<u8> {
1080 data.iter().flat_map(|v| v.to_le_bytes()).collect()
1081 }
1082
1083 fn build_wavelet_batch(&self) -> Result<RecordBatch> {
1084 use arrow_array::{LargeBinaryArray, UInt32Array, UInt64Array};
1085 let mut nid_b = Vec::new();
1086 let mut bid_b = Vec::new();
1087 let mut words_b: Vec<Vec<u8>> = Vec::new();
1088 let mut pr_b = Vec::new();
1089 let mut bl_b = Vec::new();
1090
1091 for (i, node) in self.wavelet.nodes.iter().enumerate() {
1092 let mut pr: u64 = 0;
1093 if node.words.is_empty() {
1094 nid_b.push(i as u32);
1095 bid_b.push(0u32);
1096 words_b.push(Vec::new());
1097 pr_b.push(0u64);
1098 bl_b.push(node.len as u64);
1099 } else {
1100 for (bi, chunk) in node.words.chunks(BLOCK_WORDS).enumerate() {
1101 nid_b.push(i as u32);
1102 bid_b.push(bi as u32);
1103 words_b.push(Self::u64_to_bytes(chunk));
1104 pr_b.push(pr);
1105 bl_b.push(node.len as u64);
1106 pr += chunk.iter().map(|w| w.count_ones() as u64).sum::<u64>();
1107 }
1108 }
1109 }
1110 let refs: Vec<&[u8]> = words_b.iter().map(|v| v.as_slice()).collect();
1111 let schema = Arc::new(Self::block_schema());
1112 Ok(RecordBatch::try_new(
1113 schema,
1114 vec![
1115 Arc::new(UInt32Array::from(nid_b)),
1116 Arc::new(UInt32Array::from(bid_b)),
1117 Arc::new(LargeBinaryArray::from(refs)),
1118 Arc::new(UInt64Array::from(pr_b)),
1119 Arc::new(UInt64Array::from(bl_b)),
1120 ],
1121 )?)
1122 }
1123
1124 fn block_schema() -> arrow_schema::Schema {
1125 arrow_schema::Schema::new(vec![
1126 Field::new("node_id", DataType::UInt32, false),
1127 Field::new("block_id", DataType::UInt32, false),
1128 Field::new("words", DataType::LargeBinary, false),
1129 Field::new("prefix_rank", DataType::UInt64, false),
1130 Field::new("bit_len", DataType::UInt64, false),
1131 ])
1132 }
1133}
1134
1135#[derive(Debug)]
1138struct LazyFMIndex {
1139 wavelet: LazyHuffmanWaveletTree,
1140 row_ids: Vec<u64>,
1141 sa_samples: Vec<u64>,
1142 doc_start_positions: Vec<u64>,
1143 c_table: Vec<usize>,
1144 fully_prewarmed: AtomicBool,
1145}
1146
1147impl LazyFMIndex {
1148 async fn prewarm(&self) -> Result<()> {
1150 if self.fully_prewarmed.load(Ordering::Acquire) {
1151 return Ok(());
1152 }
1153 self.wavelet.load_all().await.inspect(|_| {
1154 self.fully_prewarmed.store(true, Ordering::Release);
1155 })
1156 }
1157
1158 fn backward_search(&self, pattern: &[u8]) -> (usize, usize) {
1159 if pattern.is_empty() || self.wavelet.len == 0 {
1160 return (0, 0);
1161 }
1162 let (mut lo, mut hi) = (0, self.wavelet.len);
1163 for &b in pattern.iter().rev() {
1164 let c = self.c_table[b as usize];
1165 let (occ_lo, occ_hi) = self.wavelet.rank_pair(b, lo, hi);
1166 lo = c + occ_lo;
1167 hi = c + occ_hi;
1168 if lo >= hi {
1169 return (0, 0);
1170 }
1171 }
1172 (lo, hi)
1173 }
1174
1175 async fn backward_search_async(&self, pattern: &[u8]) -> Result<(usize, usize)> {
1176 if pattern.is_empty() || self.wavelet.len == 0 {
1177 return Ok((0, 0));
1178 }
1179 let (mut lo, mut hi) = (0, self.wavelet.len);
1180 for &b in pattern.iter().rev() {
1181 let c = self.c_table[b as usize];
1182 let (occ_lo, occ_hi) = self.wavelet.rank_pair_async(b, lo, hi).await?;
1183 lo = c + occ_lo;
1184 hi = c + occ_hi;
1185 if lo >= hi {
1186 return Ok((0, 0));
1187 }
1188 }
1189 Ok((lo, hi))
1190 }
1191
1192 #[inline]
1193 fn locate(&self, mut pos: usize) -> usize {
1194 let mut steps = 0;
1195 let n = self.wavelet.len;
1196 loop {
1197 if pos.is_multiple_of(SA_SAMPLE_RATE) && (pos / SA_SAMPLE_RATE) < self.sa_samples.len()
1198 {
1199 return (self.sa_samples[pos / SA_SAMPLE_RATE] as usize + steps) % n;
1200 }
1201 let c = self.wavelet.access(pos);
1202 pos = self.c_table[c as usize] + self.wavelet.rank(c, pos);
1203 steps += 1;
1204 if steps >= n {
1205 log::warn!("FM-Index SA locate exceeded {n} steps, possible index corruption");
1206 return 0;
1207 }
1208 }
1209 }
1210
1211 async fn locate_async(&self, mut pos: usize) -> Result<usize> {
1212 let mut steps = 0;
1213 let n = self.wavelet.len;
1214 loop {
1215 if pos.is_multiple_of(SA_SAMPLE_RATE) && (pos / SA_SAMPLE_RATE) < self.sa_samples.len()
1216 {
1217 return Ok((self.sa_samples[pos / SA_SAMPLE_RATE] as usize + steps) % n);
1218 }
1219 let c = self.wavelet.access_async(pos).await?;
1220 pos = self.c_table[c as usize] + self.wavelet.rank_async(c, pos).await?;
1221 steps += 1;
1222 if steps >= n {
1223 log::warn!("FM-Index SA locate exceeded {n} steps, possible index corruption");
1224 return Ok(0);
1225 }
1226 }
1227 }
1228
1229 #[inline]
1230 fn doc_for_position(&self, text_pos: usize) -> usize {
1231 let tp = text_pos as u64;
1232 match self.doc_start_positions.binary_search(&tp) {
1233 Ok(idx) => idx,
1234 Err(idx) => idx - 1,
1235 }
1236 }
1237
1238 #[cfg(test)]
1239 fn search(&self, pattern: &[u8]) -> RoaringBitmap {
1240 let (lo, hi) = self.backward_search(pattern);
1241 if lo >= hi {
1242 return RoaringBitmap::new();
1243 }
1244 let mut result = RoaringBitmap::new();
1245 for i in lo..hi {
1246 let text_pos = self.locate(i);
1247 let doc_idx = self.doc_for_position(text_pos);
1248 result.insert(self.row_ids[doc_idx] as u32);
1249 }
1250 result
1251 }
1252
1253 fn search_row_addrs(&self, pattern: &[u8]) -> Vec<u64> {
1255 let (lo, hi) = self.backward_search(pattern);
1256 if lo >= hi {
1257 return Vec::new();
1258 }
1259 let mut seen = std::collections::HashSet::new();
1260 let mut result = Vec::new();
1261 for i in lo..hi {
1262 let text_pos = self.locate(i);
1263 let doc_idx = self.doc_for_position(text_pos);
1264 let row_addr = self.row_ids[doc_idx];
1265 if seen.insert(row_addr) {
1266 result.push(row_addr);
1267 }
1268 }
1269 result
1270 }
1271
1272 async fn search_row_addrs_async(&self, pattern: &[u8]) -> Result<Vec<u64>> {
1273 let (lo, hi) = self.backward_search_async(pattern).await?;
1274 if lo >= hi {
1275 return Ok(Vec::new());
1276 }
1277 let mut seen = std::collections::HashSet::new();
1278 let mut result = Vec::new();
1279 for i in lo..hi {
1280 let text_pos = self.locate_async(i).await?;
1281 let doc_idx = self.doc_for_position(text_pos);
1282 let row_addr = self.row_ids[doc_idx];
1283 if seen.insert(row_addr) {
1284 result.push(row_addr);
1285 }
1286 }
1287 Ok(result)
1288 }
1289
1290 #[allow(clippy::too_many_arguments)]
1291 async fn from_reader(
1292 reader: Arc<dyn crate::scalar::IndexReader>,
1293 num_bwt_nodes: usize,
1294 huffman_codes: [HuffmanCode; 256],
1295 children: Vec<(WaveletChild, WaveletChild)>,
1296 c_table: Vec<usize>,
1297 bwt_len: usize,
1298 total_wavelet_rows: usize,
1299 num_sa_blocks: usize,
1300 sa_samples_len: usize,
1301 row_ids: Vec<u64>,
1302 doc_start_positions: Vec<u64>,
1303 ) -> Result<Self> {
1304 use arrow_array::UInt64Array;
1305
1306 let meta = reader
1307 .read_range(
1308 0..total_wavelet_rows,
1309 Some(&["node_id", "prefix_rank", "bit_len"]),
1310 )
1311 .await?;
1312 let nid_col = meta
1313 .column_by_name("node_id")
1314 .unwrap()
1315 .as_any()
1316 .downcast_ref::<arrow_array::UInt32Array>()
1317 .unwrap();
1318 let pr_col = meta
1319 .column_by_name("prefix_rank")
1320 .unwrap()
1321 .as_any()
1322 .downcast_ref::<UInt64Array>()
1323 .unwrap();
1324 let bl_col = meta
1325 .column_by_name("bit_len")
1326 .unwrap()
1327 .as_any()
1328 .downcast_ref::<UInt64Array>()
1329 .unwrap();
1330
1331 struct NM {
1332 prs: Vec<u64>,
1333 offset: usize,
1334 blen: usize,
1335 }
1336 let mut nms: Vec<NM> = (0..num_bwt_nodes)
1337 .map(|_| NM {
1338 prs: Vec::new(),
1339 offset: 0,
1340 blen: 0,
1341 })
1342 .collect();
1343 for row in 0..meta.num_rows() {
1344 let nid = nid_col.value(row) as usize;
1345 if nid >= num_bwt_nodes {
1346 continue;
1347 }
1348 let nm = &mut nms[nid];
1349 if nm.prs.is_empty() {
1350 nm.offset = row;
1351 }
1352 nm.prs.push(pr_col.value(row));
1353 nm.blen = bl_col.value(row) as usize;
1354 }
1355
1356 let mut bwt_nodes = Vec::with_capacity(num_bwt_nodes);
1357 for nm in &nms {
1358 bwt_nodes.push(LazyRankBitVec::new(
1359 nm.prs.clone(),
1360 nm.prs.len(),
1361 reader.clone(),
1362 nm.offset,
1363 nm.blen,
1364 ));
1365 }
1366 let wavelet = LazyHuffmanWaveletTree {
1367 nodes: bwt_nodes,
1368 codes: huffman_codes,
1369 children,
1370 len: bwt_len,
1371 };
1372
1373 let mut sa_samples = Vec::with_capacity(sa_samples_len);
1375 let sa_batch = reader
1376 .read_range(
1377 total_wavelet_rows..total_wavelet_rows + num_sa_blocks,
1378 Some(&["words"]),
1379 )
1380 .await?;
1381 let words_col = sa_batch
1382 .column_by_name("words")
1383 .unwrap()
1384 .as_any()
1385 .downcast_ref::<arrow_array::LargeBinaryArray>()
1386 .unwrap();
1387 for i in 0..sa_batch.num_rows() {
1388 let raw = words_col.value(i);
1389 for chunk in raw.chunks_exact(8) {
1390 sa_samples.push(u64::from_le_bytes(chunk.try_into().unwrap()));
1391 }
1392 }
1393 sa_samples.truncate(sa_samples_len);
1394
1395 Ok(Self {
1396 wavelet,
1397 row_ids,
1398 sa_samples,
1399 doc_start_positions,
1400 c_table,
1401 fully_prewarmed: AtomicBool::new(false),
1402 })
1403 }
1404
1405 fn deep_size(&self) -> usize {
1406 self.wavelet.deep_size()
1407 + self.row_ids.len() * 8
1408 + self.sa_samples.len() * 8
1409 + self.doc_start_positions.len() * 8
1410 + self.c_table.len() * std::mem::size_of::<usize>()
1411 }
1412}
1413
1414#[derive(Debug)]
1417struct FMIndexPartition {
1418 #[allow(dead_code)]
1419 id: u64,
1420 fm: LazyFMIndex,
1421}
1422
1423#[derive(Debug)]
1424pub struct FMIndexScalarIndex {
1425 partitions: Vec<Arc<FMIndexPartition>>,
1426 io_parallelism: usize,
1427}
1428
1429impl DeepSizeOf for FMIndexScalarIndex {
1430 fn deep_size_of_children(&self, _ctx: &mut lance_core::deepsize::Context) -> usize {
1431 self.partitions.iter().map(|p| p.fm.deep_size()).sum()
1432 }
1433}
1434
1435impl FMIndexScalarIndex {
1436 async fn load_partition(
1437 store: &dyn IndexStore,
1438 filename: &str,
1439 pid: u64,
1440 ) -> Result<FMIndexPartition> {
1441 let reader = store.open_index_file(filename).await?;
1442 let md = &reader.schema().metadata;
1443
1444 let parse = |key: &str| -> Result<usize> {
1445 md.get(key)
1446 .ok_or_else(|| Error::invalid_input(format!("missing {key}")))?
1447 .parse()
1448 .map_err(|e| Error::invalid_input(format!("invalid {key}: {e}")))
1449 };
1450
1451 let num_bwt_nodes = parse("num_bwt_nodes")?;
1452 let bwt_len = parse("bwt_len")?;
1453 let num_sa_blocks = parse("num_sa_blocks")?;
1454 let sa_samples_len = parse("sa_samples_len")?;
1455 let total_wavelet_rows = parse("total_wavelet_rows")?;
1456
1457 let c_table = FMIndex::deserialize_c_table(&hex_decode(
1458 md.get("c_table")
1459 .ok_or_else(|| Error::invalid_input("missing c_table"))?,
1460 )?);
1461 let huffman_codes = FMIndex::deserialize_huffman_codes(&hex_decode(
1462 md.get("huffman_codes")
1463 .ok_or_else(|| Error::invalid_input("missing huffman_codes"))?,
1464 )?);
1465 let children = FMIndex::deserialize_tree_topology(&hex_decode(
1466 md.get("tree_topology")
1467 .ok_or_else(|| Error::invalid_input("missing tree_topology"))?,
1468 )?);
1469
1470 let row_ids_hex = md
1472 .get("row_ids")
1473 .ok_or_else(|| Error::invalid_input("missing row_ids"))?;
1474 let row_ids_bytes = hex_decode(row_ids_hex)?;
1475 let row_ids: Vec<u64> = row_ids_bytes
1476 .chunks_exact(8)
1477 .map(|c| u64::from_le_bytes(c.try_into().unwrap()))
1478 .collect();
1479
1480 let doc_starts_hex = md
1481 .get("doc_start_positions")
1482 .ok_or_else(|| Error::invalid_input("missing doc_start_positions"))?;
1483 let doc_starts_bytes = hex_decode(doc_starts_hex)?;
1484 let doc_start_positions: Vec<u64> = doc_starts_bytes
1485 .chunks_exact(8)
1486 .map(|c| u64::from_le_bytes(c.try_into().unwrap()))
1487 .collect();
1488
1489 let fm = Box::pin(LazyFMIndex::from_reader(
1490 reader,
1491 num_bwt_nodes,
1492 huffman_codes,
1493 children,
1494 c_table,
1495 bwt_len,
1496 total_wavelet_rows,
1497 num_sa_blocks,
1498 sa_samples_len,
1499 row_ids,
1500 doc_start_positions,
1501 ))
1502 .await?;
1503 Ok(FMIndexPartition { id: pid, fm })
1504 }
1505
1506 async fn load(
1507 store: Arc<dyn IndexStore>,
1508 _fri: Option<Arc<dyn RowIdRemapper>>,
1509 _cache: &LanceCache,
1510 ) -> Result<Arc<Self>> {
1511 let files = store.list_files_with_sizes().await?;
1512 let mut pfiles: Vec<(u64, String)> = Vec::new();
1513 for f in &files {
1514 if let Some(id) = fmindex_partition_id_from_path(&f.path) {
1515 pfiles.push((id, f.path.clone()));
1516 }
1517 }
1518 if pfiles.is_empty() {
1519 return Err(Error::invalid_input("no FM-Index partition files found"));
1520 }
1521 pfiles.sort_by_key(|(id, _)| *id);
1522 let io_parallelism = store.io_parallelism().max(1);
1523 let mut parts = futures::stream::iter(pfiles)
1524 .map(|(id, name)| {
1525 let store = Arc::clone(&store);
1526 async move {
1527 let partition = Self::load_partition(store.as_ref(), &name, id).await?;
1528 Result::<_>::Ok((id, Arc::new(partition)))
1529 }
1530 })
1531 .buffer_unordered(io_parallelism)
1532 .try_collect::<Vec<_>>()
1533 .await?;
1534 parts.sort_by_key(|(id, _)| *id);
1535 let parts = parts
1536 .into_iter()
1537 .map(|(_, partition)| partition)
1538 .collect::<Vec<_>>();
1539 Ok(Arc::new(Self {
1540 partitions: parts,
1541 io_parallelism,
1542 }))
1543 }
1544
1545 fn partition_parallelism(&self) -> usize {
1546 self.io_parallelism.max(1).min(self.partitions.len().max(1))
1547 }
1548
1549 async fn prewarm_partitions(&self) -> Result<()> {
1550 futures::stream::iter(self.partitions.iter().cloned())
1551 .map(|partition| async move { partition.fm.prewarm().await })
1552 .buffer_unordered(self.partition_parallelism())
1553 .try_collect::<Vec<_>>()
1554 .await?;
1555 Ok(())
1556 }
1557
1558 async fn search_string_contains(&self, pattern: &[u8]) -> Result<SearchResult> {
1559 use lance_select::RowAddrTreeMap;
1560
1561 let pattern: Arc<[u8]> = Arc::from(pattern);
1562 let tree = futures::stream::iter(self.partitions.iter().cloned())
1563 .map(|partition| {
1564 let pattern = Arc::clone(&pattern);
1565 async move {
1566 if partition.fm.fully_prewarmed.load(Ordering::Acquire) {
1567 spawn_cpu(move || {
1568 Result::<Vec<u64>>::Ok(partition.fm.search_row_addrs(pattern.as_ref()))
1569 })
1570 .await
1571 } else {
1572 partition.fm.search_row_addrs_async(pattern.as_ref()).await
1573 }
1574 }
1575 })
1576 .buffer_unordered(self.partition_parallelism())
1577 .try_fold(RowAddrTreeMap::new(), |mut tree, row_addrs| async move {
1578 for row_addr in row_addrs {
1579 tree.insert(row_addr);
1580 }
1581 Result::Ok(tree)
1582 })
1583 .await?;
1584
1585 Ok(SearchResult::Exact(lance_select::NullableRowAddrSet::new(
1586 tree,
1587 Default::default(),
1588 )))
1589 }
1590}
1591
1592#[async_trait]
1593impl Index for FMIndexScalarIndex {
1594 fn as_any(&self) -> &dyn std::any::Any {
1595 self
1596 }
1597 fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
1598 self
1599 }
1600 async fn prewarm(&self) -> Result<()> {
1601 self.prewarm_partitions().await
1602 }
1603 fn statistics(&self) -> Result<serde_json::Value> {
1604 Ok(serde_json::json!({
1605 "type": "Fm",
1606 "num_partitions": self.partitions.len(),
1607 "total_bwt_len": self.partitions.iter().map(|p| p.fm.wavelet.len).sum::<usize>(),
1608 "total_docs": self.partitions.iter().map(|p| p.fm.row_ids.len()).sum::<usize>(),
1609 }))
1610 }
1611 fn index_type(&self) -> IndexType {
1612 IndexType::Fm
1613 }
1614 async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
1615 let mut frags = RoaringBitmap::new();
1616 for p in &self.partitions {
1617 for &rid in &p.fm.row_ids {
1618 frags.insert((rid >> 32) as u32);
1619 }
1620 }
1621 Ok(frags)
1622 }
1623}
1624
1625#[async_trait]
1626impl ScalarIndex for FMIndexScalarIndex {
1627 async fn search(
1628 &self,
1629 query: &dyn AnyQuery,
1630 _metrics: &dyn MetricsCollector,
1631 ) -> Result<SearchResult> {
1632 let tq = query
1633 .as_any()
1634 .downcast_ref::<TextQuery>()
1635 .ok_or_else(|| Error::invalid_input("Fm only supports TextQuery"))?;
1636 match tq {
1637 TextQuery::StringContains(pattern) => {
1638 self.search_string_contains(pattern.as_bytes()).await
1639 }
1640 TextQuery::Regex(_) => Err(Error::invalid_input(
1644 "FMIndex does not support regular expression queries",
1645 )),
1646 }
1647 }
1648 fn can_remap(&self) -> bool {
1649 false
1650 }
1651 async fn remap(&self, _: &RowAddrRemap, _: &dyn IndexStore) -> Result<CreatedIndex> {
1652 Err(Error::not_supported("Fm does not support remap"))
1653 }
1654 async fn update(
1655 &self,
1656 new_data: SendableRecordBatchStream,
1657 dest: &dyn IndexStore,
1658 _old_data_filter: Option<OldIndexDataFilter>,
1659 ) -> Result<CreatedIndex> {
1660 let files = write_partitioned_fmindex_stream(new_data, dest).await?;
1661 Ok(CreatedIndex {
1662 index_details: prost_types::Any::from_msg(&pb::FmIndexDetails {}).unwrap(),
1663 index_version: FMINDEX_INDEX_VERSION,
1664 files,
1665 })
1666 }
1667 fn update_criteria(&self) -> UpdateCriteria {
1668 UpdateCriteria::requires_old_data(
1669 TrainingCriteria::new(TrainingOrdering::None).with_row_addr(),
1670 )
1671 }
1672 fn derive_index_params(&self) -> Result<ScalarIndexParams> {
1673 Ok(ScalarIndexParams::for_builtin(BuiltinIndexType::Fm))
1674 }
1675}
1676
1677#[derive(Debug)]
1680struct FMIndexPartitionJob {
1681 partition_id: u64,
1682 texts: Vec<(u64, Vec<u8>)>,
1683}
1684
1685#[derive(Debug, Clone, Copy)]
1686struct FMIndexPartitionConfig {
1687 num_workers: usize,
1688 max_rows: usize,
1689 max_bytes: usize,
1690 queue_size: usize,
1691 resume_existing: bool,
1692}
1693
1694impl FMIndexPartitionConfig {
1695 fn from_env() -> Self {
1696 Self {
1697 num_workers: fmindex_num_workers(),
1698 max_rows: fmindex_partition_rows(),
1699 max_bytes: fmindex_partition_bytes(),
1700 queue_size: fmindex_write_queue_size(),
1701 resume_existing: fmindex_resume_existing_partitions(),
1702 }
1703 }
1704
1705 fn normalized(self) -> Self {
1706 Self {
1707 num_workers: self.num_workers.max(1),
1708 max_rows: self.max_rows.max(1),
1709 max_bytes: self.max_bytes.max(1),
1710 queue_size: self.queue_size.max(1),
1711 resume_existing: self.resume_existing,
1712 }
1713 }
1714}
1715
1716async fn write_partitioned_fmindex_stream(
1717 stream: SendableRecordBatchStream,
1718 store: &dyn IndexStore,
1719) -> Result<Vec<IndexFile>> {
1720 write_partitioned_fmindex_stream_with_config(stream, store, FMIndexPartitionConfig::from_env())
1721 .await
1722}
1723
1724async fn write_partitioned_fmindex_stream_with_config(
1725 mut stream: SendableRecordBatchStream,
1726 store: &dyn IndexStore,
1727 config: FMIndexPartitionConfig,
1728) -> Result<Vec<IndexFile>> {
1729 let config = config.normalized();
1730 log::info!(
1731 "building FMIndex with {} workers, partition rows {}, partition bytes {}",
1732 config.num_workers,
1733 config.max_rows,
1734 config.max_bytes
1735 );
1736
1737 let (sender, receiver): (
1738 async_channel::Sender<FMIndexPartitionJob>,
1739 async_channel::Receiver<FMIndexPartitionJob>,
1740 ) = async_channel::bounded(config.queue_size);
1741 let store = store.clone_arc();
1742 let mut completed_files = if config.resume_existing {
1743 store
1744 .list_files_with_sizes()
1745 .await?
1746 .into_iter()
1747 .filter_map(|file| fmindex_partition_id_from_path(&file.path).map(|id| (id, file)))
1748 .collect::<HashMap<_, _>>()
1749 } else {
1750 HashMap::new()
1751 };
1752 if !completed_files.is_empty() {
1753 log::info!(
1754 "resuming FMIndex build with {} existing partition files",
1755 completed_files.len()
1756 );
1757 }
1758 let mut files = Vec::new();
1759 let mut worker_tasks = Vec::with_capacity(config.num_workers);
1760 for _ in 0..config.num_workers {
1761 let receiver = receiver.clone();
1762 let store = store.clone();
1763 worker_tasks.push(tokio::task::spawn(async move {
1764 let mut files = Vec::new();
1765 while let Ok(job) = receiver.recv().await {
1766 files.push(
1767 write_fmindex_partition_owned(job.texts, store.clone(), job.partition_id)
1768 .await?,
1769 );
1770 }
1771 Result::Ok(files)
1772 }));
1773 }
1774 drop(receiver);
1775
1776 let producer_result = async {
1777 let mut partition = Vec::with_capacity(config.max_rows.min(PARTITION_SIZE));
1778 let mut partition_bytes = 0usize;
1779 let mut partition_id = 0;
1780
1781 while let Some(batch) = stream.next().await {
1782 let batch = batch?;
1783 let row_addrs: &arrow_array::UInt64Array = batch
1786 .column_by_name(ROW_ADDR)
1787 .or_else(|| batch.column_by_name("_rowid"))
1788 .and_then(|c| c.as_any().downcast_ref())
1789 .ok_or_else(|| {
1790 Error::invalid_input("Fm training data must include _rowaddr or _rowid column")
1791 })?;
1792 let value_col = batch
1794 .column_by_name(VALUE_COLUMN_NAME)
1795 .unwrap_or_else(|| batch.column(0));
1796 for i in 0..batch.num_rows() {
1797 let rid = row_addrs.value(i);
1798 if let Some(bytes) = extract_sanitized_text_bytes(value_col.as_ref(), i)? {
1799 partition_bytes = partition_bytes.saturating_add(bytes.len().saturating_add(1));
1800 partition.push((rid, bytes));
1801 if fmindex_partition_limit_reached(
1802 partition.len(),
1803 partition_bytes,
1804 config.max_rows,
1805 config.max_bytes,
1806 ) {
1807 finish_fmindex_partition(
1808 &sender,
1809 store.as_ref(),
1810 &mut partition,
1811 &mut partition_bytes,
1812 partition_id,
1813 config.max_rows,
1814 &mut FMIndexPartitionResumeState {
1815 completed_files: &mut completed_files,
1816 output_files: &mut files,
1817 },
1818 )
1819 .await?;
1820 partition_id += 1;
1821 }
1822 }
1823 }
1824 }
1825
1826 if !partition.is_empty() {
1827 finish_fmindex_partition(
1828 &sender,
1829 store.as_ref(),
1830 &mut partition,
1831 &mut partition_bytes,
1832 partition_id,
1833 config.max_rows,
1834 &mut FMIndexPartitionResumeState {
1835 completed_files: &mut completed_files,
1836 output_files: &mut files,
1837 },
1838 )
1839 .await?;
1840 }
1841
1842 Result::Ok(())
1843 }
1844 .await;
1845 drop(sender);
1846
1847 let mut worker_error = None;
1848 for worker_task in worker_tasks {
1849 match worker_task.await {
1850 Ok(Ok(worker_files)) => files.extend(worker_files),
1851 Ok(Err(err)) => {
1852 if worker_error.is_none() {
1853 worker_error = Some(err);
1854 }
1855 }
1856 Err(err) => {
1857 if worker_error.is_none() {
1858 worker_error = Some(Error::execution(format!(
1859 "FMIndex partition worker failed: {err}"
1860 )));
1861 }
1862 }
1863 }
1864 }
1865 if let Some(err) = worker_error {
1866 return Err(err);
1867 }
1868 producer_result?;
1869
1870 for (partition_id, file) in completed_files.drain() {
1871 log::info!(
1872 "deleting stale FMIndex partition {partition_id} from previous build: {}",
1873 file.path
1874 );
1875 store.delete_index_file(&file.path).await?;
1876 }
1877 if files.is_empty() {
1878 return Ok(vec![write_empty_fmindex_partition(store.as_ref()).await?]);
1879 }
1880
1881 files.sort_unstable_by_key(|(partition_id, _)| *partition_id);
1882 let files = files.into_iter().map(|(_, file)| file).collect();
1883
1884 Ok(files)
1885}
1886
1887fn fmindex_partition_limit_reached(
1888 rows: usize,
1889 bytes: usize,
1890 max_rows: usize,
1891 max_bytes: usize,
1892) -> bool {
1893 rows >= max_rows || bytes >= max_bytes
1896}
1897
1898struct FMIndexPartitionResumeState<'a> {
1899 completed_files: &'a mut HashMap<u64, IndexFile>,
1900 output_files: &'a mut Vec<(u64, IndexFile)>,
1901}
1902
1903async fn finish_fmindex_partition(
1904 sender: &async_channel::Sender<FMIndexPartitionJob>,
1905 store: &dyn IndexStore,
1906 partition: &mut Vec<(u64, Vec<u8>)>,
1907 partition_bytes: &mut usize,
1908 partition_id: u64,
1909 max_rows: usize,
1910 resume_state: &mut FMIndexPartitionResumeState<'_>,
1911) -> Result<()> {
1912 let texts = std::mem::replace(partition, Vec::with_capacity(max_rows.min(PARTITION_SIZE)));
1913 *partition_bytes = 0;
1914 if let Some(file) = resume_state.completed_files.remove(&partition_id) {
1915 let fingerprint = fmindex_partition_fingerprint(&texts);
1916 if fmindex_partition_matches(store, &file, &fingerprint).await? {
1917 resume_state.output_files.push((partition_id, file));
1918 return Ok(());
1919 }
1920 log::info!(
1921 "rebuilding stale FMIndex partition {partition_id}: {}",
1922 file.path
1923 );
1924 store.delete_index_file(&file.path).await?;
1925 }
1926 sender
1927 .send(FMIndexPartitionJob {
1928 partition_id,
1929 texts,
1930 })
1931 .await
1932 .map_err(|err| {
1933 Error::execution(format!(
1934 "failed to schedule FMIndex partition {partition_id}: {err}"
1935 ))
1936 })
1937}
1938
1939fn fmindex_partition_fingerprint(texts: &[(u64, Vec<u8>)]) -> String {
1940 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
1941 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
1942
1943 fn update(hash: &mut u64, bytes: &[u8]) {
1944 for byte in bytes {
1945 *hash ^= *byte as u64;
1946 *hash = hash.wrapping_mul(FNV_PRIME);
1947 }
1948 }
1949
1950 let mut hash = FNV_OFFSET;
1951 update(&mut hash, b"lance-fmindex-partition-v1");
1952 update(&mut hash, &(texts.len() as u64).to_le_bytes());
1953 for (row_id, bytes) in texts {
1954 update(&mut hash, &row_id.to_le_bytes());
1955 update(&mut hash, &(bytes.len() as u64).to_le_bytes());
1956 update(&mut hash, bytes);
1957 }
1958 format!("{hash:016x}")
1959}
1960
1961async fn fmindex_partition_matches(
1962 store: &dyn IndexStore,
1963 file: &IndexFile,
1964 expected_fingerprint: &str,
1965) -> Result<bool> {
1966 let reader = store.open_index_file(&file.path).await?;
1967 Ok(reader
1968 .schema()
1969 .metadata
1970 .get(FMINDEX_PARTITION_FINGERPRINT_KEY)
1971 .is_some_and(|fingerprint| fingerprint == expected_fingerprint))
1972}
1973
1974fn sanitize_text_bytes(bytes: &[u8]) -> Vec<u8> {
1975 bytes
1976 .iter()
1977 .map(|&b| {
1978 if b == SENTINEL_BYTE || b == 0x00 {
1979 b' '
1980 } else {
1981 b
1982 }
1983 })
1984 .collect()
1985}
1986
1987fn extract_sanitized_text_bytes(
1988 array: &dyn arrow_array::Array,
1989 index: usize,
1990) -> Result<Option<Vec<u8>>> {
1991 if array.is_null(index) {
1992 return Ok(None);
1993 }
1994 match array.data_type() {
1995 DataType::Utf8 => Ok(Some(sanitize_text_bytes(
1996 array
1997 .as_any()
1998 .downcast_ref::<arrow_array::StringArray>()
1999 .unwrap()
2000 .value(index)
2001 .as_bytes(),
2002 ))),
2003 DataType::LargeUtf8 => Ok(Some(sanitize_text_bytes(
2004 array
2005 .as_any()
2006 .downcast_ref::<arrow_array::LargeStringArray>()
2007 .unwrap()
2008 .value(index)
2009 .as_bytes(),
2010 ))),
2011 DataType::Binary => Ok(Some(sanitize_text_bytes(
2012 array
2013 .as_any()
2014 .downcast_ref::<arrow_array::BinaryArray>()
2015 .unwrap()
2016 .value(index),
2017 ))),
2018 DataType::LargeBinary => Ok(Some(sanitize_text_bytes(
2019 array
2020 .as_any()
2021 .downcast_ref::<arrow_array::LargeBinaryArray>()
2022 .unwrap()
2023 .value(index),
2024 ))),
2025 _ => Err(Error::invalid_input(format!(
2026 "Fm does not support data type: {:?}",
2027 array.data_type()
2028 ))),
2029 }
2030}
2031
2032#[cfg(test)]
2033fn extract_text_bytes(array: &dyn arrow_array::Array, index: usize) -> Result<Option<Vec<u8>>> {
2034 if array.is_null(index) {
2035 return Ok(None);
2036 }
2037 match array.data_type() {
2038 DataType::Utf8 => Ok(Some(
2039 array
2040 .as_any()
2041 .downcast_ref::<arrow_array::StringArray>()
2042 .unwrap()
2043 .value(index)
2044 .as_bytes()
2045 .to_vec(),
2046 )),
2047 DataType::LargeUtf8 => Ok(Some(
2048 array
2049 .as_any()
2050 .downcast_ref::<arrow_array::LargeStringArray>()
2051 .unwrap()
2052 .value(index)
2053 .as_bytes()
2054 .to_vec(),
2055 )),
2056 DataType::Binary => Ok(Some(
2057 array
2058 .as_any()
2059 .downcast_ref::<arrow_array::BinaryArray>()
2060 .unwrap()
2061 .value(index)
2062 .to_vec(),
2063 )),
2064 DataType::LargeBinary => Ok(Some(
2065 array
2066 .as_any()
2067 .downcast_ref::<arrow_array::LargeBinaryArray>()
2068 .unwrap()
2069 .value(index)
2070 .to_vec(),
2071 )),
2072 _ => Err(Error::invalid_input(format!(
2073 "Fm does not support data type: {:?}",
2074 array.data_type()
2075 ))),
2076 }
2077}
2078
2079fn hex_encode(data: &[u8]) -> String {
2080 data.iter().map(|b| format!("{b:02x}")).collect()
2081}
2082fn hex_decode(s: &str) -> Result<Vec<u8>> {
2083 if !s.len().is_multiple_of(2) {
2084 return Err(Error::invalid_input("invalid hex length"));
2085 }
2086 (0..s.len())
2087 .step_by(2)
2088 .map(|i| {
2089 u8::from_str_radix(&s[i..i + 2], 16)
2090 .map_err(|e| Error::invalid_input(format!("invalid hex: {e}")))
2091 })
2092 .collect()
2093}
2094
2095async fn write_fmindex(
2102 fm: &FMIndex,
2103 store: &dyn IndexStore,
2104 filename: &str,
2105 partition_fingerprint: Option<&str>,
2106) -> Result<IndexFile> {
2107 let schema = Arc::new(FMIndex::block_schema());
2108
2109 let mut writer = store.new_index_file(filename, schema.clone()).await?;
2110
2111 let wb = fm.build_wavelet_batch()?;
2113 let nw = wb.num_rows();
2114 writer.write_record_batch(wb).await?;
2115
2116 let u64s_per_block = BLOCK_WORDS; let mut sa_nid = Vec::new();
2119 let mut sa_bid = Vec::new();
2120 let mut sa_words: Vec<Vec<u8>> = Vec::new();
2121 let mut sa_pr = Vec::new();
2122 let mut sa_bl = Vec::new();
2123 for (bi, chunk) in fm.sa_samples.chunks(u64s_per_block).enumerate() {
2124 sa_nid.push(u32::MAX);
2125 sa_bid.push(bi as u32);
2126 sa_words.push(FMIndex::u64_to_bytes(chunk));
2127 sa_pr.push(0u64);
2128 sa_bl.push(fm.sa_samples.len() as u64);
2129 }
2130 let num_sa_blocks = sa_nid.len();
2131 if num_sa_blocks > 0 {
2132 let refs: Vec<&[u8]> = sa_words.iter().map(|v| v.as_slice()).collect();
2133 writer
2134 .write_record_batch(RecordBatch::try_new(
2135 schema.clone(),
2136 vec![
2137 Arc::new(arrow_array::UInt32Array::from(sa_nid)),
2138 Arc::new(arrow_array::UInt32Array::from(sa_bid)),
2139 Arc::new(arrow_array::LargeBinaryArray::from(refs)),
2140 Arc::new(arrow_array::UInt64Array::from(sa_pr)),
2141 Arc::new(arrow_array::UInt64Array::from(sa_bl)),
2142 ],
2143 )?)
2144 .await?;
2145 }
2146
2147 let mut metadata = HashMap::new();
2149 metadata.insert("num_bwt_nodes".into(), fm.wavelet.nodes.len().to_string());
2150 metadata.insert("bwt_len".into(), fm.wavelet.len.to_string());
2151 metadata.insert("num_sa_blocks".into(), num_sa_blocks.to_string());
2152 metadata.insert("sa_samples_len".into(), fm.sa_samples.len().to_string());
2153 metadata.insert("total_wavelet_rows".into(), nw.to_string());
2154 metadata.insert("sa_sample_rate".into(), SA_SAMPLE_RATE.to_string());
2155 metadata.insert("alphabet_size".into(), fm.alphabet_size.to_string());
2156 if let Some(fingerprint) = partition_fingerprint {
2157 metadata.insert(
2158 FMINDEX_PARTITION_FINGERPRINT_KEY.into(),
2159 fingerprint.to_string(),
2160 );
2161 }
2162 metadata.insert("c_table".into(), hex_encode(&fm.serialize_c_table()));
2163 metadata.insert(
2164 "huffman_codes".into(),
2165 hex_encode(&fm.serialize_huffman_codes()),
2166 );
2167 metadata.insert(
2168 "tree_topology".into(),
2169 hex_encode(&fm.serialize_tree_topology()),
2170 );
2171 let row_ids_bytes: Vec<u8> = fm.row_ids.iter().flat_map(|&v| v.to_le_bytes()).collect();
2173 metadata.insert("row_ids".into(), hex_encode(&row_ids_bytes));
2174 let doc_starts_bytes: Vec<u8> = fm
2176 .doc_start_positions
2177 .iter()
2178 .flat_map(|&v| v.to_le_bytes())
2179 .collect();
2180 metadata.insert("doc_start_positions".into(), hex_encode(&doc_starts_bytes));
2181
2182 writer.finish_with_metadata(metadata).await
2183}
2184
2185#[cfg(test)]
2186async fn write_partitioned_fmindex(
2187 texts: &[(u64, Vec<u8>)],
2188 store: &dyn IndexStore,
2189) -> Result<Vec<IndexFile>> {
2190 if texts.is_empty() {
2191 return Ok(vec![write_empty_fmindex_partition(store).await?]);
2192 }
2193 let mut files = Vec::new();
2194 for (pid, chunk) in texts.chunks(PARTITION_SIZE).enumerate() {
2195 files.push(write_fmindex_partition(chunk, store, pid as u64).await?);
2196 }
2197 Ok(files)
2198}
2199
2200#[cfg(test)]
2201async fn write_fmindex_partition(
2202 texts: &[(u64, Vec<u8>)],
2203 store: &dyn IndexStore,
2204 partition_id: u64,
2205) -> Result<IndexFile> {
2206 let fingerprint = fmindex_partition_fingerprint(texts);
2207 let refs: Vec<(u64, &[u8])> = texts.iter().map(|(id, t)| (*id, t.as_slice())).collect();
2208 let fm = FMIndex::build(&refs)?;
2209 write_fmindex(
2210 &fm,
2211 store,
2212 &fmindex_partition_path(partition_id),
2213 Some(&fingerprint),
2214 )
2215 .await
2216}
2217
2218async fn write_fmindex_partition_owned(
2219 texts: Vec<(u64, Vec<u8>)>,
2220 store: Arc<dyn IndexStore>,
2221 partition_id: u64,
2222) -> Result<(u64, IndexFile)> {
2223 let fingerprint = fmindex_partition_fingerprint(&texts);
2224 let fm = spawn_cpu(move || {
2225 let refs: Vec<(u64, &[u8])> = texts.iter().map(|(id, t)| (*id, t.as_slice())).collect();
2226 FMIndex::build(&refs)
2227 })
2228 .await?;
2229 let file = write_fmindex(
2230 &fm,
2231 store.as_ref(),
2232 &fmindex_partition_path(partition_id),
2233 Some(&fingerprint),
2234 )
2235 .await?;
2236 Ok((partition_id, file))
2237}
2238
2239async fn write_empty_fmindex_partition(store: &dyn IndexStore) -> Result<IndexFile> {
2240 let fm = FMIndex::build(&[])?;
2241 let fingerprint = fmindex_partition_fingerprint(&[]);
2242 write_fmindex(&fm, store, &fmindex_partition_path(0), Some(&fingerprint)).await
2243}
2244
2245#[derive(Debug, Default)]
2248pub struct FMIndexPlugin;
2249
2250#[async_trait]
2251impl BasicTrainer for FMIndexPlugin {
2252 fn new_training_request(
2253 &self,
2254 _params: &str,
2255 field: &Field,
2256 ) -> Result<Box<dyn TrainingRequest>> {
2257 match field.data_type() {
2258 DataType::Utf8 | DataType::LargeUtf8 | DataType::Binary | DataType::LargeBinary => {}
2259 _ => {
2260 return Err(Error::invalid_input(format!(
2261 "FM-Index does not support {:?}",
2262 field.data_type()
2263 )));
2264 }
2265 }
2266 Ok(Box::new(DefaultTrainingRequest::new(
2267 TrainingCriteria::new(TrainingOrdering::None).with_row_addr(),
2268 )))
2269 }
2270 async fn train_index(
2271 &self,
2272 data: SendableRecordBatchStream,
2273 store: &dyn IndexStore,
2274 _req: Box<dyn TrainingRequest>,
2275 _fids: Option<Vec<u32>>,
2276 _progress: Arc<dyn crate::progress::IndexBuildProgress>,
2277 ) -> Result<CreatedIndex> {
2278 let files = write_partitioned_fmindex_stream(data, store).await?;
2279 Ok(CreatedIndex {
2280 index_details: prost_types::Any::from_msg(&pb::FmIndexDetails {}).unwrap(),
2281 index_version: FMINDEX_INDEX_VERSION,
2282 files,
2283 })
2284 }
2285}
2286
2287#[async_trait]
2288impl ScalarIndexPlugin for FMIndexPlugin {
2289 fn basic_trainer(&self) -> Option<&dyn BasicTrainer> {
2290 Some(self)
2291 }
2292
2293 fn name(&self) -> &str {
2294 "Fm"
2295 }
2296 fn provides_exact_answer(&self) -> bool {
2297 true
2298 }
2299 fn version(&self) -> u32 {
2300 FMINDEX_INDEX_VERSION
2301 }
2302 fn new_query_parser(
2303 &self,
2304 index_name: String,
2305 _details: &prost_types::Any,
2306 ) -> Option<Box<dyn ScalarQueryParser>> {
2307 Some(Box::new(TextQueryParser::new(
2308 index_name,
2309 self.name().to_string(),
2310 false,
2312 false,
2314 )))
2315 }
2316 async fn load_index(
2317 &self,
2318 store: Arc<dyn IndexStore>,
2319 details: &prost_types::Any,
2320 fri: Option<Arc<dyn RowIdRemapper>>,
2321 cache: &LanceCache,
2322 ) -> Result<Arc<dyn ScalarIndex>> {
2323 let _ = details.to_msg::<pb::FmIndexDetails>().unwrap_or_default();
2324 Ok(FMIndexScalarIndex::load(store, fri, cache).await? as Arc<dyn ScalarIndex>)
2325 }
2326 async fn load_statistics(
2327 &self,
2328 _: Arc<dyn IndexStore>,
2329 _: &prost_types::Any,
2330 ) -> Result<Option<serde_json::Value>> {
2331 Ok(None)
2332 }
2333}
2334
2335#[cfg(test)]
2336mod tests {
2337 use super::*;
2338 use arrow_array::{BinaryArray, LargeBinaryArray, LargeStringArray, StringArray, UInt64Array};
2339 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
2340 use futures::stream;
2341 use lance_core::{ROW_ADDR, cache::LanceCache};
2342 use lance_io::object_store::ObjectStore;
2343 use object_store::path::Path;
2344 use std::sync::Arc;
2345
2346 use crate::scalar::lance_format::LanceIndexStore;
2347 use crate::scalar::registry::BasicTrainer;
2348
2349 #[derive(Debug, Clone)]
2350 struct FailNewFileStore {
2351 inner: Arc<dyn IndexStore>,
2352 }
2353
2354 impl DeepSizeOf for FailNewFileStore {
2355 fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
2356 0
2357 }
2358 }
2359
2360 #[async_trait::async_trait]
2361 impl IndexStore for FailNewFileStore {
2362 fn as_any(&self) -> &dyn std::any::Any {
2363 self
2364 }
2365
2366 fn clone_arc(&self) -> Arc<dyn IndexStore> {
2367 Arc::new(self.clone())
2368 }
2369
2370 fn io_parallelism(&self) -> usize {
2371 self.inner.io_parallelism()
2372 }
2373
2374 fn with_io_priority(&self, io_priority: u64) -> Arc<dyn IndexStore> {
2375 Arc::new(Self {
2376 inner: self.inner.with_io_priority(io_priority),
2377 })
2378 }
2379
2380 async fn new_index_file(
2381 &self,
2382 _name: &str,
2383 _schema: Arc<arrow_schema::Schema>,
2384 ) -> Result<Box<dyn crate::scalar::IndexWriter>> {
2385 Err(Error::execution(
2386 "injected FMIndex write failure".to_string(),
2387 ))
2388 }
2389
2390 async fn open_index_file(&self, name: &str) -> Result<Arc<dyn crate::scalar::IndexReader>> {
2391 self.inner.open_index_file(name).await
2392 }
2393
2394 async fn copy_index_file(
2395 &self,
2396 name: &str,
2397 dest_store: &dyn IndexStore,
2398 ) -> Result<IndexFile> {
2399 self.inner.copy_index_file(name, dest_store).await
2400 }
2401
2402 async fn rename_index_file(&self, name: &str, new_name: &str) -> Result<IndexFile> {
2403 self.inner.rename_index_file(name, new_name).await
2404 }
2405
2406 async fn delete_index_file(&self, name: &str) -> Result<()> {
2407 self.inner.delete_index_file(name).await
2408 }
2409
2410 async fn list_files_with_sizes(&self) -> Result<Vec<IndexFile>> {
2411 self.inner.list_files_with_sizes().await
2412 }
2413 }
2414
2415 fn loaded_wavelet_blocks(index: &FMIndexScalarIndex) -> usize {
2416 index
2417 .partitions
2418 .iter()
2419 .flat_map(|partition| partition.fm.wavelet.nodes.iter())
2420 .map(|node| {
2421 node.blocks
2422 .iter()
2423 .filter(|block| block.get().is_some())
2424 .count()
2425 })
2426 .sum()
2427 }
2428
2429 fn total_wavelet_blocks(index: &FMIndexScalarIndex) -> usize {
2430 index
2431 .partitions
2432 .iter()
2433 .flat_map(|partition| partition.fm.wavelet.nodes.iter())
2434 .map(|node| node.blocks.len())
2435 .sum()
2436 }
2437
2438 #[test]
2439 fn test_index_size_ratio() {
2440 let docs: Vec<Vec<u8>> = (0..200)
2441 .map(|i| {
2442 format!(
2443 "document {} with enough text to test size ratio properly end",
2444 i
2445 )
2446 .into_bytes()
2447 })
2448 .collect();
2449 let texts: Vec<(u64, &[u8])> = docs
2450 .iter()
2451 .enumerate()
2452 .map(|(i, d)| (i as u64, d.as_slice()))
2453 .collect();
2454 let fm = FMIndex::build(&texts).unwrap();
2455
2456 let text_size: usize = docs.iter().map(|d| d.len()).sum();
2457 let wavelet_size = fm.wavelet.deep_size();
2458 let sa_size = fm.sa_samples.len() * 8;
2459 let total = wavelet_size + sa_size;
2460
2461 let ratio = total as f64 / text_size as f64;
2462 assert!(
2463 ratio < 1.5,
2464 "index should be much smaller than text, got ratio={ratio:.2}"
2465 );
2466 }
2467
2468 #[test]
2469 fn test_serialization_roundtrip() {
2470 let texts: Vec<(u64, &[u8])> = vec![
2471 (10, b"alpha beta gamma"),
2472 (20, b"beta gamma delta"),
2473 (30, b"gamma delta epsilon"),
2474 ];
2475 let fm = FMIndex::build(&texts).unwrap();
2476
2477 let hc_bytes = fm.serialize_huffman_codes();
2479 let hc = FMIndex::deserialize_huffman_codes(&hc_bytes);
2480 for (i, (loaded, original)) in hc.iter().zip(fm.wavelet.codes.iter()).enumerate() {
2481 assert_eq!(loaded.bits, original.bits, "bits mismatch at {i}");
2482 assert_eq!(loaded.length, original.length, "length mismatch at {i}");
2483 assert_eq!(loaded.node_path, original.node_path, "path mismatch at {i}");
2484 }
2485
2486 let topo_bytes = fm.serialize_tree_topology();
2488 let topo = FMIndex::deserialize_tree_topology(&topo_bytes);
2489 assert_eq!(topo.len(), fm.wavelet.children.len());
2490
2491 let ct_bytes = fm.serialize_c_table();
2493 let ct = FMIndex::deserialize_c_table(&ct_bytes);
2494 assert_eq!(ct, fm.c_table);
2495 }
2496
2497 #[test]
2498 fn test_hex_roundtrip() {
2499 let data = vec![0u8, 1, 127, 255, 42];
2500 let encoded = hex_encode(&data);
2501 let decoded = hex_decode(&encoded).unwrap();
2502 assert_eq!(data, decoded);
2503 }
2504
2505 #[test]
2506 fn test_partition_limit_reached_by_rows_or_bytes() {
2507 assert!(fmindex_partition_limit_reached(10, 5, 10, 100));
2508 assert!(fmindex_partition_limit_reached(3, 100, 10, 100));
2509 assert!(!fmindex_partition_limit_reached(3, 99, 10, 100));
2510 }
2511
2512 #[test]
2513 fn test_large_sa_sampling() {
2514 let docs: Vec<Vec<u8>> = (0..50)
2516 .map(|i| {
2517 format!(
2518 "document number {} with lots of text to ensure we have enough bytes for multiple SA samples across the suffix array positions",
2519 i
2520 )
2521 .into_bytes()
2522 })
2523 .collect();
2524 let texts: Vec<(u64, &[u8])> = docs
2525 .iter()
2526 .enumerate()
2527 .map(|(i, d)| (i as u64, d.as_slice()))
2528 .collect();
2529 let fm = FMIndex::build(&texts).unwrap();
2530
2531 assert!(fm.sa_samples.len() > 1, "should have multiple SA samples");
2532 }
2533
2534 #[tokio::test(flavor = "multi_thread")]
2535 async fn test_write_and_load_roundtrip() {
2536 let texts: Vec<(u64, &[u8])> = vec![
2537 (0, b"hello world foo bar"),
2538 (1, b"hello rust baz qux"),
2539 (2, b"goodbye world quux"),
2540 ];
2541 let fm = FMIndex::build(&texts).unwrap();
2542
2543 let tempdir = tempfile::tempdir().unwrap();
2544 let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap();
2545 let store = Arc::new(LanceIndexStore::new(
2546 Arc::new(ObjectStore::local()),
2547 index_dir,
2548 Arc::new(LanceCache::no_cache()),
2549 ));
2550
2551 write_fmindex(&fm, store.as_ref(), &fmindex_partition_path(0), None)
2553 .await
2554 .unwrap();
2555
2556 let part =
2558 FMIndexScalarIndex::load_partition(store.as_ref(), &fmindex_partition_path(0), 0)
2559 .await
2560 .unwrap();
2561
2562 let r = part.fm.search(b"hello");
2564 assert!(r.contains(0));
2565 assert!(r.contains(1));
2566 assert!(!r.contains(2));
2567
2568 let r = part.fm.search(b"world");
2569 assert!(r.contains(0));
2570 assert!(!r.contains(1));
2571 assert!(r.contains(2));
2572
2573 assert!(part.fm.search(b"xyz").is_empty());
2574 }
2575
2576 #[tokio::test(flavor = "multi_thread")]
2577 async fn test_partitioned_write_and_load() {
2578 let docs: Vec<Vec<u8>> = (0..30)
2579 .map(|i| format!("document {i} hello world test data").into_bytes())
2580 .collect();
2581 let texts: Vec<(u64, Vec<u8>)> = docs
2582 .into_iter()
2583 .enumerate()
2584 .map(|(i, d)| (i as u64, d))
2585 .collect();
2586
2587 let tempdir = tempfile::tempdir().unwrap();
2588 let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap();
2589 let store = Arc::new(LanceIndexStore::new(
2590 Arc::new(ObjectStore::local()),
2591 index_dir,
2592 Arc::new(LanceCache::no_cache()),
2593 ));
2594
2595 write_partitioned_fmindex(&texts, store.as_ref())
2596 .await
2597 .unwrap();
2598
2599 let index = FMIndexScalarIndex::load(store, None, &LanceCache::no_cache())
2600 .await
2601 .unwrap();
2602
2603 let r = index
2605 .search(
2606 &TextQuery::StringContains("hello world".to_string()),
2607 &crate::metrics::NoOpMetricsCollector,
2608 )
2609 .await
2610 .unwrap();
2611 match r {
2612 SearchResult::Exact(set) => {
2613 assert_eq!(set.len(), Some(30));
2614 }
2615 _ => panic!("expected exact result"),
2616 }
2617
2618 let r = index
2619 .search(
2620 &TextQuery::StringContains("document 15".to_string()),
2621 &crate::metrics::NoOpMetricsCollector,
2622 )
2623 .await
2624 .unwrap();
2625 match r {
2626 SearchResult::Exact(set) => {
2627 assert_eq!(set.len(), Some(1));
2628 }
2629 _ => panic!("expected exact result"),
2630 }
2631
2632 let r = index
2633 .search(
2634 &TextQuery::StringContains("nonexistent".to_string()),
2635 &crate::metrics::NoOpMetricsCollector,
2636 )
2637 .await
2638 .unwrap();
2639 match r {
2640 SearchResult::Exact(set) => {
2641 assert_eq!(set.len(), Some(0));
2642 }
2643 _ => panic!("expected exact result"),
2644 }
2645 }
2646
2647 #[tokio::test(flavor = "multi_thread")]
2648 async fn test_public_prewarm_loads_lazy_blocks() {
2649 let docs: Vec<Vec<u8>> = (0..30)
2650 .map(|i| format!("document {i} hello world test data").into_bytes())
2651 .collect();
2652 let texts: Vec<(u64, Vec<u8>)> = docs
2653 .into_iter()
2654 .enumerate()
2655 .map(|(i, d)| (i as u64, d))
2656 .collect();
2657
2658 let tempdir = tempfile::tempdir().unwrap();
2659 let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap();
2660 let store = Arc::new(LanceIndexStore::new(
2661 Arc::new(ObjectStore::local()),
2662 index_dir,
2663 Arc::new(LanceCache::no_cache()),
2664 ));
2665
2666 write_partitioned_fmindex(&texts, store.as_ref())
2667 .await
2668 .unwrap();
2669
2670 let index = FMIndexScalarIndex::load(store, None, &LanceCache::no_cache())
2671 .await
2672 .unwrap();
2673 let total_blocks = total_wavelet_blocks(index.as_ref());
2674 assert!(total_blocks > 0);
2675 assert_eq!(loaded_wavelet_blocks(index.as_ref()), 0);
2676
2677 index.prewarm().await.unwrap();
2678 assert_eq!(loaded_wavelet_blocks(index.as_ref()), total_blocks);
2679
2680 let r = index
2681 .search(
2682 &TextQuery::StringContains("hello world".to_string()),
2683 &crate::metrics::NoOpMetricsCollector,
2684 )
2685 .await
2686 .unwrap();
2687 match r {
2688 SearchResult::Exact(set) => {
2689 assert_eq!(set.len(), Some(30));
2690 }
2691 _ => panic!("expected exact result"),
2692 }
2693 assert_eq!(loaded_wavelet_blocks(index.as_ref()), total_blocks);
2694 }
2695
2696 #[tokio::test(flavor = "multi_thread")]
2697 async fn test_lazy_rank_terminal_block_boundary() {
2698 let tempdir = tempfile::tempdir().unwrap();
2699 let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap();
2700 let store = Arc::new(LanceIndexStore::new(
2701 Arc::new(ObjectStore::local()),
2702 index_dir,
2703 Arc::new(LanceCache::no_cache()),
2704 ));
2705 let schema = Arc::new(arrow_schema::Schema::new(vec![Field::new(
2706 "words",
2707 DataType::LargeBinary,
2708 false,
2709 )]));
2710 let words = vec![u64::MAX; BLOCK_WORDS];
2711 let bytes = FMIndex::u64_to_bytes(&words);
2712 let batch = RecordBatch::try_new(
2713 schema.clone(),
2714 vec![Arc::new(LargeBinaryArray::from(vec![bytes.as_slice()]))],
2715 )
2716 .unwrap();
2717 let mut writer = store.new_index_file("rank.lance", schema).await.unwrap();
2718 writer.write_record_batch(batch).await.unwrap();
2719 writer.finish().await.unwrap();
2720
2721 let reader = store.open_index_file("rank.lance").await.unwrap();
2722 let bitvec = LazyRankBitVec::new(vec![0], 1, reader, 0, BLOCK_BITS);
2723 bitvec.load_block_for_rank(BLOCK_BITS).await.unwrap();
2724
2725 assert_eq!(bitvec.rank1(BLOCK_BITS), BLOCK_BITS);
2726 assert_eq!(bitvec.rank0(BLOCK_BITS), 0);
2727 }
2728
2729 #[tokio::test(flavor = "multi_thread")]
2730 async fn test_contains_search_does_not_full_prewarm_partitions() {
2731 let docs: Vec<Vec<u8>> = (0..30)
2732 .map(|i| format!("document {i} hello world test data").into_bytes())
2733 .collect();
2734 let texts: Vec<(u64, Vec<u8>)> = docs
2735 .into_iter()
2736 .enumerate()
2737 .map(|(i, d)| (i as u64, d))
2738 .collect();
2739
2740 let tempdir = tempfile::tempdir().unwrap();
2741 let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap();
2742 let store = Arc::new(LanceIndexStore::new(
2743 Arc::new(ObjectStore::local()),
2744 index_dir,
2745 Arc::new(LanceCache::no_cache()),
2746 ));
2747
2748 write_partitioned_fmindex(&texts, store.as_ref())
2749 .await
2750 .unwrap();
2751
2752 let index = FMIndexScalarIndex::load(store, None, &LanceCache::no_cache())
2753 .await
2754 .unwrap();
2755 assert_eq!(loaded_wavelet_blocks(index.as_ref()), 0);
2756 assert!(
2757 index
2758 .partitions
2759 .iter()
2760 .all(|partition| !partition.fm.fully_prewarmed.load(Ordering::Acquire))
2761 );
2762
2763 let r = index
2764 .search(
2765 &TextQuery::StringContains("document 15".to_string()),
2766 &crate::metrics::NoOpMetricsCollector,
2767 )
2768 .await
2769 .unwrap();
2770 match r {
2771 SearchResult::Exact(set) => {
2772 assert_eq!(set.len(), Some(1));
2773 }
2774 _ => panic!("expected exact result"),
2775 }
2776
2777 assert!(
2778 index
2779 .partitions
2780 .iter()
2781 .all(|partition| !partition.fm.fully_prewarmed.load(Ordering::Acquire))
2782 );
2783 }
2784
2785 #[tokio::test(flavor = "multi_thread")]
2786 async fn test_plugin_train_and_load() {
2787 let docs = vec!["hello world", "hello rust", "goodbye world"];
2788 let row_addrs: Vec<u64> = vec![0, 1, 2];
2789 let schema = Arc::new(arrow_schema::Schema::new(vec![
2790 arrow_schema::Field::new(
2791 crate::scalar::registry::VALUE_COLUMN_NAME,
2792 DataType::Utf8,
2793 false,
2794 ),
2795 arrow_schema::Field::new(ROW_ADDR, DataType::UInt64, false),
2796 ]));
2797 let batch = RecordBatch::try_new(
2798 schema.clone(),
2799 vec![
2800 Arc::new(StringArray::from(docs)),
2801 Arc::new(UInt64Array::from(row_addrs)),
2802 ],
2803 )
2804 .unwrap();
2805
2806 let tempdir = tempfile::tempdir().unwrap();
2807 let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap();
2808 let store = Arc::new(LanceIndexStore::new(
2809 Arc::new(ObjectStore::local()),
2810 index_dir,
2811 Arc::new(LanceCache::no_cache()),
2812 ));
2813
2814 let stream = RecordBatchStreamAdapter::new(batch.schema(), stream::iter(vec![Ok(batch)]));
2815 let req = FMIndexPlugin
2816 .new_training_request("", &arrow_schema::Field::new("val", DataType::Utf8, false))
2817 .unwrap();
2818 let created = FMIndexPlugin
2819 .train_index(
2820 Box::pin(stream),
2821 store.as_ref(),
2822 req,
2823 None,
2824 Arc::new(crate::progress::NoopIndexBuildProgress),
2825 )
2826 .await
2827 .unwrap();
2828
2829 let index = FMIndexPlugin
2830 .load_index(store, &created.index_details, None, &LanceCache::no_cache())
2831 .await
2832 .unwrap();
2833
2834 let r = index
2835 .search(
2836 &TextQuery::StringContains("hello".to_string()),
2837 &crate::metrics::NoOpMetricsCollector,
2838 )
2839 .await
2840 .unwrap();
2841 match r {
2842 SearchResult::Exact(set) => {
2843 assert_eq!(set.len(), Some(2));
2844 }
2845 _ => panic!("expected exact result"),
2846 }
2847 }
2848
2849 #[tokio::test(flavor = "multi_thread")]
2850 async fn test_stream_train_splits_partition_by_bytes() {
2851 let docs = vec!["abcd", "efgh", "ijkl"];
2852 let row_addrs: Vec<u64> = vec![0, 1, 2];
2853 let schema = Arc::new(arrow_schema::Schema::new(vec![
2854 arrow_schema::Field::new(
2855 crate::scalar::registry::VALUE_COLUMN_NAME,
2856 DataType::Utf8,
2857 false,
2858 ),
2859 arrow_schema::Field::new(ROW_ADDR, DataType::UInt64, false),
2860 ]));
2861 let batch = RecordBatch::try_new(
2862 schema.clone(),
2863 vec![
2864 Arc::new(StringArray::from(docs)),
2865 Arc::new(UInt64Array::from(row_addrs)),
2866 ],
2867 )
2868 .unwrap();
2869
2870 let tempdir = tempfile::tempdir().unwrap();
2871 let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap();
2872 let store = Arc::new(LanceIndexStore::new(
2873 Arc::new(ObjectStore::local()),
2874 index_dir,
2875 Arc::new(LanceCache::no_cache()),
2876 ));
2877
2878 let stream = RecordBatchStreamAdapter::new(schema, stream::iter(vec![Ok(batch)]));
2879 let files = write_partitioned_fmindex_stream_with_config(
2880 Box::pin(stream),
2881 store.as_ref(),
2882 FMIndexPartitionConfig {
2883 num_workers: 2,
2884 max_rows: 100,
2885 max_bytes: 5,
2886 queue_size: 1,
2887 resume_existing: false,
2888 },
2889 )
2890 .await
2891 .unwrap();
2892
2893 assert_eq!(files.len(), 3);
2894 assert_eq!(files[0].path, fmindex_partition_path(0));
2895 assert_eq!(files[1].path, fmindex_partition_path(1));
2896 assert_eq!(files[2].path, fmindex_partition_path(2));
2897
2898 let index = FMIndexScalarIndex::load(store, None, &LanceCache::no_cache())
2899 .await
2900 .unwrap();
2901 let r = index
2902 .search(
2903 &TextQuery::StringContains("efgh".to_string()),
2904 &crate::metrics::NoOpMetricsCollector,
2905 )
2906 .await
2907 .unwrap();
2908 match r {
2909 SearchResult::Exact(set) => {
2910 assert_eq!(set.len(), Some(1));
2911 }
2912 _ => panic!("expected exact result"),
2913 }
2914 }
2915
2916 #[tokio::test(flavor = "multi_thread")]
2917 async fn test_stream_train_resumes_existing_partitions() {
2918 let schema = Arc::new(arrow_schema::Schema::new(vec![
2919 arrow_schema::Field::new(
2920 crate::scalar::registry::VALUE_COLUMN_NAME,
2921 DataType::Utf8,
2922 false,
2923 ),
2924 arrow_schema::Field::new(ROW_ADDR, DataType::UInt64, false),
2925 ]));
2926 let tempdir = tempfile::tempdir().unwrap();
2927 let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap();
2928 let store = Arc::new(LanceIndexStore::new(
2929 Arc::new(ObjectStore::local()),
2930 index_dir,
2931 Arc::new(LanceCache::no_cache()),
2932 ));
2933
2934 let first_batch = RecordBatch::try_new(
2935 schema.clone(),
2936 vec![
2937 Arc::new(StringArray::from(vec!["first partition"])),
2938 Arc::new(UInt64Array::from(vec![0])),
2939 ],
2940 )
2941 .unwrap();
2942 let first_stream =
2943 RecordBatchStreamAdapter::new(schema.clone(), stream::iter(vec![Ok(first_batch)]));
2944 let first_files = write_partitioned_fmindex_stream_with_config(
2945 Box::pin(first_stream),
2946 store.as_ref(),
2947 FMIndexPartitionConfig {
2948 num_workers: 1,
2949 max_rows: 1,
2950 max_bytes: 1024,
2951 queue_size: 1,
2952 resume_existing: false,
2953 },
2954 )
2955 .await
2956 .unwrap();
2957 assert_eq!(first_files.len(), 1);
2958 assert_eq!(first_files[0].path, fmindex_partition_path(0));
2959
2960 let resumed_batch = RecordBatch::try_new(
2961 schema.clone(),
2962 vec![
2963 Arc::new(StringArray::from(vec![
2964 "first partition",
2965 "second partition",
2966 ])),
2967 Arc::new(UInt64Array::from(vec![0, 1])),
2968 ],
2969 )
2970 .unwrap();
2971 let resumed_stream =
2972 RecordBatchStreamAdapter::new(schema.clone(), stream::iter(vec![Ok(resumed_batch)]));
2973 let resumed_files = write_partitioned_fmindex_stream_with_config(
2974 Box::pin(resumed_stream),
2975 store.as_ref(),
2976 FMIndexPartitionConfig {
2977 num_workers: 1,
2978 max_rows: 1,
2979 max_bytes: 1024,
2980 queue_size: 1,
2981 resume_existing: true,
2982 },
2983 )
2984 .await
2985 .unwrap();
2986
2987 let resumed_paths = resumed_files
2988 .iter()
2989 .map(|file| file.path.clone())
2990 .collect::<Vec<_>>();
2991 assert_eq!(
2992 resumed_paths,
2993 vec![fmindex_partition_path(0), fmindex_partition_path(1)]
2994 );
2995
2996 let index = FMIndexScalarIndex::load(store, None, &LanceCache::no_cache())
2997 .await
2998 .unwrap();
2999 let r = index
3000 .search(
3001 &TextQuery::StringContains("partition".to_string()),
3002 &crate::metrics::NoOpMetricsCollector,
3003 )
3004 .await
3005 .unwrap();
3006 match r {
3007 SearchResult::Exact(set) => {
3008 assert_eq!(set.len(), Some(2));
3009 }
3010 _ => panic!("expected exact result"),
3011 }
3012 }
3013
3014 #[tokio::test(flavor = "multi_thread")]
3015 async fn test_stream_train_rebuilds_stale_resume_partitions() {
3016 let schema = Arc::new(arrow_schema::Schema::new(vec![
3017 arrow_schema::Field::new(
3018 crate::scalar::registry::VALUE_COLUMN_NAME,
3019 DataType::Utf8,
3020 false,
3021 ),
3022 arrow_schema::Field::new(ROW_ADDR, DataType::UInt64, false),
3023 ]));
3024 let tempdir = tempfile::tempdir().unwrap();
3025 let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap();
3026 let store = Arc::new(LanceIndexStore::new(
3027 Arc::new(ObjectStore::local()),
3028 index_dir,
3029 Arc::new(LanceCache::no_cache()),
3030 ));
3031
3032 let stale_batch = RecordBatch::try_new(
3033 schema.clone(),
3034 vec![
3035 Arc::new(StringArray::from(vec!["stale zero", "stale one"])),
3036 Arc::new(UInt64Array::from(vec![0, 1])),
3037 ],
3038 )
3039 .unwrap();
3040 let stale_stream =
3041 RecordBatchStreamAdapter::new(schema.clone(), stream::iter(vec![Ok(stale_batch)]));
3042 write_partitioned_fmindex_stream_with_config(
3043 Box::pin(stale_stream),
3044 store.as_ref(),
3045 FMIndexPartitionConfig {
3046 num_workers: 1,
3047 max_rows: 1,
3048 max_bytes: 1024,
3049 queue_size: 1,
3050 resume_existing: false,
3051 },
3052 )
3053 .await
3054 .unwrap();
3055
3056 let fresh_batch = RecordBatch::try_new(
3057 schema.clone(),
3058 vec![
3059 Arc::new(StringArray::from(vec!["fresh zero"])),
3060 Arc::new(UInt64Array::from(vec![0])),
3061 ],
3062 )
3063 .unwrap();
3064 let fresh_stream =
3065 RecordBatchStreamAdapter::new(schema.clone(), stream::iter(vec![Ok(fresh_batch)]));
3066 let fresh_files = write_partitioned_fmindex_stream_with_config(
3067 Box::pin(fresh_stream),
3068 store.as_ref(),
3069 FMIndexPartitionConfig {
3070 num_workers: 1,
3071 max_rows: 10,
3072 max_bytes: 1024,
3073 queue_size: 1,
3074 resume_existing: true,
3075 },
3076 )
3077 .await
3078 .unwrap();
3079
3080 assert_eq!(fresh_files.len(), 1);
3081 assert_eq!(fresh_files[0].path, fmindex_partition_path(0));
3082 let remaining_paths = store
3083 .list_files_with_sizes()
3084 .await
3085 .unwrap()
3086 .iter()
3087 .map(|file| file.path.clone())
3088 .collect::<Vec<_>>();
3089 assert_eq!(remaining_paths, vec![fmindex_partition_path(0)]);
3090
3091 let index = FMIndexScalarIndex::load(store, None, &LanceCache::no_cache())
3092 .await
3093 .unwrap();
3094 let r = index
3095 .search(
3096 &TextQuery::StringContains("fresh zero".to_string()),
3097 &crate::metrics::NoOpMetricsCollector,
3098 )
3099 .await
3100 .unwrap();
3101 match r {
3102 SearchResult::Exact(set) => assert_eq!(set.len(), Some(1)),
3103 _ => panic!("expected exact result"),
3104 }
3105
3106 let r = index
3107 .search(
3108 &TextQuery::StringContains("stale one".to_string()),
3109 &crate::metrics::NoOpMetricsCollector,
3110 )
3111 .await
3112 .unwrap();
3113 match r {
3114 SearchResult::Exact(set) => assert_eq!(set.len(), Some(0)),
3115 _ => panic!("expected exact result"),
3116 }
3117 }
3118
3119 #[tokio::test(flavor = "multi_thread")]
3120 async fn test_stream_train_propagates_worker_write_error() {
3121 let schema = Arc::new(arrow_schema::Schema::new(vec![
3122 arrow_schema::Field::new(
3123 crate::scalar::registry::VALUE_COLUMN_NAME,
3124 DataType::Utf8,
3125 false,
3126 ),
3127 arrow_schema::Field::new(ROW_ADDR, DataType::UInt64, false),
3128 ]));
3129 let batch = RecordBatch::try_new(
3130 schema.clone(),
3131 vec![
3132 Arc::new(StringArray::from(vec!["hello"])),
3133 Arc::new(UInt64Array::from(vec![0])),
3134 ],
3135 )
3136 .unwrap();
3137
3138 let tempdir = tempfile::tempdir().unwrap();
3139 let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap();
3140 let inner = Arc::new(LanceIndexStore::new(
3141 Arc::new(ObjectStore::local()),
3142 index_dir,
3143 Arc::new(LanceCache::no_cache()),
3144 )) as Arc<dyn IndexStore>;
3145 let store = FailNewFileStore { inner };
3146
3147 let stream = RecordBatchStreamAdapter::new(schema, stream::iter(vec![Ok(batch)]));
3148 let err = write_partitioned_fmindex_stream_with_config(
3149 Box::pin(stream),
3150 &store,
3151 FMIndexPartitionConfig {
3152 num_workers: 1,
3153 max_rows: 1,
3154 max_bytes: 1024,
3155 queue_size: 1,
3156 resume_existing: false,
3157 },
3158 )
3159 .await
3160 .unwrap_err();
3161
3162 assert!(format!("{err}").contains("injected FMIndex write failure"));
3163 }
3164
3165 #[tokio::test]
3166 async fn test_fail_new_file_store_with_io_priority_preserves_failure() {
3167 let tempdir = tempfile::tempdir().unwrap();
3170 let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap();
3171 let inner = Arc::new(LanceIndexStore::new(
3172 Arc::new(ObjectStore::local()),
3173 index_dir,
3174 Arc::new(LanceCache::no_cache()),
3175 )) as Arc<dyn IndexStore>;
3176 let store = FailNewFileStore { inner };
3177
3178 let reprioritized = store.with_io_priority(7);
3179
3180 let schema = Arc::new(arrow_schema::Schema::new(vec![arrow_schema::Field::new(
3181 "x",
3182 DataType::UInt64,
3183 false,
3184 )]));
3185 let err = reprioritized
3186 .new_index_file("test", schema)
3187 .await
3188 .err()
3189 .expect("new_index_file should fail");
3190 assert!(format!("{err}").contains("injected FMIndex write failure"));
3191 }
3192
3193 #[tokio::test(flavor = "multi_thread")]
3194 async fn test_plugin_train_streams_multiple_partitions() {
3195 fn training_batch(
3196 schema: Arc<arrow_schema::Schema>,
3197 start: usize,
3198 len: usize,
3199 ) -> RecordBatch {
3200 let docs = vec!["x"; len];
3201 let row_addrs: Vec<u64> = (start..start + len).map(|i| i as u64).collect();
3202 RecordBatch::try_new(
3203 schema,
3204 vec![
3205 Arc::new(StringArray::from(docs)),
3206 Arc::new(UInt64Array::from(row_addrs)),
3207 ],
3208 )
3209 .unwrap()
3210 }
3211
3212 let total_rows = PARTITION_SIZE + 5;
3213 let first_batch_rows = PARTITION_SIZE - 3;
3214 let schema = Arc::new(arrow_schema::Schema::new(vec![
3215 arrow_schema::Field::new(
3216 crate::scalar::registry::VALUE_COLUMN_NAME,
3217 DataType::Utf8,
3218 false,
3219 ),
3220 arrow_schema::Field::new(ROW_ADDR, DataType::UInt64, false),
3221 ]));
3222 let batches = vec![
3223 Ok(training_batch(schema.clone(), 0, first_batch_rows)),
3224 Ok(training_batch(
3225 schema.clone(),
3226 first_batch_rows,
3227 total_rows - first_batch_rows,
3228 )),
3229 ];
3230
3231 let tempdir = tempfile::tempdir().unwrap();
3232 let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap();
3233 let store = Arc::new(LanceIndexStore::new(
3234 Arc::new(ObjectStore::local()),
3235 index_dir,
3236 Arc::new(LanceCache::no_cache()),
3237 ));
3238
3239 let stream = RecordBatchStreamAdapter::new(schema, stream::iter(batches));
3240 let req = FMIndexPlugin
3241 .new_training_request("", &arrow_schema::Field::new("val", DataType::Utf8, false))
3242 .unwrap();
3243 let created = FMIndexPlugin
3244 .train_index(
3245 Box::pin(stream),
3246 store.as_ref(),
3247 req,
3248 None,
3249 Arc::new(crate::progress::NoopIndexBuildProgress),
3250 )
3251 .await
3252 .unwrap();
3253
3254 assert_eq!(created.files.len(), 2);
3255 assert_eq!(created.files[0].path, fmindex_partition_path(0));
3256 assert_eq!(created.files[1].path, fmindex_partition_path(1));
3257
3258 let index = FMIndexPlugin
3259 .load_index(store, &created.index_details, None, &LanceCache::no_cache())
3260 .await
3261 .unwrap();
3262 let r = index
3263 .search(
3264 &TextQuery::StringContains("x".to_string()),
3265 &crate::metrics::NoOpMetricsCollector,
3266 )
3267 .await
3268 .unwrap();
3269 match r {
3270 SearchResult::Exact(set) => {
3271 assert_eq!(set.len(), Some(total_rows as u64));
3272 }
3273 _ => panic!("expected exact result"),
3274 }
3275 }
3276
3277 #[test]
3278 fn test_build_wavelet_batch() {
3279 let texts: Vec<(u64, &[u8])> = vec![(0, b"hello world"), (1, b"test data")];
3280 let fm = FMIndex::build(&texts).unwrap();
3281 let batch = fm.build_wavelet_batch().unwrap();
3282 assert!(batch.num_rows() > 0);
3283 assert_eq!(batch.num_columns(), 5);
3284 }
3285
3286 #[test]
3287 fn test_extract_text_bytes_types() {
3288 let utf8 = StringArray::from(vec!["hello"]);
3289 assert_eq!(
3290 extract_text_bytes(&utf8, 0).unwrap(),
3291 Some(b"hello".to_vec())
3292 );
3293
3294 let large_utf8 = LargeStringArray::from(vec!["world"]);
3295 assert_eq!(
3296 extract_text_bytes(&large_utf8, 0).unwrap(),
3297 Some(b"world".to_vec())
3298 );
3299
3300 let binary = BinaryArray::from(vec![b"bytes" as &[u8]]);
3301 assert_eq!(
3302 extract_text_bytes(&binary, 0).unwrap(),
3303 Some(b"bytes".to_vec())
3304 );
3305 let binary_with_sentinels = BinaryArray::from(vec![b"a\xFFb\0c" as &[u8]]);
3306 assert_eq!(
3307 extract_sanitized_text_bytes(&binary_with_sentinels, 0).unwrap(),
3308 Some(b"a b c".to_vec())
3309 );
3310
3311 let large_binary = LargeBinaryArray::from(vec![b"large" as &[u8]]);
3312 assert_eq!(
3313 extract_text_bytes(&large_binary, 0).unwrap(),
3314 Some(b"large".to_vec())
3315 );
3316
3317 let nullable = StringArray::from(vec![None::<&str>]);
3319 assert_eq!(extract_text_bytes(&nullable, 0).unwrap(), None);
3320 }
3321
3322 #[test]
3323 fn test_fmindex_statistics() {
3324 let rt = tokio::runtime::Builder::new_current_thread()
3325 .build()
3326 .unwrap();
3327 rt.block_on(async {
3328 let docs: Vec<Vec<u8>> = (0..10).map(|i| format!("doc {i}").into_bytes()).collect();
3329 let texts: Vec<(u64, Vec<u8>)> = docs
3330 .into_iter()
3331 .enumerate()
3332 .map(|(i, d)| (i as u64, d))
3333 .collect();
3334
3335 let tempdir = tempfile::tempdir().unwrap();
3336 let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap();
3337 let store = Arc::new(LanceIndexStore::new(
3338 Arc::new(ObjectStore::local()),
3339 index_dir,
3340 Arc::new(LanceCache::no_cache()),
3341 ));
3342
3343 write_partitioned_fmindex(&texts, store.as_ref())
3344 .await
3345 .unwrap();
3346 let index = FMIndexScalarIndex::load(store, None, &LanceCache::no_cache())
3347 .await
3348 .unwrap();
3349
3350 let stats = index.statistics().unwrap();
3351 assert_eq!(stats["type"], "Fm");
3352 assert_eq!(stats["total_docs"], 10);
3353 assert!(stats["total_bwt_len"].as_u64().unwrap() > 0);
3354 });
3355 }
3356}