1use std::fmt::{Debug, Display};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
7use std::{
8 cmp::{Reverse, min},
9 collections::BinaryHeap,
10};
11use std::{collections::HashMap, ops::Range, time::Instant};
12
13use crate::metrics::NoOpMetricsCollector;
14use crate::prefilter::NoFilter;
15use crate::scalar::registry::{TrainingCriteria, TrainingOrdering};
16use arrow::array::{FixedSizeListBuilder, Float32Builder};
17use arrow::datatypes::{self, Float32Type, Int32Type, UInt64Type};
18use arrow::{
19 array::{
20 AsArray, LargeBinaryBuilder, ListBuilder, StringBuilder, UInt32Builder, UInt64Builder,
21 },
22 buffer::{Buffer, OffsetBuffer},
23};
24use arrow::{buffer::ScalarBuffer, datatypes::UInt32Type};
25use arrow_array::{
26 Array, ArrayRef, Float32Array, LargeBinaryArray, ListArray, OffsetSizeTrait, RecordBatch,
27 UInt32Array, UInt64Array,
28};
29use arrow_schema::{DataType, Field, Schema, SchemaRef};
30use async_trait::async_trait;
31use datafusion::execution::SendableRecordBatchStream;
32use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
33use deepsize::DeepSizeOf;
34use fst::{Automaton, IntoStreamer, Streamer};
35use futures::{FutureExt, Stream, StreamExt, TryStreamExt, stream};
36use itertools::Itertools;
37use lance_arrow::{RecordBatchExt, iter_str_array};
38use lance_core::cache::{CacheKey, LanceCache, WeakLanceCache};
39use lance_core::error::{DataFusionResult, LanceOptionExt};
40use lance_core::utils::mask::{RowAddrMask, RowAddrTreeMap};
41use lance_core::utils::tokio::{get_num_compute_intensive_cpus, spawn_cpu};
42use lance_core::utils::tracing::{IO_TYPE_LOAD_SCALAR_PART, TRACE_IO_EVENTS};
43use lance_core::{Error, ROW_ID, ROW_ID_FIELD, Result};
44use roaring::RoaringBitmap;
45use std::sync::LazyLock;
46use tokio::task::spawn_blocking;
47use tracing::{info, instrument};
48
49use super::encoding::PositionBlockBuilder;
50use super::iter::PostingListIterator;
51use super::{InvertedIndexBuilder, InvertedIndexParams, wand::*};
52use super::{
53 builder::{
54 BLOCK_SIZE, ScoredDoc, doc_file_path, inverted_list_schema_for_version, posting_file_path,
55 token_file_path,
56 },
57 iter::PlainPostingListIterator,
58 query::*,
59 scorer::{B, IndexBM25Scorer, K1, Scorer, idf},
60};
61use super::{
62 builder::{InnerBuilder, PositionRecorder},
63 iter::CompressedPostingListIterator,
64};
65use crate::Index;
66use crate::frag_reuse::FragReuseIndex;
67use crate::pbold;
68use crate::scalar::inverted::lance_tokenizer::TextTokenizer;
69use crate::scalar::inverted::scorer::MemBM25Scorer;
70use crate::scalar::inverted::tokenizer::lance_tokenizer::LanceTokenizer;
71use crate::scalar::{
72 AnyQuery, BuiltinIndexType, CreatedIndex, IndexReader, IndexStore, MetricsCollector,
73 ScalarIndex, ScalarIndexParams, SearchResult, TokenQuery, UpdateCriteria,
74};
75use crate::{prefilter::PreFilter, scalar::inverted::iter::take_fst_keys};
76use std::str::FromStr;
77
78pub const INVERTED_INDEX_VERSION_V1: u32 = 1;
82pub const INVERTED_INDEX_VERSION_V2: u32 = 2;
83pub const TOKENS_FILE: &str = "tokens.lance";
84pub const INVERT_LIST_FILE: &str = "invert.lance";
85pub const DOCS_FILE: &str = "docs.lance";
86pub const METADATA_FILE: &str = "metadata.lance";
87
88pub const TOKEN_COL: &str = "_token";
89pub const TOKEN_ID_COL: &str = "_token_id";
90pub const TOKEN_FST_BYTES_COL: &str = "_token_fst_bytes";
91pub const TOKEN_NEXT_ID_COL: &str = "_token_next_id";
92pub const TOKEN_TOTAL_LENGTH_COL: &str = "_token_total_length";
93pub const FREQUENCY_COL: &str = "_frequency";
94pub const POSITION_COL: &str = "_position";
95pub const COMPRESSED_POSITION_COL: &str = "_compressed_position";
96pub const POSITION_BLOCK_OFFSET_COL: &str = "_position_block_offset";
97pub const POSTING_COL: &str = "_posting";
98pub const MAX_SCORE_COL: &str = "_max_score";
99pub const LENGTH_COL: &str = "_length";
100pub const BLOCK_MAX_SCORE_COL: &str = "_block_max_score";
101pub const NUM_TOKEN_COL: &str = "_num_tokens";
102pub const SCORE_COL: &str = "_score";
103pub const TOKEN_SET_FORMAT_KEY: &str = "token_set_format";
104pub const POSTING_TAIL_CODEC_KEY: &str = "posting_tail_codec";
105pub const POSITIONS_LAYOUT_KEY: &str = "positions_layout";
106pub const POSITIONS_CODEC_KEY: &str = "positions_codec";
107pub const POSTING_TAIL_CODEC_FIXED32_V1: &str = "fixed32_v1";
108pub const POSTING_TAIL_CODEC_VARINT_DELTA_V1: &str = "varint_delta_v1";
109pub const POSITIONS_LAYOUT_SHARED_STREAM_V2: &str = "shared_stream_v2";
110pub const POSITIONS_CODEC_VARINT_DOC_DELTA_V2: &str = "varint_doc_delta_v2";
111pub const POSITIONS_CODEC_PACKED_DELTA_V1: &str = "packed_delta_v1";
112pub const DELETED_FRAGMENTS_COL: &str = "deleted_fragments";
113
114pub const ESTIMATED_MAX_TOKENS_PER_ROW: usize = 4 * 1024;
116
117pub static SCORE_FIELD: LazyLock<Field> =
118 LazyLock::new(|| Field::new(SCORE_COL, DataType::Float32, true));
119pub static FTS_SCHEMA: LazyLock<SchemaRef> =
120 LazyLock::new(|| Arc::new(Schema::new(vec![ROW_ID_FIELD.clone(), SCORE_FIELD.clone()])));
121static ROW_ID_SCHEMA: LazyLock<SchemaRef> =
122 LazyLock::new(|| Arc::new(Schema::new(vec![ROW_ID_FIELD.clone()])));
123
124fn resolve_fts_format_version(
125 value: Option<&str>,
126) -> std::result::Result<InvertedListFormatVersion, Error> {
127 value.unwrap_or("1").parse()
128}
129
130pub fn current_fts_format_version() -> InvertedListFormatVersion {
131 resolve_fts_format_version(std::env::var("LANCE_FTS_FORMAT_VERSION").ok().as_deref())
132 .expect("failed to parse LANCE_FTS_FORMAT_VERSION")
133}
134
135pub fn max_supported_fts_format_version() -> InvertedListFormatVersion {
136 InvertedListFormatVersion::V2
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
140pub enum InvertedListFormatVersion {
141 #[default]
142 V1,
143 V2,
144}
145
146impl InvertedListFormatVersion {
147 pub fn from_posting_tail_codec(codec: PostingTailCodec) -> Self {
148 match codec {
149 PostingTailCodec::Fixed32 => Self::V1,
150 PostingTailCodec::VarintDelta => Self::V2,
151 }
152 }
153
154 pub fn index_version(self) -> u32 {
155 match self {
156 Self::V1 => INVERTED_INDEX_VERSION_V1,
157 Self::V2 => INVERTED_INDEX_VERSION_V2,
158 }
159 }
160
161 pub fn posting_tail_codec(self) -> PostingTailCodec {
162 match self {
163 Self::V1 => PostingTailCodec::Fixed32,
164 Self::V2 => PostingTailCodec::VarintDelta,
165 }
166 }
167
168 pub fn position_codec(self) -> Option<PositionStreamCodec> {
169 match self {
170 Self::V1 => None,
171 Self::V2 => Some(PositionStreamCodec::PackedDelta),
172 }
173 }
174
175 pub fn uses_shared_position_stream(self) -> bool {
176 matches!(self, Self::V2)
177 }
178}
179
180impl FromStr for InvertedListFormatVersion {
181 type Err = Error;
182
183 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
184 match s.trim() {
185 "1" | "v1" | "V1" => Ok(Self::V1),
186 "2" | "v2" | "V2" => Ok(Self::V2),
187 other => Err(Error::index(format!(
188 "unsupported FTS format version {}, expected 1 or 2",
189 other
190 ))),
191 }
192 }
193}
194
195#[derive(Debug)]
196struct PartitionCandidates {
197 tokens_by_position: Vec<String>,
198 candidates: Vec<DocCandidate>,
199}
200
201impl PartitionCandidates {
202 fn empty() -> Self {
203 Self {
204 tokens_by_position: Vec::new(),
205 candidates: Vec::new(),
206 }
207 }
208}
209
210#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default)]
211pub enum TokenSetFormat {
212 Arrow,
213 #[default]
214 Fst,
215}
216
217impl Display for TokenSetFormat {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 match self {
220 Self::Arrow => f.write_str("arrow"),
221 Self::Fst => f.write_str("fst"),
222 }
223 }
224}
225
226impl FromStr for TokenSetFormat {
227 type Err = Error;
228
229 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
230 match s.trim() {
231 "" => Ok(Self::Arrow),
232 "arrow" => Ok(Self::Arrow),
233 "fst" => Ok(Self::Fst),
234 other => Err(Error::index(format!(
235 "unsupported token set format {}",
236 other
237 ))),
238 }
239 }
240}
241
242impl DeepSizeOf for TokenSetFormat {
243 fn deep_size_of_children(&self, _: &mut deepsize::Context) -> usize {
244 0
245 }
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
249pub enum PositionStreamCodec {
250 VarintDocDelta,
251 #[default]
252 PackedDelta,
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
256pub enum PostingTailCodec {
257 Fixed32,
258 #[default]
259 VarintDelta,
260}
261
262impl PostingTailCodec {
263 pub fn as_str(self) -> &'static str {
264 match self {
265 Self::Fixed32 => POSTING_TAIL_CODEC_FIXED32_V1,
266 Self::VarintDelta => POSTING_TAIL_CODEC_VARINT_DELTA_V1,
267 }
268 }
269
270 fn from_metadata_value(value: &str) -> Result<Self> {
271 match value.trim() {
272 POSTING_TAIL_CODEC_FIXED32_V1 => Ok(Self::Fixed32),
273 POSTING_TAIL_CODEC_VARINT_DELTA_V1 => Ok(Self::VarintDelta),
274 other => Err(Error::index(format!(
275 "unsupported posting tail codec {}",
276 other
277 ))),
278 }
279 }
280}
281
282pub(super) fn parse_posting_tail_codec(
283 metadata: &HashMap<String, String>,
284) -> Result<PostingTailCodec> {
285 Ok(metadata
286 .get(POSTING_TAIL_CODEC_KEY)
287 .map(|codec| PostingTailCodec::from_metadata_value(codec))
288 .transpose()?
289 .unwrap_or(PostingTailCodec::Fixed32))
290}
291
292impl PositionStreamCodec {
293 pub fn as_str(self) -> &'static str {
294 match self {
295 Self::VarintDocDelta => POSITIONS_CODEC_VARINT_DOC_DELTA_V2,
296 Self::PackedDelta => POSITIONS_CODEC_PACKED_DELTA_V1,
297 }
298 }
299
300 fn from_metadata_value(value: &str) -> Result<Self> {
301 match value.trim() {
302 POSITIONS_CODEC_VARINT_DOC_DELTA_V2 => Ok(Self::VarintDocDelta),
303 POSITIONS_CODEC_PACKED_DELTA_V1 => Ok(Self::PackedDelta),
304 other => Err(Error::index(format!(
305 "unsupported positions codec {}",
306 other
307 ))),
308 }
309 }
310}
311
312fn parse_shared_position_codec(metadata: &HashMap<String, String>) -> Result<PositionStreamCodec> {
313 if let Some(codec) = metadata.get(POSITIONS_CODEC_KEY) {
314 return PositionStreamCodec::from_metadata_value(codec);
315 }
316
317 match metadata
318 .get(POSITIONS_LAYOUT_KEY)
319 .map(|layout| layout.as_str())
320 {
321 Some(POSITIONS_LAYOUT_SHARED_STREAM_V2) => Ok(PositionStreamCodec::VarintDocDelta),
322 _ => Ok(PositionStreamCodec::VarintDocDelta),
323 }
324}
325
326pub(super) fn parse_format_version_from_metadata(
327 metadata: &HashMap<String, String>,
328) -> Result<InvertedListFormatVersion> {
329 if metadata.contains_key(POSITIONS_CODEC_KEY) || metadata.contains_key(POSITIONS_LAYOUT_KEY) {
330 return Ok(InvertedListFormatVersion::V2);
331 }
332 if parse_posting_tail_codec(metadata)? == PostingTailCodec::VarintDelta {
333 Ok(InvertedListFormatVersion::V2)
334 } else {
335 Ok(InvertedListFormatVersion::V1)
336 }
337}
338
339#[derive(Clone)]
340pub struct InvertedIndex {
341 params: InvertedIndexParams,
342 store: Arc<dyn IndexStore>,
343 tokenizer: Box<dyn LanceTokenizer>,
344 token_set_format: TokenSetFormat,
345 pub(crate) partitions: Vec<Arc<InvertedPartition>>,
346 deleted_fragments: RoaringBitmap,
349}
350
351impl Debug for InvertedIndex {
352 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353 f.debug_struct("InvertedIndex")
354 .field("params", &self.params)
355 .field("token_set_format", &self.token_set_format)
356 .field("partitions", &self.partitions)
357 .field("deleted_fragments", &self.deleted_fragments)
358 .finish()
359 }
360}
361
362impl DeepSizeOf for InvertedIndex {
363 fn deep_size_of_children(&self, context: &mut deepsize::Context) -> usize {
364 self.partitions.deep_size_of_children(context)
365 }
366}
367
368impl InvertedIndex {
369 fn format_version(&self) -> InvertedListFormatVersion {
370 self.partitions
371 .first()
372 .map(|partition| {
373 InvertedListFormatVersion::from_posting_tail_codec(
374 partition.inverted_list.posting_tail_codec(),
375 )
376 })
377 .unwrap_or_else(current_fts_format_version)
378 }
379
380 fn index_version(&self) -> u32 {
381 match self.token_set_format {
382 TokenSetFormat::Arrow => 0,
383 TokenSetFormat::Fst => self.format_version().index_version(),
384 }
385 }
386
387 fn posting_tail_codec(&self) -> PostingTailCodec {
388 self.partitions
389 .first()
390 .map(|partition| partition.inverted_list.posting_tail_codec())
391 .unwrap_or_default()
392 }
393
394 fn to_builder(&self) -> InvertedIndexBuilder {
395 self.to_builder_with_offset(None)
396 }
397
398 fn to_builder_with_offset(&self, fragment_mask: Option<u64>) -> InvertedIndexBuilder {
399 if self.is_legacy() {
400 InvertedIndexBuilder::from_existing_index(
402 self.params.clone(),
403 None,
404 Vec::new(),
405 self.token_set_format,
406 fragment_mask,
407 self.deleted_fragments.clone(),
408 )
409 .with_posting_tail_codec(self.posting_tail_codec())
410 } else {
411 let partitions = match fragment_mask {
412 Some(fragment_mask) => self
413 .partitions
414 .iter()
415 .filter(|part| part.belongs_to_fragment(fragment_mask))
419 .map(|part| part.id())
420 .collect(),
421 None => self.partitions.iter().map(|part| part.id()).collect(),
422 };
423
424 InvertedIndexBuilder::from_existing_index(
425 self.params.clone(),
426 Some(self.store.clone()),
427 partitions,
428 self.token_set_format,
429 fragment_mask,
430 self.deleted_fragments.clone(),
431 )
432 .with_format_version(self.format_version())
433 }
434 }
435
436 pub fn tokenizer(&self) -> Box<dyn LanceTokenizer> {
437 self.tokenizer.clone()
438 }
439
440 pub fn params(&self) -> &InvertedIndexParams {
441 &self.params
442 }
443
444 pub fn partition_count(&self) -> usize {
446 self.partitions.len()
447 }
448
449 pub fn deleted_fragments(&self) -> &RoaringBitmap {
455 &self.deleted_fragments
456 }
457
458 #[instrument(level = "debug", skip_all)]
464 pub async fn bm25_search(
465 &self,
466 tokens: Arc<Tokens>,
467 params: Arc<FtsSearchParams>,
468 operator: Operator,
469 prefilter: Arc<dyn PreFilter>,
470 metrics: Arc<dyn MetricsCollector>,
471 ) -> Result<(Vec<u64>, Vec<f32>)> {
472 let limit = params.limit.unwrap_or(usize::MAX);
473 if limit == 0 {
474 return Ok((Vec::new(), Vec::new()));
475 }
476 let mask = prefilter.mask();
477
478 let mut candidates = BinaryHeap::new();
479 let parts = self
480 .partitions
481 .iter()
482 .map(|part| {
483 let part = part.clone();
484 let tokens = tokens.clone();
485 let params = params.clone();
486 let mask = mask.clone();
487 let metrics = metrics.clone();
488 async move {
489 let postings = part
490 .load_posting_lists(tokens.as_ref(), params.as_ref(), metrics.as_ref())
491 .await?;
492 if postings.is_empty() {
493 return Result::Ok(PartitionCandidates::empty());
494 }
495 let mut tokens_by_position = vec![String::new(); postings.len()];
496 for posting in &postings {
497 let idx = posting.term_index() as usize;
498 tokens_by_position[idx] = posting.token().to_owned();
499 }
500 let params = params.clone();
501 let mask = mask.clone();
502 let metrics = metrics.clone();
503 spawn_cpu(move || {
504 let candidates = part.bm25_search(
505 params.as_ref(),
506 operator,
507 mask,
508 postings,
509 metrics.as_ref(),
510 )?;
511 Ok(PartitionCandidates {
512 tokens_by_position,
513 candidates,
514 })
515 })
516 .await
517 }
518 })
519 .collect::<Vec<_>>();
520 let mut parts = stream::iter(parts).buffer_unordered(get_num_compute_intensive_cpus());
521 let scorer = IndexBM25Scorer::new(self.partitions.iter().map(|part| part.as_ref()));
522 let mut idf_cache: HashMap<String, f32> = HashMap::new();
523 while let Some(res) = parts.try_next().await? {
524 if res.candidates.is_empty() {
525 continue;
526 }
527 let mut idf_by_position = Vec::with_capacity(res.tokens_by_position.len());
528 for token in &res.tokens_by_position {
529 let idf_weight = match idf_cache.get(token) {
530 Some(weight) => *weight,
531 None => {
532 let weight = scorer.query_weight(token);
533 idf_cache.insert(token.clone(), weight);
534 weight
535 }
536 };
537 idf_by_position.push(idf_weight);
538 }
539 for DocCandidate {
540 row_id,
541 freqs,
542 doc_length,
543 } in res.candidates
544 {
545 let mut score = 0.0;
546 for (term_index, freq) in freqs.into_iter() {
547 debug_assert!((term_index as usize) < idf_by_position.len());
548 score +=
549 idf_by_position[term_index as usize] * scorer.doc_weight(freq, doc_length);
550 }
551 if candidates.len() < limit {
552 candidates.push(Reverse(ScoredDoc::new(row_id, score)));
553 } else if candidates.peek().unwrap().0.score.0 < score {
554 candidates.pop();
555 candidates.push(Reverse(ScoredDoc::new(row_id, score)));
556 }
557 }
558 }
559
560 Ok(candidates
561 .into_sorted_vec()
562 .into_iter()
563 .map(|Reverse(doc)| (doc.row_id, doc.score.0))
564 .unzip())
565 }
566
567 async fn load_legacy_index(
568 store: Arc<dyn IndexStore>,
569 frag_reuse_index: Option<Arc<FragReuseIndex>>,
570 index_cache: &LanceCache,
571 ) -> Result<Arc<Self>> {
572 log::warn!("loading legacy FTS index");
573 let tokens_fut = tokio::spawn({
574 let store = store.clone();
575 async move {
576 let token_reader = store.open_index_file(TOKENS_FILE).await?;
577 let tokenizer = token_reader
578 .schema()
579 .metadata
580 .get("tokenizer")
581 .map(|s| serde_json::from_str::<InvertedIndexParams>(s))
582 .transpose()?
583 .unwrap_or_default();
584 let tokens = TokenSet::load(token_reader, TokenSetFormat::Arrow).await?;
585 Result::Ok((tokenizer, tokens))
586 }
587 });
588 let invert_list_fut = tokio::spawn({
589 let store = store.clone();
590 let index_cache_clone = index_cache.clone();
591 async move {
592 let invert_list_reader = store.open_index_file(INVERT_LIST_FILE).await?;
593 let invert_list =
594 PostingListReader::try_new(invert_list_reader, &index_cache_clone).await?;
595 Result::Ok(Arc::new(invert_list))
596 }
597 });
598 let docs_fut = tokio::spawn({
599 let store = store.clone();
600 async move {
601 let docs_reader = store.open_index_file(DOCS_FILE).await?;
602 let docs = DocSet::load(docs_reader, true, frag_reuse_index).await?;
603 Result::Ok(docs)
604 }
605 });
606
607 let (tokenizer_config, tokens) = tokens_fut.await??;
608 let inverted_list = invert_list_fut.await??;
609 let docs = docs_fut.await??;
610
611 let tokenizer = tokenizer_config.build()?;
612
613 Ok(Arc::new(Self {
614 params: tokenizer_config,
615 store: store.clone(),
616 tokenizer,
617 token_set_format: TokenSetFormat::Arrow,
618 partitions: vec![Arc::new(InvertedPartition {
619 id: 0,
620 store,
621 tokens,
622 inverted_list,
623 docs,
624 token_set_format: TokenSetFormat::Arrow,
625 })],
626 deleted_fragments: RoaringBitmap::new(),
627 }))
628 }
629
630 pub fn is_legacy(&self) -> bool {
631 self.partitions.len() == 1 && self.partitions[0].is_legacy()
632 }
633
634 pub async fn load(
635 store: Arc<dyn IndexStore>,
636 frag_reuse_index: Option<Arc<FragReuseIndex>>,
637 index_cache: &LanceCache,
638 ) -> Result<Arc<Self>>
639 where
640 Self: Sized,
641 {
642 match store.open_index_file(METADATA_FILE).await {
647 Ok(reader) => {
648 let params = reader
649 .schema()
650 .metadata
651 .get("params")
652 .ok_or(Error::index("params not found in metadata".to_owned()))?;
653 let params = serde_json::from_str::<InvertedIndexParams>(params)?;
654 let partitions = reader
655 .schema()
656 .metadata
657 .get("partitions")
658 .ok_or(Error::index("partitions not found in metadata".to_owned()))?;
659 let partitions: Vec<u64> = serde_json::from_str(partitions)?;
660 let token_set_format = reader
661 .schema()
662 .metadata
663 .get(TOKEN_SET_FORMAT_KEY)
664 .map(|name| TokenSetFormat::from_str(name))
665 .transpose()?
666 .unwrap_or(TokenSetFormat::Arrow);
667
668 let deleted_fragments = if reader.num_rows() > 0 {
670 let metadata_batch = reader.read_range(0..1, None).await?;
671 if let Some(col) = metadata_batch.column_by_name(DELETED_FRAGMENTS_COL) {
672 let arr = col.as_binary_opt::<i32>().expect_ok()?;
673 RoaringBitmap::deserialize_from(arr.value(0))?
674 } else {
675 RoaringBitmap::new()
676 }
677 } else {
678 RoaringBitmap::new()
679 };
680
681 let format = token_set_format;
682 let partitions = partitions.into_iter().map(|id| {
683 let store = store.clone();
684 let frag_reuse_index_clone = frag_reuse_index.clone();
685 let index_cache_for_part =
686 index_cache.with_key_prefix(format!("part-{}", id).as_str());
687 let token_set_format = format;
688 async move {
689 Result::Ok(Arc::new(
690 InvertedPartition::load(
691 store,
692 id,
693 frag_reuse_index_clone,
694 &index_cache_for_part,
695 token_set_format,
696 )
697 .await?,
698 ))
699 }
700 });
701 let partitions = stream::iter(partitions)
702 .buffer_unordered(store.io_parallelism())
703 .try_collect::<Vec<_>>()
704 .await?;
705
706 let tokenizer = params.build()?;
707 Ok(Arc::new(Self {
708 params,
709 store,
710 tokenizer,
711 token_set_format,
712 partitions,
713 deleted_fragments,
714 }))
715 }
716 Err(_) => {
717 Self::load_legacy_index(store, frag_reuse_index, index_cache).await
719 }
720 }
721 }
722}
723
724#[async_trait]
725impl Index for InvertedIndex {
726 fn as_any(&self) -> &dyn std::any::Any {
727 self
728 }
729
730 fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
731 self
732 }
733
734 fn as_vector_index(self: Arc<Self>) -> Result<Arc<dyn crate::vector::VectorIndex>> {
735 Err(Error::invalid_input(
736 "inverted index cannot be cast to vector index",
737 ))
738 }
739
740 fn statistics(&self) -> Result<serde_json::Value> {
741 let num_tokens = self
742 .partitions
743 .iter()
744 .map(|part| part.tokens.len())
745 .sum::<usize>();
746 let num_docs = self
747 .partitions
748 .iter()
749 .map(|part| part.docs.len())
750 .sum::<usize>();
751 Ok(serde_json::json!({
752 "params": self.params,
753 "num_tokens": num_tokens,
754 "num_docs": num_docs,
755 }))
756 }
757
758 async fn prewarm(&self) -> Result<()> {
759 let io_parallelism = self.store.io_parallelism();
760 let prewarm_futures = self
761 .partitions
762 .iter()
763 .map(Arc::clone)
764 .map(|part| async move {
765 part.inverted_list.prewarm().await?;
766 Result::Ok(())
767 });
768 stream::iter(prewarm_futures)
769 .buffer_unordered(io_parallelism)
770 .try_collect::<Vec<_>>()
771 .await?;
772 Ok(())
773 }
774
775 fn index_type(&self) -> crate::IndexType {
776 crate::IndexType::Inverted
777 }
778
779 async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
780 unimplemented!()
781 }
782}
783
784impl InvertedIndex {
785 async fn do_search(&self, text: &str) -> Result<RecordBatch> {
787 let params = FtsSearchParams::new();
788 let mut tokenizer = self.tokenizer.clone();
789 let tokens = collect_query_tokens(text, &mut tokenizer);
790
791 let (doc_ids, _) = self
792 .bm25_search(
793 Arc::new(tokens),
794 params.into(),
795 Operator::And,
796 Arc::new(NoFilter),
797 Arc::new(NoOpMetricsCollector),
798 )
799 .boxed()
800 .await?;
801
802 Ok(RecordBatch::try_new(
803 ROW_ID_SCHEMA.clone(),
804 vec![Arc::new(UInt64Array::from(doc_ids))],
805 )?)
806 }
807}
808
809#[async_trait]
810impl ScalarIndex for InvertedIndex {
811 #[instrument(level = "debug", skip_all)]
813 async fn search(
814 &self,
815 query: &dyn AnyQuery,
816 _metrics: &dyn MetricsCollector,
817 ) -> Result<SearchResult> {
818 let query = query.as_any().downcast_ref::<TokenQuery>().unwrap();
819
820 match query {
821 TokenQuery::TokensContains(text) => {
822 let records = self.do_search(text).await?;
823 let row_ids = records
824 .column(0)
825 .as_any()
826 .downcast_ref::<UInt64Array>()
827 .unwrap();
828 let row_ids = row_ids.iter().flatten().collect_vec();
829 Ok(SearchResult::at_most(RowAddrTreeMap::from_iter(row_ids)))
830 }
831 }
832 }
833
834 fn can_remap(&self) -> bool {
835 true
836 }
837
838 async fn remap(
839 &self,
840 mapping: &HashMap<u64, Option<u64>>,
841 dest_store: &dyn IndexStore,
842 ) -> Result<CreatedIndex> {
843 self.to_builder()
844 .remap(mapping, self.store.clone(), dest_store)
845 .await?;
846
847 let details = pbold::InvertedIndexDetails::try_from(&self.params)?;
848
849 Ok(CreatedIndex {
850 index_details: prost_types::Any::from_msg(&details).unwrap(),
851 index_version: self.index_version(),
852 files: Some(dest_store.list_files_with_sizes().await?),
853 })
854 }
855
856 async fn update(
857 &self,
858 new_data: SendableRecordBatchStream,
859 dest_store: &dyn IndexStore,
860 old_data_filter: Option<crate::scalar::OldIndexDataFilter>,
861 ) -> Result<CreatedIndex> {
862 self.to_builder()
863 .update(new_data, dest_store, old_data_filter)
864 .await?;
865
866 let details = pbold::InvertedIndexDetails::try_from(&self.params)?;
867
868 Ok(CreatedIndex {
869 index_details: prost_types::Any::from_msg(&details).unwrap(),
870 index_version: self.index_version(),
871 files: Some(dest_store.list_files_with_sizes().await?),
872 })
873 }
874
875 fn update_criteria(&self) -> UpdateCriteria {
876 let criteria = TrainingCriteria::new(TrainingOrdering::None).with_row_id();
877 if self.is_legacy() {
878 UpdateCriteria::requires_old_data(criteria)
879 } else {
880 UpdateCriteria::only_new_data(criteria)
881 }
882 }
883
884 fn derive_index_params(&self) -> Result<ScalarIndexParams> {
885 let mut params = self.params.clone();
886 if params.base_tokenizer.is_empty() {
887 params.base_tokenizer = "simple".to_string();
888 }
889
890 let params_json = serde_json::to_string(¶ms)?;
891
892 Ok(ScalarIndexParams {
893 index_type: BuiltinIndexType::Inverted.as_str().to_string(),
894 params: Some(params_json),
895 })
896 }
897}
898
899#[derive(Debug, Clone, DeepSizeOf)]
900pub struct InvertedPartition {
901 id: u64,
903 store: Arc<dyn IndexStore>,
904 pub(crate) tokens: TokenSet,
905 pub(crate) inverted_list: Arc<PostingListReader>,
906 pub(crate) docs: DocSet,
907 token_set_format: TokenSetFormat,
908}
909
910impl InvertedPartition {
911 pub fn belongs_to_fragment(&self, fragment_mask: u64) -> bool {
922 (self.id() & fragment_mask) == fragment_mask
923 }
924
925 pub fn id(&self) -> u64 {
926 self.id
927 }
928
929 pub fn store(&self) -> &dyn IndexStore {
930 self.store.as_ref()
931 }
932
933 pub fn is_legacy(&self) -> bool {
934 self.inverted_list.lengths.is_none()
935 }
936
937 pub async fn load(
938 store: Arc<dyn IndexStore>,
939 id: u64,
940 frag_reuse_index: Option<Arc<FragReuseIndex>>,
941 index_cache: &LanceCache,
942 token_set_format: TokenSetFormat,
943 ) -> Result<Self> {
944 let token_file = store.open_index_file(&token_file_path(id)).await?;
945 let tokens = TokenSet::load(token_file, token_set_format).await?;
946 let invert_list_file = store.open_index_file(&posting_file_path(id)).await?;
947 let inverted_list = PostingListReader::try_new(invert_list_file, index_cache).await?;
948 let docs_file = store.open_index_file(&doc_file_path(id)).await?;
949 let docs = DocSet::load(docs_file, false, frag_reuse_index).await?;
950
951 Ok(Self {
952 id,
953 store,
954 tokens,
955 inverted_list: Arc::new(inverted_list),
956 docs,
957 token_set_format,
958 })
959 }
960
961 fn map(&self, token: &str) -> Option<u32> {
962 self.tokens.get(token)
963 }
964
965 pub fn expand_fuzzy(&self, tokens: &Tokens, params: &FtsSearchParams) -> Result<Tokens> {
966 let mut new_tokens = Vec::with_capacity(min(tokens.len(), params.max_expansions));
967 for token in tokens {
968 let fuzziness = match params.fuzziness {
969 Some(fuzziness) => fuzziness,
970 None => MatchQuery::auto_fuzziness(token),
971 };
972 let lev = fst::automaton::Levenshtein::new(token, fuzziness)
973 .map_err(|e| Error::index(format!("failed to construct the fuzzy query: {}", e)))?;
974
975 let base_len = tokens.token_type().prefix_len(token) as u32;
976 if let TokenMap::Fst(ref map) = self.tokens.tokens {
977 match base_len + params.prefix_length {
978 0 => take_fst_keys(map.search(lev), &mut new_tokens, params.max_expansions),
979 prefix_length => {
980 let prefix = &token[..min(prefix_length as usize, token.len())];
981 let prefix = fst::automaton::Str::new(prefix).starts_with();
982 take_fst_keys(
983 map.search(lev.intersection(prefix)),
984 &mut new_tokens,
985 params.max_expansions,
986 )
987 }
988 }
989 } else {
990 return Err(Error::index(
991 "tokens is not fst, which is not expected".to_owned(),
992 ));
993 }
994 }
995 Ok(Tokens::new(new_tokens, tokens.token_type().clone()))
996 }
997
998 #[instrument(level = "debug", skip_all)]
1002 pub async fn load_posting_lists(
1003 &self,
1004 tokens: &Tokens,
1005 params: &FtsSearchParams,
1006 metrics: &dyn MetricsCollector,
1007 ) -> Result<Vec<PostingIterator>> {
1008 let is_fuzzy = matches!(params.fuzziness, Some(n) if n != 0);
1009 let is_phrase_query = params.phrase_slop.is_some();
1010 let tokens = match is_fuzzy {
1011 true => self.expand_fuzzy(tokens, params)?,
1012 false => tokens.clone(),
1013 };
1014 let mut token_ids = Vec::with_capacity(tokens.len());
1015 for token in tokens {
1016 let token_id = self.map(&token);
1017 if let Some(token_id) = token_id {
1018 token_ids.push((token_id, token));
1019 } else if is_phrase_query {
1020 return Ok(Vec::new());
1022 }
1023 }
1024 if token_ids.is_empty() {
1025 return Ok(Vec::new());
1026 }
1027 if !is_phrase_query {
1028 token_ids.sort_unstable_by_key(|(token_id, _)| *token_id);
1029 token_ids.dedup_by_key(|(token_id, _)| *token_id);
1030 }
1031
1032 let num_docs = self.docs.len();
1033 stream::iter(token_ids)
1034 .enumerate()
1035 .map(|(position, (token_id, token))| async move {
1036 let posting = self
1037 .inverted_list
1038 .posting_list(token_id, is_phrase_query, metrics)
1039 .await?;
1040
1041 let query_weight = idf(posting.len(), num_docs);
1042
1043 Result::Ok(PostingIterator::with_query_weight(
1044 token,
1045 token_id,
1046 position as u32,
1047 query_weight,
1048 posting,
1049 num_docs,
1050 ))
1051 })
1052 .buffered(self.store.io_parallelism())
1053 .try_collect::<Vec<_>>()
1054 .await
1055 }
1056
1057 #[instrument(level = "debug", skip_all)]
1058 pub fn bm25_search(
1059 &self,
1060 params: &FtsSearchParams,
1061 operator: Operator,
1062 mask: Arc<RowAddrMask>,
1063 postings: Vec<PostingIterator>,
1064 metrics: &dyn MetricsCollector,
1065 ) -> Result<Vec<DocCandidate>> {
1066 if postings.is_empty() {
1067 return Ok(Vec::new());
1068 }
1069
1070 let scorer = IndexBM25Scorer::new(std::iter::once(self));
1072 let mut wand = Wand::new(operator, postings.into_iter(), &self.docs, scorer);
1073 let hits = wand.search(params, mask, metrics)?;
1074 Ok(hits)
1076 }
1077
1078 pub async fn into_builder(self) -> Result<InnerBuilder> {
1079 let mut builder = InnerBuilder::new_with_posting_tail_codec(
1080 self.id,
1081 self.inverted_list.has_positions(),
1082 self.token_set_format,
1083 self.inverted_list.posting_tail_codec(),
1084 );
1085 builder.tokens = self.tokens;
1086 builder.docs = self.docs;
1087
1088 builder
1089 .posting_lists
1090 .reserve_exact(self.inverted_list.len());
1091 for posting_list in self
1092 .inverted_list
1093 .read_all(self.inverted_list.has_positions())
1094 .await?
1095 {
1096 let posting_list = posting_list?;
1097 builder
1098 .posting_lists
1099 .push(posting_list.into_builder(&builder.docs));
1100 }
1101 Ok(builder)
1102 }
1103}
1104
1105#[derive(Debug, Clone)]
1108pub enum TokenMap {
1109 HashMap(HashMap<String, u32>),
1110 Fst(fst::Map<Vec<u8>>),
1111}
1112
1113impl Default for TokenMap {
1114 fn default() -> Self {
1115 Self::HashMap(HashMap::new())
1116 }
1117}
1118
1119impl DeepSizeOf for TokenMap {
1120 fn deep_size_of_children(&self, ctx: &mut deepsize::Context) -> usize {
1121 match self {
1122 Self::HashMap(map) => map.deep_size_of_children(ctx),
1123 Self::Fst(map) => map.as_fst().size(),
1124 }
1125 }
1126}
1127
1128impl TokenMap {
1129 pub fn len(&self) -> usize {
1130 match self {
1131 Self::HashMap(map) => map.len(),
1132 Self::Fst(map) => map.len(),
1133 }
1134 }
1135
1136 pub fn is_empty(&self) -> bool {
1137 self.len() == 0
1138 }
1139}
1140
1141#[derive(Debug, Clone, Default, DeepSizeOf)]
1143pub struct TokenSet {
1144 pub(crate) tokens: TokenMap,
1146 pub(crate) next_id: u32,
1147 total_length: usize,
1148}
1149
1150impl TokenSet {
1151 pub fn into_mut(self) -> Self {
1152 let tokens = match self.tokens {
1153 TokenMap::HashMap(map) => map,
1154 TokenMap::Fst(map) => {
1155 let mut new_map = HashMap::with_capacity(map.len());
1156 let mut stream = map.into_stream();
1157 while let Some((token, token_id)) = stream.next() {
1158 new_map.insert(String::from_utf8_lossy(token).into_owned(), token_id as u32);
1159 }
1160
1161 new_map
1162 }
1163 };
1164
1165 Self {
1166 tokens: TokenMap::HashMap(tokens),
1167 next_id: self.next_id,
1168 total_length: self.total_length,
1169 }
1170 }
1171
1172 pub fn len(&self) -> usize {
1173 self.tokens.len()
1174 }
1175
1176 pub fn is_empty(&self) -> bool {
1177 self.len() == 0
1178 }
1179
1180 pub fn to_batch(self, format: TokenSetFormat) -> Result<RecordBatch> {
1181 match format {
1182 TokenSetFormat::Arrow => self.into_arrow_batch(),
1183 TokenSetFormat::Fst => self.into_fst_batch(),
1184 }
1185 }
1186
1187 fn into_arrow_batch(self) -> Result<RecordBatch> {
1188 let mut token_builder = StringBuilder::with_capacity(self.tokens.len(), self.total_length);
1189 let mut token_id_builder = UInt32Builder::with_capacity(self.tokens.len());
1190
1191 match self.tokens {
1192 TokenMap::Fst(map) => {
1193 let mut stream = map.stream();
1194 while let Some((token, token_id)) = stream.next() {
1195 token_builder.append_value(String::from_utf8_lossy(token));
1196 token_id_builder.append_value(token_id as u32);
1197 }
1198 }
1199 TokenMap::HashMap(map) => {
1200 for (token, token_id) in map.into_iter().sorted_unstable() {
1201 token_builder.append_value(token);
1202 token_id_builder.append_value(token_id);
1203 }
1204 }
1205 }
1206
1207 let token_col = token_builder.finish();
1208 let token_id_col = token_id_builder.finish();
1209
1210 let schema = arrow_schema::Schema::new(vec![
1211 arrow_schema::Field::new(TOKEN_COL, DataType::Utf8, false),
1212 arrow_schema::Field::new(TOKEN_ID_COL, DataType::UInt32, false),
1213 ]);
1214
1215 let batch = RecordBatch::try_new(
1216 Arc::new(schema),
1217 vec![
1218 Arc::new(token_col) as ArrayRef,
1219 Arc::new(token_id_col) as ArrayRef,
1220 ],
1221 )?;
1222 Ok(batch)
1223 }
1224
1225 fn into_fst_batch(mut self) -> Result<RecordBatch> {
1226 let fst_map = match std::mem::take(&mut self.tokens) {
1227 TokenMap::Fst(map) => map,
1228 TokenMap::HashMap(map) => Self::build_fst_from_map(map)?,
1229 };
1230 let bytes = fst_map.into_fst().into_inner();
1231
1232 let mut fst_builder = LargeBinaryBuilder::with_capacity(1, bytes.len());
1233 fst_builder.append_value(bytes);
1234 let fst_col = fst_builder.finish();
1235
1236 let mut next_id_builder = UInt32Builder::with_capacity(1);
1237 next_id_builder.append_value(self.next_id);
1238 let next_id_col = next_id_builder.finish();
1239
1240 let mut total_length_builder = UInt64Builder::with_capacity(1);
1241 total_length_builder.append_value(self.total_length as u64);
1242 let total_length_col = total_length_builder.finish();
1243
1244 let schema = arrow_schema::Schema::new(vec![
1245 arrow_schema::Field::new(TOKEN_FST_BYTES_COL, DataType::LargeBinary, false),
1246 arrow_schema::Field::new(TOKEN_NEXT_ID_COL, DataType::UInt32, false),
1247 arrow_schema::Field::new(TOKEN_TOTAL_LENGTH_COL, DataType::UInt64, false),
1248 ]);
1249
1250 let batch = RecordBatch::try_new(
1251 Arc::new(schema),
1252 vec![
1253 Arc::new(fst_col) as ArrayRef,
1254 Arc::new(next_id_col) as ArrayRef,
1255 Arc::new(total_length_col) as ArrayRef,
1256 ],
1257 )?;
1258 Ok(batch)
1259 }
1260
1261 fn build_fst_from_map(map: HashMap<String, u32>) -> Result<fst::Map<Vec<u8>>> {
1262 let mut entries: Vec<_> = map.into_iter().collect();
1263 entries.sort_unstable_by(|(lhs, _), (rhs, _)| lhs.cmp(rhs));
1264 let mut builder = fst::MapBuilder::memory();
1265 for (token, token_id) in entries {
1266 builder
1267 .insert(&token, token_id as u64)
1268 .map_err(|e| Error::index(format!("failed to insert token {}: {}", token, e)))?;
1269 }
1270 Ok(builder.into_map())
1271 }
1272
1273 pub async fn load(reader: Arc<dyn IndexReader>, format: TokenSetFormat) -> Result<Self> {
1274 match format {
1275 TokenSetFormat::Arrow => Self::load_arrow(reader).await,
1276 TokenSetFormat::Fst => Self::load_fst(reader).await,
1277 }
1278 }
1279
1280 async fn load_arrow(reader: Arc<dyn IndexReader>) -> Result<Self> {
1281 let batch = reader.read_range(0..reader.num_rows(), None).await?;
1282
1283 let (tokens, next_id, total_length) = spawn_blocking(move || {
1284 let mut next_id = 0;
1285 let mut total_length = 0;
1286 let mut tokens = fst::MapBuilder::memory();
1287
1288 let token_col = batch[TOKEN_COL].as_string::<i32>();
1289 let token_id_col = batch[TOKEN_ID_COL].as_primitive::<datatypes::UInt32Type>();
1290
1291 for (token, &token_id) in token_col.iter().zip(token_id_col.values().iter()) {
1292 let token =
1293 token.ok_or(Error::index("found null token in token set".to_owned()))?;
1294 next_id = next_id.max(token_id + 1);
1295 total_length += token.len();
1296 tokens.insert(token, token_id as u64).map_err(|e| {
1297 Error::index(format!("failed to insert token {}: {}", token, e))
1298 })?;
1299 }
1300
1301 Ok::<_, Error>((tokens.into_map(), next_id, total_length))
1302 })
1303 .await
1304 .map_err(|err| Error::execution(format!("failed to spawn blocking task: {}", err)))??;
1305
1306 Ok(Self {
1307 tokens: TokenMap::Fst(tokens),
1308 next_id,
1309 total_length,
1310 })
1311 }
1312
1313 async fn load_fst(reader: Arc<dyn IndexReader>) -> Result<Self> {
1314 let batch = reader.read_range(0..reader.num_rows(), None).await?;
1315 if batch.num_rows() == 0 {
1316 return Err(Error::index("token set batch is empty".to_owned()));
1317 }
1318
1319 let fst_col = batch[TOKEN_FST_BYTES_COL].as_binary::<i64>();
1320 let bytes = fst_col.value(0);
1321 let map = fst::Map::new(bytes.to_vec())
1322 .map_err(|e| Error::index(format!("failed to load fst tokens: {}", e)))?;
1323
1324 let next_id_col = batch[TOKEN_NEXT_ID_COL].as_primitive::<datatypes::UInt32Type>();
1325 let total_length_col =
1326 batch[TOKEN_TOTAL_LENGTH_COL].as_primitive::<datatypes::UInt64Type>();
1327
1328 let next_id = next_id_col
1329 .values()
1330 .first()
1331 .copied()
1332 .ok_or(Error::index("token next id column is empty".to_owned()))?;
1333
1334 let total_length = total_length_col
1335 .values()
1336 .first()
1337 .copied()
1338 .ok_or(Error::index(
1339 "token total length column is empty".to_owned(),
1340 ))?;
1341
1342 Ok(Self {
1343 tokens: TokenMap::Fst(map),
1344 next_id,
1345 total_length: usize::try_from(total_length).map_err(|_| {
1346 Error::index(format!(
1347 "token total length {} overflows usize",
1348 total_length
1349 ))
1350 })?,
1351 })
1352 }
1353
1354 pub fn add(&mut self, token: String) -> u32 {
1355 let next_id = self.next_id();
1356 let len = token.len();
1357 let token_id = match self.tokens {
1358 TokenMap::HashMap(ref mut map) => *map.entry(token).or_insert(next_id),
1359 _ => unreachable!("tokens must be HashMap while indexing"),
1360 };
1361
1362 if token_id == next_id {
1364 self.next_id += 1;
1365 self.total_length += len;
1366 }
1367
1368 token_id
1369 }
1370
1371 pub(crate) fn get_or_add(&mut self, token: &str) -> u32 {
1372 let next_id = self.next_id;
1373 match self.tokens {
1374 TokenMap::HashMap(ref mut map) => {
1375 if let Some(&token_id) = map.get(token) {
1376 return token_id;
1377 }
1378
1379 map.insert(token.to_owned(), next_id);
1380 }
1381 _ => unreachable!("tokens must be HashMap while indexing"),
1382 }
1383
1384 self.next_id += 1;
1385 self.total_length += token.len();
1386 next_id
1387 }
1388
1389 pub fn get(&self, token: &str) -> Option<u32> {
1390 match self.tokens {
1391 TokenMap::HashMap(ref map) => map.get(token).copied(),
1392 TokenMap::Fst(ref map) => map.get(token).map(|id| id as u32),
1393 }
1394 }
1395
1396 pub fn remap(&mut self, removed_token_ids: &[u32]) {
1398 if removed_token_ids.is_empty() {
1399 return;
1400 }
1401
1402 let mut map = match std::mem::take(&mut self.tokens) {
1403 TokenMap::HashMap(map) => map,
1404 TokenMap::Fst(map) => {
1405 let mut new_map = HashMap::with_capacity(map.len());
1406 let mut stream = map.into_stream();
1407 while let Some((token, token_id)) = stream.next() {
1408 new_map.insert(String::from_utf8_lossy(token).into_owned(), token_id as u32);
1409 }
1410
1411 new_map
1412 }
1413 };
1414
1415 map.retain(
1416 |_, token_id| match removed_token_ids.binary_search(token_id) {
1417 Ok(_) => false,
1418 Err(index) => {
1419 *token_id -= index as u32;
1420 true
1421 }
1422 },
1423 );
1424
1425 self.tokens = TokenMap::HashMap(map);
1426 }
1427
1428 pub fn next_id(&self) -> u32 {
1429 self.next_id
1430 }
1431
1432 pub(crate) fn memory_size(&self) -> usize {
1433 match &self.tokens {
1434 TokenMap::HashMap(map) => {
1435 self.total_length
1436 + map.capacity()
1437 * (std::mem::size_of::<String>()
1438 + std::mem::size_of::<u32>()
1439 + std::mem::size_of::<usize>())
1440 }
1441 TokenMap::Fst(map) => map.as_fst().size(),
1442 }
1443 }
1444}
1445
1446pub struct PostingListReader {
1447 reader: Arc<dyn IndexReader>,
1448
1449 offsets: Option<Vec<usize>>,
1451
1452 max_scores: Option<Vec<f32>>,
1455
1456 lengths: Option<Vec<u32>>,
1458
1459 has_position: bool,
1460 posting_tail_codec: PostingTailCodec,
1461 positions_layout: PositionsLayout,
1462
1463 index_cache: WeakLanceCache,
1464}
1465
1466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1467enum PositionsLayout {
1468 None,
1469 LegacyPerDoc,
1470 SharedStream(PositionStreamCodec),
1471}
1472
1473impl std::fmt::Debug for PostingListReader {
1474 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1475 f.debug_struct("InvertedListReader")
1476 .field("offsets", &self.offsets)
1477 .field("max_scores", &self.max_scores)
1478 .finish()
1479 }
1480}
1481
1482impl DeepSizeOf for PostingListReader {
1483 fn deep_size_of_children(&self, context: &mut deepsize::Context) -> usize {
1484 self.offsets.deep_size_of_children(context)
1485 + self.max_scores.deep_size_of_children(context)
1486 + self.lengths.deep_size_of_children(context)
1487 }
1488}
1489
1490impl PostingListReader {
1491 pub(crate) async fn try_new(
1492 reader: Arc<dyn IndexReader>,
1493 index_cache: &LanceCache,
1494 ) -> Result<Self> {
1495 let positions_layout = if reader.schema().field(COMPRESSED_POSITION_COL).is_some() {
1496 PositionsLayout::SharedStream(parse_shared_position_codec(&reader.schema().metadata)?)
1497 } else if reader.schema().field(POSITION_COL).is_some() {
1498 PositionsLayout::LegacyPerDoc
1499 } else {
1500 PositionsLayout::None
1501 };
1502 let posting_tail_codec = parse_posting_tail_codec(&reader.schema().metadata)?;
1503 let has_position = positions_layout != PositionsLayout::None;
1504 let (offsets, max_scores, lengths) = if reader.schema().field(POSTING_COL).is_none() {
1505 let (offsets, max_scores) = Self::load_metadata(reader.schema())?;
1506 (Some(offsets), max_scores, None)
1507 } else {
1508 let metadata = reader
1509 .read_range(0..reader.num_rows(), Some(&[MAX_SCORE_COL, LENGTH_COL]))
1510 .await?;
1511 let max_scores = metadata[MAX_SCORE_COL]
1512 .as_primitive::<Float32Type>()
1513 .values()
1514 .to_vec();
1515 let lengths = metadata[LENGTH_COL]
1516 .as_primitive::<UInt32Type>()
1517 .values()
1518 .to_vec();
1519 (None, Some(max_scores), Some(lengths))
1520 };
1521
1522 Ok(Self {
1523 reader,
1524 offsets,
1525 max_scores,
1526 lengths,
1527 has_position,
1528 posting_tail_codec,
1529 positions_layout,
1530 index_cache: WeakLanceCache::from(index_cache),
1531 })
1532 }
1533
1534 fn load_metadata(
1537 schema: &lance_core::datatypes::Schema,
1538 ) -> Result<(Vec<usize>, Option<Vec<f32>>)> {
1539 let offsets = schema
1540 .metadata
1541 .get("offsets")
1542 .ok_or(Error::index("offsets not found in metadata".to_owned()))?;
1543 let offsets = serde_json::from_str(offsets)?;
1544
1545 let max_scores = schema
1546 .metadata
1547 .get("max_scores")
1548 .map(|max_scores| serde_json::from_str(max_scores))
1549 .transpose()?;
1550 Ok((offsets, max_scores))
1551 }
1552
1553 pub fn len(&self) -> usize {
1555 match self.offsets {
1556 Some(ref offsets) => offsets.len(),
1557 None => self.reader.num_rows(),
1558 }
1559 }
1560
1561 pub fn is_empty(&self) -> bool {
1562 self.len() == 0
1563 }
1564
1565 pub(crate) fn has_positions(&self) -> bool {
1566 self.has_position
1567 }
1568
1569 pub(crate) fn posting_tail_codec(&self) -> PostingTailCodec {
1570 self.posting_tail_codec
1571 }
1572
1573 pub(crate) fn posting_len(&self, token_id: u32) -> usize {
1574 let token_id = token_id as usize;
1575
1576 match self.offsets {
1577 Some(ref offsets) => {
1578 let next_offset = offsets
1579 .get(token_id + 1)
1580 .copied()
1581 .unwrap_or(self.reader.num_rows());
1582 next_offset - offsets[token_id]
1583 }
1584 None => {
1585 if let Some(lengths) = &self.lengths {
1586 lengths[token_id] as usize
1587 } else {
1588 panic!("posting list reader is not initialized")
1589 }
1590 }
1591 }
1592 }
1593
1594 pub(crate) async fn posting_batch(
1595 &self,
1596 token_id: u32,
1597 with_position: bool,
1598 ) -> Result<RecordBatch> {
1599 if self.offsets.is_some() {
1600 self.posting_batch_legacy(token_id, with_position).await
1601 } else {
1602 let token_id = token_id as usize;
1603 let columns = if with_position {
1604 match self.positions_layout {
1605 PositionsLayout::SharedStream(_) => {
1606 vec![
1607 POSTING_COL,
1608 COMPRESSED_POSITION_COL,
1609 POSITION_BLOCK_OFFSET_COL,
1610 ]
1611 }
1612 PositionsLayout::LegacyPerDoc => vec![POSTING_COL, POSITION_COL],
1613 PositionsLayout::None => vec![POSTING_COL],
1614 }
1615 } else {
1616 vec![POSTING_COL]
1617 };
1618 let batch = self
1619 .reader
1620 .read_range(token_id..token_id + 1, Some(&columns))
1621 .await?;
1622 Ok(batch)
1623 }
1624 }
1625
1626 async fn posting_batch_legacy(
1627 &self,
1628 token_id: u32,
1629 with_position: bool,
1630 ) -> Result<RecordBatch> {
1631 let mut columns = vec![ROW_ID, FREQUENCY_COL];
1632 if with_position {
1633 columns.push(POSITION_COL);
1634 }
1635
1636 let length = self.posting_len(token_id);
1637 let token_id = token_id as usize;
1638 let offset = self.offsets.as_ref().unwrap()[token_id];
1639 let batch = self
1640 .reader
1641 .read_range(offset..offset + length, Some(&columns))
1642 .await?;
1643 Ok(batch)
1644 }
1645
1646 #[instrument(level = "debug", skip(self, metrics))]
1647 pub(crate) async fn posting_list(
1648 &self,
1649 token_id: u32,
1650 is_phrase_query: bool,
1651 metrics: &dyn MetricsCollector,
1652 ) -> Result<PostingList> {
1653 let cache_key = PostingListKey { token_id };
1654 let mut posting = self
1655 .index_cache
1656 .get_or_insert_with_key(cache_key, || async move {
1657 metrics.record_part_load();
1658 info!(target: TRACE_IO_EVENTS, r#type=IO_TYPE_LOAD_SCALAR_PART, index_type="inverted", part_id=token_id);
1659 let batch = self.posting_batch(token_id, false).await?;
1660 self.posting_list_from_batch(&batch, token_id)
1661 })
1662 .await?
1663 .as_ref()
1664 .clone();
1665
1666 if is_phrase_query {
1667 let positions = self.read_positions(token_id).await?;
1669 posting.set_positions(positions);
1670 }
1671
1672 Ok(posting)
1673 }
1674
1675 fn posting_list_from_batch_parts(
1676 batch: &RecordBatch,
1677 max_score: Option<f32>,
1678 length: Option<u32>,
1679 posting_tail_codec: PostingTailCodec,
1680 ) -> Result<PostingList> {
1681 let posting_list =
1682 PostingList::from_batch_with_tail_codec(batch, max_score, length, posting_tail_codec)?;
1683 Ok(posting_list)
1684 }
1685
1686 pub(crate) fn posting_list_from_batch(
1687 &self,
1688 batch: &RecordBatch,
1689 token_id: u32,
1690 ) -> Result<PostingList> {
1691 Self::posting_list_from_batch_parts(
1692 batch,
1693 self.max_scores
1694 .as_ref()
1695 .map(|max_scores| max_scores[token_id as usize]),
1696 self.lengths
1697 .as_ref()
1698 .map(|lengths| lengths[token_id as usize]),
1699 self.posting_tail_codec,
1700 )
1701 }
1702
1703 fn build_prewarm_posting_lists(
1704 batch: RecordBatch,
1705 offsets: Option<Vec<usize>>,
1706 max_scores: Option<Vec<f32>>,
1707 lengths: Option<Vec<u32>>,
1708 posting_tail_codec: PostingTailCodec,
1709 ) -> Result<Vec<(u32, PostingList)>> {
1710 let token_count = if let Some(offsets) = offsets.as_ref() {
1711 offsets.len()
1712 } else if let Some(lengths) = lengths.as_ref() {
1713 lengths.len()
1714 } else {
1715 batch.num_rows()
1716 };
1717
1718 let mut posting_lists = Vec::with_capacity(token_count);
1719 for token_id in 0..token_count {
1720 let batch = if let Some(offsets) = offsets.as_ref() {
1721 let start = offsets[token_id];
1722 let end = if token_id + 1 < offsets.len() {
1723 offsets[token_id + 1]
1724 } else {
1725 batch.num_rows()
1726 };
1727 batch.slice(start, end - start)
1728 } else {
1729 batch.slice(token_id, 1)
1730 };
1731 let batch = batch.shrink_to_fit()?;
1732 let posting_list = Self::posting_list_from_batch_parts(
1733 &batch,
1734 max_scores.as_ref().map(|scores| scores[token_id]),
1735 lengths.as_ref().map(|lengths| lengths[token_id]),
1736 posting_tail_codec,
1737 )?;
1738 posting_lists.push((token_id as u32, posting_list));
1739 }
1740
1741 Ok(posting_lists)
1742 }
1743
1744 async fn prewarm(&self) -> Result<()> {
1745 let read_batch_start = Instant::now();
1746 let batch = self.read_batch(false).await?;
1747 let read_batch_elapsed = read_batch_start.elapsed();
1748
1749 let legacy_layout = self.offsets.is_some();
1750 let offsets = self.offsets.clone();
1751 let max_scores = self.max_scores.clone();
1752 let lengths = self.lengths.clone();
1753 let posting_tail_codec = self.posting_tail_codec;
1754 let populate_start = Instant::now();
1755 let posting_lists = spawn_blocking(move || {
1756 Self::build_prewarm_posting_lists(
1757 batch,
1758 offsets,
1759 max_scores,
1760 lengths,
1761 posting_tail_codec,
1762 )
1763 })
1764 .await
1765 .map_err(|err| {
1766 Error::internal(format!(
1767 "Failed to build prewarm posting lists in blocking task: {err}"
1768 ))
1769 })??;
1770 for (token_id, posting_list) in posting_lists {
1771 self.index_cache
1772 .insert_with_key(&PostingListKey { token_id }, Arc::new(posting_list))
1773 .await;
1774 }
1775 let populate_elapsed = populate_start.elapsed();
1776
1777 info!(
1778 legacy_layout,
1779 token_count = self.len(),
1780 read_batch_ms = read_batch_elapsed.as_secs_f64() * 1000.0,
1781 post_read_loop_ms = populate_elapsed.as_secs_f64() * 1000.0,
1782 "posting list prewarm timing"
1783 );
1784
1785 Ok(())
1786 }
1787
1788 pub(crate) async fn read_batch(&self, with_position: bool) -> Result<RecordBatch> {
1789 let columns = self.posting_columns(with_position);
1790 let batch = self
1791 .reader
1792 .read_range(0..self.reader.num_rows(), Some(&columns))
1793 .await?;
1794 Ok(batch)
1795 }
1796
1797 pub(crate) async fn read_all(
1798 &self,
1799 with_position: bool,
1800 ) -> Result<impl Iterator<Item = Result<PostingList>> + '_> {
1801 let batch = self.read_batch(with_position).await?;
1802 Ok((0..self.len()).map(move |i| {
1803 let token_id = i as u32;
1804 let range = self.posting_list_range(token_id);
1805 let batch = batch.slice(i, range.end - range.start);
1806 self.posting_list_from_batch(&batch, token_id)
1807 }))
1808 }
1809
1810 async fn read_positions(&self, token_id: u32) -> Result<CompressedPositionStorage> {
1811 let positions = self.index_cache.get_or_insert_with_key(PositionKey { token_id }, || async move {
1812 let positions = match self.positions_layout {
1813 PositionsLayout::None => {
1814 return Err(Error::invalid_input(
1815 "position is not found but required for phrase queries, try recreating the index with position".to_owned(),
1816 ));
1817 }
1818 PositionsLayout::LegacyPerDoc => {
1819 let batch = self
1820 .reader
1821 .read_range(self.posting_list_range(token_id), Some(&[POSITION_COL]))
1822 .await
1823 .map_err(|e| match e {
1824 Error::Schema { .. } => Error::invalid_input("position is not found but required for phrase queries, try recreating the index with position".to_owned()),
1825 e => e,
1826 })?;
1827 CompressedPositionStorage::LegacyPerDoc(
1828 batch[POSITION_COL].as_list::<i32>().value(0).as_list::<i32>().clone(),
1829 )
1830 }
1831 PositionsLayout::SharedStream(codec) => {
1832 let batch = self
1833 .reader
1834 .read_range(
1835 self.posting_list_range(token_id),
1836 Some(&[COMPRESSED_POSITION_COL, POSITION_BLOCK_OFFSET_COL]),
1837 )
1838 .await
1839 .map_err(|e| match e {
1840 Error::Schema { .. } => Error::invalid_input("position is not found but required for phrase queries, try recreating the index with position".to_owned()),
1841 e => e,
1842 })?;
1843 let bytes = batch[COMPRESSED_POSITION_COL]
1844 .as_binary::<i64>()
1845 .value(0)
1846 .to_vec();
1847 let block_offsets = batch[POSITION_BLOCK_OFFSET_COL]
1848 .as_list::<i32>()
1849 .value(0)
1850 .as_primitive::<UInt32Type>()
1851 .values()
1852 .to_vec();
1853 CompressedPositionStorage::SharedStream(SharedPositionStream::new(
1854 codec,
1855 block_offsets,
1856 bytes,
1857 ))
1858 }
1859 };
1860 Result::Ok(Positions(positions))
1861 }).await?;
1862 Ok(positions.0.clone())
1863 }
1864
1865 fn posting_list_range(&self, token_id: u32) -> Range<usize> {
1866 match self.offsets {
1867 Some(ref offsets) => {
1868 let offset = offsets[token_id as usize];
1869 let posting_len = self.posting_len(token_id);
1870 offset..offset + posting_len
1871 }
1872 None => {
1873 let token_id = token_id as usize;
1874 token_id..token_id + 1
1875 }
1876 }
1877 }
1878
1879 fn posting_columns(&self, with_position: bool) -> Vec<&'static str> {
1880 let mut base_columns = match self.offsets {
1881 Some(_) => vec![ROW_ID, FREQUENCY_COL],
1882 None => vec![POSTING_COL],
1883 };
1884 if with_position {
1885 match self.positions_layout {
1886 PositionsLayout::None => {}
1887 PositionsLayout::LegacyPerDoc => base_columns.push(POSITION_COL),
1888 PositionsLayout::SharedStream(_) => {
1889 base_columns.push(COMPRESSED_POSITION_COL);
1890 base_columns.push(POSITION_BLOCK_OFFSET_COL);
1891 }
1892 }
1893 }
1894 base_columns
1895 }
1896}
1897
1898#[derive(Clone)]
1901pub struct Positions(CompressedPositionStorage);
1902
1903impl DeepSizeOf for Positions {
1904 fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
1905 match &self.0 {
1906 CompressedPositionStorage::LegacyPerDoc(positions) => {
1907 positions.get_buffer_memory_size()
1908 }
1909 CompressedPositionStorage::SharedStream(stream) => stream.size(),
1910 }
1911 }
1912}
1913
1914#[derive(Debug, Clone)]
1916pub struct PostingListKey {
1917 pub token_id: u32,
1918}
1919
1920impl CacheKey for PostingListKey {
1921 type ValueType = PostingList;
1922
1923 fn key(&self) -> std::borrow::Cow<'_, str> {
1924 format!("postings-{}", self.token_id).into()
1925 }
1926}
1927
1928#[derive(Debug, Clone)]
1929pub struct PositionKey {
1930 pub token_id: u32,
1931}
1932
1933impl CacheKey for PositionKey {
1934 type ValueType = Positions;
1935
1936 fn key(&self) -> std::borrow::Cow<'_, str> {
1937 format!("positions-{}", self.token_id).into()
1938 }
1939}
1940
1941#[derive(Debug, Clone, PartialEq)]
1942pub enum CompressedPositionStorage {
1943 LegacyPerDoc(ListArray),
1944 SharedStream(SharedPositionStream),
1945}
1946
1947impl DeepSizeOf for CompressedPositionStorage {
1948 fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
1949 match self {
1950 Self::LegacyPerDoc(positions) => positions.get_buffer_memory_size(),
1951 Self::SharedStream(stream) => stream.size(),
1952 }
1953 }
1954}
1955
1956#[derive(Debug, Clone, PartialEq, Eq, Default)]
1957pub struct SharedPositionStream {
1958 codec: PositionStreamCodec,
1959 block_offsets: Vec<u32>,
1960 bytes: Vec<u8>,
1961}
1962
1963impl SharedPositionStream {
1964 pub fn new(codec: PositionStreamCodec, block_offsets: Vec<u32>, bytes: Vec<u8>) -> Self {
1965 Self {
1966 codec,
1967 block_offsets,
1968 bytes,
1969 }
1970 }
1971
1972 pub fn codec(&self) -> PositionStreamCodec {
1973 self.codec
1974 }
1975
1976 pub fn block_count(&self) -> usize {
1977 self.block_offsets.len()
1978 }
1979
1980 pub fn block_range(&self, index: usize) -> Range<usize> {
1981 let start = self.block_offsets[index] as usize;
1982 let end = self
1983 .block_offsets
1984 .get(index + 1)
1985 .map(|offset| *offset as usize)
1986 .unwrap_or(self.bytes.len());
1987 start..end
1988 }
1989
1990 pub fn block(&self, index: usize) -> &[u8] {
1991 let range = self.block_range(index);
1992 &self.bytes[range]
1993 }
1994
1995 pub fn bytes(&self) -> &[u8] {
1996 &self.bytes
1997 }
1998
1999 pub fn block_offsets(&self) -> &[u32] {
2000 &self.block_offsets
2001 }
2002
2003 pub fn size(&self) -> usize {
2004 self.block_offsets.capacity() * std::mem::size_of::<u32>() + self.bytes.capacity()
2005 }
2006}
2007
2008#[derive(Debug, Clone, DeepSizeOf)]
2009pub enum PostingList {
2010 Plain(PlainPostingList),
2011 Compressed(CompressedPostingList),
2012}
2013
2014impl PostingList {
2015 pub fn from_batch(
2016 batch: &RecordBatch,
2017 max_score: Option<f32>,
2018 length: Option<u32>,
2019 ) -> Result<Self> {
2020 let posting_tail_codec = parse_posting_tail_codec(batch.schema_ref().metadata())?;
2021 Self::from_batch_with_tail_codec(batch, max_score, length, posting_tail_codec)
2022 }
2023
2024 pub fn from_batch_with_tail_codec(
2025 batch: &RecordBatch,
2026 max_score: Option<f32>,
2027 length: Option<u32>,
2028 posting_tail_codec: PostingTailCodec,
2029 ) -> Result<Self> {
2030 match batch.column_by_name(POSTING_COL) {
2031 Some(_) => {
2032 debug_assert!(max_score.is_some() && length.is_some());
2033 let posting = CompressedPostingList::from_batch(
2034 batch,
2035 max_score.unwrap(),
2036 length.unwrap(),
2037 posting_tail_codec,
2038 );
2039 Ok(Self::Compressed(posting))
2040 }
2041 None => {
2042 let posting = PlainPostingList::from_batch(batch, max_score);
2043 Ok(Self::Plain(posting))
2044 }
2045 }
2046 }
2047
2048 pub fn iter(&self) -> PostingListIterator<'_> {
2049 PostingListIterator::new(self)
2050 }
2051
2052 pub fn has_position(&self) -> bool {
2053 match self {
2054 Self::Plain(posting) => posting.positions.is_some(),
2055 Self::Compressed(posting) => posting.positions.is_some(),
2056 }
2057 }
2058
2059 pub fn set_positions(&mut self, positions: CompressedPositionStorage) {
2060 match self {
2061 Self::Plain(posting) => match positions {
2062 CompressedPositionStorage::LegacyPerDoc(positions) => {
2063 posting.positions = Some(positions)
2064 }
2065 CompressedPositionStorage::SharedStream(_) => {
2066 unreachable!("shared position stream is not supported for plain postings")
2067 }
2068 },
2069 Self::Compressed(posting) => {
2070 posting.positions = Some(positions);
2071 }
2072 }
2073 }
2074
2075 pub fn max_score(&self) -> Option<f32> {
2076 match self {
2077 Self::Plain(posting) => posting.max_score,
2078 Self::Compressed(posting) => Some(posting.max_score),
2079 }
2080 }
2081
2082 pub fn len(&self) -> usize {
2083 match self {
2084 Self::Plain(posting) => posting.len(),
2085 Self::Compressed(posting) => posting.length as usize,
2086 }
2087 }
2088
2089 pub fn is_empty(&self) -> bool {
2090 self.len() == 0
2091 }
2092
2093 pub fn into_builder(self, docs: &DocSet) -> PostingListBuilder {
2094 let posting_tail_codec = match &self {
2095 Self::Plain(_) => PostingTailCodec::Fixed32,
2096 Self::Compressed(posting) => posting.posting_tail_codec,
2097 };
2098 let mut builder = PostingListBuilder::new_with_posting_tail_codec(
2099 self.has_position(),
2100 posting_tail_codec,
2101 );
2102 match self {
2103 Self::Plain(posting) => {
2105 struct Item {
2109 doc_id: u32,
2110 positions: PositionRecorder,
2111 }
2112 let doc_ids = docs
2113 .row_ids
2114 .iter()
2115 .enumerate()
2116 .map(|(doc_id, row_id)| (*row_id, doc_id as u32))
2117 .collect::<HashMap<_, _>>();
2118 let mut items = Vec::with_capacity(posting.len());
2119 for (row_id, freq, positions) in posting.iter() {
2120 let freq = freq as u32;
2121 let positions = match positions {
2122 Some(positions) => {
2123 PositionRecorder::Position(positions.collect::<Vec<_>>().into())
2124 }
2125 None => PositionRecorder::Count(freq),
2126 };
2127 items.push(Item {
2128 doc_id: doc_ids[&row_id],
2129 positions,
2130 });
2131 }
2132 items.sort_unstable_by_key(|item| item.doc_id);
2133 for item in items {
2134 builder.add(item.doc_id, item.positions);
2135 }
2136 }
2137 Self::Compressed(posting) => {
2138 posting.iter().for_each(|(doc_id, freq, positions)| {
2139 let positions = match positions {
2140 Some(positions) => {
2141 PositionRecorder::Position(positions.collect::<Vec<_>>().into())
2142 }
2143 None => PositionRecorder::Count(freq),
2144 };
2145 builder.add(doc_id, positions);
2146 });
2147 }
2148 }
2149 builder
2150 }
2151}
2152
2153#[derive(Debug, PartialEq, Clone)]
2154pub struct PlainPostingList {
2155 pub row_ids: ScalarBuffer<u64>,
2156 pub frequencies: ScalarBuffer<f32>,
2157 pub max_score: Option<f32>,
2158 pub positions: Option<ListArray>, }
2160
2161impl DeepSizeOf for PlainPostingList {
2162 fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
2163 self.row_ids.len() * std::mem::size_of::<u64>()
2164 + self.frequencies.len() * std::mem::size_of::<u32>()
2165 + self
2166 .positions
2167 .as_ref()
2168 .map(Array::get_buffer_memory_size)
2169 .unwrap_or(0)
2170 }
2171}
2172
2173impl PlainPostingList {
2174 pub fn new(
2175 row_ids: ScalarBuffer<u64>,
2176 frequencies: ScalarBuffer<f32>,
2177 max_score: Option<f32>,
2178 positions: Option<ListArray>,
2179 ) -> Self {
2180 Self {
2181 row_ids,
2182 frequencies,
2183 max_score,
2184 positions,
2185 }
2186 }
2187
2188 pub fn from_batch(batch: &RecordBatch, max_score: Option<f32>) -> Self {
2189 let row_ids = batch[ROW_ID].as_primitive::<UInt64Type>().values().clone();
2190 let frequencies = batch[FREQUENCY_COL]
2191 .as_primitive::<Float32Type>()
2192 .values()
2193 .clone();
2194 let positions = batch
2195 .column_by_name(POSITION_COL)
2196 .map(|col| col.as_list::<i32>().clone());
2197
2198 Self::new(row_ids, frequencies, max_score, positions)
2199 }
2200
2201 pub fn len(&self) -> usize {
2202 self.row_ids.len()
2203 }
2204
2205 pub fn is_empty(&self) -> bool {
2206 self.len() == 0
2207 }
2208
2209 pub fn iter(&self) -> PlainPostingListIterator<'_> {
2210 Box::new(
2211 self.row_ids
2212 .iter()
2213 .zip(self.frequencies.iter())
2214 .enumerate()
2215 .map(|(idx, (doc_id, freq))| {
2216 (
2217 *doc_id,
2218 *freq,
2219 self.positions.as_ref().map(|p| {
2220 let start = p.value_offsets()[idx] as usize;
2221 let end = p.value_offsets()[idx + 1] as usize;
2222 Box::new(
2223 p.values().as_primitive::<Int32Type>().values()[start..end]
2224 .iter()
2225 .map(|pos| *pos as u32),
2226 ) as _
2227 }),
2228 )
2229 }),
2230 )
2231 }
2232
2233 #[inline]
2234 pub fn doc(&self, i: usize) -> LocatedDocInfo {
2235 LocatedDocInfo::new(self.row_ids[i], self.frequencies[i])
2236 }
2237
2238 pub fn positions(&self, index: usize) -> Option<Arc<dyn Array>> {
2239 self.positions
2240 .as_ref()
2241 .map(|positions| positions.value(index))
2242 }
2243
2244 pub fn max_score(&self) -> Option<f32> {
2245 self.max_score
2246 }
2247
2248 pub fn row_id(&self, i: usize) -> u64 {
2249 self.row_ids[i]
2250 }
2251}
2252
2253#[derive(Debug, PartialEq, Clone)]
2254pub struct CompressedPostingList {
2255 pub max_score: f32,
2256 pub length: u32,
2257 pub blocks: LargeBinaryArray,
2260 pub posting_tail_codec: PostingTailCodec,
2261 pub positions: Option<CompressedPositionStorage>,
2262}
2263
2264impl DeepSizeOf for CompressedPostingList {
2265 fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
2266 self.blocks.get_buffer_memory_size()
2267 + self
2268 .positions
2269 .as_ref()
2270 .map(|positions| match positions {
2271 CompressedPositionStorage::LegacyPerDoc(positions) => {
2272 positions.get_buffer_memory_size()
2273 }
2274 CompressedPositionStorage::SharedStream(stream) => stream.size(),
2275 })
2276 .unwrap_or(0)
2277 }
2278}
2279
2280impl CompressedPostingList {
2281 pub fn new(
2282 blocks: LargeBinaryArray,
2283 max_score: f32,
2284 length: u32,
2285 posting_tail_codec: PostingTailCodec,
2286 positions: Option<CompressedPositionStorage>,
2287 ) -> Self {
2288 Self {
2289 max_score,
2290 length,
2291 blocks,
2292 posting_tail_codec,
2293 positions,
2294 }
2295 }
2296
2297 pub fn from_batch(
2298 batch: &RecordBatch,
2299 max_score: f32,
2300 length: u32,
2301 posting_tail_codec: PostingTailCodec,
2302 ) -> Self {
2303 debug_assert_eq!(batch.num_rows(), 1);
2304 let blocks = batch[POSTING_COL]
2305 .as_list::<i32>()
2306 .value(0)
2307 .as_binary::<i64>()
2308 .clone();
2309 let positions = if let Some(col) = batch.column_by_name(COMPRESSED_POSITION_COL) {
2310 let bytes = col.as_binary::<i64>().value(0).to_vec();
2311 let block_offsets = batch[POSITION_BLOCK_OFFSET_COL]
2312 .as_list::<i32>()
2313 .value(0)
2314 .as_primitive::<UInt32Type>()
2315 .values()
2316 .to_vec();
2317 let codec = parse_shared_position_codec(batch.schema_ref().metadata())
2318 .expect("shared position stream codec metadata should be valid");
2319 Some(CompressedPositionStorage::SharedStream(
2320 SharedPositionStream::new(codec, block_offsets, bytes),
2321 ))
2322 } else {
2323 batch.column_by_name(POSITION_COL).map(|col| {
2324 CompressedPositionStorage::LegacyPerDoc(
2325 col.as_list::<i32>().value(0).as_list::<i32>().clone(),
2326 )
2327 })
2328 };
2329
2330 Self {
2331 max_score,
2332 length,
2333 blocks,
2334 posting_tail_codec,
2335 positions,
2336 }
2337 }
2338
2339 pub fn iter(&self) -> CompressedPostingListIterator {
2340 CompressedPostingListIterator::new(
2341 self.length as usize,
2342 self.blocks.clone(),
2343 self.posting_tail_codec,
2344 self.positions.clone(),
2345 )
2346 }
2347
2348 pub fn block_max_score(&self, block_idx: usize) -> f32 {
2349 let block = self.blocks.value(block_idx);
2350 block[0..4].try_into().map(f32::from_le_bytes).unwrap()
2351 }
2352
2353 pub fn block_least_doc_id(&self, block_idx: usize) -> u32 {
2354 let block = self.blocks.value(block_idx);
2355 let remainder = self.length as usize % BLOCK_SIZE;
2356 let is_remainder_block = remainder > 0 && block_idx + 1 == self.blocks.len();
2357 if is_remainder_block {
2358 super::encoding::read_posting_tail_first_doc(block, self.posting_tail_codec)
2359 } else {
2360 block[4..8].try_into().map(u32::from_le_bytes).unwrap()
2361 }
2362 }
2363}
2364
2365#[derive(Debug, Clone, PartialEq, Eq, Default)]
2366struct EncodedBlocks {
2367 offsets: Vec<u32>,
2368 bytes: Vec<u8>,
2369}
2370
2371impl EncodedBlocks {
2372 fn len(&self) -> usize {
2373 self.offsets.len()
2374 }
2375
2376 fn size(&self) -> usize {
2377 self.offsets.capacity() * std::mem::size_of::<u32>() + self.bytes.capacity()
2378 }
2379
2380 fn push_full_block(&mut self, doc_ids: &[u32], frequencies: &[u32]) -> Result<usize> {
2381 let start = self.bytes.len();
2382 self.offsets.push(start as u32);
2383 super::encoding::encode_full_posting_block_into(doc_ids, frequencies, &mut self.bytes)?;
2384 Ok(self.bytes.len() - start)
2385 }
2386
2387 fn block(&self, index: usize) -> &[u8] {
2388 let (start, end) = self.block_range(index);
2389 &self.bytes[start..end]
2390 }
2391
2392 fn block_range(&self, index: usize) -> (usize, usize) {
2393 let start = self.offsets[index] as usize;
2394 let end = self
2395 .offsets
2396 .get(index + 1)
2397 .map(|offset| *offset as usize)
2398 .unwrap_or(self.bytes.len());
2399 (start, end)
2400 }
2401
2402 fn set_block_score(&mut self, index: usize, score: f32) {
2403 let (start, _) = self.block_range(index);
2404 self.bytes[start..start + 4].copy_from_slice(&score.to_le_bytes());
2405 }
2406
2407 fn append_remainder_block_with_codec(
2408 &mut self,
2409 doc_ids: &[u32],
2410 frequencies: &[u32],
2411 codec: PostingTailCodec,
2412 ) -> Result<()> {
2413 self.offsets.push(self.bytes.len() as u32);
2414 super::encoding::encode_remainder_posting_block_into(
2415 doc_ids,
2416 frequencies,
2417 codec,
2418 &mut self.bytes,
2419 )
2420 }
2421
2422 fn into_array(mut self) -> LargeBinaryArray {
2423 let mut offsets = Vec::with_capacity(self.offsets.len() + 1);
2424 offsets.extend(self.offsets.into_iter().map(i64::from));
2425 offsets.push(self.bytes.len() as i64);
2426 LargeBinaryArray::new(
2427 OffsetBuffer::new(ScalarBuffer::from(offsets)),
2428 Buffer::from_vec(std::mem::take(&mut self.bytes)),
2429 None,
2430 )
2431 }
2432
2433 fn iter(&self) -> impl Iterator<Item = &[u8]> {
2434 (0..self.len()).map(|index| self.block(index))
2435 }
2436}
2437
2438#[derive(Debug, Clone, PartialEq, Eq, Default)]
2439struct EncodedPositionBlocks {
2440 offsets: Vec<u32>,
2441 bytes: Vec<u8>,
2442}
2443
2444impl EncodedPositionBlocks {
2445 fn size(&self) -> usize {
2446 self.offsets.capacity() * std::mem::size_of::<u32>() + self.bytes.capacity()
2447 }
2448
2449 fn block(&self, index: usize) -> &[u8] {
2450 let start = self.offsets[index] as usize;
2451 let end = self
2452 .offsets
2453 .get(index + 1)
2454 .map(|offset| *offset as usize)
2455 .unwrap_or(self.bytes.len());
2456 &self.bytes[start..end]
2457 }
2458
2459 fn push_encoded_block(&mut self, block: &[u8]) -> usize {
2460 let start = self.bytes.len();
2461 self.offsets.push(start as u32);
2462 self.bytes.extend_from_slice(block);
2463 self.bytes.len() - start
2464 }
2465
2466 fn into_stream(self) -> SharedPositionStream {
2467 SharedPositionStream::new(PositionStreamCodec::PackedDelta, self.offsets, self.bytes)
2468 }
2469}
2470
2471#[derive(Debug)]
2472pub struct PostingListBuilder {
2473 with_positions: bool,
2474 posting_tail_codec: PostingTailCodec,
2475 encoded_blocks: Option<Box<EncodedBlocks>>,
2476 encoded_position_blocks: Option<Box<EncodedPositionBlocks>>,
2477 tail_entries: Vec<RawDocInfo>,
2478 tail_positions: PositionBlockBuilder,
2479 open_doc_id: Option<u32>,
2480 open_doc_frequency: u32,
2481 open_doc_last_position: Option<u32>,
2482 memory_size_bytes: u32,
2483 len: u32,
2484}
2485
2486pub(super) struct PostingListBatchBuilder {
2487 schema: SchemaRef,
2488 postings: ListBuilder<LargeBinaryBuilder>,
2489 max_scores: Float32Builder,
2490 lengths: UInt32Builder,
2491 positions: BatchPositionsBuilder,
2492 len: usize,
2493}
2494
2495enum BatchPositionsBuilder {
2496 None,
2497 Legacy(ListBuilder<ListBuilder<LargeBinaryBuilder>>),
2498 Shared {
2499 bytes: LargeBinaryBuilder,
2500 block_offsets: ListBuilder<UInt32Builder>,
2501 },
2502}
2503
2504struct PostingListParts<'a> {
2505 with_positions: bool,
2506 posting_tail_codec: PostingTailCodec,
2507 length: usize,
2508 encoded_blocks: EncodedBlocks,
2509 encoded_position_blocks: EncodedPositionBlocks,
2510 tail_entries: &'a [RawDocInfo],
2511 tail_position_block: Option<Vec<u8>>,
2512}
2513
2514impl PostingListBatchBuilder {
2515 pub fn new(
2516 schema: SchemaRef,
2517 with_positions: bool,
2518 format_version: InvertedListFormatVersion,
2519 capacity: usize,
2520 ) -> Self {
2521 let positions = if !with_positions {
2522 BatchPositionsBuilder::None
2523 } else if format_version.uses_shared_position_stream() {
2524 BatchPositionsBuilder::Shared {
2525 bytes: LargeBinaryBuilder::with_capacity(capacity, 0),
2526 block_offsets: ListBuilder::with_capacity(UInt32Builder::new(), capacity),
2527 }
2528 } else {
2529 BatchPositionsBuilder::Legacy(ListBuilder::with_capacity(
2530 ListBuilder::new(LargeBinaryBuilder::new()),
2531 capacity,
2532 ))
2533 };
2534 Self {
2535 schema,
2536 postings: ListBuilder::with_capacity(LargeBinaryBuilder::new(), capacity),
2537 max_scores: Float32Builder::with_capacity(capacity),
2538 lengths: UInt32Builder::with_capacity(capacity),
2539 positions,
2540 len: 0,
2541 }
2542 }
2543
2544 pub fn len(&self) -> usize {
2545 self.len
2546 }
2547
2548 pub fn is_empty(&self) -> bool {
2549 self.len == 0
2550 }
2551
2552 fn append(
2553 &mut self,
2554 compressed: LargeBinaryArray,
2555 max_score: f32,
2556 length: u32,
2557 positions: Option<&CompressedPositionStorage>,
2558 ) -> Result<()> {
2559 {
2560 let values = self.postings.values();
2561 for index in 0..compressed.len() {
2562 values.append_value(compressed.value(index));
2563 }
2564 }
2565 self.postings.append(true);
2566 self.max_scores.append_value(max_score);
2567 self.lengths.append_value(length);
2568
2569 match &mut self.positions {
2570 BatchPositionsBuilder::None => {}
2571 BatchPositionsBuilder::Shared {
2572 bytes,
2573 block_offsets,
2574 } => {
2575 let positions = positions.ok_or_else(|| {
2576 Error::index(format!(
2577 "positions builder missing position data for posting length {}",
2578 length
2579 ))
2580 })?;
2581 let CompressedPositionStorage::SharedStream(positions) = positions else {
2582 return Err(Error::index(
2583 "shared positions builder received legacy positions".to_owned(),
2584 ));
2585 };
2586 bytes.append_value(positions.bytes());
2587 let offsets_builder = block_offsets.values();
2588 for &offset in positions.block_offsets() {
2589 offsets_builder.append_value(offset);
2590 }
2591 block_offsets.append(true);
2592 }
2593 BatchPositionsBuilder::Legacy(position_lists) => {
2594 let positions = positions.ok_or_else(|| {
2595 Error::index(format!(
2596 "positions builder missing position data for posting length {}",
2597 length
2598 ))
2599 })?;
2600 let CompressedPositionStorage::LegacyPerDoc(positions) = positions else {
2601 return Err(Error::index(
2602 "legacy positions builder received shared position stream".to_owned(),
2603 ));
2604 };
2605 let docs_builder = position_lists.values();
2606 for doc_idx in 0..positions.len() {
2607 let doc_positions = positions.value(doc_idx);
2608 let compressed_positions = doc_positions.as_binary::<i64>();
2609 for block_idx in 0..compressed_positions.len() {
2610 docs_builder
2611 .values()
2612 .append_value(compressed_positions.value(block_idx));
2613 }
2614 docs_builder.append(true);
2615 }
2616 position_lists.append(true);
2617 }
2618 }
2619
2620 self.len += 1;
2621 Ok(())
2622 }
2623
2624 pub fn finish(&mut self) -> Result<RecordBatch> {
2625 let mut columns = vec![
2626 Arc::new(self.postings.finish()) as ArrayRef,
2627 Arc::new(self.max_scores.finish()) as ArrayRef,
2628 Arc::new(self.lengths.finish()) as ArrayRef,
2629 ];
2630 match &mut self.positions {
2631 BatchPositionsBuilder::None => {}
2632 BatchPositionsBuilder::Legacy(position_lists) => {
2633 columns.push(Arc::new(position_lists.finish()) as ArrayRef);
2634 }
2635 BatchPositionsBuilder::Shared {
2636 bytes,
2637 block_offsets,
2638 } => {
2639 columns.push(Arc::new(bytes.finish()) as ArrayRef);
2640 columns.push(Arc::new(block_offsets.finish()) as ArrayRef);
2641 }
2642 }
2643 self.len = 0;
2644 RecordBatch::try_new(self.schema.clone(), columns).map_err(Error::from)
2645 }
2646}
2647
2648impl PostingListBuilder {
2649 pub fn size(&self) -> u64 {
2650 self.memory_size_bytes as u64
2651 }
2652
2653 pub fn has_positions(&self) -> bool {
2654 self.with_positions
2655 }
2656
2657 pub fn new(with_position: bool) -> Self {
2658 Self::new_with_posting_tail_codec(
2659 with_position,
2660 current_fts_format_version().posting_tail_codec(),
2661 )
2662 }
2663
2664 pub fn new_with_posting_tail_codec(
2665 with_position: bool,
2666 posting_tail_codec: PostingTailCodec,
2667 ) -> Self {
2668 Self {
2669 with_positions: with_position,
2670 posting_tail_codec,
2671 encoded_blocks: None,
2672 encoded_position_blocks: None,
2673 tail_entries: Vec::new(),
2674 tail_positions: PositionBlockBuilder::default(),
2675 open_doc_id: None,
2676 open_doc_frequency: 0,
2677 open_doc_last_position: None,
2678 len: 0,
2679 memory_size_bytes: 0,
2680 }
2681 }
2682
2683 pub fn len(&self) -> usize {
2684 self.len as usize
2685 }
2686
2687 pub fn is_empty(&self) -> bool {
2688 self.len == 0
2689 }
2690
2691 pub fn iter(&self) -> std::vec::IntoIter<(u32, u32, Option<Vec<u32>>)> {
2692 self.collect_entries().into_iter()
2693 }
2694
2695 pub fn for_each_entry<E>(
2696 &self,
2697 mut visit: impl FnMut(u32, u32, Option<Vec<u32>>) -> std::result::Result<(), E>,
2698 ) -> std::result::Result<(), E> {
2699 let mut doc_ids = Vec::with_capacity(BLOCK_SIZE);
2700 let mut frequencies = Vec::with_capacity(BLOCK_SIZE);
2701 let mut decoded_positions = Vec::new();
2702 let mut position_block_index = 0usize;
2703
2704 if let Some(encoded_blocks) = self.encoded_blocks.as_deref() {
2705 for block in encoded_blocks.iter() {
2706 doc_ids.clear();
2707 frequencies.clear();
2708 super::encoding::decode_full_posting_block(block, &mut doc_ids, &mut frequencies);
2709 decoded_positions.clear();
2710 if self.with_positions {
2711 let position_blocks = self
2712 .encoded_position_blocks
2713 .as_deref()
2714 .expect("positions must exist for posting list");
2715 super::encoding::decode_position_stream_block(
2716 position_blocks.block(position_block_index),
2717 &frequencies,
2718 PositionStreamCodec::PackedDelta,
2719 &mut decoded_positions,
2720 )
2721 .expect("position stream decoding should succeed");
2722 position_block_index += 1;
2723 }
2724 let mut offset = 0usize;
2725 for (doc_id, frequency) in doc_ids.iter().copied().zip(frequencies.iter().copied())
2726 {
2727 let positions = self.with_positions.then(|| {
2728 let end = offset + frequency as usize;
2729 let doc_positions = decoded_positions[offset..end].to_vec();
2730 offset = end;
2731 doc_positions
2732 });
2733 visit(doc_id, frequency, positions)?;
2734 }
2735 }
2736 }
2737
2738 let mut decoded_tail_positions = Vec::new();
2739 if self.with_positions && !self.tail_entries.is_empty() {
2740 let tail_frequencies = self
2741 .tail_entries
2742 .iter()
2743 .map(|entry| entry.frequency)
2744 .collect::<Vec<_>>();
2745 self.tail_positions
2746 .decode_into(tail_frequencies.as_slice(), &mut decoded_tail_positions)
2747 .expect("tail position stream decoding should succeed");
2748 }
2749 let mut tail_offset = 0usize;
2750 for entry in &self.tail_entries {
2751 let positions = self.with_positions.then(|| {
2752 let end = tail_offset + entry.frequency as usize;
2753 let doc_positions = decoded_tail_positions[tail_offset..end].to_vec();
2754 tail_offset = end;
2755 doc_positions
2756 });
2757 visit(entry.doc_id, entry.frequency, positions)?;
2758 }
2759
2760 Ok(())
2761 }
2762
2763 pub fn add(&mut self, doc_id: u32, term_positions: PositionRecorder) {
2764 debug_assert!(
2765 self.open_doc_id.is_none(),
2766 "cannot add closed doc while a positions doc is still open"
2767 );
2768 let tail_entries_capacity_before = self.tail_entries.capacity();
2769 self.tail_entries
2770 .push(RawDocInfo::new(doc_id, term_positions.len()));
2771 let tail_entries_capacity_after = self.tail_entries.capacity();
2772 if tail_entries_capacity_after > tail_entries_capacity_before {
2773 self.add_memory_bytes(
2774 (tail_entries_capacity_after - tail_entries_capacity_before)
2775 * std::mem::size_of::<RawDocInfo>(),
2776 );
2777 }
2778 if let PositionRecorder::Position(positions_in_doc) = term_positions {
2779 debug_assert!(self.with_positions);
2780 let old_size = self.tail_positions.size();
2781 self.tail_positions
2782 .append_doc_positions(positions_in_doc.as_slice())
2783 .expect("position stream encoding should succeed");
2784 self.adjust_tail_positions_size(old_size);
2785 }
2786 self.len += 1;
2787
2788 if self.tail_entries.len() == BLOCK_SIZE {
2789 self.flush_tail_block()
2790 .expect("posting list block compression should succeed");
2791 }
2792 }
2793
2794 pub fn add_occurrence(&mut self, doc_id: u32, position: u32) -> Result<bool> {
2795 if !self.with_positions {
2796 return Err(Error::index(
2797 "cannot append streamed positions to a posting list without positions".to_owned(),
2798 ));
2799 }
2800
2801 match self.open_doc_id {
2802 Some(open_doc_id) if open_doc_id == doc_id => {
2803 let old_size = self.tail_positions.size();
2804 self.tail_positions
2805 .append_position(position, self.open_doc_last_position)?;
2806 self.adjust_tail_positions_size(old_size);
2807 self.open_doc_frequency += 1;
2808 self.open_doc_last_position = Some(position);
2809 Ok(false)
2810 }
2811 Some(open_doc_id) => Err(Error::index(format!(
2812 "posting list received doc {} before finishing open doc {}",
2813 doc_id, open_doc_id
2814 ))),
2815 None => {
2816 let old_size = self.tail_positions.size();
2817 self.tail_positions.append_position(position, None)?;
2818 self.adjust_tail_positions_size(old_size);
2819 self.open_doc_id = Some(doc_id);
2820 self.open_doc_frequency = 1;
2821 self.open_doc_last_position = Some(position);
2822 self.len += 1;
2823 Ok(true)
2824 }
2825 }
2826 }
2827
2828 pub fn finish_open_doc(&mut self, doc_id: u32) -> Result<()> {
2829 if !self.with_positions {
2830 return Ok(());
2831 }
2832 match self.open_doc_id {
2833 Some(open_doc_id) if open_doc_id == doc_id => {
2834 let tail_entries_capacity_before = self.tail_entries.capacity();
2835 self.tail_entries
2836 .push(RawDocInfo::new(doc_id, self.open_doc_frequency));
2837 let tail_entries_capacity_after = self.tail_entries.capacity();
2838 if tail_entries_capacity_after > tail_entries_capacity_before {
2839 self.add_memory_bytes(
2840 (tail_entries_capacity_after - tail_entries_capacity_before)
2841 * std::mem::size_of::<RawDocInfo>(),
2842 );
2843 }
2844 self.open_doc_id = None;
2845 self.open_doc_frequency = 0;
2846 self.open_doc_last_position = None;
2847 if self.tail_entries.len() == BLOCK_SIZE {
2848 self.flush_tail_block()?;
2849 }
2850 Ok(())
2851 }
2852 Some(open_doc_id) => Err(Error::index(format!(
2853 "attempted to finish doc {} while doc {} is still open",
2854 doc_id, open_doc_id
2855 ))),
2856 None => Ok(()),
2857 }
2858 }
2859
2860 fn collect_entries(&self) -> Vec<(u32, u32, Option<Vec<u32>>)> {
2861 let mut entries = Vec::with_capacity(self.len());
2862 self.for_each_entry(|doc_id, frequency, positions| {
2863 entries.push((doc_id, frequency, positions));
2864 Ok::<(), ()>(())
2865 })
2866 .expect("collecting posting list entries should not fail");
2867 entries
2868 }
2869
2870 fn encoded_blocks_mut(&mut self) -> &mut EncodedBlocks {
2871 if self.encoded_blocks.is_none() {
2872 self.encoded_blocks = Some(Box::default());
2873 self.add_memory_bytes(std::mem::size_of::<EncodedBlocks>());
2874 }
2875 self.encoded_blocks
2876 .as_deref_mut()
2877 .expect("encoded blocks must exist")
2878 }
2879
2880 fn encoded_position_blocks_mut(&mut self) -> &mut EncodedPositionBlocks {
2881 if self.encoded_position_blocks.is_none() {
2882 self.encoded_position_blocks = Some(Box::default());
2883 self.add_memory_bytes(std::mem::size_of::<EncodedPositionBlocks>());
2884 }
2885 self.encoded_position_blocks
2886 .as_deref_mut()
2887 .expect("encoded position blocks must exist")
2888 }
2889
2890 fn flush_tail_block(&mut self) -> Result<()> {
2891 if self.tail_entries.is_empty() {
2892 return Ok(());
2893 }
2894 debug_assert!(
2895 self.open_doc_id.is_none(),
2896 "cannot flush a posting block while a document is still open"
2897 );
2898 debug_assert_eq!(self.tail_entries.len(), BLOCK_SIZE);
2899 let mut doc_ids = [0u32; BLOCK_SIZE];
2900 let mut frequencies = [0u32; BLOCK_SIZE];
2901 for (index, entry) in self.tail_entries.iter().enumerate() {
2902 doc_ids[index] = entry.doc_id;
2903 frequencies[index] = entry.frequency;
2904 }
2905 let encoded_blocks_size_before = self
2906 .encoded_blocks
2907 .as_ref()
2908 .map(|encoded_blocks| encoded_blocks.size())
2909 .unwrap_or(0usize);
2910 self.encoded_blocks_mut()
2911 .push_full_block(&doc_ids, &frequencies)?;
2912 let encoded_blocks_size_after = self
2913 .encoded_blocks
2914 .as_ref()
2915 .map(|encoded_blocks| encoded_blocks.size())
2916 .unwrap_or(0usize);
2917 if encoded_blocks_size_after > encoded_blocks_size_before {
2918 self.add_memory_bytes(encoded_blocks_size_after - encoded_blocks_size_before);
2919 }
2920 if self.with_positions {
2921 let encoded_positions_size_before = self
2922 .encoded_position_blocks
2923 .as_ref()
2924 .map(|encoded| encoded.size())
2925 .unwrap_or(0usize);
2926 let released_tail_positions_bytes = self.tail_positions.size();
2927 let tail_position_block = std::mem::take(&mut self.tail_positions).finish();
2928 self.encoded_position_blocks_mut()
2929 .push_encoded_block(tail_position_block.as_slice());
2930 let encoded_positions_size_after = self
2931 .encoded_position_blocks
2932 .as_ref()
2933 .map(|encoded| encoded.size())
2934 .unwrap_or(0usize);
2935 if released_tail_positions_bytes > 0 {
2936 self.subtract_memory_bytes(released_tail_positions_bytes);
2937 }
2938 if encoded_positions_size_after > encoded_positions_size_before {
2939 self.add_memory_bytes(encoded_positions_size_after - encoded_positions_size_before);
2940 }
2941 }
2942 self.tail_entries.clear();
2943 Ok(())
2944 }
2945
2946 fn adjust_tail_positions_size(&mut self, old_size: usize) {
2947 let new_size = self.tail_positions.size();
2948 if new_size > old_size {
2949 self.add_memory_bytes(new_size - old_size);
2950 } else if old_size > new_size {
2951 self.subtract_memory_bytes(old_size - new_size);
2952 }
2953 }
2954
2955 fn add_memory_bytes(&mut self, bytes: usize) {
2956 self.memory_size_bytes = self
2957 .memory_size_bytes
2958 .checked_add(
2959 u32::try_from(bytes).expect("posting list memory size delta overflowed u32"),
2960 )
2961 .expect("posting list memory size overflowed u32");
2962 }
2963
2964 fn subtract_memory_bytes(&mut self, bytes: usize) {
2965 self.memory_size_bytes = self
2966 .memory_size_bytes
2967 .checked_sub(
2968 u32::try_from(bytes).expect("posting list memory size delta overflowed u32"),
2969 )
2970 .expect("posting list memory size underflowed u32");
2971 }
2972
2973 fn build_position_columns(
2974 positions: Option<CompressedPositionStorage>,
2975 ) -> Result<Vec<ArrayRef>> {
2976 let Some(positions) = positions else {
2977 return Ok(Vec::new());
2978 };
2979 match positions {
2980 CompressedPositionStorage::LegacyPerDoc(positions) => {
2981 Ok(vec![Arc::new(ListArray::try_new(
2982 Arc::new(Field::new("item", positions.data_type().clone(), true)),
2983 OffsetBuffer::new(ScalarBuffer::from(vec![0_i32, positions.len() as i32])),
2984 Arc::new(positions) as ArrayRef,
2985 None,
2986 )?) as ArrayRef])
2987 }
2988 CompressedPositionStorage::SharedStream(positions) => {
2989 let mut columns = Vec::with_capacity(2);
2990 columns.push(
2991 Arc::new(LargeBinaryArray::from(vec![Some(positions.bytes())])) as ArrayRef,
2992 );
2993
2994 let mut offsets_builder = ListBuilder::new(UInt32Builder::new());
2995 for &offset in positions.block_offsets() {
2996 offsets_builder.values().append_value(offset);
2997 }
2998 offsets_builder.append(true);
2999 columns.push(Arc::new(offsets_builder.finish()) as ArrayRef);
3000 Ok(columns)
3001 }
3002 }
3003 }
3004
3005 fn build_batch(
3006 self,
3007 compressed: LargeBinaryArray,
3008 max_score: f32,
3009 schema: SchemaRef,
3010 positions: Option<CompressedPositionStorage>,
3011 ) -> Result<RecordBatch> {
3012 let length = self.len();
3013 let offsets = OffsetBuffer::new(ScalarBuffer::from(vec![0, compressed.len() as i32]));
3014 let mut columns = vec![
3015 Arc::new(ListArray::try_new(
3016 Arc::new(Field::new("item", datatypes::DataType::LargeBinary, true)),
3017 offsets,
3018 Arc::new(compressed),
3019 None,
3020 )?) as ArrayRef,
3021 Arc::new(Float32Array::from_iter_values(std::iter::once(max_score))) as ArrayRef,
3022 Arc::new(UInt32Array::from_iter_values(std::iter::once(
3023 length as u32,
3024 ))) as ArrayRef,
3025 ];
3026 columns.extend(Self::build_position_columns(positions)?);
3027
3028 let batch = RecordBatch::try_new(schema, columns)?;
3029 Ok(batch)
3030 }
3031
3032 fn build_legacy_positions(&self) -> Result<ListArray> {
3033 let mut positions_builder = ListBuilder::new(LargeBinaryBuilder::new());
3034 self.for_each_entry(|_doc_id, frequency, positions| {
3035 let positions = positions.ok_or_else(|| {
3036 Error::index(format!(
3037 "legacy position writer missing positions for frequency {}",
3038 frequency
3039 ))
3040 })?;
3041 let compressed = super::encoding::compress_positions(positions.as_slice())?;
3042 for block_idx in 0..compressed.len() {
3043 positions_builder
3044 .values()
3045 .append_value(compressed.value(block_idx));
3046 }
3047 positions_builder.append(true);
3048 Ok::<(), Error>(())
3049 })?;
3050 Ok(positions_builder.finish())
3051 }
3052
3053 pub(super) fn append_to_batch_with_docs(
3054 self,
3055 docs: &DocSet,
3056 batch_builder: &mut PostingListBatchBuilder,
3057 format_version: InvertedListFormatVersion,
3058 ) -> Result<()> {
3059 let legacy_positions =
3060 if self.with_positions && !format_version.uses_shared_position_stream() {
3061 Some(self.build_legacy_positions()?)
3062 } else {
3063 None
3064 };
3065 let Self {
3066 with_positions,
3067 posting_tail_codec,
3068 encoded_blocks,
3069 encoded_position_blocks,
3070 tail_entries,
3071 tail_positions,
3072 open_doc_id,
3073 open_doc_frequency,
3074 open_doc_last_position,
3075 len,
3076 ..
3077 } = self;
3078 debug_assert!(open_doc_id.is_none());
3079 debug_assert_eq!(open_doc_frequency, 0);
3080 debug_assert!(open_doc_last_position.is_none());
3081 let parts = PostingListParts {
3082 with_positions,
3083 posting_tail_codec,
3084 length: len as usize,
3085 encoded_blocks: encoded_blocks
3086 .map(|encoded_blocks| *encoded_blocks)
3087 .unwrap_or_default(),
3088 encoded_position_blocks: encoded_position_blocks
3089 .map(|encoded_positions| *encoded_positions)
3090 .unwrap_or_default(),
3091 tail_entries: tail_entries.as_slice(),
3092 tail_position_block: with_positions.then(|| tail_positions.finish()),
3093 };
3094 let (compressed, shared_positions, max_score) =
3095 Self::build_compressed_with_scores_from_parts(parts, docs)?;
3096 let positions = match legacy_positions {
3097 Some(positions) => Some(CompressedPositionStorage::LegacyPerDoc(positions)),
3098 None => shared_positions.map(CompressedPositionStorage::SharedStream),
3099 };
3100 batch_builder.append(compressed, max_score, len, positions.as_ref())
3101 }
3102
3103 fn extend_tail_components(
3104 tail_entries: &[RawDocInfo],
3105 doc_ids: &mut Vec<u32>,
3106 frequencies: &mut Vec<u32>,
3107 ) {
3108 doc_ids.clear();
3109 frequencies.clear();
3110 doc_ids.extend(tail_entries.iter().map(|entry| entry.doc_id));
3111 frequencies.extend(tail_entries.iter().map(|entry| entry.frequency));
3112 }
3113
3114 fn build_compressed_with_scores_from_parts(
3115 parts: PostingListParts<'_>,
3116 docs: &DocSet,
3117 ) -> Result<(LargeBinaryArray, Option<SharedPositionStream>, f32)> {
3118 let PostingListParts {
3119 with_positions,
3120 posting_tail_codec,
3121 length,
3122 mut encoded_blocks,
3123 mut encoded_position_blocks,
3124 tail_entries,
3125 tail_position_block,
3126 } = parts;
3127 let avgdl = docs.average_length();
3128 let idf_scale = idf(length, docs.len()) * (K1 + 1.0);
3129 let mut max_score = f32::MIN;
3130 let mut doc_ids = Vec::with_capacity(BLOCK_SIZE);
3131 let mut frequencies = Vec::with_capacity(BLOCK_SIZE);
3132
3133 for index in 0..encoded_blocks.len() {
3134 let block = encoded_blocks.block(index);
3135 doc_ids.clear();
3136 frequencies.clear();
3137 super::encoding::decode_full_posting_block(block, &mut doc_ids, &mut frequencies);
3138 let block_score = compute_block_score(
3139 docs,
3140 avgdl,
3141 idf_scale,
3142 doc_ids.iter().copied(),
3143 frequencies.iter().copied(),
3144 );
3145 max_score = max_score.max(block_score);
3146 encoded_blocks.set_block_score(index, block_score);
3147 }
3148
3149 if !tail_entries.is_empty() {
3150 Self::extend_tail_components(tail_entries, &mut doc_ids, &mut frequencies);
3151 let block_score = compute_block_score(
3152 docs,
3153 avgdl,
3154 idf_scale,
3155 doc_ids.iter().copied(),
3156 frequencies.iter().copied(),
3157 );
3158 max_score = max_score.max(block_score);
3159 encoded_blocks.append_remainder_block_with_codec(
3160 doc_ids.as_slice(),
3161 frequencies.as_slice(),
3162 posting_tail_codec,
3163 )?;
3164 encoded_blocks.set_block_score(encoded_blocks.len() - 1, block_score);
3165 if with_positions {
3166 encoded_position_blocks.push_encoded_block(
3167 tail_position_block
3168 .as_deref()
3169 .expect("tail position block must exist for postings with positions"),
3170 );
3171 }
3172 }
3173
3174 Ok((
3175 encoded_blocks.into_array(),
3176 with_positions.then(|| encoded_position_blocks.into_stream()),
3177 max_score,
3178 ))
3179 }
3180
3181 fn build_compressed_with_block_scores_from_parts(
3182 with_positions: bool,
3183 posting_tail_codec: PostingTailCodec,
3184 mut encoded_blocks: EncodedBlocks,
3185 mut encoded_position_blocks: EncodedPositionBlocks,
3186 tail_entries: &[RawDocInfo],
3187 tail_position_block: Option<Vec<u8>>,
3188 mut block_max_scores: impl Iterator<Item = f32>,
3189 ) -> Result<(LargeBinaryArray, Option<SharedPositionStream>, f32)> {
3190 let mut max_score = f32::MIN;
3191 let mut doc_ids = Vec::with_capacity(BLOCK_SIZE);
3192 let mut frequencies = Vec::with_capacity(BLOCK_SIZE);
3193
3194 for index in 0..encoded_blocks.len() {
3195 let block_score = block_max_scores
3196 .next()
3197 .ok_or_else(|| Error::index("missing block max score".to_owned()))?;
3198 max_score = max_score.max(block_score);
3199 encoded_blocks.set_block_score(index, block_score);
3200 }
3201
3202 if !tail_entries.is_empty() {
3203 let block_score = block_max_scores
3204 .next()
3205 .ok_or_else(|| Error::index("missing tail block max score".to_owned()))?;
3206 max_score = max_score.max(block_score);
3207 Self::extend_tail_components(tail_entries, &mut doc_ids, &mut frequencies);
3208 encoded_blocks.append_remainder_block_with_codec(
3209 doc_ids.as_slice(),
3210 frequencies.as_slice(),
3211 posting_tail_codec,
3212 )?;
3213 encoded_blocks.set_block_score(encoded_blocks.len() - 1, block_score);
3214 if with_positions {
3215 encoded_position_blocks.push_encoded_block(
3216 tail_position_block
3217 .as_deref()
3218 .expect("tail position block must exist for postings with positions"),
3219 );
3220 }
3221 }
3222
3223 Ok((
3224 encoded_blocks.into_array(),
3225 with_positions.then(|| encoded_position_blocks.into_stream()),
3226 max_score,
3227 ))
3228 }
3229
3230 pub fn to_batch(self, block_max_scores: Vec<f32>) -> Result<RecordBatch> {
3231 let format_version = if self.posting_tail_codec == PostingTailCodec::Fixed32 {
3232 InvertedListFormatVersion::V1
3233 } else {
3234 InvertedListFormatVersion::V2
3235 };
3236 let schema = inverted_list_schema_for_version(self.has_positions(), format_version);
3237 let legacy_positions =
3238 if self.with_positions && !format_version.uses_shared_position_stream() {
3239 Some(self.build_legacy_positions()?)
3240 } else {
3241 None
3242 };
3243 let Self {
3244 with_positions,
3245 posting_tail_codec,
3246 encoded_blocks,
3247 encoded_position_blocks,
3248 tail_entries,
3249 tail_positions,
3250 open_doc_id,
3251 open_doc_frequency,
3252 open_doc_last_position,
3253 len,
3254 ..
3255 } = self;
3256 debug_assert!(open_doc_id.is_none());
3257 debug_assert_eq!(open_doc_frequency, 0);
3258 debug_assert!(open_doc_last_position.is_none());
3259 let (compressed, shared_positions, max_score) =
3260 Self::build_compressed_with_block_scores_from_parts(
3261 with_positions,
3262 posting_tail_codec,
3263 encoded_blocks
3264 .map(|encoded_blocks| *encoded_blocks)
3265 .unwrap_or_default(),
3266 encoded_position_blocks
3267 .map(|encoded_positions| *encoded_positions)
3268 .unwrap_or_default(),
3269 tail_entries.as_slice(),
3270 with_positions.then(|| tail_positions.finish()),
3271 block_max_scores.into_iter(),
3272 )?;
3273 let builder = Self {
3274 with_positions,
3275 posting_tail_codec,
3276 encoded_blocks: None,
3277 encoded_position_blocks: None,
3278 tail_entries: Vec::new(),
3279 tail_positions: PositionBlockBuilder::default(),
3280 open_doc_id: None,
3281 open_doc_frequency: 0,
3282 open_doc_last_position: None,
3283 memory_size_bytes: 0,
3284 len,
3285 };
3286 let positions = match legacy_positions {
3287 Some(positions) => Some(CompressedPositionStorage::LegacyPerDoc(positions)),
3288 None => shared_positions.map(CompressedPositionStorage::SharedStream),
3289 };
3290 builder.build_batch(compressed, max_score, schema, positions)
3291 }
3292
3293 pub fn to_batch_with_docs(self, docs: &DocSet, schema: SchemaRef) -> Result<RecordBatch> {
3294 let format_version = if schema.column_with_name(POSITION_COL).is_some()
3295 && schema.column_with_name(COMPRESSED_POSITION_COL).is_none()
3296 {
3297 InvertedListFormatVersion::V1
3298 } else {
3299 InvertedListFormatVersion::V2
3300 };
3301 let legacy_positions =
3302 if self.with_positions && !format_version.uses_shared_position_stream() {
3303 Some(self.build_legacy_positions()?)
3304 } else {
3305 None
3306 };
3307 let Self {
3308 with_positions,
3309 posting_tail_codec,
3310 encoded_blocks,
3311 encoded_position_blocks,
3312 tail_entries,
3313 tail_positions,
3314 open_doc_id,
3315 open_doc_frequency,
3316 open_doc_last_position,
3317 len,
3318 ..
3319 } = self;
3320 debug_assert!(open_doc_id.is_none());
3321 debug_assert_eq!(open_doc_frequency, 0);
3322 debug_assert!(open_doc_last_position.is_none());
3323 let parts = PostingListParts {
3324 with_positions,
3325 posting_tail_codec,
3326 length: len as usize,
3327 encoded_blocks: encoded_blocks
3328 .map(|encoded_blocks| *encoded_blocks)
3329 .unwrap_or_default(),
3330 encoded_position_blocks: encoded_position_blocks
3331 .map(|encoded_positions| *encoded_positions)
3332 .unwrap_or_default(),
3333 tail_entries: tail_entries.as_slice(),
3334 tail_position_block: with_positions.then(|| tail_positions.finish()),
3335 };
3336 let (compressed, shared_positions, max_score) =
3337 Self::build_compressed_with_scores_from_parts(parts, docs)?;
3338 let builder = Self {
3339 with_positions,
3340 posting_tail_codec,
3341 encoded_blocks: None,
3342 encoded_position_blocks: None,
3343 tail_entries: Vec::new(),
3344 tail_positions: PositionBlockBuilder::default(),
3345 open_doc_id: None,
3346 open_doc_frequency: 0,
3347 open_doc_last_position: None,
3348 memory_size_bytes: 0,
3349 len,
3350 };
3351 let positions = match legacy_positions {
3352 Some(positions) => Some(CompressedPositionStorage::LegacyPerDoc(positions)),
3353 None => shared_positions.map(CompressedPositionStorage::SharedStream),
3354 };
3355 builder.build_batch(compressed, max_score, schema, positions)
3356 }
3357
3358 pub fn remap(&mut self, removed: &[u32]) {
3359 let mut cursor = 0;
3360 let mut new_builder =
3361 Self::new_with_posting_tail_codec(self.has_positions(), self.posting_tail_codec);
3362 for (doc_id, freq, positions) in self.iter() {
3363 while cursor < removed.len() && removed[cursor] < doc_id {
3364 cursor += 1;
3365 }
3366 if cursor < removed.len() && removed[cursor] == doc_id {
3367 continue;
3368 }
3369 let positions = match positions {
3370 Some(positions) => PositionRecorder::Position(positions.into()),
3371 None => PositionRecorder::Count(freq),
3372 };
3373 new_builder.add(doc_id - cursor as u32, positions);
3374 }
3375
3376 *self = new_builder;
3377 }
3378}
3379
3380fn compute_block_score(
3381 docs: &DocSet,
3382 avgdl: f32,
3383 idf_scale: f32,
3384 doc_ids: impl Iterator<Item = u32>,
3385 frequencies: impl Iterator<Item = u32>,
3386) -> f32 {
3387 let mut block_max_score = f32::MIN;
3388 for (doc_id, freq) in doc_ids.zip(frequencies) {
3389 let doc_norm = K1 * (1.0 - B + B * docs.num_tokens(doc_id) as f32 / avgdl);
3390 let freq = freq as f32;
3391 let score = freq / (freq + doc_norm);
3392 block_max_score = block_max_score.max(score);
3393 }
3394 block_max_score * idf_scale
3395}
3396
3397#[derive(Debug, Clone, DeepSizeOf, Copy)]
3398pub enum DocInfo {
3399 Located(LocatedDocInfo),
3400 Raw(RawDocInfo),
3401}
3402
3403impl DocInfo {
3404 pub fn doc_id(&self) -> u64 {
3405 match self {
3406 Self::Raw(info) => info.doc_id as u64,
3407 Self::Located(info) => info.row_id,
3408 }
3409 }
3410
3411 pub fn frequency(&self) -> u32 {
3412 match self {
3413 Self::Raw(info) => info.frequency,
3414 Self::Located(info) => info.frequency as u32,
3415 }
3416 }
3417}
3418
3419impl Eq for DocInfo {}
3420
3421impl PartialEq for DocInfo {
3422 fn eq(&self, other: &Self) -> bool {
3423 self.doc_id() == other.doc_id()
3424 }
3425}
3426
3427impl PartialOrd for DocInfo {
3428 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
3429 Some(self.cmp(other))
3430 }
3431}
3432
3433impl Ord for DocInfo {
3434 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
3435 self.doc_id().cmp(&other.doc_id())
3436 }
3437}
3438
3439#[derive(Debug, Clone, Default, DeepSizeOf, Copy)]
3440pub struct LocatedDocInfo {
3441 pub row_id: u64,
3442 pub frequency: f32,
3443}
3444
3445impl LocatedDocInfo {
3446 pub fn new(row_id: u64, frequency: f32) -> Self {
3447 Self { row_id, frequency }
3448 }
3449}
3450
3451impl Eq for LocatedDocInfo {}
3452
3453impl PartialEq for LocatedDocInfo {
3454 fn eq(&self, other: &Self) -> bool {
3455 self.row_id == other.row_id
3456 }
3457}
3458
3459impl PartialOrd for LocatedDocInfo {
3460 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
3461 Some(self.cmp(other))
3462 }
3463}
3464
3465impl Ord for LocatedDocInfo {
3466 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
3467 self.row_id.cmp(&other.row_id)
3468 }
3469}
3470
3471#[derive(Debug, Clone, Default, DeepSizeOf, Copy)]
3472pub struct RawDocInfo {
3473 pub doc_id: u32,
3474 pub frequency: u32,
3475}
3476
3477impl RawDocInfo {
3478 pub fn new(doc_id: u32, frequency: u32) -> Self {
3479 Self { doc_id, frequency }
3480 }
3481}
3482
3483impl Eq for RawDocInfo {}
3484
3485impl PartialEq for RawDocInfo {
3486 fn eq(&self, other: &Self) -> bool {
3487 self.doc_id == other.doc_id
3488 }
3489}
3490
3491impl PartialOrd for RawDocInfo {
3492 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
3493 Some(self.cmp(other))
3494 }
3495}
3496
3497impl Ord for RawDocInfo {
3498 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
3499 self.doc_id.cmp(&other.doc_id)
3500 }
3501}
3502
3503#[derive(Debug, Clone, Default, DeepSizeOf)]
3506pub struct DocSet {
3507 row_ids: Vec<u64>,
3508 num_tokens: Vec<u32>,
3509 inv: Vec<(u64, u32)>,
3511
3512 total_tokens: u64,
3513}
3514
3515impl DocSet {
3516 #[inline]
3517 pub fn len(&self) -> usize {
3518 self.row_ids.len()
3519 }
3520
3521 pub fn is_empty(&self) -> bool {
3522 self.len() == 0
3523 }
3524
3525 pub fn iter(&self) -> impl Iterator<Item = (&u64, &u32)> {
3526 self.row_ids.iter().zip(self.num_tokens.iter())
3527 }
3528
3529 pub fn row_id(&self, doc_id: u32) -> u64 {
3530 self.row_ids[doc_id as usize]
3531 }
3532
3533 pub fn doc_id(&self, row_id: u64) -> Option<u64> {
3534 if self.inv.is_empty() {
3535 match self.row_ids.binary_search(&row_id) {
3537 Ok(_) => Some(row_id),
3538 Err(_) => None,
3539 }
3540 } else {
3541 match self.inv.binary_search_by_key(&row_id, |x| x.0) {
3542 Ok(idx) => Some(self.inv[idx].1 as u64),
3543 Err(_) => None,
3544 }
3545 }
3546 }
3547 pub fn total_tokens_num(&self) -> u64 {
3548 self.total_tokens
3549 }
3550
3551 #[inline]
3552 pub fn average_length(&self) -> f32 {
3553 self.total_tokens as f32 / self.len() as f32
3554 }
3555
3556 pub fn calculate_block_max_scores<'a>(
3557 &self,
3558 doc_ids: impl Iterator<Item = &'a u32>,
3559 freqs: impl Iterator<Item = &'a u32>,
3560 ) -> Vec<f32> {
3561 let avgdl = self.average_length();
3562 let length = doc_ids.size_hint().0;
3563 let num_blocks = length.div_ceil(BLOCK_SIZE);
3564 let mut block_max_scores = Vec::with_capacity(num_blocks);
3565 let idf_scale = idf(length, self.len()) * (K1 + 1.0);
3566 let mut max_score = f32::MIN;
3567 for (i, (doc_id, freq)) in doc_ids.zip(freqs).enumerate() {
3568 let doc_norm = K1 * (1.0 - B + B * self.num_tokens(*doc_id) as f32 / avgdl);
3569 let freq = *freq as f32;
3570 let score = freq / (freq + doc_norm);
3571 if score > max_score {
3572 max_score = score;
3573 }
3574 if (i + 1) % BLOCK_SIZE == 0 {
3575 max_score *= idf_scale;
3576 block_max_scores.push(max_score);
3577 max_score = f32::MIN;
3578 }
3579 }
3580 if !length.is_multiple_of(BLOCK_SIZE) {
3581 max_score *= idf_scale;
3582 block_max_scores.push(max_score);
3583 }
3584 block_max_scores
3585 }
3586
3587 pub fn to_batch(&self) -> Result<RecordBatch> {
3588 let row_id_col = UInt64Array::from_iter_values(self.row_ids.iter().cloned());
3589 let num_tokens_col = UInt32Array::from_iter_values(self.num_tokens.iter().cloned());
3590
3591 let schema = arrow_schema::Schema::new(vec![
3592 arrow_schema::Field::new(ROW_ID, DataType::UInt64, false),
3593 arrow_schema::Field::new(NUM_TOKEN_COL, DataType::UInt32, false),
3594 ]);
3595
3596 let batch = RecordBatch::try_new(
3597 Arc::new(schema),
3598 vec![
3599 Arc::new(row_id_col) as ArrayRef,
3600 Arc::new(num_tokens_col) as ArrayRef,
3601 ],
3602 )?;
3603 Ok(batch)
3604 }
3605
3606 pub async fn load(
3607 reader: Arc<dyn IndexReader>,
3608 is_legacy: bool,
3609 frag_reuse_index: Option<Arc<FragReuseIndex>>,
3610 ) -> Result<Self> {
3611 let batch = reader.read_range(0..reader.num_rows(), None).await?;
3612 let row_id_col = batch[ROW_ID].as_primitive::<datatypes::UInt64Type>();
3613 let num_tokens_col = batch[NUM_TOKEN_COL].as_primitive::<datatypes::UInt32Type>();
3614
3615 if is_legacy {
3617 let (row_ids, num_tokens): (Vec<_>, Vec<_>) = row_id_col
3618 .values()
3619 .iter()
3620 .filter_map(|id| {
3621 if let Some(frag_reuse_index_ref) = frag_reuse_index.as_ref() {
3622 frag_reuse_index_ref.remap_row_id(*id)
3623 } else {
3624 Some(*id)
3625 }
3626 })
3627 .zip(num_tokens_col.values().iter())
3628 .sorted_unstable_by_key(|x| x.0)
3629 .unzip();
3630
3631 let total_tokens = num_tokens.iter().map(|&x| x as u64).sum();
3632 return Ok(Self {
3633 row_ids,
3634 num_tokens,
3635 inv: Vec::new(),
3636 total_tokens,
3637 });
3638 }
3639
3640 if let Some(frag_reuse_index_ref) = frag_reuse_index.as_ref() {
3643 let mut row_ids = Vec::with_capacity(row_id_col.len());
3644 let mut num_tokens = Vec::with_capacity(num_tokens_col.len());
3645 for (row_id, num_token) in row_id_col.values().iter().zip(num_tokens_col.values()) {
3646 if let Some(new_row_id) = frag_reuse_index_ref.remap_row_id(*row_id) {
3647 row_ids.push(new_row_id);
3648 num_tokens.push(*num_token);
3649 }
3650 }
3651
3652 let mut inv: Vec<(u64, u32)> = row_ids
3653 .iter()
3654 .enumerate()
3655 .map(|(doc_id, row_id)| (*row_id, doc_id as u32))
3656 .collect();
3657 inv.sort_unstable_by_key(|entry| entry.0);
3658
3659 let total_tokens = num_tokens.iter().map(|&x| x as u64).sum();
3660 return Ok(Self {
3661 row_ids,
3662 num_tokens,
3663 inv,
3664 total_tokens,
3665 });
3666 }
3667
3668 let row_ids = row_id_col.values().to_vec();
3669 let num_tokens = num_tokens_col.values().to_vec();
3670 let mut inv: Vec<(u64, u32)> = row_ids
3671 .iter()
3672 .enumerate()
3673 .map(|(doc_id, row_id)| (*row_id, doc_id as u32))
3674 .collect();
3675 if !row_ids.is_sorted() {
3676 inv.sort_unstable_by_key(|entry| entry.0);
3677 }
3678 let total_tokens = num_tokens.iter().map(|&x| x as u64).sum();
3679 Ok(Self {
3680 row_ids,
3681 num_tokens,
3682 inv,
3683 total_tokens,
3684 })
3685 }
3686
3687 pub fn remap(&mut self, mapping: &HashMap<u64, Option<u64>>) -> Vec<u32> {
3690 let mut removed = Vec::new();
3691 let len = self.len();
3692 let row_ids = std::mem::replace(&mut self.row_ids, Vec::with_capacity(len));
3693 let num_tokens = std::mem::replace(&mut self.num_tokens, Vec::with_capacity(len));
3694 for (doc_id, (row_id, num_token)) in std::iter::zip(row_ids, num_tokens).enumerate() {
3695 match mapping.get(&row_id) {
3696 Some(Some(new_row_id)) => {
3697 self.row_ids.push(*new_row_id);
3698 self.num_tokens.push(num_token);
3699 }
3700 Some(None) => {
3701 removed.push(doc_id as u32);
3702 }
3703 None => {
3704 self.row_ids.push(row_id);
3705 self.num_tokens.push(num_token);
3706 }
3707 }
3708 }
3709 removed
3710 }
3711
3712 #[inline]
3713 pub fn num_tokens(&self, doc_id: u32) -> u32 {
3714 self.num_tokens[doc_id as usize]
3715 }
3716
3717 #[inline]
3720 pub fn num_tokens_by_row_id(&self, row_id: u64) -> u32 {
3721 self.row_ids
3722 .binary_search(&row_id)
3723 .map(|idx| self.num_tokens[idx])
3724 .unwrap_or(0)
3725 }
3726
3727 pub fn append(&mut self, row_id: u64, num_tokens: u32) -> u32 {
3730 self.row_ids.push(row_id);
3731 self.num_tokens.push(num_tokens);
3732 self.total_tokens += num_tokens as u64;
3733 self.row_ids.len() as u32 - 1
3734 }
3735
3736 pub(crate) fn memory_size(&self) -> usize {
3737 self.row_ids.capacity() * std::mem::size_of::<u64>()
3738 + self.num_tokens.capacity() * std::mem::size_of::<u32>()
3739 + self.inv.capacity() * std::mem::size_of::<(u64, u32)>()
3740 }
3741}
3742
3743pub fn flat_full_text_search(
3744 batches: &[&RecordBatch],
3745 doc_col: &str,
3746 query: &str,
3747 tokenizer: Option<Box<dyn LanceTokenizer>>,
3748) -> Result<Vec<u64>> {
3749 if batches.is_empty() {
3750 return Ok(vec![]);
3751 }
3752
3753 if is_phrase_query(query) {
3754 return Err(Error::invalid_input(
3755 "phrase query is not supported for flat full text search, try using FTS index",
3756 ));
3757 }
3758
3759 match batches[0][doc_col].data_type() {
3760 DataType::Utf8 => do_flat_full_text_search::<i32>(batches, doc_col, query, tokenizer),
3761 DataType::LargeUtf8 => do_flat_full_text_search::<i64>(batches, doc_col, query, tokenizer),
3762 data_type => Err(Error::invalid_input(format!(
3763 "unsupported data type {} for inverted index",
3764 data_type
3765 ))),
3766 }
3767}
3768
3769fn do_flat_full_text_search<Offset: OffsetSizeTrait>(
3770 batches: &[&RecordBatch],
3771 doc_col: &str,
3772 query: &str,
3773 tokenizer: Option<Box<dyn LanceTokenizer>>,
3774) -> Result<Vec<u64>> {
3775 let mut results = Vec::new();
3776 let mut tokenizer =
3777 tokenizer.unwrap_or_else(|| InvertedIndexParams::default().build().unwrap());
3778 let query_tokens = collect_query_tokens(query, &mut tokenizer);
3779
3780 for batch in batches {
3781 let row_id_array = batch[ROW_ID].as_primitive::<UInt64Type>();
3782 let doc_array = batch[doc_col].as_string::<Offset>();
3783 for i in 0..row_id_array.len() {
3784 let doc = doc_array.value(i);
3785 if has_query_token(doc, &mut tokenizer, &query_tokens) {
3786 results.push(row_id_array.value(i));
3787 assert!(doc.contains(query));
3790 }
3791 }
3792 }
3793
3794 Ok(results)
3795}
3796
3797const FLAT_ROW_ID_COL_IDX: usize = 0;
3798const FLAT_ALL_TOKENS_COL_IDX: usize = 1;
3799const FLAT_QUERY_TOKEN_COUNTS_COL_IDX: usize = 2;
3800
3801const BYTES_ACCUMULATED_WARNING_THRESHOLD: u64 = 1024 * 1024 * 1024; async fn tokenize_and_count(
3818 input: impl Stream<Item = DataFusionResult<RecordBatch>> + Send,
3819 tokenizer: Box<dyn LanceTokenizer>,
3820 query_tokens: Arc<Tokens>,
3821 doc_col_idx: usize,
3822) -> DataFusionResult<RecordBatch> {
3823 let output_schema = Arc::new(Schema::new(vec![
3824 ROW_ID_FIELD.clone(),
3825 Field::new("all_tokens", DataType::UInt64, false),
3826 Field::new(
3827 "query_token_counts",
3828 DataType::FixedSizeList(
3829 Arc::new(Field::new("item", DataType::UInt64, true)),
3830 query_tokens.len() as i32,
3831 ),
3832 false,
3833 ),
3834 ]));
3835 let output_schema_clone = output_schema.clone();
3836 let bytes_accumulated = Arc::new(AtomicU64::new(0));
3837 let bytes_warning_emitted = Arc::new(AtomicBool::new(false));
3838
3839 let batches = input
3840 .map(move |batch| {
3841 let mut tokenizer = tokenizer.box_clone();
3842 let output_schema = output_schema.clone();
3843 let query_tokens = query_tokens.clone();
3844 let bytes_accumulated = bytes_accumulated.clone();
3845 let bytes_warning_emitted = bytes_warning_emitted.clone();
3846 spawn_cpu(move || {
3847 let batch = batch?;
3848 let mut all_token_counts = UInt64Builder::with_capacity(batch.num_rows());
3849 let mut query_token_counts = FixedSizeListBuilder::with_capacity(
3850 UInt64Builder::with_capacity(batch.num_rows() * query_tokens.len()),
3851 query_tokens.len() as i32,
3852 batch.num_rows(),
3853 );
3854 let mut temp_query_token_counts = Vec::with_capacity(query_tokens.len());
3855 let doc_iter = iter_str_array(batch.column(doc_col_idx));
3856 for doc in doc_iter {
3857 let Some(doc) = doc else {
3858 all_token_counts.append_value(0);
3859 query_token_counts
3860 .values()
3861 .append_value_n(0, query_tokens.len());
3862 query_token_counts.append(true);
3863 continue;
3864 };
3865
3866 temp_query_token_counts.clear();
3867 temp_query_token_counts.extend(std::iter::repeat_n(0, query_tokens.len()));
3868
3869 let mut stream = tokenizer.token_stream_for_doc(doc);
3870 let mut all_tokens = 0;
3871 while let Some(token) = stream.next() {
3872 all_tokens += 1;
3873 if let Some(token_index) = query_tokens.token_index(&token.text) {
3874 temp_query_token_counts[token_index] += 1;
3875 }
3876 }
3877 all_token_counts.append_value(all_tokens);
3878 for count in temp_query_token_counts.iter().copied() {
3879 query_token_counts.values().append_value(count);
3880 }
3881 query_token_counts.append(true);
3882 }
3883 let row_ids = batch[ROW_ID].clone();
3884 let all_token_counts = all_token_counts.finish();
3885 let query_token_counts = query_token_counts.finish();
3886 let result_batch = RecordBatch::try_new(
3887
3888 output_schema,
3889 vec![
3890 row_ids,
3891 Arc::new(all_token_counts) as ArrayRef,
3892 Arc::new(query_token_counts) as ArrayRef,
3893 ],
3894 )?;
3895 let bytes_accumulated = bytes_accumulated.fetch_add(result_batch.get_array_memory_size() as u64, Ordering::Relaxed);
3896 if bytes_accumulated > BYTES_ACCUMULATED_WARNING_THRESHOLD && !bytes_warning_emitted.swap(true, Ordering::Relaxed) {
3897 tracing::warn!("Flat full text search is accumulating a large number of bytes. Consider using an FTS index instead.");
3898 }
3899
3900 DataFusionResult::Ok(result_batch)
3901 })
3902 })
3903 .buffered(get_num_compute_intensive_cpus())
3904 .try_collect::<Vec<_>>()
3905 .await?;
3906
3907 Ok(arrow::compute::concat_batches(
3908 &output_schema_clone,
3909 &batches,
3910 )?)
3911}
3912
3913fn initialize_scorer(
3918 index: &Option<InvertedIndex>,
3919 query_tokens: &Tokens,
3920 counted_input: &RecordBatch,
3921) -> MemBM25Scorer {
3922 let mut total_tokens = 0;
3923 let mut num_docs = 0;
3924 let mut all_token_counts = vec![0; query_tokens.len()];
3925
3926 if let Some(index) = index {
3927 let index_bm25_scorer = IndexBM25Scorer::new(index.partitions.iter().map(|p| p.as_ref()));
3928 for (token_index, token) in query_tokens.into_iter().enumerate() {
3929 let token_nq = index_bm25_scorer.num_docs_containing_token(token);
3930 all_token_counts[token_index] = token_nq as u64;
3931 }
3932 total_tokens += index_bm25_scorer.total_tokens();
3933 num_docs += index_bm25_scorer.num_docs();
3934 }
3935
3936 num_docs += counted_input.num_rows();
3937 total_tokens += arrow::compute::sum(
3938 counted_input
3939 .column(FLAT_ALL_TOKENS_COL_IDX)
3940 .as_primitive::<UInt64Type>(),
3941 )
3942 .unwrap_or_default();
3943
3944 let mut input_token_counters = counted_input
3945 .column(FLAT_QUERY_TOKEN_COUNTS_COL_IDX)
3946 .as_fixed_size_list()
3947 .values()
3948 .as_primitive::<UInt64Type>()
3949 .values()
3950 .iter()
3951 .copied();
3952
3953 for _ in 0..counted_input.num_rows() {
3954 for token_count in all_token_counts.iter_mut() {
3955 *token_count += input_token_counters.next().unwrap_or_default();
3956 }
3957 }
3958
3959 let token_counts_map = all_token_counts
3960 .into_iter()
3961 .enumerate()
3962 .map(|(token_index, count)| {
3963 (
3964 query_tokens.get_token(token_index).to_string(),
3965 count as usize,
3966 )
3967 })
3968 .collect::<HashMap<String, usize>>();
3969 MemBM25Scorer::new(total_tokens, num_docs, token_counts_map)
3970}
3971
3972fn flat_bm25_score(
3973 query_tokens: &Tokens,
3974 counted_input: &RecordBatch,
3975 scorer: &MemBM25Scorer,
3976) -> Result<RecordBatch> {
3977 let mut row_ids_builder = UInt64Builder::with_capacity(counted_input.num_rows());
3978 let mut scores_builder = Float32Builder::with_capacity(counted_input.num_rows());
3979
3980 let mut row_ids_iter = counted_input
3981 .column(FLAT_ROW_ID_COL_IDX)
3982 .as_primitive::<UInt64Type>()
3983 .values()
3984 .iter()
3985 .copied();
3986 let mut all_token_counts_iter = counted_input
3987 .column(FLAT_ALL_TOKENS_COL_IDX)
3988 .as_primitive::<UInt64Type>()
3989 .values()
3990 .iter()
3991 .copied();
3992 let mut query_token_counts_iter = counted_input
3993 .column(FLAT_QUERY_TOKEN_COUNTS_COL_IDX)
3994 .as_fixed_size_list()
3995 .values()
3996 .as_primitive::<UInt64Type>()
3997 .values()
3998 .iter()
3999 .copied();
4000 for _ in 0..counted_input.num_rows() {
4001 let num_tokens_in_doc = all_token_counts_iter.next().expect_ok()?;
4002 let row_id = row_ids_iter.next().expect_ok()?;
4003 if num_tokens_in_doc == 0 {
4004 for _ in query_tokens {
4005 query_token_counts_iter.next().expect_ok()?;
4006 }
4007 continue;
4008 }
4009 let doc_norm = K1 * (1.0 - B + B * num_tokens_in_doc as f32 / scorer.avg_doc_length());
4010 let mut score = 0.0;
4011 for token in query_tokens {
4012 let freq = query_token_counts_iter.next().expect_ok()? as f32;
4013 let idf = idf(scorer.num_docs_containing_token(token), scorer.num_docs());
4014 score += idf * (freq * (K1 + 1.0) / (freq + doc_norm));
4015 }
4016 if score > 0.0 {
4017 row_ids_builder.append_value(row_id);
4018 scores_builder.append_value(score);
4019 }
4020 }
4021
4022 let row_ids = row_ids_builder.finish();
4023 let scores = scores_builder.finish();
4024 let batch = RecordBatch::try_new(
4025 FTS_SCHEMA.clone(),
4026 vec![Arc::new(row_ids) as ArrayRef, Arc::new(scores) as ArrayRef],
4027 )?;
4028 Ok(batch)
4029}
4030
4031pub async fn flat_bm25_search_stream(
4032 input: SendableRecordBatchStream,
4033 doc_col: String,
4034 query: String,
4035 index: &Option<InvertedIndex>,
4036 target_batch_size: usize,
4037) -> DataFusionResult<SendableRecordBatchStream> {
4038 let mut tokenizer = match index {
4039 Some(index) => index.tokenizer(),
4040 None => Box::new(TextTokenizer::new(
4041 tantivy::tokenizer::TextAnalyzer::builder(
4042 tantivy::tokenizer::SimpleTokenizer::default(),
4043 )
4044 .build(),
4045 )),
4046 };
4047 let query_tokens = Arc::new(collect_query_tokens(&query, &mut tokenizer));
4048
4049 let input_schema = input.schema();
4050 let doc_col_idx = input_schema.index_of(&doc_col)?;
4051
4052 const ACCUMULATE_BYTES: usize = 256 * 1024;
4054 const SLICE_BYTES: usize = 512 * 1024;
4056
4057 let chunked = lance_arrow::stream::rechunk_stream_by_size(
4060 input,
4061 input_schema,
4062 ACCUMULATE_BYTES,
4063 SLICE_BYTES,
4064 );
4065
4066 let counted_input =
4071 tokenize_and_count(chunked, tokenizer, query_tokens.clone(), doc_col_idx).await?;
4072
4073 let scorer = initialize_scorer(index, query_tokens.as_ref(), &counted_input);
4075 let scores = flat_bm25_score(query_tokens.as_ref(), &counted_input, &scorer)?;
4076
4077 let num_out_batches = scores.num_rows().div_ceil(target_batch_size);
4079 let mut batches = Vec::with_capacity(num_out_batches);
4080 for i in 0..num_out_batches {
4081 let start = i * target_batch_size;
4082 let len = (scores.num_rows() - start).min(target_batch_size);
4083 batches.push(Ok(scores.slice(start, len)));
4084 }
4085 Ok(Box::pin(RecordBatchStreamAdapter::new(
4086 FTS_SCHEMA.clone(),
4087 stream::iter(batches),
4088 )))
4089}
4090
4091pub fn is_phrase_query(query: &str) -> bool {
4092 query.starts_with('\"') && query.ends_with('\"')
4093}
4094
4095#[cfg(test)]
4096mod tests {
4097 use crate::scalar::inverted::lance_tokenizer::DocType;
4098 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
4099 use futures::stream;
4100 use lance_core::cache::LanceCache;
4101 use lance_core::utils::tempfile::TempObjDir;
4102 use lance_io::object_store::ObjectStore;
4103
4104 use crate::metrics::NoOpMetricsCollector;
4105 use crate::prefilter::NoFilter;
4106 use crate::scalar::ScalarIndex;
4107 use crate::scalar::inverted::builder::{InnerBuilder, PositionRecorder, inverted_list_schema};
4108 use crate::scalar::inverted::encoding::{
4109 compress_positions, compress_posting_list_with_tail_codec,
4110 decompress_posting_list_with_tail_codec, encode_position_stream_block_into,
4111 };
4112 use crate::scalar::inverted::query::{FtsSearchParams, Operator};
4113 use crate::scalar::lance_format::LanceIndexStore;
4114 use arrow::array::{AsArray, LargeBinaryBuilder, ListBuilder, UInt32Builder};
4115 use arrow::datatypes::{Float32Type, UInt32Type};
4116 use arrow_array::{ArrayRef, Float32Array, RecordBatch, StringArray, UInt32Array, UInt64Array};
4117 use arrow_schema::{DataType, Field, Schema};
4118 use std::collections::HashMap;
4119 use std::sync::Arc;
4120
4121 use super::*;
4122
4123 #[tokio::test]
4124 async fn test_posting_builder_remap() {
4125 let posting_tail_codec = PostingTailCodec::Fixed32;
4126 let mut builder =
4127 PostingListBuilder::new_with_posting_tail_codec(false, posting_tail_codec);
4128 let n = BLOCK_SIZE + 3;
4129 for i in 0..n {
4130 builder.add(i as u32, PositionRecorder::Count(1));
4131 }
4132 let removed = vec![5, 7];
4133 builder.remap(&removed);
4134
4135 let mut expected =
4136 PostingListBuilder::new_with_posting_tail_codec(false, posting_tail_codec);
4137 for i in 0..n - removed.len() {
4138 expected.add(i as u32, PositionRecorder::Count(1));
4139 }
4140 let expected_entries = expected.iter().collect::<Vec<_>>();
4141 let actual_entries = builder.iter().collect::<Vec<_>>();
4142 assert_eq!(actual_entries, expected_entries);
4143
4144 let batch = builder.to_batch(vec![1.0, 2.0]).unwrap();
4147 let (doc_ids, freqs) = decompress_posting_list_with_tail_codec(
4148 (n - removed.len()) as u32,
4149 batch[POSTING_COL]
4150 .as_list::<i32>()
4151 .value(0)
4152 .as_binary::<i64>(),
4153 posting_tail_codec,
4154 )
4155 .unwrap();
4156 assert!(
4157 doc_ids
4158 .iter()
4159 .zip(expected_entries.iter().map(|(doc_id, _, _)| doc_id))
4160 .all(|(a, b)| a == b)
4161 );
4162 assert!(
4163 freqs
4164 .iter()
4165 .zip(expected_entries.iter().map(|(_, freq, _)| freq))
4166 .all(|(a, b)| a == b)
4167 );
4168 }
4169
4170 #[test]
4171 fn test_posting_builder_size_tracking_matches_structure() {
4172 fn tracked_memory_size(builder: &PostingListBuilder) -> u64 {
4173 let encoded_blocks_size = builder
4174 .encoded_blocks
4175 .iter()
4176 .map(|encoded_blocks| std::mem::size_of::<EncodedBlocks>() + encoded_blocks.size())
4177 .sum::<usize>();
4178 let encoded_positions_size = builder
4179 .encoded_position_blocks
4180 .as_ref()
4181 .map(|positions| std::mem::size_of::<EncodedPositionBlocks>() + positions.size())
4182 .unwrap_or(0usize);
4183 (encoded_blocks_size
4184 + builder.tail_entries.capacity() * std::mem::size_of::<RawDocInfo>()
4185 + builder.tail_positions.size()
4186 + encoded_positions_size) as u64
4187 }
4188
4189 let mut builder = PostingListBuilder::new(true);
4190 for doc_id in 0..(BLOCK_SIZE + 5) as u32 {
4191 builder.add(
4192 doc_id,
4193 PositionRecorder::Position(smallvec::smallvec![1, 3, 5]),
4194 );
4195 }
4196
4197 assert_eq!(builder.size(), tracked_memory_size(&builder));
4198 }
4199
4200 #[test]
4201 fn test_posting_builder_flush_releases_tail_position_capacity() {
4202 let mut builder = PostingListBuilder::new(true);
4203 let positions = smallvec::SmallVec::<[u32; 2]>::from_vec((0..1024).collect());
4204 for doc_id in 0..BLOCK_SIZE as u32 {
4205 builder.add(doc_id, PositionRecorder::Position(positions.clone()));
4206 }
4207
4208 assert_eq!(builder.tail_positions.size(), 0);
4209 assert_eq!(builder.size(), {
4210 let encoded_blocks_size = builder
4211 .encoded_blocks
4212 .iter()
4213 .map(|encoded_blocks| std::mem::size_of::<EncodedBlocks>() + encoded_blocks.size())
4214 .sum::<usize>();
4215 let encoded_positions_size = builder
4216 .encoded_position_blocks
4217 .as_ref()
4218 .map(|positions| std::mem::size_of::<EncodedPositionBlocks>() + positions.size())
4219 .unwrap_or(0usize);
4220 (encoded_blocks_size
4221 + builder.tail_entries.capacity() * std::mem::size_of::<RawDocInfo>()
4222 + builder.tail_positions.size()
4223 + encoded_positions_size) as u64
4224 });
4225 }
4226
4227 #[test]
4228 fn test_posting_builder_streamed_positions_roundtrip() {
4229 let mut builder = PostingListBuilder::new(true);
4230 assert!(builder.add_occurrence(0, 1).unwrap());
4231 assert!(!builder.add_occurrence(0, 4).unwrap());
4232 assert!(!builder.add_occurrence(0, 9).unwrap());
4233 builder.finish_open_doc(0).unwrap();
4234
4235 assert!(builder.add_occurrence(2, 3).unwrap());
4236 builder.finish_open_doc(2).unwrap();
4237
4238 let entries = builder.iter().collect::<Vec<_>>();
4239 assert_eq!(
4240 entries,
4241 vec![
4242 (0_u32, 3_u32, Some(vec![1_u32, 4_u32, 9_u32])),
4243 (2_u32, 1_u32, Some(vec![3_u32])),
4244 ]
4245 );
4246 }
4247
4248 #[test]
4249 fn test_posting_builder_roundtrip_shared_positions() {
4250 let entries = vec![
4251 (0_u32, vec![1_u32, 5]),
4252 (2, vec![0, 4, 9]),
4253 (4, vec![7]),
4254 (8, vec![3, 10]),
4255 (13, vec![2, 11, 30]),
4256 ];
4257 let mut builder =
4258 PostingListBuilder::new_with_posting_tail_codec(true, PostingTailCodec::VarintDelta);
4259 for (doc_id, positions) in &entries {
4260 builder.add(
4261 *doc_id,
4262 PositionRecorder::Position(positions.clone().into()),
4263 );
4264 }
4265
4266 let batch = builder.to_batch(vec![1.0]).unwrap();
4267 assert!(batch.column_by_name(COMPRESSED_POSITION_COL).is_some());
4268 assert!(batch.column_by_name(POSITION_COL).is_none());
4269 assert_eq!(
4270 batch.schema_ref().metadata().get(POSTING_TAIL_CODEC_KEY),
4271 Some(&PostingTailCodec::VarintDelta.as_str().to_owned())
4272 );
4273 assert_eq!(
4274 batch.schema_ref().metadata().get(POSITIONS_LAYOUT_KEY),
4275 Some(&POSITIONS_LAYOUT_SHARED_STREAM_V2.to_owned())
4276 );
4277 assert_eq!(
4278 batch.schema_ref().metadata().get(POSITIONS_CODEC_KEY),
4279 Some(&PositionStreamCodec::PackedDelta.as_str().to_owned())
4280 );
4281
4282 let posting =
4283 PostingList::from_batch(&batch, Some(1.0), Some(entries.len() as u32)).unwrap();
4284 let actual = posting
4285 .iter()
4286 .map(|(doc_id, freq, positions)| {
4287 (doc_id as u32, freq, positions.unwrap().collect::<Vec<_>>())
4288 })
4289 .collect::<Vec<_>>();
4290 let expected = entries
4291 .iter()
4292 .map(|(doc_id, positions)| (*doc_id, positions.len() as u32, positions.clone()))
4293 .collect::<Vec<_>>();
4294 assert_eq!(actual, expected);
4295 }
4296
4297 #[test]
4298 fn test_posting_builder_roundtrip_legacy_positions() {
4299 let entries = vec![(0_u32, vec![1_u32, 5]), (2, vec![0, 4, 9]), (4, vec![7])];
4300 let mut builder =
4301 PostingListBuilder::new_with_posting_tail_codec(true, PostingTailCodec::Fixed32);
4302 for (doc_id, positions) in &entries {
4303 builder.add(
4304 *doc_id,
4305 PositionRecorder::Position(positions.clone().into()),
4306 );
4307 }
4308
4309 let batch = builder.to_batch(vec![1.0]).unwrap();
4310 assert!(batch.column_by_name(POSITION_COL).is_some());
4311 assert!(batch.column_by_name(COMPRESSED_POSITION_COL).is_none());
4312 assert_eq!(
4313 batch.schema_ref().metadata().get(POSTING_TAIL_CODEC_KEY),
4314 None
4315 );
4316 assert_eq!(
4317 batch.schema_ref().metadata().get(POSITIONS_LAYOUT_KEY),
4318 None
4319 );
4320 assert_eq!(batch.schema_ref().metadata().get(POSITIONS_CODEC_KEY), None);
4321
4322 let posting =
4323 PostingList::from_batch(&batch, Some(1.0), Some(entries.len() as u32)).unwrap();
4324 let actual = posting
4325 .iter()
4326 .map(|(doc_id, freq, positions)| {
4327 (doc_id as u32, freq, positions.unwrap().collect::<Vec<_>>())
4328 })
4329 .collect::<Vec<_>>();
4330 let expected = entries
4331 .iter()
4332 .map(|(doc_id, positions)| (*doc_id, positions.len() as u32, positions.clone()))
4333 .collect::<Vec<_>>();
4334 assert_eq!(actual, expected);
4335 }
4336
4337 #[test]
4338 fn test_resolve_fts_format_version_defaults_to_v1() {
4339 assert_eq!(
4340 resolve_fts_format_version(None).unwrap(),
4341 InvertedListFormatVersion::V1
4342 );
4343 assert_eq!(
4344 resolve_fts_format_version(Some("2")).unwrap(),
4345 InvertedListFormatVersion::V2
4346 );
4347 }
4348
4349 #[test]
4350 fn test_legacy_compressed_positions_still_readable() {
4351 let doc_ids = [1_u32, 3_u32];
4352 let frequencies = [2_u32, 3_u32];
4353 let posting = compress_posting_list_with_tail_codec(
4354 doc_ids.len(),
4355 doc_ids.iter(),
4356 frequencies.iter(),
4357 std::iter::once(1.0_f32),
4358 PostingTailCodec::Fixed32,
4359 )
4360 .unwrap();
4361
4362 let mut posting_builder = ListBuilder::new(LargeBinaryBuilder::new());
4363 for idx in 0..posting.len() {
4364 posting_builder.values().append_value(posting.value(idx));
4365 }
4366 posting_builder.append(true);
4367
4368 let mut positions_builder = ListBuilder::new(ListBuilder::new(LargeBinaryBuilder::new()));
4369 for positions in [vec![1_u32, 5_u32], vec![0_u32, 4_u32, 9_u32]] {
4370 let compressed = compress_positions(&positions).unwrap();
4371 let doc_builder = positions_builder.values();
4372 for idx in 0..compressed.len() {
4373 doc_builder.values().append_value(compressed.value(idx));
4374 }
4375 doc_builder.append(true);
4376 }
4377 positions_builder.append(true);
4378
4379 let schema = Arc::new(Schema::new(vec![
4380 Field::new(
4381 POSTING_COL,
4382 DataType::List(Arc::new(Field::new("item", DataType::LargeBinary, true))),
4383 false,
4384 ),
4385 Field::new(MAX_SCORE_COL, DataType::Float32, false),
4386 Field::new(LENGTH_COL, DataType::UInt32, false),
4387 Field::new(
4388 POSITION_COL,
4389 DataType::List(Arc::new(Field::new(
4390 "item",
4391 DataType::List(Arc::new(Field::new("item", DataType::LargeBinary, true))),
4392 true,
4393 ))),
4394 false,
4395 ),
4396 ]));
4397 let batch = RecordBatch::try_new(
4398 schema,
4399 vec![
4400 Arc::new(posting_builder.finish()) as ArrayRef,
4401 Arc::new(Float32Array::from(vec![1.0])) as ArrayRef,
4402 Arc::new(UInt32Array::from(vec![doc_ids.len() as u32])) as ArrayRef,
4403 Arc::new(positions_builder.finish()) as ArrayRef,
4404 ],
4405 )
4406 .unwrap();
4407
4408 let posting =
4409 PostingList::from_batch(&batch, Some(1.0), Some(doc_ids.len() as u32)).unwrap();
4410 let actual = posting
4411 .iter()
4412 .map(|(doc_id, freq, positions)| {
4413 (doc_id as u32, freq, positions.unwrap().collect::<Vec<_>>())
4414 })
4415 .collect::<Vec<_>>();
4416 assert_eq!(actual, vec![(1, 2, vec![1, 5]), (3, 3, vec![0, 4, 9]),]);
4417 }
4418
4419 #[test]
4420 fn test_shared_stream_v2_without_codec_still_readable() {
4421 let doc_ids = [1_u32, 3_u32];
4422 let frequencies = [2_u32, 3_u32];
4423 let posting = compress_posting_list_with_tail_codec(
4424 doc_ids.len(),
4425 doc_ids.iter(),
4426 frequencies.iter(),
4427 std::iter::once(1.0_f32),
4428 PostingTailCodec::Fixed32,
4429 )
4430 .unwrap();
4431
4432 let mut posting_builder = ListBuilder::new(LargeBinaryBuilder::new());
4433 for idx in 0..posting.len() {
4434 posting_builder.values().append_value(posting.value(idx));
4435 }
4436 posting_builder.append(true);
4437
4438 let positions = vec![1_u32, 5_u32, 0_u32, 4_u32, 9_u32];
4439 let mut encoded_positions = Vec::new();
4440 encode_position_stream_block_into(
4441 &positions,
4442 &frequencies,
4443 PositionStreamCodec::VarintDocDelta,
4444 &mut encoded_positions,
4445 )
4446 .unwrap();
4447
4448 let mut position_offsets = ListBuilder::new(UInt32Builder::new());
4449 position_offsets.values().append_value(0);
4450 position_offsets.append(true);
4451
4452 let schema = Arc::new(Schema::new_with_metadata(
4453 vec![
4454 Field::new(
4455 POSTING_COL,
4456 DataType::List(Arc::new(Field::new("item", DataType::LargeBinary, true))),
4457 false,
4458 ),
4459 Field::new(MAX_SCORE_COL, DataType::Float32, false),
4460 Field::new(LENGTH_COL, DataType::UInt32, false),
4461 Field::new(COMPRESSED_POSITION_COL, DataType::LargeBinary, false),
4462 Field::new(
4463 POSITION_BLOCK_OFFSET_COL,
4464 DataType::List(Arc::new(Field::new("item", DataType::UInt32, true))),
4465 false,
4466 ),
4467 ],
4468 HashMap::from([(
4469 POSITIONS_LAYOUT_KEY.to_owned(),
4470 POSITIONS_LAYOUT_SHARED_STREAM_V2.to_owned(),
4471 )]),
4472 ));
4473 let batch = RecordBatch::try_new(
4474 schema,
4475 vec![
4476 Arc::new(posting_builder.finish()) as ArrayRef,
4477 Arc::new(Float32Array::from(vec![1.0])) as ArrayRef,
4478 Arc::new(UInt32Array::from(vec![doc_ids.len() as u32])) as ArrayRef,
4479 Arc::new(arrow_array::LargeBinaryArray::from(vec![Some(
4480 encoded_positions.as_slice(),
4481 )])) as ArrayRef,
4482 Arc::new(position_offsets.finish()) as ArrayRef,
4483 ],
4484 )
4485 .unwrap();
4486
4487 let posting =
4488 PostingList::from_batch(&batch, Some(1.0), Some(doc_ids.len() as u32)).unwrap();
4489 let actual = posting
4490 .iter()
4491 .map(|(doc_id, freq, positions)| {
4492 (doc_id as u32, freq, positions.unwrap().collect::<Vec<_>>())
4493 })
4494 .collect::<Vec<_>>();
4495 assert_eq!(actual, vec![(1, 2, vec![1, 5]), (3, 3, vec![0, 4, 9]),]);
4496 }
4497
4498 #[test]
4499 fn test_shared_position_stream_is_smaller_for_sparse_positions() {
4500 let mut builder =
4501 PostingListBuilder::new_with_posting_tail_codec(true, PostingTailCodec::VarintDelta);
4502 let mut legacy_positions = Vec::with_capacity(BLOCK_SIZE * 4);
4503 for doc_id in 0..(BLOCK_SIZE * 4) as u32 {
4504 let mut positions = vec![doc_id * 3 + 1];
4505 if doc_id % 8 == 0 {
4506 positions.push(doc_id * 3 + 2);
4507 }
4508 builder.add(doc_id, PositionRecorder::Position(positions.clone().into()));
4509 legacy_positions.push(positions);
4510 }
4511
4512 let batch = builder.to_batch(vec![1.0; 4]).unwrap();
4513 let shared_positions_size = batch[COMPRESSED_POSITION_COL].get_buffer_memory_size()
4514 + batch[POSITION_BLOCK_OFFSET_COL].get_buffer_memory_size();
4515
4516 let mut positions_builder = ListBuilder::new(ListBuilder::new(LargeBinaryBuilder::new()));
4517 for positions in legacy_positions {
4518 let compressed = compress_positions(&positions).unwrap();
4519 let doc_builder = positions_builder.values();
4520 for idx in 0..compressed.len() {
4521 doc_builder.values().append_value(compressed.value(idx));
4522 }
4523 doc_builder.append(true);
4524 }
4525 positions_builder.append(true);
4526 let legacy_positions_size = positions_builder.finish().get_buffer_memory_size();
4527
4528 assert!(
4529 shared_positions_size < legacy_positions_size,
4530 "expected shared position stream to be smaller than legacy per-doc storage, shared={shared_positions_size}, legacy={legacy_positions_size}",
4531 );
4532 }
4533
4534 #[test]
4535 fn test_posting_list_batch_matches_docset_scoring() {
4536 let mut docs = DocSet::default();
4537 let num_docs = BLOCK_SIZE + 3;
4538 for doc_id in 0..num_docs as u32 {
4539 docs.append(doc_id as u64, doc_id % 7 + 1);
4540 }
4541
4542 let doc_ids = (0..num_docs as u32).collect::<Vec<_>>();
4543 let freqs = doc_ids
4544 .iter()
4545 .map(|doc_id| doc_id % 5 + 1)
4546 .collect::<Vec<_>>();
4547
4548 let mut builder_scores = PostingListBuilder::new(false);
4549 let mut builder_docs = PostingListBuilder::new(false);
4550 for (&doc_id, &freq) in doc_ids.iter().zip(freqs.iter()) {
4551 builder_scores.add(doc_id, PositionRecorder::Count(freq));
4552 builder_docs.add(doc_id, PositionRecorder::Count(freq));
4553 }
4554
4555 let block_max_scores = docs.calculate_block_max_scores(doc_ids.iter(), freqs.iter());
4556 let batch_scores = builder_scores.to_batch(block_max_scores).unwrap();
4557 let batch_docs = builder_docs
4558 .to_batch_with_docs(&docs, inverted_list_schema(false))
4559 .unwrap();
4560
4561 let scores_posting = batch_scores[POSTING_COL].as_list::<i32>().value(0);
4562 let scores_posting = scores_posting.as_binary::<i64>();
4563 let docs_posting = batch_docs[POSTING_COL].as_list::<i32>().value(0);
4564 let docs_posting = docs_posting.as_binary::<i64>();
4565 assert_eq!(scores_posting, docs_posting);
4566
4567 let score_left = batch_scores[MAX_SCORE_COL]
4568 .as_primitive::<Float32Type>()
4569 .value(0);
4570 let score_right = batch_docs[MAX_SCORE_COL]
4571 .as_primitive::<Float32Type>()
4572 .value(0);
4573 assert!((score_left - score_right).abs() < 1e-6);
4574
4575 let len_left = batch_scores[LENGTH_COL]
4576 .as_primitive::<UInt32Type>()
4577 .value(0);
4578 let len_right = batch_docs[LENGTH_COL].as_primitive::<UInt32Type>().value(0);
4579 assert_eq!(len_left, len_right);
4580 }
4581
4582 #[tokio::test]
4583 async fn test_remap_to_empty_posting_list() {
4584 let tmpdir = TempObjDir::default();
4585 let store = Arc::new(LanceIndexStore::new(
4586 ObjectStore::local().into(),
4587 tmpdir.clone(),
4588 Arc::new(LanceCache::no_cache()),
4589 ));
4590
4591 let mut builder = InnerBuilder::new(0, false, TokenSetFormat::default());
4592
4593 builder.tokens.add("lance".to_owned());
4598 builder.tokens.add("lake".to_owned());
4599 builder.posting_lists.push(PostingListBuilder::new(false));
4600 builder.posting_lists.push(PostingListBuilder::new(false));
4601 builder.posting_lists[0].add(0, PositionRecorder::Count(1));
4602 builder.posting_lists[1].add(1, PositionRecorder::Count(2));
4603 builder.posting_lists[1].add(2, PositionRecorder::Count(3));
4604 builder.docs.append(0, 1);
4605 builder.docs.append(1, 1);
4606 builder.docs.append(2, 1);
4607 builder.write(store.as_ref()).await.unwrap();
4608
4609 let index = InvertedPartition::load(
4610 store.clone(),
4611 0,
4612 None,
4613 &LanceCache::no_cache(),
4614 TokenSetFormat::default(),
4615 )
4616 .await
4617 .unwrap();
4618 let mut builder = index.into_builder().await.unwrap();
4619
4620 let mapping = HashMap::from([(0, None), (2, Some(3))]);
4621 builder.remap(&mapping).await.unwrap();
4622
4623 assert_eq!(builder.tokens.len(), 1);
4625 assert_eq!(builder.tokens.get("lake"), Some(0));
4626 assert_eq!(builder.posting_lists.len(), 1);
4627 assert_eq!(builder.posting_lists[0].len(), 2);
4628 assert_eq!(builder.docs.len(), 2);
4629 assert_eq!(builder.docs.row_id(0), 1);
4630 assert_eq!(builder.docs.row_id(1), 3);
4631
4632 builder.write(store.as_ref()).await.unwrap();
4633
4634 let mapping = HashMap::from([(1, None), (3, None)]);
4636 builder.remap(&mapping).await.unwrap();
4637
4638 assert_eq!(builder.tokens.len(), 0);
4639 assert_eq!(builder.posting_lists.len(), 0);
4640 assert_eq!(builder.docs.len(), 0);
4641
4642 builder.write(store.as_ref()).await.unwrap();
4643 }
4644
4645 #[tokio::test]
4646 async fn test_posting_cache_conflict_across_partitions() {
4647 let tmpdir = TempObjDir::default();
4648 let store = Arc::new(LanceIndexStore::new(
4649 ObjectStore::local().into(),
4650 tmpdir.clone(),
4651 Arc::new(LanceCache::no_cache()),
4652 ));
4653
4654 let mut builder1 = InnerBuilder::new(0, false, TokenSetFormat::default());
4656 builder1.tokens.add("test".to_owned());
4657 builder1.posting_lists.push(PostingListBuilder::new(false));
4658 builder1.posting_lists[0].add(0, PositionRecorder::Count(1));
4659 builder1.docs.append(100, 1); builder1.write(store.as_ref()).await.unwrap();
4661
4662 let mut builder2 = InnerBuilder::new(1, false, TokenSetFormat::default());
4664 builder2.tokens.add("test".to_owned()); builder2.posting_lists.push(PostingListBuilder::new(false));
4666 builder2.posting_lists[0].add(0, PositionRecorder::Count(2));
4667 builder2.posting_lists[0].add(1, PositionRecorder::Count(1));
4668 builder2.posting_lists[0].add(2, PositionRecorder::Count(3));
4669 builder2.posting_lists[0].add(3, PositionRecorder::Count(1));
4670 builder2.docs.append(200, 2); builder2.docs.append(201, 1); builder2.docs.append(202, 3); builder2.docs.append(203, 1); builder2.write(store.as_ref()).await.unwrap();
4675
4676 let metadata = std::collections::HashMap::from_iter(vec![
4678 (
4679 "partitions".to_owned(),
4680 serde_json::to_string(&vec![0u64, 1u64]).unwrap(),
4681 ),
4682 (
4683 "params".to_owned(),
4684 serde_json::to_string(&InvertedIndexParams::default()).unwrap(),
4685 ),
4686 (
4687 TOKEN_SET_FORMAT_KEY.to_owned(),
4688 TokenSetFormat::default().to_string(),
4689 ),
4690 ]);
4691 let mut writer = store
4692 .new_index_file(METADATA_FILE, Arc::new(arrow_schema::Schema::empty()))
4693 .await
4694 .unwrap();
4695 writer.finish_with_metadata(metadata).await.unwrap();
4696
4697 let cache = Arc::new(LanceCache::with_capacity(4096));
4699 let index = InvertedIndex::load(store.clone(), None, cache.as_ref())
4700 .await
4701 .unwrap();
4702
4703 assert_eq!(index.partitions.len(), 2);
4705 assert_eq!(index.partitions[0].tokens.len(), 1);
4706 assert_eq!(index.partitions[1].tokens.len(), 1);
4707
4708 if index.partitions[0].id() == 0 {
4713 assert_eq!(index.partitions[0].inverted_list.posting_len(0), 1);
4715 assert_eq!(index.partitions[1].inverted_list.posting_len(0), 4);
4716 assert_eq!(index.partitions[0].docs.len(), 1);
4717 assert_eq!(index.partitions[1].docs.len(), 4);
4718 } else {
4719 assert_eq!(index.partitions[0].inverted_list.posting_len(0), 4);
4721 assert_eq!(index.partitions[1].inverted_list.posting_len(0), 1);
4722 assert_eq!(index.partitions[0].docs.len(), 4);
4723 assert_eq!(index.partitions[1].docs.len(), 1);
4724 }
4725
4726 index.prewarm().await.unwrap();
4728
4729 let tokens = Arc::new(Tokens::new(vec!["test".to_string()], DocType::Text));
4730 let params = Arc::new(FtsSearchParams::new().with_limit(Some(10)));
4731 let prefilter = Arc::new(NoFilter);
4732 let metrics = Arc::new(NoOpMetricsCollector);
4733
4734 let (row_ids, scores) = index
4735 .bm25_search(tokens, params, Operator::Or, prefilter, metrics)
4736 .await
4737 .unwrap();
4738
4739 assert_eq!(row_ids.len(), 5, "row_ids: {:?}", row_ids);
4742 assert!(!row_ids.is_empty(), "Should find at least some documents");
4743 assert_eq!(row_ids.len(), scores.len());
4744
4745 for &score in &scores {
4747 assert!(score > 0.0, "All scores should be positive");
4748 }
4749
4750 assert!(
4752 row_ids.contains(&100),
4753 "Should contain row_id from partition 0"
4754 );
4755 assert!(
4756 row_ids.iter().any(|&id| id >= 200),
4757 "Should contain row_id from partition 1"
4758 );
4759 }
4760
4761 #[tokio::test]
4762 async fn test_modern_prewarm_shrinks_cached_posting_buffers() {
4763 let tmpdir = TempObjDir::default();
4764 let store = Arc::new(LanceIndexStore::new(
4765 ObjectStore::local().into(),
4766 tmpdir.clone(),
4767 Arc::new(LanceCache::no_cache()),
4768 ));
4769
4770 let mut builder = InnerBuilder::new(0, false, TokenSetFormat::default());
4771 builder.tokens.add("alpha".to_owned());
4772 builder.tokens.add("beta".to_owned());
4773 builder.posting_lists.push(PostingListBuilder::new(false));
4774 builder.posting_lists.push(PostingListBuilder::new(false));
4775 builder.posting_lists[0].add(0, PositionRecorder::Count(1));
4776 builder.posting_lists[0].add(1, PositionRecorder::Count(2));
4777 builder.posting_lists[1].add(2, PositionRecorder::Count(3));
4778 builder.posting_lists[1].add(3, PositionRecorder::Count(4));
4779 builder.docs.append(100, 1);
4780 builder.docs.append(101, 2);
4781 builder.docs.append(102, 3);
4782 builder.docs.append(103, 4);
4783 builder.write(store.as_ref()).await.unwrap();
4784
4785 let metadata = std::collections::HashMap::from_iter(vec![
4786 (
4787 "partitions".to_owned(),
4788 serde_json::to_string(&vec![0u64]).unwrap(),
4789 ),
4790 (
4791 "params".to_owned(),
4792 serde_json::to_string(&InvertedIndexParams::default()).unwrap(),
4793 ),
4794 (
4795 TOKEN_SET_FORMAT_KEY.to_owned(),
4796 TokenSetFormat::default().to_string(),
4797 ),
4798 ]);
4799 let mut writer = store
4800 .new_index_file(METADATA_FILE, Arc::new(arrow_schema::Schema::empty()))
4801 .await
4802 .unwrap();
4803 writer.finish_with_metadata(metadata).await.unwrap();
4804
4805 let cache = Arc::new(LanceCache::with_capacity(4096));
4806 let index = InvertedIndex::load(store.clone(), None, cache.as_ref())
4807 .await
4808 .unwrap();
4809 let inverted_list = &index.partitions[0].inverted_list;
4810 assert!(
4811 inverted_list.offsets.is_none(),
4812 "test should use modern posting layout"
4813 );
4814
4815 inverted_list.prewarm().await.unwrap();
4816
4817 let alpha = inverted_list
4818 .index_cache
4819 .get_with_key(&PostingListKey { token_id: 0 })
4820 .await
4821 .unwrap();
4822 let beta = inverted_list
4823 .index_cache
4824 .get_with_key(&PostingListKey { token_id: 1 })
4825 .await
4826 .unwrap();
4827
4828 let PostingList::Compressed(alpha) = alpha.as_ref() else {
4829 panic!("expected compressed posting list for token 0");
4830 };
4831 let PostingList::Compressed(beta) = beta.as_ref() else {
4832 panic!("expected compressed posting list for token 1");
4833 };
4834
4835 assert_ne!(
4836 alpha.blocks.values().as_ptr(),
4837 beta.blocks.values().as_ptr(),
4838 "prewarm should not leave cached posting lists sharing the same values buffer"
4839 );
4840 }
4841 #[test]
4842 fn test_block_max_scores_capacity_matches_block_count() {
4843 let mut docs = DocSet::default();
4844 let num_docs = BLOCK_SIZE * 3 + 7;
4845 let doc_ids = (0..num_docs as u32).collect::<Vec<_>>();
4846 for doc_id in &doc_ids {
4847 docs.append(*doc_id as u64, 1);
4848 }
4849
4850 let freqs = vec![1_u32; doc_ids.len()];
4851 let block_max_scores = docs.calculate_block_max_scores(doc_ids.iter(), freqs.iter());
4852 let expected_blocks = doc_ids.len().div_ceil(BLOCK_SIZE);
4853
4854 assert_eq!(block_max_scores.len(), expected_blocks);
4855 assert_eq!(block_max_scores.capacity(), expected_blocks);
4856 }
4857
4858 #[tokio::test]
4859 async fn test_bm25_search_uses_global_idf() {
4860 let tmpdir = TempObjDir::default();
4861 let store = Arc::new(LanceIndexStore::new(
4862 ObjectStore::local().into(),
4863 tmpdir.clone(),
4864 Arc::new(LanceCache::no_cache()),
4865 ));
4866
4867 let mut builder0 = InnerBuilder::new(0, false, TokenSetFormat::default());
4869 builder0.tokens.add("alpha".to_owned());
4870 builder0.tokens.add("beta".to_owned());
4871 builder0.posting_lists.push(PostingListBuilder::new(false));
4872 builder0.posting_lists.push(PostingListBuilder::new(false));
4873 builder0.posting_lists[0].add(0, PositionRecorder::Count(1));
4874 builder0.posting_lists[1].add(1, PositionRecorder::Count(1));
4875 builder0.posting_lists[1].add(2, PositionRecorder::Count(1));
4876 builder0.docs.append(100, 1);
4877 builder0.docs.append(101, 1);
4878 builder0.docs.append(102, 1);
4879 builder0.write(store.as_ref()).await.unwrap();
4880
4881 let mut builder1 = InnerBuilder::new(1, false, TokenSetFormat::default());
4883 builder1.tokens.add("alpha".to_owned());
4884 builder1.posting_lists.push(PostingListBuilder::new(false));
4885 builder1.posting_lists[0].add(0, PositionRecorder::Count(1));
4886 builder1.docs.append(200, 1);
4887 builder1.write(store.as_ref()).await.unwrap();
4888
4889 let metadata = std::collections::HashMap::from_iter(vec![
4890 (
4891 "partitions".to_owned(),
4892 serde_json::to_string(&vec![0u64, 1u64]).unwrap(),
4893 ),
4894 (
4895 "params".to_owned(),
4896 serde_json::to_string(&InvertedIndexParams::default()).unwrap(),
4897 ),
4898 (
4899 TOKEN_SET_FORMAT_KEY.to_owned(),
4900 TokenSetFormat::default().to_string(),
4901 ),
4902 ]);
4903 let mut writer = store
4904 .new_index_file(METADATA_FILE, Arc::new(arrow_schema::Schema::empty()))
4905 .await
4906 .unwrap();
4907 writer.finish_with_metadata(metadata).await.unwrap();
4908
4909 let cache = Arc::new(LanceCache::with_capacity(4096));
4910 let index = InvertedIndex::load(store.clone(), None, cache.as_ref())
4911 .await
4912 .unwrap();
4913
4914 let tokens = Arc::new(Tokens::new(vec!["alpha".to_string()], DocType::Text));
4915 let params = Arc::new(FtsSearchParams::new().with_limit(Some(10)));
4916 let prefilter = Arc::new(NoFilter);
4917 let metrics = Arc::new(NoOpMetricsCollector);
4918
4919 let (row_ids, scores) = index
4920 .bm25_search(tokens, params, Operator::Or, prefilter, metrics)
4921 .await
4922 .unwrap();
4923
4924 assert_eq!(row_ids.len(), 2);
4925 assert!(row_ids.contains(&100));
4926 assert!(row_ids.contains(&200));
4927 assert_eq!(row_ids.len(), scores.len());
4928
4929 let expected_idf = idf(2, 4);
4930 for score in scores {
4931 assert!(
4932 (score - expected_idf).abs() < 1e-6,
4933 "score: {}, expected: {}",
4934 score,
4935 expected_idf
4936 );
4937 }
4938 }
4939
4940 #[tokio::test]
4941 async fn test_phrase_query_reads_legacy_per_doc_positions() {
4942 let tmpdir = TempObjDir::default();
4943 let store = Arc::new(LanceIndexStore::new(
4944 ObjectStore::local().into(),
4945 tmpdir.clone(),
4946 Arc::new(LanceCache::no_cache()),
4947 ));
4948
4949 let mut builder = InnerBuilder::new_with_format_version(
4950 0,
4951 true,
4952 TokenSetFormat::default(),
4953 InvertedListFormatVersion::V1,
4954 );
4955 builder.tokens.add("hello".to_owned());
4956 builder.tokens.add("world".to_owned());
4957 builder
4958 .posting_lists
4959 .push(PostingListBuilder::new_with_posting_tail_codec(
4960 true,
4961 PostingTailCodec::Fixed32,
4962 ));
4963 builder
4964 .posting_lists
4965 .push(PostingListBuilder::new_with_posting_tail_codec(
4966 true,
4967 PostingTailCodec::Fixed32,
4968 ));
4969 builder.posting_lists[0].add(0, PositionRecorder::Position(vec![0].into()));
4970 builder.posting_lists[1].add(0, PositionRecorder::Position(vec![1].into()));
4971 builder.posting_lists[0].add(1, PositionRecorder::Position(vec![0].into()));
4972 builder.posting_lists[1].add(1, PositionRecorder::Position(vec![2].into()));
4973 builder.docs.append(100, 2);
4974 builder.docs.append(101, 2);
4975 builder.write(store.as_ref()).await.unwrap();
4976
4977 let metadata = std::collections::HashMap::from_iter(vec![
4978 (
4979 "partitions".to_owned(),
4980 serde_json::to_string(&vec![0_u64]).unwrap(),
4981 ),
4982 (
4983 "params".to_owned(),
4984 serde_json::to_string(&InvertedIndexParams::default().with_position(true)).unwrap(),
4985 ),
4986 (
4987 TOKEN_SET_FORMAT_KEY.to_owned(),
4988 TokenSetFormat::default().to_string(),
4989 ),
4990 ]);
4991 let mut writer = store
4992 .new_index_file(METADATA_FILE, Arc::new(arrow_schema::Schema::empty()))
4993 .await
4994 .unwrap();
4995 writer.finish_with_metadata(metadata).await.unwrap();
4996
4997 let cache = Arc::new(LanceCache::with_capacity(4096));
4998 let index = InvertedIndex::load(store.clone(), None, cache.as_ref())
4999 .await
5000 .unwrap();
5001
5002 let tokens = Arc::new(Tokens::new(
5003 vec!["hello".to_owned(), "world".to_owned()],
5004 DocType::Text,
5005 ));
5006 let params = Arc::new(
5007 FtsSearchParams::new()
5008 .with_limit(Some(10))
5009 .with_phrase_slop(Some(0)),
5010 );
5011 let prefilter = Arc::new(NoFilter);
5012 let metrics = Arc::new(NoOpMetricsCollector);
5013
5014 let (row_ids, _scores) = index
5015 .bm25_search(tokens, params, Operator::And, prefilter, metrics)
5016 .await
5017 .unwrap();
5018
5019 assert_eq!(row_ids, vec![100]);
5020 }
5021
5022 #[tokio::test]
5023 async fn test_update_preserves_loaded_v2_format_version() -> Result<()> {
5024 let src_dir = TempObjDir::default();
5025 let dest_dir = TempObjDir::default();
5026 let src_store = Arc::new(LanceIndexStore::new(
5027 ObjectStore::local().into(),
5028 src_dir.clone(),
5029 Arc::new(LanceCache::no_cache()),
5030 ));
5031 let dest_store = Arc::new(LanceIndexStore::new(
5032 ObjectStore::local().into(),
5033 dest_dir.clone(),
5034 Arc::new(LanceCache::no_cache()),
5035 ));
5036
5037 let format_version = InvertedListFormatVersion::V2;
5038 let posting_tail_codec = format_version.posting_tail_codec();
5039 let mut partition = InnerBuilder::new_with_format_version(
5040 0,
5041 false,
5042 TokenSetFormat::default(),
5043 format_version,
5044 );
5045 partition.tokens.add("hello".to_owned());
5046 let mut posting_list =
5047 PostingListBuilder::new_with_posting_tail_codec(false, posting_tail_codec);
5048 posting_list.add(0, PositionRecorder::Count(1));
5049 partition.posting_lists.push(posting_list);
5050 partition.docs.append(100, 1);
5051 partition.write(src_store.as_ref()).await?;
5052
5053 let metadata = HashMap::from([
5054 (
5055 "partitions".to_owned(),
5056 serde_json::to_string(&vec![0_u64]).unwrap(),
5057 ),
5058 (
5059 "params".to_owned(),
5060 serde_json::to_string(&InvertedIndexParams::default()).unwrap(),
5061 ),
5062 (
5063 TOKEN_SET_FORMAT_KEY.to_owned(),
5064 TokenSetFormat::default().to_string(),
5065 ),
5066 (
5067 POSTING_TAIL_CODEC_KEY.to_owned(),
5068 posting_tail_codec.as_str().to_owned(),
5069 ),
5070 ]);
5071 let mut writer = src_store
5072 .new_index_file(METADATA_FILE, Arc::new(arrow_schema::Schema::empty()))
5073 .await
5074 .unwrap();
5075 writer.finish_with_metadata(metadata).await.unwrap();
5076
5077 let index = InvertedIndex::load(src_store, None, &LanceCache::no_cache()).await?;
5078 assert_eq!(index.index_version(), format_version.index_version());
5079
5080 let schema = Arc::new(Schema::new(vec![
5081 Field::new("doc", DataType::Utf8, true),
5082 Field::new(ROW_ID, DataType::UInt64, false),
5083 ]));
5084 let docs = Arc::new(StringArray::from(vec![Some("hello again")]));
5085 let row_ids = Arc::new(UInt64Array::from(vec![101u64]));
5086 let batch = RecordBatch::try_new(schema.clone(), vec![docs, row_ids])?;
5087 let stream = RecordBatchStreamAdapter::new(schema, stream::iter(vec![Ok(batch)]));
5088 let created = index
5089 .update(Box::pin(stream), dest_store.as_ref(), None)
5090 .await?;
5091
5092 assert_eq!(created.index_version, format_version.index_version());
5093
5094 let updated = InvertedIndex::load(dest_store, None, &LanceCache::no_cache()).await?;
5095 assert_eq!(updated.index_version(), format_version.index_version());
5096 assert_eq!(updated.partitions.len(), 2);
5097 for partition in &updated.partitions {
5098 assert_eq!(
5099 partition.inverted_list.posting_tail_codec(),
5100 posting_tail_codec
5101 );
5102 }
5103
5104 Ok(())
5105 }
5106
5107 #[tokio::test]
5108 async fn test_modern_index_without_deleted_col_has_empty_bitmap() {
5109 let tmpdir = TempObjDir::default();
5113 let store = Arc::new(LanceIndexStore::new(
5114 ObjectStore::local().into(),
5115 tmpdir.clone(),
5116 Arc::new(LanceCache::no_cache()),
5117 ));
5118
5119 let mut builder = InnerBuilder::new(0, false, TokenSetFormat::default());
5120 builder.tokens.add("test".to_owned());
5121 builder.posting_lists.push(PostingListBuilder::new(false));
5122 builder.posting_lists[0].add(0, PositionRecorder::Count(1));
5123 builder.docs.append(100, 1);
5124 builder.write(store.as_ref()).await.unwrap();
5125
5126 let metadata = std::collections::HashMap::from_iter(vec![
5129 (
5130 "partitions".to_owned(),
5131 serde_json::to_string(&vec![0u64]).unwrap(),
5132 ),
5133 (
5134 "params".to_owned(),
5135 serde_json::to_string(&InvertedIndexParams::default()).unwrap(),
5136 ),
5137 (
5138 TOKEN_SET_FORMAT_KEY.to_owned(),
5139 TokenSetFormat::default().to_string(),
5140 ),
5141 ]);
5142 let mut writer = store
5143 .new_index_file(METADATA_FILE, Arc::new(arrow_schema::Schema::empty()))
5144 .await
5145 .unwrap();
5146 writer.finish_with_metadata(metadata).await.unwrap();
5147
5148 let index = InvertedIndex::load(store, None, &LanceCache::no_cache())
5149 .await
5150 .unwrap();
5151 assert!(
5152 index.deleted_fragments().is_empty(),
5153 "index without deleted_fragments column should have empty bitmap"
5154 );
5155 }
5156}