Skip to main content

lance_index/scalar/inverted/
index.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use 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
78// Version 0: Arrow TokenSetFormat (legacy)
79// Version 1: Fst TokenSetFormat with per-doc compressed positions
80// Version 2: Fst TokenSetFormat with shared posting-list position streams.
81pub 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
114// Just a heuristic when we need to pre-allocate memory for tokens
115pub 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    // Fragments which are contained in the index, but no longer in the dataset.
347    // These should be pruned at search time since we don't prune them at update time.
348    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            // for legacy format, we re-create the index in the new format
401            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 partitions that belong to the specified fragment
416                    // The mask contains fragment_id in high 32 bits, we check if partition's
417                    // fragment_id matches by comparing the masked result with the original mask
418                    .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    /// Returns the number of partitions in this inverted index.
445    pub fn partition_count(&self) -> usize {
446        self.partitions.len()
447    }
448
449    /// Returns the set of fragments which are contained in the index, but no longer in the dataset.
450    ///
451    /// Most other indices remove data from deleted fragments when the index updates (copy-on-write).
452    /// However, this would require an expensive copy of the FTS index.  Instead, we track the deleted
453    /// fragments and prune them at search time (merge-on-read).
454    pub fn deleted_fragments(&self) -> &RoaringBitmap {
455        &self.deleted_fragments
456    }
457
458    // search the documents that contain the query
459    // return the row ids of the documents sorted by bm25 score
460    // ref: https://en.wikipedia.org/wiki/Okapi_BM25
461    // we first calculate in-partition BM25 scores,
462    // then re-calculate the scores for the top k documents across all partitions
463    #[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        // for new index format, there is a metadata file and multiple partitions,
643        // each partition is a separate index containing tokens, inverted list and docs.
644        // for old index format, there is no metadata file, and it's just like a single partition
645
646        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                // Load deleted_fragments if present (optional for backward compatibility)
669                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                // old index format
718                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    /// Search docs match the input text.
786    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    // return the row ids of the documents that contain the query
812    #[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(&params)?;
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    // 0 for legacy format
902    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    /// Check if this partition belongs to the specified fragment.
912    ///
913    /// This method encapsulates the bit manipulation logic for fragment filtering
914    /// in distributed indexing scenarios.
915    ///
916    /// # Arguments
917    /// * `fragment_mask` - A mask with fragment_id in high 32 bits
918    ///
919    /// # Returns
920    /// * `true` if the partition belongs to the fragment, `false` otherwise
921    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    // search the documents that contain the query
999    // return the doc info and the doc length
1000    // ref: https://en.wikipedia.org/wiki/Okapi_BM25
1001    #[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                // if the token is not found, we can't do phrase query
1021                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 local_metrics = LocalMetricsCollector::default();
1071        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        // local_metrics.dump_into(metrics);
1075        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// at indexing, we use HashMap because we need it to be mutable,
1106// at searching, we use fst::Map because it's more efficient
1107#[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// TokenSet is a mapping from tokens to token ids
1142#[derive(Debug, Clone, Default, DeepSizeOf)]
1143pub struct TokenSet {
1144    // token -> token_id
1145    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        // add token if it doesn't exist
1363        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    // the `removed_token_ids` must be sorted
1397    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    // legacy format only
1450    offsets: Option<Vec<usize>>,
1451
1452    // from metadata for legacy format
1453    // from column for new format
1454    max_scores: Option<Vec<f32>>,
1455
1456    // new format only
1457    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    // for legacy format
1535    // returns the offsets and max scores
1536    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    // the number of posting lists
1554    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            // hit the cache and when the cache was populated, the positions column was not loaded
1668            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/// New type just to allow Positions implement DeepSizeOf so it can be put
1899/// in the cache.
1900#[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// Cache key implementations for type-safe cache access
1915#[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            // legacy format
2104            Self::Plain(posting) => {
2105                // convert the posting list to the new format:
2106                // 1. map row ids to doc ids
2107                // 2. sort the posting list by doc ids
2108                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>, // List of Int32
2159}
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    // each binary is a block of compressed data
2258    // that contains `BLOCK_SIZE` doc ids and then `BLOCK_SIZE` frequencies
2259    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// DocSet is a mapping from row ids to the number of tokens in the document
3504// It's used to sort the documents by the bm25 score
3505#[derive(Debug, Clone, Default, DeepSizeOf)]
3506pub struct DocSet {
3507    row_ids: Vec<u64>,
3508    num_tokens: Vec<u32>,
3509    // (row_id, doc_id) pairs sorted by row_id
3510    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            // in legacy format, the row id is doc id
3536            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        // for legacy format, the row id is doc id; sorting keeps binary search viable
3616        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 frag reuse happened, we'll need to remap the row_ids. And after row_ids been
3641        // remapped, we'll need resort to make sure binary_search works.
3642        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    // remap the row ids to the new row ids
3688    // returns the removed doc ids
3689    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    // this can be used only if it's a legacy format,
3718    // which store the sorted row ids so that we can use binary search
3719    #[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    // append a document to the doc set
3728    // returns the doc_id (the number of documents before appending)
3729    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                // What is this assertion for?  Why would doc contain query?  Don't we reach
3788                // here only if they share at least one token?  Why is it not debug_assert?
3789                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
3801/// If we accumulate this many bytes we warn the user they probably want to use an FTS index instead.
3802const BYTES_ACCUMULATED_WARNING_THRESHOLD: u64 = 1024 * 1024 * 1024; // 1GB
3803
3804/// Consumes a stream of record batches and produces token counts
3805///
3806/// The resulting batch will have three columns:
3807/// - row_id: the row id of the document
3808/// - all_tokens: the total number of tokens in the document
3809/// - query_token_counts: a fixed size list of the count of each query token in the document
3810///
3811/// This is an unbounded accumulation, however, for most queries, the per-row
3812/// growth will be fairly small.  As a result we can process millions of tokens
3813/// with fairly modest memory usage.
3814///
3815/// However, it is unwise to do a flat search across billions of rows.  An FTS
3816/// index should be created instead.
3817async 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
3913/// Initialize the BM25 scorer
3914///
3915/// In order to calculate BM25 scores we need to know token counts for the entire corpus.  We extract these from the
3916/// counted input of the flat search combined with any counts recorded for the indexed portion.
3917fn 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    // Accumulate small batches until this threshold before dispatching a task.
4053    const ACCUMULATE_BYTES: usize = 256 * 1024;
4054    // Slice oversized batches down to roughly this size.
4055    const SLICE_BYTES: usize = 512 * 1024;
4056
4057    // Phase 1 - rechunk the input stream into appropriately sized chunks.  Tokenization is
4058    // fairly CPU-intensive, and we don't need too much data to justify a new thread task.
4059    let chunked = lance_arrow::stream::rechunk_stream_by_size(
4060        input,
4061        input_schema,
4062        ACCUMULATE_BYTES,
4063        SLICE_BYTES,
4064    );
4065
4066    // Phase 2 - For each row we need to know the total number of tokens and the count of each
4067    // of the query tokens.  For example, if the query is "book" and the row is "the book shop"
4068    // and we are tokenizing with a whitespace tokenizer, we need to know that there are 3 tokens
4069    // and the token book appears once.
4070    let counted_input =
4071        tokenize_and_count(chunked, tokenizer, query_tokens.clone(), doc_col_idx).await?;
4072
4073    // Phase 3 - Calculate final scores (this is fairly cheap, probably don't need to parallelize)
4074    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    // Finally we emit batches according to the target batch size
4078    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        // BLOCK_SIZE + 3 elements should be reduced to BLOCK_SIZE + 1,
4145        // there are still 2 blocks.
4146        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        // index of docs:
4594        // 0: lance
4595        // 1: lake lake
4596        // 2: lake lake lake
4597        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        // after remap, the doc 0 is removed, and the doc 2 is updated to 3
4624        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        // remap to delete all docs
4635        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        // Create first partition with one token and posting list length 1
4655        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); // row_id=100, num_tokens=1
4660        builder1.write(store.as_ref()).await.unwrap();
4661
4662        // Create second partition with one token and posting list length 4
4663        let mut builder2 = InnerBuilder::new(1, false, TokenSetFormat::default());
4664        builder2.tokens.add("test".to_owned()); // Use same token to test cache prefix fix
4665        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); // row_id=200, num_tokens=2
4671        builder2.docs.append(201, 1); // row_id=201, num_tokens=1
4672        builder2.docs.append(202, 3); // row_id=202, num_tokens=3
4673        builder2.docs.append(203, 1); // row_id=203, num_tokens=1
4674        builder2.write(store.as_ref()).await.unwrap();
4675
4676        // Create metadata file with both partitions
4677        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        // Load the inverted index
4698        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        // Verify the index structure
4704        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        // Verify the partitions were loaded correctly
4709
4710        // Verify posting list lengths (note: partition order may differ from creation order)
4711        // Verify based on actual loading order
4712        if index.partitions[0].id() == 0 {
4713            // If partition[0] is ID=0, then it should have 1 document
4714            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            // If partition[0] is ID=1, then it should have 4 documents
4720            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        // Prewarm the inverted index (this loads posting lists into cache)
4727        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        // Verify that we got search results
4740        // Expected to find 5 documents: 1 from first partition, 4 from second partition
4741        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        // All scores should be positive since all documents contain the search token
4746        for &score in &scores {
4747            assert!(score > 0.0, "All scores should be positive");
4748        }
4749
4750        // Check that we got results from both partitions
4751        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        // Partition 0: 3 docs, only one contains "alpha".
4868        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        // Partition 1: 1 doc, contains "alpha".
4882        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        // An index created before the deleted_fragments feature was added
5110        // will have a metadata file with num_rows=0 (no record batch data).
5111        // The load path should gracefully handle this with an empty bitmap.
5112        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        // Write a metadata file WITHOUT the deleted_fragments column
5127        // (simulates an older index version)
5128        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}