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};
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::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::tracing::{IO_TYPE_LOAD_SCALAR_PART, TRACE_IO_EVENTS};
42use lance_core::{Error, ROW_ID, ROW_ID_FIELD, Result};
43use lance_core::{
44    container::list::ExpLinkedList,
45    utils::tokio::{get_num_compute_intensive_cpus, spawn_cpu},
46};
47use roaring::RoaringBitmap;
48use std::sync::LazyLock;
49use tokio::task::spawn_blocking;
50use tracing::{info, instrument};
51
52use super::{InvertedIndexBuilder, InvertedIndexParams, wand::*};
53use super::{
54    builder::{
55        BLOCK_SIZE, ScoredDoc, doc_file_path, inverted_list_schema, posting_file_path,
56        token_file_path,
57    },
58    iter::PlainPostingListIterator,
59    query::*,
60    scorer::{B, IndexBM25Scorer, K1, Scorer, idf},
61};
62use super::{
63    builder::{InnerBuilder, PositionRecorder},
64    encoding::{compress_posting_list, compress_posting_list_with_scores},
65    iter::CompressedPostingListIterator,
66};
67use super::{encoding::compress_positions, iter::PostingListIterator};
68use crate::Index;
69use crate::frag_reuse::FragReuseIndex;
70use crate::pbold;
71use crate::scalar::inverted::lance_tokenizer::TextTokenizer;
72use crate::scalar::inverted::scorer::MemBM25Scorer;
73use crate::scalar::inverted::tokenizer::lance_tokenizer::LanceTokenizer;
74use crate::scalar::{
75    AnyQuery, BuiltinIndexType, CreatedIndex, IndexReader, IndexStore, MetricsCollector,
76    ScalarIndex, ScalarIndexParams, SearchResult, TokenQuery, UpdateCriteria,
77};
78use crate::{prefilter::PreFilter, scalar::inverted::iter::take_fst_keys};
79use std::str::FromStr;
80
81// Version 0: Arrow TokenSetFormat (legacy)
82// Version 1: Fst TokenSetFormat (new default, incompatible clients < 0.38)
83pub const INVERTED_INDEX_VERSION: u32 = 1;
84pub const TOKENS_FILE: &str = "tokens.lance";
85pub const INVERT_LIST_FILE: &str = "invert.lance";
86pub const DOCS_FILE: &str = "docs.lance";
87pub const METADATA_FILE: &str = "metadata.lance";
88
89pub const TOKEN_COL: &str = "_token";
90pub const TOKEN_ID_COL: &str = "_token_id";
91pub const TOKEN_FST_BYTES_COL: &str = "_token_fst_bytes";
92pub const TOKEN_NEXT_ID_COL: &str = "_token_next_id";
93pub const TOKEN_TOTAL_LENGTH_COL: &str = "_token_total_length";
94pub const FREQUENCY_COL: &str = "_frequency";
95pub const POSITION_COL: &str = "_position";
96pub const COMPRESSED_POSITION_COL: &str = "_compressed_position";
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";
104
105// Just a heuristic when we need to pre-allocate memory for tokens
106pub const ESTIMATED_MAX_TOKENS_PER_ROW: usize = 4 * 1024;
107
108pub static SCORE_FIELD: LazyLock<Field> =
109    LazyLock::new(|| Field::new(SCORE_COL, DataType::Float32, true));
110pub static FTS_SCHEMA: LazyLock<SchemaRef> =
111    LazyLock::new(|| Arc::new(Schema::new(vec![ROW_ID_FIELD.clone(), SCORE_FIELD.clone()])));
112static ROW_ID_SCHEMA: LazyLock<SchemaRef> =
113    LazyLock::new(|| Arc::new(Schema::new(vec![ROW_ID_FIELD.clone()])));
114
115#[derive(Debug)]
116struct PartitionCandidates {
117    tokens_by_position: Vec<String>,
118    candidates: Vec<DocCandidate>,
119}
120
121impl PartitionCandidates {
122    fn empty() -> Self {
123        Self {
124            tokens_by_position: Vec::new(),
125            candidates: Vec::new(),
126        }
127    }
128}
129
130#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default)]
131pub enum TokenSetFormat {
132    Arrow,
133    #[default]
134    Fst,
135}
136
137impl Display for TokenSetFormat {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        match self {
140            Self::Arrow => f.write_str("arrow"),
141            Self::Fst => f.write_str("fst"),
142        }
143    }
144}
145
146impl FromStr for TokenSetFormat {
147    type Err = Error;
148
149    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
150        match s.trim() {
151            "" => Ok(Self::Arrow),
152            "arrow" => Ok(Self::Arrow),
153            "fst" => Ok(Self::Fst),
154            other => Err(Error::index(format!(
155                "unsupported token set format {}",
156                other
157            ))),
158        }
159    }
160}
161
162impl DeepSizeOf for TokenSetFormat {
163    fn deep_size_of_children(&self, _: &mut deepsize::Context) -> usize {
164        0
165    }
166}
167
168#[derive(Clone)]
169pub struct InvertedIndex {
170    params: InvertedIndexParams,
171    store: Arc<dyn IndexStore>,
172    tokenizer: Box<dyn LanceTokenizer>,
173    token_set_format: TokenSetFormat,
174    pub(crate) partitions: Vec<Arc<InvertedPartition>>,
175}
176
177impl Debug for InvertedIndex {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        f.debug_struct("InvertedIndex")
180            .field("params", &self.params)
181            .field("token_set_format", &self.token_set_format)
182            .field("partitions", &self.partitions)
183            .finish()
184    }
185}
186
187impl DeepSizeOf for InvertedIndex {
188    fn deep_size_of_children(&self, context: &mut deepsize::Context) -> usize {
189        self.partitions.deep_size_of_children(context)
190    }
191}
192
193impl InvertedIndex {
194    fn to_builder(&self) -> InvertedIndexBuilder {
195        self.to_builder_with_offset(None)
196    }
197
198    fn to_builder_with_offset(&self, fragment_mask: Option<u64>) -> InvertedIndexBuilder {
199        if self.is_legacy() {
200            // for legacy format, we re-create the index in the new format
201            InvertedIndexBuilder::from_existing_index(
202                self.params.clone(),
203                None,
204                Vec::new(),
205                self.token_set_format,
206                fragment_mask,
207            )
208        } else {
209            let partitions = match fragment_mask {
210                Some(fragment_mask) => self
211                    .partitions
212                    .iter()
213                    // Filter partitions that belong to the specified fragment
214                    // The mask contains fragment_id in high 32 bits, we check if partition's
215                    // fragment_id matches by comparing the masked result with the original mask
216                    .filter(|part| part.belongs_to_fragment(fragment_mask))
217                    .map(|part| part.id())
218                    .collect(),
219                None => self.partitions.iter().map(|part| part.id()).collect(),
220            };
221
222            InvertedIndexBuilder::from_existing_index(
223                self.params.clone(),
224                Some(self.store.clone()),
225                partitions,
226                self.token_set_format,
227                fragment_mask,
228            )
229        }
230    }
231
232    pub fn tokenizer(&self) -> Box<dyn LanceTokenizer> {
233        self.tokenizer.clone()
234    }
235
236    pub fn params(&self) -> &InvertedIndexParams {
237        &self.params
238    }
239
240    /// Returns the number of partitions in this inverted index.
241    pub fn partition_count(&self) -> usize {
242        self.partitions.len()
243    }
244
245    // search the documents that contain the query
246    // return the row ids of the documents sorted by bm25 score
247    // ref: https://en.wikipedia.org/wiki/Okapi_BM25
248    // we first calculate in-partition BM25 scores,
249    // then re-calculate the scores for the top k documents across all partitions
250    #[instrument(level = "debug", skip_all)]
251    pub async fn bm25_search(
252        &self,
253        tokens: Arc<Tokens>,
254        params: Arc<FtsSearchParams>,
255        operator: Operator,
256        prefilter: Arc<dyn PreFilter>,
257        metrics: Arc<dyn MetricsCollector>,
258    ) -> Result<(Vec<u64>, Vec<f32>)> {
259        let limit = params.limit.unwrap_or(usize::MAX);
260        if limit == 0 {
261            return Ok((Vec::new(), Vec::new()));
262        }
263        let mask = prefilter.mask();
264
265        let mut candidates = BinaryHeap::new();
266        let parts = self
267            .partitions
268            .iter()
269            .map(|part| {
270                let part = part.clone();
271                let tokens = tokens.clone();
272                let params = params.clone();
273                let mask = mask.clone();
274                let metrics = metrics.clone();
275                async move {
276                    let postings = part
277                        .load_posting_lists(tokens.as_ref(), params.as_ref(), metrics.as_ref())
278                        .await?;
279                    if postings.is_empty() {
280                        return Result::Ok(PartitionCandidates::empty());
281                    }
282                    let mut tokens_by_position = vec![String::new(); postings.len()];
283                    for posting in &postings {
284                        let idx = posting.term_index() as usize;
285                        tokens_by_position[idx] = posting.token().to_owned();
286                    }
287                    let params = params.clone();
288                    let mask = mask.clone();
289                    let metrics = metrics.clone();
290                    spawn_cpu(move || {
291                        let candidates = part.bm25_search(
292                            params.as_ref(),
293                            operator,
294                            mask,
295                            postings,
296                            metrics.as_ref(),
297                        )?;
298                        Ok(PartitionCandidates {
299                            tokens_by_position,
300                            candidates,
301                        })
302                    })
303                    .await
304                }
305            })
306            .collect::<Vec<_>>();
307        let mut parts = stream::iter(parts).buffer_unordered(get_num_compute_intensive_cpus());
308        let scorer = IndexBM25Scorer::new(self.partitions.iter().map(|part| part.as_ref()));
309        let mut idf_cache: HashMap<String, f32> = HashMap::new();
310        while let Some(res) = parts.try_next().await? {
311            if res.candidates.is_empty() {
312                continue;
313            }
314            let mut idf_by_position = Vec::with_capacity(res.tokens_by_position.len());
315            for token in &res.tokens_by_position {
316                let idf_weight = match idf_cache.get(token) {
317                    Some(weight) => *weight,
318                    None => {
319                        let weight = scorer.query_weight(token);
320                        idf_cache.insert(token.clone(), weight);
321                        weight
322                    }
323                };
324                idf_by_position.push(idf_weight);
325            }
326            for DocCandidate {
327                row_id,
328                freqs,
329                doc_length,
330            } in res.candidates
331            {
332                let mut score = 0.0;
333                for (term_index, freq) in freqs.into_iter() {
334                    debug_assert!((term_index as usize) < idf_by_position.len());
335                    score +=
336                        idf_by_position[term_index as usize] * scorer.doc_weight(freq, doc_length);
337                }
338                if candidates.len() < limit {
339                    candidates.push(Reverse(ScoredDoc::new(row_id, score)));
340                } else if candidates.peek().unwrap().0.score.0 < score {
341                    candidates.pop();
342                    candidates.push(Reverse(ScoredDoc::new(row_id, score)));
343                }
344            }
345        }
346
347        Ok(candidates
348            .into_sorted_vec()
349            .into_iter()
350            .map(|Reverse(doc)| (doc.row_id, doc.score.0))
351            .unzip())
352    }
353
354    async fn load_legacy_index(
355        store: Arc<dyn IndexStore>,
356        frag_reuse_index: Option<Arc<FragReuseIndex>>,
357        index_cache: &LanceCache,
358    ) -> Result<Arc<Self>> {
359        log::warn!("loading legacy FTS index");
360        let tokens_fut = tokio::spawn({
361            let store = store.clone();
362            async move {
363                let token_reader = store.open_index_file(TOKENS_FILE).await?;
364                let tokenizer = token_reader
365                    .schema()
366                    .metadata
367                    .get("tokenizer")
368                    .map(|s| serde_json::from_str::<InvertedIndexParams>(s))
369                    .transpose()?
370                    .unwrap_or_default();
371                let tokens = TokenSet::load(token_reader, TokenSetFormat::Arrow).await?;
372                Result::Ok((tokenizer, tokens))
373            }
374        });
375        let invert_list_fut = tokio::spawn({
376            let store = store.clone();
377            let index_cache_clone = index_cache.clone();
378            async move {
379                let invert_list_reader = store.open_index_file(INVERT_LIST_FILE).await?;
380                let invert_list =
381                    PostingListReader::try_new(invert_list_reader, &index_cache_clone).await?;
382                Result::Ok(Arc::new(invert_list))
383            }
384        });
385        let docs_fut = tokio::spawn({
386            let store = store.clone();
387            async move {
388                let docs_reader = store.open_index_file(DOCS_FILE).await?;
389                let docs = DocSet::load(docs_reader, true, frag_reuse_index).await?;
390                Result::Ok(docs)
391            }
392        });
393
394        let (tokenizer_config, tokens) = tokens_fut.await??;
395        let inverted_list = invert_list_fut.await??;
396        let docs = docs_fut.await??;
397
398        let tokenizer = tokenizer_config.build()?;
399
400        Ok(Arc::new(Self {
401            params: tokenizer_config,
402            store: store.clone(),
403            tokenizer,
404            token_set_format: TokenSetFormat::Arrow,
405            partitions: vec![Arc::new(InvertedPartition {
406                id: 0,
407                store,
408                tokens,
409                inverted_list,
410                docs,
411                token_set_format: TokenSetFormat::Arrow,
412            })],
413        }))
414    }
415
416    pub fn is_legacy(&self) -> bool {
417        self.partitions.len() == 1 && self.partitions[0].is_legacy()
418    }
419
420    pub async fn load(
421        store: Arc<dyn IndexStore>,
422        frag_reuse_index: Option<Arc<FragReuseIndex>>,
423        index_cache: &LanceCache,
424    ) -> Result<Arc<Self>>
425    where
426        Self: Sized,
427    {
428        // for new index format, there is a metadata file and multiple partitions,
429        // each partition is a separate index containing tokens, inverted list and docs.
430        // for old index format, there is no metadata file, and it's just like a single partition
431
432        match store.open_index_file(METADATA_FILE).await {
433            Ok(reader) => {
434                let params = reader
435                    .schema()
436                    .metadata
437                    .get("params")
438                    .ok_or(Error::index("params not found in metadata".to_owned()))?;
439                let params = serde_json::from_str::<InvertedIndexParams>(params)?;
440                let partitions = reader
441                    .schema()
442                    .metadata
443                    .get("partitions")
444                    .ok_or(Error::index("partitions not found in metadata".to_owned()))?;
445                let partitions: Vec<u64> = serde_json::from_str(partitions)?;
446                let token_set_format = reader
447                    .schema()
448                    .metadata
449                    .get(TOKEN_SET_FORMAT_KEY)
450                    .map(|name| TokenSetFormat::from_str(name))
451                    .transpose()?
452                    .unwrap_or(TokenSetFormat::Arrow);
453
454                let format = token_set_format;
455                let partitions = partitions.into_iter().map(|id| {
456                    let store = store.clone();
457                    let frag_reuse_index_clone = frag_reuse_index.clone();
458                    let index_cache_for_part =
459                        index_cache.with_key_prefix(format!("part-{}", id).as_str());
460                    let token_set_format = format;
461                    async move {
462                        Result::Ok(Arc::new(
463                            InvertedPartition::load(
464                                store,
465                                id,
466                                frag_reuse_index_clone,
467                                &index_cache_for_part,
468                                token_set_format,
469                            )
470                            .await?,
471                        ))
472                    }
473                });
474                let partitions = stream::iter(partitions)
475                    .buffer_unordered(store.io_parallelism())
476                    .try_collect::<Vec<_>>()
477                    .await?;
478
479                let tokenizer = params.build()?;
480                Ok(Arc::new(Self {
481                    params,
482                    store,
483                    tokenizer,
484                    token_set_format,
485                    partitions,
486                }))
487            }
488            Err(_) => {
489                // old index format
490                Self::load_legacy_index(store, frag_reuse_index, index_cache).await
491            }
492        }
493    }
494}
495
496#[async_trait]
497impl Index for InvertedIndex {
498    fn as_any(&self) -> &dyn std::any::Any {
499        self
500    }
501
502    fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
503        self
504    }
505
506    fn as_vector_index(self: Arc<Self>) -> Result<Arc<dyn crate::vector::VectorIndex>> {
507        Err(Error::invalid_input(
508            "inverted index cannot be cast to vector index",
509        ))
510    }
511
512    fn statistics(&self) -> Result<serde_json::Value> {
513        let num_tokens = self
514            .partitions
515            .iter()
516            .map(|part| part.tokens.len())
517            .sum::<usize>();
518        let num_docs = self
519            .partitions
520            .iter()
521            .map(|part| part.docs.len())
522            .sum::<usize>();
523        Ok(serde_json::json!({
524            "params": self.params,
525            "num_tokens": num_tokens,
526            "num_docs": num_docs,
527        }))
528    }
529
530    async fn prewarm(&self) -> Result<()> {
531        for part in &self.partitions {
532            part.inverted_list.prewarm().await?;
533        }
534        Ok(())
535    }
536
537    fn index_type(&self) -> crate::IndexType {
538        crate::IndexType::Inverted
539    }
540
541    async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
542        unimplemented!()
543    }
544}
545
546impl InvertedIndex {
547    /// Search docs match the input text.
548    async fn do_search(&self, text: &str) -> Result<RecordBatch> {
549        let params = FtsSearchParams::new();
550        let mut tokenizer = self.tokenizer.clone();
551        let tokens = collect_query_tokens(text, &mut tokenizer);
552
553        let (doc_ids, _) = self
554            .bm25_search(
555                Arc::new(tokens),
556                params.into(),
557                Operator::And,
558                Arc::new(NoFilter),
559                Arc::new(NoOpMetricsCollector),
560            )
561            .boxed()
562            .await?;
563
564        Ok(RecordBatch::try_new(
565            ROW_ID_SCHEMA.clone(),
566            vec![Arc::new(UInt64Array::from(doc_ids))],
567        )?)
568    }
569}
570
571#[async_trait]
572impl ScalarIndex for InvertedIndex {
573    // return the row ids of the documents that contain the query
574    #[instrument(level = "debug", skip_all)]
575    async fn search(
576        &self,
577        query: &dyn AnyQuery,
578        _metrics: &dyn MetricsCollector,
579    ) -> Result<SearchResult> {
580        let query = query.as_any().downcast_ref::<TokenQuery>().unwrap();
581
582        match query {
583            TokenQuery::TokensContains(text) => {
584                let records = self.do_search(text).await?;
585                let row_ids = records
586                    .column(0)
587                    .as_any()
588                    .downcast_ref::<UInt64Array>()
589                    .unwrap();
590                let row_ids = row_ids.iter().flatten().collect_vec();
591                Ok(SearchResult::at_most(RowAddrTreeMap::from_iter(row_ids)))
592            }
593        }
594    }
595
596    fn can_remap(&self) -> bool {
597        true
598    }
599
600    async fn remap(
601        &self,
602        mapping: &HashMap<u64, Option<u64>>,
603        dest_store: &dyn IndexStore,
604    ) -> Result<CreatedIndex> {
605        self.to_builder()
606            .remap(mapping, self.store.clone(), dest_store)
607            .await?;
608
609        let details = pbold::InvertedIndexDetails::try_from(&self.params)?;
610
611        // Use version 0 for Arrow format (legacy), version 1 for Fst format (new)
612        let index_version = match self.token_set_format {
613            TokenSetFormat::Arrow => 0,
614            TokenSetFormat::Fst => INVERTED_INDEX_VERSION,
615        };
616
617        Ok(CreatedIndex {
618            index_details: prost_types::Any::from_msg(&details).unwrap(),
619            index_version,
620        })
621    }
622
623    async fn update(
624        &self,
625        new_data: SendableRecordBatchStream,
626        dest_store: &dyn IndexStore,
627        _valid_old_fragments: Option<&RoaringBitmap>,
628    ) -> Result<CreatedIndex> {
629        self.to_builder().update(new_data, dest_store).await?;
630
631        let details = pbold::InvertedIndexDetails::try_from(&self.params)?;
632
633        // Use version 0 for Arrow format (legacy), version 1 for Fst format (new)
634        let index_version = match self.token_set_format {
635            TokenSetFormat::Arrow => 0,
636            TokenSetFormat::Fst => INVERTED_INDEX_VERSION,
637        };
638
639        Ok(CreatedIndex {
640            index_details: prost_types::Any::from_msg(&details).unwrap(),
641            index_version,
642        })
643    }
644
645    fn update_criteria(&self) -> UpdateCriteria {
646        let criteria = TrainingCriteria::new(TrainingOrdering::None).with_row_id();
647        if self.is_legacy() {
648            UpdateCriteria::requires_old_data(criteria)
649        } else {
650            UpdateCriteria::only_new_data(criteria)
651        }
652    }
653
654    fn derive_index_params(&self) -> Result<ScalarIndexParams> {
655        let mut params = self.params.clone();
656        if params.base_tokenizer.is_empty() {
657            params.base_tokenizer = "simple".to_string();
658        }
659
660        let params_json = serde_json::to_string(&params)?;
661
662        Ok(ScalarIndexParams {
663            index_type: BuiltinIndexType::Inverted.as_str().to_string(),
664            params: Some(params_json),
665        })
666    }
667}
668
669#[derive(Debug, Clone, DeepSizeOf)]
670pub struct InvertedPartition {
671    // 0 for legacy format
672    id: u64,
673    store: Arc<dyn IndexStore>,
674    pub(crate) tokens: TokenSet,
675    pub(crate) inverted_list: Arc<PostingListReader>,
676    pub(crate) docs: DocSet,
677    token_set_format: TokenSetFormat,
678}
679
680impl InvertedPartition {
681    /// Check if this partition belongs to the specified fragment.
682    ///
683    /// This method encapsulates the bit manipulation logic for fragment filtering
684    /// in distributed indexing scenarios.
685    ///
686    /// # Arguments
687    /// * `fragment_mask` - A mask with fragment_id in high 32 bits
688    ///
689    /// # Returns
690    /// * `true` if the partition belongs to the fragment, `false` otherwise
691    pub fn belongs_to_fragment(&self, fragment_mask: u64) -> bool {
692        (self.id() & fragment_mask) == fragment_mask
693    }
694
695    pub fn id(&self) -> u64 {
696        self.id
697    }
698
699    pub fn store(&self) -> &dyn IndexStore {
700        self.store.as_ref()
701    }
702
703    pub fn is_legacy(&self) -> bool {
704        self.inverted_list.lengths.is_none()
705    }
706
707    pub async fn load(
708        store: Arc<dyn IndexStore>,
709        id: u64,
710        frag_reuse_index: Option<Arc<FragReuseIndex>>,
711        index_cache: &LanceCache,
712        token_set_format: TokenSetFormat,
713    ) -> Result<Self> {
714        let token_file = store.open_index_file(&token_file_path(id)).await?;
715        let tokens = TokenSet::load(token_file, token_set_format).await?;
716        let invert_list_file = store.open_index_file(&posting_file_path(id)).await?;
717        let inverted_list = PostingListReader::try_new(invert_list_file, index_cache).await?;
718        let docs_file = store.open_index_file(&doc_file_path(id)).await?;
719        let docs = DocSet::load(docs_file, false, frag_reuse_index).await?;
720
721        Ok(Self {
722            id,
723            store,
724            tokens,
725            inverted_list: Arc::new(inverted_list),
726            docs,
727            token_set_format,
728        })
729    }
730
731    fn map(&self, token: &str) -> Option<u32> {
732        self.tokens.get(token)
733    }
734
735    pub fn expand_fuzzy(&self, tokens: &Tokens, params: &FtsSearchParams) -> Result<Tokens> {
736        let mut new_tokens = Vec::with_capacity(min(tokens.len(), params.max_expansions));
737        for token in tokens {
738            let fuzziness = match params.fuzziness {
739                Some(fuzziness) => fuzziness,
740                None => MatchQuery::auto_fuzziness(token),
741            };
742            let lev = fst::automaton::Levenshtein::new(token, fuzziness)
743                .map_err(|e| Error::index(format!("failed to construct the fuzzy query: {}", e)))?;
744
745            let base_len = tokens.token_type().prefix_len(token) as u32;
746            if let TokenMap::Fst(ref map) = self.tokens.tokens {
747                match base_len + params.prefix_length {
748                    0 => take_fst_keys(map.search(lev), &mut new_tokens, params.max_expansions),
749                    prefix_length => {
750                        let prefix = &token[..min(prefix_length as usize, token.len())];
751                        let prefix = fst::automaton::Str::new(prefix).starts_with();
752                        take_fst_keys(
753                            map.search(lev.intersection(prefix)),
754                            &mut new_tokens,
755                            params.max_expansions,
756                        )
757                    }
758                }
759            } else {
760                return Err(Error::index(
761                    "tokens is not fst, which is not expected".to_owned(),
762                ));
763            }
764        }
765        Ok(Tokens::new(new_tokens, tokens.token_type().clone()))
766    }
767
768    // search the documents that contain the query
769    // return the doc info and the doc length
770    // ref: https://en.wikipedia.org/wiki/Okapi_BM25
771    #[instrument(level = "debug", skip_all)]
772    pub async fn load_posting_lists(
773        &self,
774        tokens: &Tokens,
775        params: &FtsSearchParams,
776        metrics: &dyn MetricsCollector,
777    ) -> Result<Vec<PostingIterator>> {
778        let is_fuzzy = matches!(params.fuzziness, Some(n) if n != 0);
779        let is_phrase_query = params.phrase_slop.is_some();
780        let tokens = match is_fuzzy {
781            true => self.expand_fuzzy(tokens, params)?,
782            false => tokens.clone(),
783        };
784        let mut token_ids = Vec::with_capacity(tokens.len());
785        for token in tokens {
786            let token_id = self.map(&token);
787            if let Some(token_id) = token_id {
788                token_ids.push((token_id, token));
789            } else if is_phrase_query {
790                // if the token is not found, we can't do phrase query
791                return Ok(Vec::new());
792            }
793        }
794        if token_ids.is_empty() {
795            return Ok(Vec::new());
796        }
797        if !is_phrase_query {
798            // remove duplicates
799            token_ids.sort_unstable_by_key(|(token_id, _)| *token_id);
800            token_ids.dedup_by_key(|(token_id, _)| *token_id);
801        }
802
803        let num_docs = self.docs.len();
804        stream::iter(token_ids)
805            .enumerate()
806            .map(|(position, (token_id, token))| async move {
807                let posting = self
808                    .inverted_list
809                    .posting_list(token_id, is_phrase_query, metrics)
810                    .await?;
811
812                Result::Ok(PostingIterator::new(
813                    token,
814                    token_id,
815                    position as u32,
816                    posting,
817                    num_docs,
818                ))
819            })
820            .buffered(self.store.io_parallelism())
821            .try_collect::<Vec<_>>()
822            .await
823    }
824
825    #[instrument(level = "debug", skip_all)]
826    pub fn bm25_search(
827        &self,
828        params: &FtsSearchParams,
829        operator: Operator,
830        mask: Arc<RowAddrMask>,
831        postings: Vec<PostingIterator>,
832        metrics: &dyn MetricsCollector,
833    ) -> Result<Vec<DocCandidate>> {
834        if postings.is_empty() {
835            return Ok(Vec::new());
836        }
837
838        // let local_metrics = LocalMetricsCollector::default();
839        let scorer = IndexBM25Scorer::new(std::iter::once(self));
840        let mut wand = Wand::new(operator, postings.into_iter(), &self.docs, scorer);
841        let hits = wand.search(params, mask, metrics)?;
842        // local_metrics.dump_into(metrics);
843        Ok(hits)
844    }
845
846    pub async fn into_builder(self) -> Result<InnerBuilder> {
847        let mut builder = InnerBuilder::new(
848            self.id,
849            self.inverted_list.has_positions(),
850            self.token_set_format,
851        );
852        builder.tokens = self.tokens;
853        builder.docs = self.docs;
854
855        builder
856            .posting_lists
857            .reserve_exact(self.inverted_list.len());
858        for posting_list in self
859            .inverted_list
860            .read_all(self.inverted_list.has_positions())
861            .await?
862        {
863            let posting_list = posting_list?;
864            builder
865                .posting_lists
866                .push(posting_list.into_builder(&builder.docs));
867        }
868        Ok(builder)
869    }
870}
871
872// at indexing, we use HashMap because we need it to be mutable,
873// at searching, we use fst::Map because it's more efficient
874#[derive(Debug, Clone)]
875pub enum TokenMap {
876    HashMap(HashMap<String, u32>),
877    Fst(fst::Map<Vec<u8>>),
878}
879
880impl Default for TokenMap {
881    fn default() -> Self {
882        Self::HashMap(HashMap::new())
883    }
884}
885
886impl DeepSizeOf for TokenMap {
887    fn deep_size_of_children(&self, ctx: &mut deepsize::Context) -> usize {
888        match self {
889            Self::HashMap(map) => map.deep_size_of_children(ctx),
890            Self::Fst(map) => map.as_fst().size(),
891        }
892    }
893}
894
895impl TokenMap {
896    pub fn len(&self) -> usize {
897        match self {
898            Self::HashMap(map) => map.len(),
899            Self::Fst(map) => map.len(),
900        }
901    }
902
903    pub fn is_empty(&self) -> bool {
904        self.len() == 0
905    }
906}
907
908// TokenSet is a mapping from tokens to token ids
909#[derive(Debug, Clone, Default, DeepSizeOf)]
910pub struct TokenSet {
911    // token -> token_id
912    pub(crate) tokens: TokenMap,
913    pub(crate) next_id: u32,
914    total_length: usize,
915}
916
917impl TokenSet {
918    pub fn into_mut(self) -> Self {
919        let tokens = match self.tokens {
920            TokenMap::HashMap(map) => map,
921            TokenMap::Fst(map) => {
922                let mut new_map = HashMap::with_capacity(map.len());
923                let mut stream = map.into_stream();
924                while let Some((token, token_id)) = stream.next() {
925                    new_map.insert(String::from_utf8_lossy(token).into_owned(), token_id as u32);
926                }
927
928                new_map
929            }
930        };
931
932        Self {
933            tokens: TokenMap::HashMap(tokens),
934            next_id: self.next_id,
935            total_length: self.total_length,
936        }
937    }
938
939    pub fn len(&self) -> usize {
940        self.tokens.len()
941    }
942
943    pub fn is_empty(&self) -> bool {
944        self.len() == 0
945    }
946
947    pub fn to_batch(self, format: TokenSetFormat) -> Result<RecordBatch> {
948        match format {
949            TokenSetFormat::Arrow => self.into_arrow_batch(),
950            TokenSetFormat::Fst => self.into_fst_batch(),
951        }
952    }
953
954    fn into_arrow_batch(self) -> Result<RecordBatch> {
955        let mut token_builder = StringBuilder::with_capacity(self.tokens.len(), self.total_length);
956        let mut token_id_builder = UInt32Builder::with_capacity(self.tokens.len());
957
958        match self.tokens {
959            TokenMap::Fst(map) => {
960                let mut stream = map.stream();
961                while let Some((token, token_id)) = stream.next() {
962                    token_builder.append_value(String::from_utf8_lossy(token));
963                    token_id_builder.append_value(token_id as u32);
964                }
965            }
966            TokenMap::HashMap(map) => {
967                for (token, token_id) in map.into_iter().sorted_unstable() {
968                    token_builder.append_value(token);
969                    token_id_builder.append_value(token_id);
970                }
971            }
972        }
973
974        let token_col = token_builder.finish();
975        let token_id_col = token_id_builder.finish();
976
977        let schema = arrow_schema::Schema::new(vec![
978            arrow_schema::Field::new(TOKEN_COL, DataType::Utf8, false),
979            arrow_schema::Field::new(TOKEN_ID_COL, DataType::UInt32, false),
980        ]);
981
982        let batch = RecordBatch::try_new(
983            Arc::new(schema),
984            vec![
985                Arc::new(token_col) as ArrayRef,
986                Arc::new(token_id_col) as ArrayRef,
987            ],
988        )?;
989        Ok(batch)
990    }
991
992    fn into_fst_batch(mut self) -> Result<RecordBatch> {
993        let fst_map = match std::mem::take(&mut self.tokens) {
994            TokenMap::Fst(map) => map,
995            TokenMap::HashMap(map) => Self::build_fst_from_map(map)?,
996        };
997        let bytes = fst_map.into_fst().into_inner();
998
999        let mut fst_builder = LargeBinaryBuilder::with_capacity(1, bytes.len());
1000        fst_builder.append_value(bytes);
1001        let fst_col = fst_builder.finish();
1002
1003        let mut next_id_builder = UInt32Builder::with_capacity(1);
1004        next_id_builder.append_value(self.next_id);
1005        let next_id_col = next_id_builder.finish();
1006
1007        let mut total_length_builder = UInt64Builder::with_capacity(1);
1008        total_length_builder.append_value(self.total_length as u64);
1009        let total_length_col = total_length_builder.finish();
1010
1011        let schema = arrow_schema::Schema::new(vec![
1012            arrow_schema::Field::new(TOKEN_FST_BYTES_COL, DataType::LargeBinary, false),
1013            arrow_schema::Field::new(TOKEN_NEXT_ID_COL, DataType::UInt32, false),
1014            arrow_schema::Field::new(TOKEN_TOTAL_LENGTH_COL, DataType::UInt64, false),
1015        ]);
1016
1017        let batch = RecordBatch::try_new(
1018            Arc::new(schema),
1019            vec![
1020                Arc::new(fst_col) as ArrayRef,
1021                Arc::new(next_id_col) as ArrayRef,
1022                Arc::new(total_length_col) as ArrayRef,
1023            ],
1024        )?;
1025        Ok(batch)
1026    }
1027
1028    fn build_fst_from_map(map: HashMap<String, u32>) -> Result<fst::Map<Vec<u8>>> {
1029        let mut entries: Vec<_> = map.into_iter().collect();
1030        entries.sort_unstable_by(|(lhs, _), (rhs, _)| lhs.cmp(rhs));
1031        let mut builder = fst::MapBuilder::memory();
1032        for (token, token_id) in entries {
1033            builder
1034                .insert(&token, token_id as u64)
1035                .map_err(|e| Error::index(format!("failed to insert token {}: {}", token, e)))?;
1036        }
1037        Ok(builder.into_map())
1038    }
1039
1040    pub async fn load(reader: Arc<dyn IndexReader>, format: TokenSetFormat) -> Result<Self> {
1041        match format {
1042            TokenSetFormat::Arrow => Self::load_arrow(reader).await,
1043            TokenSetFormat::Fst => Self::load_fst(reader).await,
1044        }
1045    }
1046
1047    async fn load_arrow(reader: Arc<dyn IndexReader>) -> Result<Self> {
1048        let batch = reader.read_range(0..reader.num_rows(), None).await?;
1049
1050        let (tokens, next_id, total_length) = spawn_blocking(move || {
1051            let mut next_id = 0;
1052            let mut total_length = 0;
1053            let mut tokens = fst::MapBuilder::memory();
1054
1055            let token_col = batch[TOKEN_COL].as_string::<i32>();
1056            let token_id_col = batch[TOKEN_ID_COL].as_primitive::<datatypes::UInt32Type>();
1057
1058            for (token, &token_id) in token_col.iter().zip(token_id_col.values().iter()) {
1059                let token =
1060                    token.ok_or(Error::index("found null token in token set".to_owned()))?;
1061                next_id = next_id.max(token_id + 1);
1062                total_length += token.len();
1063                tokens.insert(token, token_id as u64).map_err(|e| {
1064                    Error::index(format!("failed to insert token {}: {}", token, e))
1065                })?;
1066            }
1067
1068            Ok::<_, Error>((tokens.into_map(), next_id, total_length))
1069        })
1070        .await
1071        .map_err(|err| Error::execution(format!("failed to spawn blocking task: {}", err)))??;
1072
1073        Ok(Self {
1074            tokens: TokenMap::Fst(tokens),
1075            next_id,
1076            total_length,
1077        })
1078    }
1079
1080    async fn load_fst(reader: Arc<dyn IndexReader>) -> Result<Self> {
1081        let batch = reader.read_range(0..reader.num_rows(), None).await?;
1082        if batch.num_rows() == 0 {
1083            return Err(Error::index("token set batch is empty".to_owned()));
1084        }
1085
1086        let fst_col = batch[TOKEN_FST_BYTES_COL].as_binary::<i64>();
1087        let bytes = fst_col.value(0);
1088        let map = fst::Map::new(bytes.to_vec())
1089            .map_err(|e| Error::index(format!("failed to load fst tokens: {}", e)))?;
1090
1091        let next_id_col = batch[TOKEN_NEXT_ID_COL].as_primitive::<datatypes::UInt32Type>();
1092        let total_length_col =
1093            batch[TOKEN_TOTAL_LENGTH_COL].as_primitive::<datatypes::UInt64Type>();
1094
1095        let next_id = next_id_col
1096            .values()
1097            .first()
1098            .copied()
1099            .ok_or(Error::index("token next id column is empty".to_owned()))?;
1100
1101        let total_length = total_length_col
1102            .values()
1103            .first()
1104            .copied()
1105            .ok_or(Error::index(
1106                "token total length column is empty".to_owned(),
1107            ))?;
1108
1109        Ok(Self {
1110            tokens: TokenMap::Fst(map),
1111            next_id,
1112            total_length: usize::try_from(total_length).map_err(|_| {
1113                Error::index(format!(
1114                    "token total length {} overflows usize",
1115                    total_length
1116                ))
1117            })?,
1118        })
1119    }
1120
1121    pub fn add(&mut self, token: String) -> u32 {
1122        let next_id = self.next_id();
1123        let len = token.len();
1124        let token_id = match self.tokens {
1125            TokenMap::HashMap(ref mut map) => *map.entry(token).or_insert(next_id),
1126            _ => unreachable!("tokens must be HashMap while indexing"),
1127        };
1128
1129        // add token if it doesn't exist
1130        if token_id == next_id {
1131            self.next_id += 1;
1132            self.total_length += len;
1133        }
1134
1135        token_id
1136    }
1137
1138    pub(crate) fn get_or_add(&mut self, token: &str) -> u32 {
1139        let next_id = self.next_id;
1140        match self.tokens {
1141            TokenMap::HashMap(ref mut map) => {
1142                if let Some(&token_id) = map.get(token) {
1143                    return token_id;
1144                }
1145
1146                map.insert(token.to_owned(), next_id);
1147            }
1148            _ => unreachable!("tokens must be HashMap while indexing"),
1149        }
1150
1151        self.next_id += 1;
1152        self.total_length += token.len();
1153        next_id
1154    }
1155
1156    pub fn get(&self, token: &str) -> Option<u32> {
1157        match self.tokens {
1158            TokenMap::HashMap(ref map) => map.get(token).copied(),
1159            TokenMap::Fst(ref map) => map.get(token).map(|id| id as u32),
1160        }
1161    }
1162
1163    // the `removed_token_ids` must be sorted
1164    pub fn remap(&mut self, removed_token_ids: &[u32]) {
1165        if removed_token_ids.is_empty() {
1166            return;
1167        }
1168
1169        let mut map = match std::mem::take(&mut self.tokens) {
1170            TokenMap::HashMap(map) => map,
1171            TokenMap::Fst(map) => {
1172                let mut new_map = HashMap::with_capacity(map.len());
1173                let mut stream = map.into_stream();
1174                while let Some((token, token_id)) = stream.next() {
1175                    new_map.insert(String::from_utf8_lossy(token).into_owned(), token_id as u32);
1176                }
1177
1178                new_map
1179            }
1180        };
1181
1182        map.retain(
1183            |_, token_id| match removed_token_ids.binary_search(token_id) {
1184                Ok(_) => false,
1185                Err(index) => {
1186                    *token_id -= index as u32;
1187                    true
1188                }
1189            },
1190        );
1191
1192        self.tokens = TokenMap::HashMap(map);
1193    }
1194
1195    pub fn next_id(&self) -> u32 {
1196        self.next_id
1197    }
1198}
1199
1200pub struct PostingListReader {
1201    reader: Arc<dyn IndexReader>,
1202
1203    // legacy format only
1204    offsets: Option<Vec<usize>>,
1205
1206    // from metadata for legacy format
1207    // from column for new format
1208    max_scores: Option<Vec<f32>>,
1209
1210    // new format only
1211    lengths: Option<Vec<u32>>,
1212
1213    has_position: bool,
1214
1215    index_cache: WeakLanceCache,
1216}
1217
1218impl std::fmt::Debug for PostingListReader {
1219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1220        f.debug_struct("InvertedListReader")
1221            .field("offsets", &self.offsets)
1222            .field("max_scores", &self.max_scores)
1223            .finish()
1224    }
1225}
1226
1227impl DeepSizeOf for PostingListReader {
1228    fn deep_size_of_children(&self, context: &mut deepsize::Context) -> usize {
1229        self.offsets.deep_size_of_children(context)
1230            + self.max_scores.deep_size_of_children(context)
1231            + self.lengths.deep_size_of_children(context)
1232    }
1233}
1234
1235impl PostingListReader {
1236    pub(crate) async fn try_new(
1237        reader: Arc<dyn IndexReader>,
1238        index_cache: &LanceCache,
1239    ) -> Result<Self> {
1240        let has_position = reader.schema().field(POSITION_COL).is_some();
1241        let (offsets, max_scores, lengths) = if reader.schema().field(POSTING_COL).is_none() {
1242            let (offsets, max_scores) = Self::load_metadata(reader.schema())?;
1243            (Some(offsets), max_scores, None)
1244        } else {
1245            let metadata = reader
1246                .read_range(0..reader.num_rows(), Some(&[MAX_SCORE_COL, LENGTH_COL]))
1247                .await?;
1248            let max_scores = metadata[MAX_SCORE_COL]
1249                .as_primitive::<Float32Type>()
1250                .values()
1251                .to_vec();
1252            let lengths = metadata[LENGTH_COL]
1253                .as_primitive::<UInt32Type>()
1254                .values()
1255                .to_vec();
1256            (None, Some(max_scores), Some(lengths))
1257        };
1258
1259        Ok(Self {
1260            reader,
1261            offsets,
1262            max_scores,
1263            lengths,
1264            has_position,
1265            index_cache: WeakLanceCache::from(index_cache),
1266        })
1267    }
1268
1269    // for legacy format
1270    // returns the offsets and max scores
1271    fn load_metadata(
1272        schema: &lance_core::datatypes::Schema,
1273    ) -> Result<(Vec<usize>, Option<Vec<f32>>)> {
1274        let offsets = schema
1275            .metadata
1276            .get("offsets")
1277            .ok_or(Error::index("offsets not found in metadata".to_owned()))?;
1278        let offsets = serde_json::from_str(offsets)?;
1279
1280        let max_scores = schema
1281            .metadata
1282            .get("max_scores")
1283            .map(|max_scores| serde_json::from_str(max_scores))
1284            .transpose()?;
1285        Ok((offsets, max_scores))
1286    }
1287
1288    // the number of posting lists
1289    pub fn len(&self) -> usize {
1290        match self.offsets {
1291            Some(ref offsets) => offsets.len(),
1292            None => self.reader.num_rows(),
1293        }
1294    }
1295
1296    pub fn is_empty(&self) -> bool {
1297        self.len() == 0
1298    }
1299
1300    pub(crate) fn has_positions(&self) -> bool {
1301        self.has_position
1302    }
1303
1304    pub(crate) fn posting_len(&self, token_id: u32) -> usize {
1305        let token_id = token_id as usize;
1306
1307        match self.offsets {
1308            Some(ref offsets) => {
1309                let next_offset = offsets
1310                    .get(token_id + 1)
1311                    .copied()
1312                    .unwrap_or(self.reader.num_rows());
1313                next_offset - offsets[token_id]
1314            }
1315            None => {
1316                if let Some(lengths) = &self.lengths {
1317                    lengths[token_id] as usize
1318                } else {
1319                    panic!("posting list reader is not initialized")
1320                }
1321            }
1322        }
1323    }
1324
1325    pub(crate) async fn posting_batch(
1326        &self,
1327        token_id: u32,
1328        with_position: bool,
1329    ) -> Result<RecordBatch> {
1330        if self.offsets.is_some() {
1331            self.posting_batch_legacy(token_id, with_position).await
1332        } else {
1333            let token_id = token_id as usize;
1334            let columns = if with_position {
1335                vec![POSTING_COL, POSITION_COL]
1336            } else {
1337                vec![POSTING_COL]
1338            };
1339            let batch = self
1340                .reader
1341                .read_range(token_id..token_id + 1, Some(&columns))
1342                .await?;
1343            Ok(batch)
1344        }
1345    }
1346
1347    async fn posting_batch_legacy(
1348        &self,
1349        token_id: u32,
1350        with_position: bool,
1351    ) -> Result<RecordBatch> {
1352        let mut columns = vec![ROW_ID, FREQUENCY_COL];
1353        if with_position {
1354            columns.push(POSITION_COL);
1355        }
1356
1357        let length = self.posting_len(token_id);
1358        let token_id = token_id as usize;
1359        let offset = self.offsets.as_ref().unwrap()[token_id];
1360        let batch = self
1361            .reader
1362            .read_range(offset..offset + length, Some(&columns))
1363            .await?;
1364        Ok(batch)
1365    }
1366
1367    #[instrument(level = "debug", skip(self, metrics))]
1368    pub(crate) async fn posting_list(
1369        &self,
1370        token_id: u32,
1371        is_phrase_query: bool,
1372        metrics: &dyn MetricsCollector,
1373    ) -> Result<PostingList> {
1374        let cache_key = PostingListKey { token_id };
1375        let mut posting = self
1376            .index_cache
1377            .get_or_insert_with_key(cache_key, || async move {
1378                metrics.record_part_load();
1379                info!(target: TRACE_IO_EVENTS, r#type=IO_TYPE_LOAD_SCALAR_PART, index_type="inverted", part_id=token_id);
1380                let batch = self.posting_batch(token_id, false).await?;
1381                self.posting_list_from_batch(&batch, token_id)
1382            })
1383            .await?
1384            .as_ref()
1385            .clone();
1386
1387        if is_phrase_query {
1388            // hit the cache and when the cache was populated, the positions column was not loaded
1389            let positions = self.read_positions(token_id).await?;
1390            posting.set_positions(positions);
1391        }
1392
1393        Ok(posting)
1394    }
1395
1396    pub(crate) fn posting_list_from_batch(
1397        &self,
1398        batch: &RecordBatch,
1399        token_id: u32,
1400    ) -> Result<PostingList> {
1401        let posting_list = PostingList::from_batch(
1402            batch,
1403            self.max_scores
1404                .as_ref()
1405                .map(|max_scores| max_scores[token_id as usize]),
1406            self.lengths
1407                .as_ref()
1408                .map(|lengths| lengths[token_id as usize]),
1409        )?;
1410        Ok(posting_list)
1411    }
1412
1413    async fn prewarm(&self) -> Result<()> {
1414        let batch = self.read_batch(false).await?;
1415        for token_id in 0..self.len() {
1416            let posting_range = self.posting_list_range(token_id as u32);
1417            let batch = batch.slice(posting_range.start, posting_range.end - posting_range.start);
1418            // Apply shrink_to_fit to create a deep copy with compacted buffers
1419            // This ensures each cached entry has its own memory, not shared references
1420            let batch = batch.shrink_to_fit()?;
1421            let posting_list = self.posting_list_from_batch(&batch, token_id as u32)?;
1422            let inserted = self
1423                .index_cache
1424                .insert_with_key(
1425                    &PostingListKey {
1426                        token_id: token_id as u32,
1427                    },
1428                    Arc::new(posting_list),
1429                )
1430                .await;
1431
1432            if !inserted {
1433                return Err(Error::internal(
1434                    "Failed to prewarm index: cache is no longer available".to_string(),
1435                ));
1436            }
1437        }
1438
1439        Ok(())
1440    }
1441
1442    pub(crate) async fn read_batch(&self, with_position: bool) -> Result<RecordBatch> {
1443        let columns = self.posting_columns(with_position);
1444        let batch = self
1445            .reader
1446            .read_range(0..self.reader.num_rows(), Some(&columns))
1447            .await?;
1448        Ok(batch)
1449    }
1450
1451    pub(crate) async fn read_all(
1452        &self,
1453        with_position: bool,
1454    ) -> Result<impl Iterator<Item = Result<PostingList>> + '_> {
1455        let batch = self.read_batch(with_position).await?;
1456        Ok((0..self.len()).map(move |i| {
1457            let token_id = i as u32;
1458            let range = self.posting_list_range(token_id);
1459            let batch = batch.slice(i, range.end - range.start);
1460            self.posting_list_from_batch(&batch, token_id)
1461        }))
1462    }
1463
1464    async fn read_positions(&self, token_id: u32) -> Result<ListArray> {
1465        let positions = self.index_cache.get_or_insert_with_key(PositionKey { token_id }, || async move {
1466            let batch = self
1467                .reader
1468                .read_range(self.posting_list_range(token_id), Some(&[POSITION_COL]))
1469                .await.map_err(|e| {
1470                    match e {
1471                        Error::Schema { .. } => Error::invalid_input("position is not found but required for phrase queries, try recreating the index with position".to_owned()),
1472                        e => e
1473                    }
1474                })?;
1475            Result::Ok(Positions(batch[POSITION_COL]
1476                .as_list::<i32>()
1477                .clone()))
1478        }).await?;
1479        Ok(positions.0.clone())
1480    }
1481
1482    fn posting_list_range(&self, token_id: u32) -> Range<usize> {
1483        match self.offsets {
1484            Some(ref offsets) => {
1485                let offset = offsets[token_id as usize];
1486                let posting_len = self.posting_len(token_id);
1487                offset..offset + posting_len
1488            }
1489            None => {
1490                let token_id = token_id as usize;
1491                token_id..token_id + 1
1492            }
1493        }
1494    }
1495
1496    fn posting_columns(&self, with_position: bool) -> Vec<&'static str> {
1497        let mut base_columns = match self.offsets {
1498            Some(_) => vec![ROW_ID, FREQUENCY_COL],
1499            None => vec![POSTING_COL],
1500        };
1501        if with_position {
1502            base_columns.push(POSITION_COL);
1503        }
1504        base_columns
1505    }
1506}
1507
1508/// New type just to allow Positions implement DeepSizeOf so it can be put
1509/// in the cache.
1510#[derive(Clone)]
1511pub struct Positions(ListArray);
1512
1513impl DeepSizeOf for Positions {
1514    fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
1515        self.0.get_buffer_memory_size()
1516    }
1517}
1518
1519// Cache key implementations for type-safe cache access
1520#[derive(Debug, Clone)]
1521pub struct PostingListKey {
1522    pub token_id: u32,
1523}
1524
1525impl CacheKey for PostingListKey {
1526    type ValueType = PostingList;
1527
1528    fn key(&self) -> std::borrow::Cow<'_, str> {
1529        format!("postings-{}", self.token_id).into()
1530    }
1531}
1532
1533#[derive(Debug, Clone)]
1534pub struct PositionKey {
1535    pub token_id: u32,
1536}
1537
1538impl CacheKey for PositionKey {
1539    type ValueType = Positions;
1540
1541    fn key(&self) -> std::borrow::Cow<'_, str> {
1542        format!("positions-{}", self.token_id).into()
1543    }
1544}
1545
1546#[derive(Debug, Clone, DeepSizeOf)]
1547pub enum PostingList {
1548    Plain(PlainPostingList),
1549    Compressed(CompressedPostingList),
1550}
1551
1552impl PostingList {
1553    pub fn from_batch(
1554        batch: &RecordBatch,
1555        max_score: Option<f32>,
1556        length: Option<u32>,
1557    ) -> Result<Self> {
1558        match batch.column_by_name(POSTING_COL) {
1559            Some(_) => {
1560                debug_assert!(max_score.is_some() && length.is_some());
1561                let posting =
1562                    CompressedPostingList::from_batch(batch, max_score.unwrap(), length.unwrap());
1563                Ok(Self::Compressed(posting))
1564            }
1565            None => {
1566                let posting = PlainPostingList::from_batch(batch, max_score);
1567                Ok(Self::Plain(posting))
1568            }
1569        }
1570    }
1571
1572    pub fn iter(&self) -> PostingListIterator<'_> {
1573        PostingListIterator::new(self)
1574    }
1575
1576    pub fn has_position(&self) -> bool {
1577        match self {
1578            Self::Plain(posting) => posting.positions.is_some(),
1579            Self::Compressed(posting) => posting.positions.is_some(),
1580        }
1581    }
1582
1583    pub fn set_positions(&mut self, positions: ListArray) {
1584        match self {
1585            Self::Plain(posting) => posting.positions = Some(positions),
1586            Self::Compressed(posting) => {
1587                posting.positions = Some(positions.value(0).as_list::<i32>().clone());
1588            }
1589        }
1590    }
1591
1592    pub fn max_score(&self) -> Option<f32> {
1593        match self {
1594            Self::Plain(posting) => posting.max_score,
1595            Self::Compressed(posting) => Some(posting.max_score),
1596        }
1597    }
1598
1599    pub fn len(&self) -> usize {
1600        match self {
1601            Self::Plain(posting) => posting.len(),
1602            Self::Compressed(posting) => posting.length as usize,
1603        }
1604    }
1605
1606    pub fn is_empty(&self) -> bool {
1607        self.len() == 0
1608    }
1609
1610    pub fn into_builder(self, docs: &DocSet) -> PostingListBuilder {
1611        let mut builder = PostingListBuilder::new(self.has_position());
1612        match self {
1613            // legacy format
1614            Self::Plain(posting) => {
1615                // convert the posting list to the new format:
1616                // 1. map row ids to doc ids
1617                // 2. sort the posting list by doc ids
1618                struct Item {
1619                    doc_id: u32,
1620                    positions: PositionRecorder,
1621                }
1622                let doc_ids = docs
1623                    .row_ids
1624                    .iter()
1625                    .enumerate()
1626                    .map(|(doc_id, row_id)| (*row_id, doc_id as u32))
1627                    .collect::<HashMap<_, _>>();
1628                let mut items = Vec::with_capacity(posting.len());
1629                for (row_id, freq, positions) in posting.iter() {
1630                    let freq = freq as u32;
1631                    let positions = match positions {
1632                        Some(positions) => {
1633                            PositionRecorder::Position(positions.collect::<Vec<_>>().into())
1634                        }
1635                        None => PositionRecorder::Count(freq),
1636                    };
1637                    items.push(Item {
1638                        doc_id: doc_ids[&row_id],
1639                        positions,
1640                    });
1641                }
1642                items.sort_unstable_by_key(|item| item.doc_id);
1643                for item in items {
1644                    builder.add(item.doc_id, item.positions);
1645                }
1646            }
1647            Self::Compressed(posting) => {
1648                posting.iter().for_each(|(doc_id, freq, positions)| {
1649                    let positions = match positions {
1650                        Some(positions) => {
1651                            PositionRecorder::Position(positions.collect::<Vec<_>>().into())
1652                        }
1653                        None => PositionRecorder::Count(freq),
1654                    };
1655                    builder.add(doc_id, positions);
1656                });
1657            }
1658        }
1659        builder
1660    }
1661}
1662
1663#[derive(Debug, PartialEq, Clone)]
1664pub struct PlainPostingList {
1665    pub row_ids: ScalarBuffer<u64>,
1666    pub frequencies: ScalarBuffer<f32>,
1667    pub max_score: Option<f32>,
1668    pub positions: Option<ListArray>, // List of Int32
1669}
1670
1671impl DeepSizeOf for PlainPostingList {
1672    fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
1673        self.row_ids.len() * std::mem::size_of::<u64>()
1674            + self.frequencies.len() * std::mem::size_of::<u32>()
1675            + self
1676                .positions
1677                .as_ref()
1678                .map(|positions| positions.get_buffer_memory_size())
1679                .unwrap_or(0)
1680    }
1681}
1682
1683impl PlainPostingList {
1684    pub fn new(
1685        row_ids: ScalarBuffer<u64>,
1686        frequencies: ScalarBuffer<f32>,
1687        max_score: Option<f32>,
1688        positions: Option<ListArray>,
1689    ) -> Self {
1690        Self {
1691            row_ids,
1692            frequencies,
1693            max_score,
1694            positions,
1695        }
1696    }
1697
1698    pub fn from_batch(batch: &RecordBatch, max_score: Option<f32>) -> Self {
1699        let row_ids = batch[ROW_ID].as_primitive::<UInt64Type>().values().clone();
1700        let frequencies = batch[FREQUENCY_COL]
1701            .as_primitive::<Float32Type>()
1702            .values()
1703            .clone();
1704        let positions = batch
1705            .column_by_name(POSITION_COL)
1706            .map(|col| col.as_list::<i32>().clone());
1707
1708        Self::new(row_ids, frequencies, max_score, positions)
1709    }
1710
1711    pub fn len(&self) -> usize {
1712        self.row_ids.len()
1713    }
1714
1715    pub fn is_empty(&self) -> bool {
1716        self.len() == 0
1717    }
1718
1719    pub fn iter(&self) -> PlainPostingListIterator<'_> {
1720        Box::new(
1721            self.row_ids
1722                .iter()
1723                .zip(self.frequencies.iter())
1724                .enumerate()
1725                .map(|(idx, (doc_id, freq))| {
1726                    (
1727                        *doc_id,
1728                        *freq,
1729                        self.positions.as_ref().map(|p| {
1730                            let start = p.value_offsets()[idx] as usize;
1731                            let end = p.value_offsets()[idx + 1] as usize;
1732                            Box::new(
1733                                p.values().as_primitive::<Int32Type>().values()[start..end]
1734                                    .iter()
1735                                    .map(|pos| *pos as u32),
1736                            ) as _
1737                        }),
1738                    )
1739                }),
1740        )
1741    }
1742
1743    #[inline]
1744    pub fn doc(&self, i: usize) -> LocatedDocInfo {
1745        LocatedDocInfo::new(self.row_ids[i], self.frequencies[i])
1746    }
1747
1748    pub fn positions(&self, index: usize) -> Option<Arc<dyn Array>> {
1749        self.positions
1750            .as_ref()
1751            .map(|positions| positions.value(index))
1752    }
1753
1754    pub fn max_score(&self) -> Option<f32> {
1755        self.max_score
1756    }
1757
1758    pub fn row_id(&self, i: usize) -> u64 {
1759        self.row_ids[i]
1760    }
1761}
1762
1763#[derive(Debug, PartialEq, Clone)]
1764pub struct CompressedPostingList {
1765    pub max_score: f32,
1766    pub length: u32,
1767    // each binary is a block of compressed data
1768    // that contains `BLOCK_SIZE` doc ids and then `BLOCK_SIZE` frequencies
1769    pub blocks: LargeBinaryArray,
1770    pub positions: Option<ListArray>,
1771}
1772
1773impl DeepSizeOf for CompressedPostingList {
1774    fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
1775        self.blocks.get_buffer_memory_size()
1776            + self
1777                .positions
1778                .as_ref()
1779                .map(|positions| positions.get_buffer_memory_size())
1780                .unwrap_or(0)
1781    }
1782}
1783
1784impl CompressedPostingList {
1785    pub fn new(
1786        blocks: LargeBinaryArray,
1787        max_score: f32,
1788        length: u32,
1789        positions: Option<ListArray>,
1790    ) -> Self {
1791        Self {
1792            max_score,
1793            length,
1794            blocks,
1795            positions,
1796        }
1797    }
1798
1799    pub fn from_batch(batch: &RecordBatch, max_score: f32, length: u32) -> Self {
1800        debug_assert_eq!(batch.num_rows(), 1);
1801        let blocks = batch[POSTING_COL]
1802            .as_list::<i32>()
1803            .value(0)
1804            .as_binary::<i64>()
1805            .clone();
1806        let positions = batch
1807            .column_by_name(POSITION_COL)
1808            .map(|col| col.as_list::<i32>().value(0).as_list::<i32>().clone());
1809
1810        Self {
1811            max_score,
1812            length,
1813            blocks,
1814            positions,
1815        }
1816    }
1817
1818    pub fn iter(&self) -> CompressedPostingListIterator {
1819        CompressedPostingListIterator::new(
1820            self.length as usize,
1821            self.blocks.clone(),
1822            self.positions.clone(),
1823        )
1824    }
1825
1826    pub fn block_max_score(&self, block_idx: usize) -> f32 {
1827        let block = self.blocks.value(block_idx);
1828        block[0..4].try_into().map(f32::from_le_bytes).unwrap()
1829    }
1830
1831    pub fn block_least_doc_id(&self, block_idx: usize) -> u32 {
1832        let block = self.blocks.value(block_idx);
1833        block[4..8].try_into().map(u32::from_le_bytes).unwrap()
1834    }
1835}
1836
1837#[derive(Debug)]
1838pub struct PostingListBuilder {
1839    pub doc_ids: ExpLinkedList<u32>,
1840    pub frequencies: ExpLinkedList<u32>,
1841    pub positions: Option<PositionBuilder>,
1842}
1843
1844impl PostingListBuilder {
1845    pub fn size(&self) -> u64 {
1846        (std::mem::size_of::<u32>() * self.doc_ids.len()
1847            + std::mem::size_of::<u32>() * self.frequencies.len()
1848            + self
1849                .positions
1850                .as_ref()
1851                .map(|positions| positions.size())
1852                .unwrap_or(0)) as u64
1853    }
1854
1855    pub fn has_positions(&self) -> bool {
1856        self.positions.is_some()
1857    }
1858
1859    pub fn new(with_position: bool) -> Self {
1860        Self {
1861            doc_ids: ExpLinkedList::new().with_capacity_limit(128),
1862            frequencies: ExpLinkedList::new().with_capacity_limit(128),
1863            positions: with_position.then(PositionBuilder::new),
1864        }
1865    }
1866
1867    pub fn len(&self) -> usize {
1868        self.doc_ids.len()
1869    }
1870
1871    pub fn is_empty(&self) -> bool {
1872        self.len() == 0
1873    }
1874
1875    pub fn iter(&self) -> impl Iterator<Item = (&u32, &u32, Option<&[u32]>)> {
1876        self.doc_ids
1877            .iter()
1878            .zip(self.frequencies.iter())
1879            .enumerate()
1880            .map(|(idx, (doc_id, freq))| {
1881                let positions = self.positions.as_ref().map(|positions| positions.get(idx));
1882                (doc_id, freq, positions)
1883            })
1884    }
1885
1886    pub fn add(&mut self, doc_id: u32, term_positions: PositionRecorder) {
1887        self.doc_ids.push(doc_id);
1888        self.frequencies.push(term_positions.len());
1889        if let Some(positions) = self.positions.as_mut() {
1890            positions.push(term_positions.into_vec());
1891        }
1892    }
1893
1894    fn build_batch(
1895        self,
1896        compressed: LargeBinaryArray,
1897        max_score: f32,
1898        schema: SchemaRef,
1899    ) -> Result<RecordBatch> {
1900        let length = self.len();
1901        let offsets = OffsetBuffer::new(ScalarBuffer::from(vec![0, compressed.len() as i32]));
1902        let mut columns = vec![
1903            Arc::new(ListArray::try_new(
1904                Arc::new(Field::new("item", datatypes::DataType::LargeBinary, true)),
1905                offsets,
1906                Arc::new(compressed),
1907                None,
1908            )?) as ArrayRef,
1909            Arc::new(Float32Array::from_iter_values(std::iter::once(max_score))) as ArrayRef,
1910            Arc::new(UInt32Array::from_iter_values(std::iter::once(
1911                length as u32,
1912            ))) as ArrayRef,
1913        ];
1914
1915        if let Some(positions) = self.positions.as_ref() {
1916            let mut position_builder = ListBuilder::new(ListBuilder::with_capacity(
1917                LargeBinaryBuilder::new(),
1918                length,
1919            ));
1920            for index in 0..length {
1921                let positions_in_doc = positions.get(index);
1922                let compressed = compress_positions(positions_in_doc)?;
1923                let inner_builder = position_builder.values();
1924                inner_builder.append_value(compressed.into_iter());
1925            }
1926            position_builder.append(true);
1927            let position_col = position_builder.finish();
1928            columns.push(Arc::new(position_col));
1929        }
1930
1931        let batch = RecordBatch::try_new(schema, columns)?;
1932        Ok(batch)
1933    }
1934
1935    // assume the posting list is sorted by doc id
1936    pub fn to_batch(self, block_max_scores: Vec<f32>) -> Result<RecordBatch> {
1937        let max_score = block_max_scores.iter().copied().fold(f32::MIN, f32::max);
1938        let schema = inverted_list_schema(self.has_positions());
1939        let compressed = compress_posting_list(
1940            self.doc_ids.len(),
1941            self.doc_ids.iter(),
1942            self.frequencies.iter(),
1943            block_max_scores.into_iter(),
1944        )?;
1945        self.build_batch(compressed, max_score, schema)
1946    }
1947
1948    pub fn to_batch_with_docs(self, docs: &DocSet, schema: SchemaRef) -> Result<RecordBatch> {
1949        let length = self.len();
1950        let avgdl = docs.average_length();
1951        let idf_scale = idf(length, docs.len()) * (K1 + 1.0);
1952        let (compressed, max_score) = compress_posting_list_with_scores(
1953            length,
1954            self.doc_ids.iter(),
1955            self.frequencies.iter(),
1956            |doc_id, freq| {
1957                let doc_norm = K1 * (1.0 - B + B * docs.num_tokens(doc_id) as f32 / avgdl);
1958                let freq = freq as f32;
1959                freq / (freq + doc_norm)
1960            },
1961            idf_scale,
1962        )?;
1963        self.build_batch(compressed, max_score, schema)
1964    }
1965
1966    pub fn remap(&mut self, removed: &[u32]) {
1967        let mut cursor = 0;
1968        let mut new_doc_ids = ExpLinkedList::with_capacity(self.len());
1969        let mut new_frequencies = ExpLinkedList::with_capacity(self.len());
1970        let mut new_positions = self.positions.as_mut().map(|_| PositionBuilder::new());
1971        for (&doc_id, &freq, positions) in self.iter() {
1972            while cursor < removed.len() && removed[cursor] < doc_id {
1973                cursor += 1;
1974            }
1975            if cursor < removed.len() && removed[cursor] == doc_id {
1976                // this doc is removed
1977                continue;
1978            }
1979            // there are cursor removed docs before this doc
1980            // so we need to shift the doc id
1981            new_doc_ids.push(doc_id - cursor as u32);
1982            new_frequencies.push(freq);
1983            if let Some(new_positions) = new_positions.as_mut() {
1984                new_positions.push(positions.unwrap().to_vec());
1985            }
1986        }
1987
1988        self.doc_ids = new_doc_ids;
1989        self.frequencies = new_frequencies;
1990        self.positions = new_positions;
1991    }
1992}
1993
1994#[derive(Debug, Clone, DeepSizeOf)]
1995pub struct PositionBuilder {
1996    positions: Vec<u32>,
1997    offsets: Vec<i32>,
1998}
1999
2000impl Default for PositionBuilder {
2001    fn default() -> Self {
2002        Self::new()
2003    }
2004}
2005
2006impl PositionBuilder {
2007    pub fn new() -> Self {
2008        Self {
2009            positions: Vec::new(),
2010            offsets: vec![0],
2011        }
2012    }
2013
2014    pub fn size(&self) -> usize {
2015        std::mem::size_of::<u32>() * self.positions.len()
2016            + std::mem::size_of::<i32>() * self.offsets.len()
2017    }
2018
2019    pub fn total_len(&self) -> usize {
2020        self.positions.len()
2021    }
2022
2023    pub fn push(&mut self, positions: Vec<u32>) {
2024        self.positions.extend(positions);
2025        self.offsets.push(self.positions.len() as i32);
2026    }
2027
2028    pub fn get(&self, i: usize) -> &[u32] {
2029        let start = self.offsets[i] as usize;
2030        let end = self.offsets[i + 1] as usize;
2031        &self.positions[start..end]
2032    }
2033}
2034
2035impl From<Vec<Vec<u32>>> for PositionBuilder {
2036    fn from(positions: Vec<Vec<u32>>) -> Self {
2037        let mut builder = Self::new();
2038        builder.offsets.reserve(positions.len());
2039        for pos in positions {
2040            builder.push(pos);
2041        }
2042        builder
2043    }
2044}
2045
2046#[derive(Debug, Clone, DeepSizeOf, Copy)]
2047pub enum DocInfo {
2048    Located(LocatedDocInfo),
2049    Raw(RawDocInfo),
2050}
2051
2052impl DocInfo {
2053    pub fn doc_id(&self) -> u64 {
2054        match self {
2055            Self::Raw(info) => info.doc_id as u64,
2056            Self::Located(info) => info.row_id,
2057        }
2058    }
2059
2060    pub fn frequency(&self) -> u32 {
2061        match self {
2062            Self::Raw(info) => info.frequency,
2063            Self::Located(info) => info.frequency as u32,
2064        }
2065    }
2066}
2067
2068impl Eq for DocInfo {}
2069
2070impl PartialEq for DocInfo {
2071    fn eq(&self, other: &Self) -> bool {
2072        self.doc_id() == other.doc_id()
2073    }
2074}
2075
2076impl PartialOrd for DocInfo {
2077    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2078        Some(self.cmp(other))
2079    }
2080}
2081
2082impl Ord for DocInfo {
2083    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2084        self.doc_id().cmp(&other.doc_id())
2085    }
2086}
2087
2088#[derive(Debug, Clone, Default, DeepSizeOf, Copy)]
2089pub struct LocatedDocInfo {
2090    pub row_id: u64,
2091    pub frequency: f32,
2092}
2093
2094impl LocatedDocInfo {
2095    pub fn new(row_id: u64, frequency: f32) -> Self {
2096        Self { row_id, frequency }
2097    }
2098}
2099
2100impl Eq for LocatedDocInfo {}
2101
2102impl PartialEq for LocatedDocInfo {
2103    fn eq(&self, other: &Self) -> bool {
2104        self.row_id == other.row_id
2105    }
2106}
2107
2108impl PartialOrd for LocatedDocInfo {
2109    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2110        Some(self.cmp(other))
2111    }
2112}
2113
2114impl Ord for LocatedDocInfo {
2115    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2116        self.row_id.cmp(&other.row_id)
2117    }
2118}
2119
2120#[derive(Debug, Clone, Default, DeepSizeOf, Copy)]
2121pub struct RawDocInfo {
2122    pub doc_id: u32,
2123    pub frequency: u32,
2124}
2125
2126impl RawDocInfo {
2127    pub fn new(doc_id: u32, frequency: u32) -> Self {
2128        Self { doc_id, frequency }
2129    }
2130}
2131
2132impl Eq for RawDocInfo {}
2133
2134impl PartialEq for RawDocInfo {
2135    fn eq(&self, other: &Self) -> bool {
2136        self.doc_id == other.doc_id
2137    }
2138}
2139
2140impl PartialOrd for RawDocInfo {
2141    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2142        Some(self.cmp(other))
2143    }
2144}
2145
2146impl Ord for RawDocInfo {
2147    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2148        self.doc_id.cmp(&other.doc_id)
2149    }
2150}
2151
2152// DocSet is a mapping from row ids to the number of tokens in the document
2153// It's used to sort the documents by the bm25 score
2154#[derive(Debug, Clone, Default, DeepSizeOf)]
2155pub struct DocSet {
2156    row_ids: Vec<u64>,
2157    num_tokens: Vec<u32>,
2158    // (row_id, doc_id) pairs sorted by row_id
2159    inv: Vec<(u64, u32)>,
2160
2161    total_tokens: u64,
2162}
2163
2164impl DocSet {
2165    #[inline]
2166    pub fn len(&self) -> usize {
2167        self.row_ids.len()
2168    }
2169
2170    pub fn is_empty(&self) -> bool {
2171        self.len() == 0
2172    }
2173
2174    pub fn iter(&self) -> impl Iterator<Item = (&u64, &u32)> {
2175        self.row_ids.iter().zip(self.num_tokens.iter())
2176    }
2177
2178    pub fn row_id(&self, doc_id: u32) -> u64 {
2179        self.row_ids[doc_id as usize]
2180    }
2181
2182    pub fn doc_id(&self, row_id: u64) -> Option<u64> {
2183        if self.inv.is_empty() {
2184            // in legacy format, the row id is doc id
2185            match self.row_ids.binary_search(&row_id) {
2186                Ok(_) => Some(row_id),
2187                Err(_) => None,
2188            }
2189        } else {
2190            match self.inv.binary_search_by_key(&row_id, |x| x.0) {
2191                Ok(idx) => Some(self.inv[idx].1 as u64),
2192                Err(_) => None,
2193            }
2194        }
2195    }
2196    pub fn total_tokens_num(&self) -> u64 {
2197        self.total_tokens
2198    }
2199
2200    #[inline]
2201    pub fn average_length(&self) -> f32 {
2202        self.total_tokens as f32 / self.len() as f32
2203    }
2204
2205    pub fn calculate_block_max_scores<'a>(
2206        &self,
2207        doc_ids: impl Iterator<Item = &'a u32>,
2208        freqs: impl Iterator<Item = &'a u32>,
2209    ) -> Vec<f32> {
2210        let avgdl = self.average_length();
2211        let length = doc_ids.size_hint().0;
2212        let num_blocks = length.div_ceil(BLOCK_SIZE);
2213        let mut block_max_scores = Vec::with_capacity(num_blocks);
2214        let idf_scale = idf(length, self.len()) * (K1 + 1.0);
2215        let mut max_score = f32::MIN;
2216        for (i, (doc_id, freq)) in doc_ids.zip(freqs).enumerate() {
2217            let doc_norm = K1 * (1.0 - B + B * self.num_tokens(*doc_id) as f32 / avgdl);
2218            let freq = *freq as f32;
2219            let score = freq / (freq + doc_norm);
2220            if score > max_score {
2221                max_score = score;
2222            }
2223            if (i + 1) % BLOCK_SIZE == 0 {
2224                max_score *= idf_scale;
2225                block_max_scores.push(max_score);
2226                max_score = f32::MIN;
2227            }
2228        }
2229        if !length.is_multiple_of(BLOCK_SIZE) {
2230            max_score *= idf_scale;
2231            block_max_scores.push(max_score);
2232        }
2233        block_max_scores
2234    }
2235
2236    pub fn to_batch(&self) -> Result<RecordBatch> {
2237        let row_id_col = UInt64Array::from_iter_values(self.row_ids.iter().cloned());
2238        let num_tokens_col = UInt32Array::from_iter_values(self.num_tokens.iter().cloned());
2239
2240        let schema = arrow_schema::Schema::new(vec![
2241            arrow_schema::Field::new(ROW_ID, DataType::UInt64, false),
2242            arrow_schema::Field::new(NUM_TOKEN_COL, DataType::UInt32, false),
2243        ]);
2244
2245        let batch = RecordBatch::try_new(
2246            Arc::new(schema),
2247            vec![
2248                Arc::new(row_id_col) as ArrayRef,
2249                Arc::new(num_tokens_col) as ArrayRef,
2250            ],
2251        )?;
2252        Ok(batch)
2253    }
2254
2255    pub async fn load(
2256        reader: Arc<dyn IndexReader>,
2257        is_legacy: bool,
2258        frag_reuse_index: Option<Arc<FragReuseIndex>>,
2259    ) -> Result<Self> {
2260        let batch = reader.read_range(0..reader.num_rows(), None).await?;
2261        let row_id_col = batch[ROW_ID].as_primitive::<datatypes::UInt64Type>();
2262        let num_tokens_col = batch[NUM_TOKEN_COL].as_primitive::<datatypes::UInt32Type>();
2263
2264        // for legacy format, the row id is doc id; sorting keeps binary search viable
2265        if is_legacy {
2266            let (row_ids, num_tokens): (Vec<_>, Vec<_>) = row_id_col
2267                .values()
2268                .iter()
2269                .filter_map(|id| {
2270                    if let Some(frag_reuse_index_ref) = frag_reuse_index.as_ref() {
2271                        frag_reuse_index_ref.remap_row_id(*id)
2272                    } else {
2273                        Some(*id)
2274                    }
2275                })
2276                .zip(num_tokens_col.values().iter())
2277                .sorted_unstable_by_key(|x| x.0)
2278                .unzip();
2279
2280            let total_tokens = num_tokens.iter().map(|&x| x as u64).sum();
2281            return Ok(Self {
2282                row_ids,
2283                num_tokens,
2284                inv: Vec::new(),
2285                total_tokens,
2286            });
2287        }
2288
2289        // if frag reuse happened, we'll need to remap the row_ids. And after row_ids been
2290        // remapped, we'll need resort to make sure binary_search works.
2291        if let Some(frag_reuse_index_ref) = frag_reuse_index.as_ref() {
2292            let mut row_ids = Vec::with_capacity(row_id_col.len());
2293            let mut num_tokens = Vec::with_capacity(num_tokens_col.len());
2294            for (row_id, num_token) in row_id_col.values().iter().zip(num_tokens_col.values()) {
2295                if let Some(new_row_id) = frag_reuse_index_ref.remap_row_id(*row_id) {
2296                    row_ids.push(new_row_id);
2297                    num_tokens.push(*num_token);
2298                }
2299            }
2300
2301            let mut inv: Vec<(u64, u32)> = row_ids
2302                .iter()
2303                .enumerate()
2304                .map(|(doc_id, row_id)| (*row_id, doc_id as u32))
2305                .collect();
2306            inv.sort_unstable_by_key(|entry| entry.0);
2307
2308            let total_tokens = num_tokens.iter().map(|&x| x as u64).sum();
2309            return Ok(Self {
2310                row_ids,
2311                num_tokens,
2312                inv,
2313                total_tokens,
2314            });
2315        }
2316
2317        let row_ids = row_id_col.values().to_vec();
2318        let num_tokens = num_tokens_col.values().to_vec();
2319        let mut inv: Vec<(u64, u32)> = row_ids
2320            .iter()
2321            .enumerate()
2322            .map(|(doc_id, row_id)| (*row_id, doc_id as u32))
2323            .collect();
2324        if !row_ids.is_sorted() {
2325            inv.sort_unstable_by_key(|entry| entry.0);
2326        }
2327        let total_tokens = num_tokens.iter().map(|&x| x as u64).sum();
2328        Ok(Self {
2329            row_ids,
2330            num_tokens,
2331            inv,
2332            total_tokens,
2333        })
2334    }
2335
2336    // remap the row ids to the new row ids
2337    // returns the removed doc ids
2338    pub fn remap(&mut self, mapping: &HashMap<u64, Option<u64>>) -> Vec<u32> {
2339        let mut removed = Vec::new();
2340        let len = self.len();
2341        let row_ids = std::mem::replace(&mut self.row_ids, Vec::with_capacity(len));
2342        let num_tokens = std::mem::replace(&mut self.num_tokens, Vec::with_capacity(len));
2343        for (doc_id, (row_id, num_token)) in std::iter::zip(row_ids, num_tokens).enumerate() {
2344            match mapping.get(&row_id) {
2345                Some(Some(new_row_id)) => {
2346                    self.row_ids.push(*new_row_id);
2347                    self.num_tokens.push(num_token);
2348                }
2349                Some(None) => {
2350                    removed.push(doc_id as u32);
2351                }
2352                None => {
2353                    self.row_ids.push(row_id);
2354                    self.num_tokens.push(num_token);
2355                }
2356            }
2357        }
2358        removed
2359    }
2360
2361    #[inline]
2362    pub fn num_tokens(&self, doc_id: u32) -> u32 {
2363        self.num_tokens[doc_id as usize]
2364    }
2365
2366    // this can be used only if it's a legacy format,
2367    // which store the sorted row ids so that we can use binary search
2368    #[inline]
2369    pub fn num_tokens_by_row_id(&self, row_id: u64) -> u32 {
2370        self.row_ids
2371            .binary_search(&row_id)
2372            .map(|idx| self.num_tokens[idx])
2373            .unwrap_or(0)
2374    }
2375
2376    // append a document to the doc set
2377    // returns the doc_id (the number of documents before appending)
2378    pub fn append(&mut self, row_id: u64, num_tokens: u32) -> u32 {
2379        self.row_ids.push(row_id);
2380        self.num_tokens.push(num_tokens);
2381        self.total_tokens += num_tokens as u64;
2382        self.row_ids.len() as u32 - 1
2383    }
2384}
2385
2386pub fn flat_full_text_search(
2387    batches: &[&RecordBatch],
2388    doc_col: &str,
2389    query: &str,
2390    tokenizer: Option<Box<dyn LanceTokenizer>>,
2391) -> Result<Vec<u64>> {
2392    if batches.is_empty() {
2393        return Ok(vec![]);
2394    }
2395
2396    if is_phrase_query(query) {
2397        return Err(Error::invalid_input(
2398            "phrase query is not supported for flat full text search, try using FTS index",
2399        ));
2400    }
2401
2402    match batches[0][doc_col].data_type() {
2403        DataType::Utf8 => do_flat_full_text_search::<i32>(batches, doc_col, query, tokenizer),
2404        DataType::LargeUtf8 => do_flat_full_text_search::<i64>(batches, doc_col, query, tokenizer),
2405        data_type => Err(Error::invalid_input(format!(
2406            "unsupported data type {} for inverted index",
2407            data_type
2408        ))),
2409    }
2410}
2411
2412fn do_flat_full_text_search<Offset: OffsetSizeTrait>(
2413    batches: &[&RecordBatch],
2414    doc_col: &str,
2415    query: &str,
2416    tokenizer: Option<Box<dyn LanceTokenizer>>,
2417) -> Result<Vec<u64>> {
2418    let mut results = Vec::new();
2419    let mut tokenizer =
2420        tokenizer.unwrap_or_else(|| InvertedIndexParams::default().build().unwrap());
2421    let query_tokens = collect_query_tokens(query, &mut tokenizer);
2422
2423    for batch in batches {
2424        let row_id_array = batch[ROW_ID].as_primitive::<UInt64Type>();
2425        let doc_array = batch[doc_col].as_string::<Offset>();
2426        for i in 0..row_id_array.len() {
2427            let doc = doc_array.value(i);
2428            if has_query_token(doc, &mut tokenizer, &query_tokens) {
2429                results.push(row_id_array.value(i));
2430                // What is this assertion for?  Why would doc contain query?  Don't we reach
2431                // here only if they share at least one token?  Why is it not debug_assert?
2432                assert!(doc.contains(query));
2433            }
2434        }
2435    }
2436
2437    Ok(results)
2438}
2439
2440const FLAT_ROW_ID_COL_IDX: usize = 0;
2441const FLAT_ALL_TOKENS_COL_IDX: usize = 1;
2442const FLAT_QUERY_TOKEN_COUNTS_COL_IDX: usize = 2;
2443
2444/// If we accumulate this many bytes we warn the user they probably want to use an FTS index instead.
2445const BYTES_ACCUMULATED_WARNING_THRESHOLD: u64 = 1024 * 1024 * 1024; // 1GB
2446
2447/// Consumes a stream of record batches and produces token counts
2448///
2449/// The resulting batch will have three columns:
2450/// - row_id: the row id of the document
2451/// - all_tokens: the total number of tokens in the document
2452/// - query_token_counts: a fixed size list of the count of each query token in the document
2453///
2454/// This is an unbounded accumulation, however, for most queries, the per-row
2455/// growth will be fairly small.  As a result we can process millions of tokens
2456/// with fairly modest memory usage.
2457///
2458/// However, it is unwise to do a flat search across billions of rows.  An FTS
2459/// index should be created instead.
2460async fn tokenize_and_count(
2461    input: impl Stream<Item = DataFusionResult<RecordBatch>> + Send,
2462    tokenizer: Box<dyn LanceTokenizer>,
2463    query_tokens: Arc<Tokens>,
2464    doc_col_idx: usize,
2465) -> DataFusionResult<RecordBatch> {
2466    let output_schema = Arc::new(Schema::new(vec![
2467        ROW_ID_FIELD.clone(),
2468        Field::new("all_tokens", DataType::UInt64, false),
2469        Field::new(
2470            "query_token_counts",
2471            DataType::FixedSizeList(
2472                Arc::new(Field::new("item", DataType::UInt64, true)),
2473                query_tokens.len() as i32,
2474            ),
2475            false,
2476        ),
2477    ]));
2478    let output_schema_clone = output_schema.clone();
2479    let bytes_accumulated = Arc::new(AtomicU64::new(0));
2480    let bytes_warning_emitted = Arc::new(AtomicBool::new(false));
2481
2482    let batches = input
2483        .map(move |batch| {
2484            let mut tokenizer = tokenizer.box_clone();
2485            let output_schema = output_schema.clone();
2486            let query_tokens = query_tokens.clone();
2487            let bytes_accumulated = bytes_accumulated.clone();
2488            let bytes_warning_emitted = bytes_warning_emitted.clone();
2489            spawn_cpu(move || {
2490                let batch = batch?;
2491                let mut all_token_counts = UInt64Builder::with_capacity(batch.num_rows());
2492                let mut query_token_counts = FixedSizeListBuilder::with_capacity(
2493                    UInt64Builder::with_capacity(batch.num_rows() * query_tokens.len()),
2494                    query_tokens.len() as i32,
2495                    batch.num_rows(),
2496                );
2497                let mut temp_query_token_counts = Vec::with_capacity(query_tokens.len());
2498                let doc_iter = iter_str_array(batch.column(doc_col_idx));
2499                for doc in doc_iter {
2500                    let Some(doc) = doc else {
2501                        all_token_counts.append_value(0);
2502                        query_token_counts
2503                            .values()
2504                            .append_value_n(0, query_tokens.len());
2505                        query_token_counts.append(true);
2506                        continue;
2507                    };
2508
2509                    temp_query_token_counts.clear();
2510                    temp_query_token_counts.extend(std::iter::repeat_n(0, query_tokens.len()));
2511
2512                    let mut stream = tokenizer.token_stream_for_doc(doc);
2513                    let mut all_tokens = 0;
2514                    while let Some(token) = stream.next() {
2515                        all_tokens += 1;
2516                        if let Some(token_index) = query_tokens.token_index(&token.text) {
2517                            temp_query_token_counts[token_index] += 1;
2518                        }
2519                    }
2520                    all_token_counts.append_value(all_tokens);
2521                    for count in temp_query_token_counts.iter().copied() {
2522                        query_token_counts.values().append_value(count);
2523                    }
2524                    query_token_counts.append(true);
2525                }
2526                let row_ids = batch[ROW_ID].clone();
2527                let all_token_counts = all_token_counts.finish();
2528                let query_token_counts = query_token_counts.finish();
2529                let result_batch = RecordBatch::try_new(
2530
2531                    output_schema,
2532                    vec![
2533                        row_ids,
2534                        Arc::new(all_token_counts) as ArrayRef,
2535                        Arc::new(query_token_counts) as ArrayRef,
2536                    ],
2537                )?;
2538                let bytes_accumulated = bytes_accumulated.fetch_add(result_batch.get_array_memory_size() as u64, Ordering::Relaxed);
2539                if bytes_accumulated > BYTES_ACCUMULATED_WARNING_THRESHOLD && !bytes_warning_emitted.swap(true, Ordering::Relaxed) {
2540                    tracing::warn!("Flat full text search is accumulating a large number of bytes.  Consider using an FTS index instead.");
2541                }
2542
2543                DataFusionResult::Ok(result_batch)
2544            })
2545        })
2546        .buffered(get_num_compute_intensive_cpus())
2547        .try_collect::<Vec<_>>()
2548        .await?;
2549
2550    Ok(arrow::compute::concat_batches(
2551        &output_schema_clone,
2552        &batches,
2553    )?)
2554}
2555
2556/// Initialize the BM25 scorer
2557///
2558/// In order to calculate BM25 scores we need to know token counts for the entire corpus.  We extract these from the
2559/// counted input of the flat search combined with any counts recorded for the indexed portion.
2560fn initialize_scorer(
2561    index: &Option<InvertedIndex>,
2562    query_tokens: &Tokens,
2563    counted_input: &RecordBatch,
2564) -> MemBM25Scorer {
2565    let mut total_tokens = 0;
2566    let mut num_docs = 0;
2567    let mut all_token_counts = vec![0; query_tokens.len()];
2568
2569    if let Some(index) = index {
2570        let index_bm25_scorer = IndexBM25Scorer::new(index.partitions.iter().map(|p| p.as_ref()));
2571        for (token_index, token) in query_tokens.into_iter().enumerate() {
2572            let token_nq = index_bm25_scorer.num_docs_containing_token(token);
2573            all_token_counts[token_index] = token_nq as u64;
2574        }
2575        total_tokens += index_bm25_scorer.total_tokens();
2576        num_docs += index_bm25_scorer.num_docs();
2577    }
2578
2579    num_docs += counted_input.num_rows();
2580    total_tokens += arrow::compute::sum(
2581        counted_input
2582            .column(FLAT_ALL_TOKENS_COL_IDX)
2583            .as_primitive::<UInt64Type>(),
2584    )
2585    .unwrap_or_default();
2586
2587    let mut input_token_counters = counted_input
2588        .column(FLAT_QUERY_TOKEN_COUNTS_COL_IDX)
2589        .as_fixed_size_list()
2590        .values()
2591        .as_primitive::<UInt64Type>()
2592        .values()
2593        .iter()
2594        .copied();
2595
2596    for _ in 0..counted_input.num_rows() {
2597        for token_count in all_token_counts.iter_mut() {
2598            *token_count += input_token_counters.next().unwrap_or_default();
2599        }
2600    }
2601
2602    let token_counts_map = all_token_counts
2603        .into_iter()
2604        .enumerate()
2605        .map(|(token_index, count)| {
2606            (
2607                query_tokens.get_token(token_index).to_string(),
2608                count as usize,
2609            )
2610        })
2611        .collect::<HashMap<String, usize>>();
2612    MemBM25Scorer::new(total_tokens, num_docs, token_counts_map)
2613}
2614
2615fn flat_bm25_score(
2616    query_tokens: &Tokens,
2617    counted_input: &RecordBatch,
2618    scorer: &MemBM25Scorer,
2619) -> Result<RecordBatch> {
2620    let mut row_ids_builder = UInt64Builder::with_capacity(counted_input.num_rows());
2621    let mut scores_builder = Float32Builder::with_capacity(counted_input.num_rows());
2622
2623    let mut row_ids_iter = counted_input
2624        .column(FLAT_ROW_ID_COL_IDX)
2625        .as_primitive::<UInt64Type>()
2626        .values()
2627        .iter()
2628        .copied();
2629    let mut all_token_counts_iter = counted_input
2630        .column(FLAT_ALL_TOKENS_COL_IDX)
2631        .as_primitive::<UInt64Type>()
2632        .values()
2633        .iter()
2634        .copied();
2635    let mut query_token_counts_iter = counted_input
2636        .column(FLAT_QUERY_TOKEN_COUNTS_COL_IDX)
2637        .as_fixed_size_list()
2638        .values()
2639        .as_primitive::<UInt64Type>()
2640        .values()
2641        .iter()
2642        .copied();
2643    for _ in 0..counted_input.num_rows() {
2644        let num_tokens_in_doc = all_token_counts_iter.next().expect_ok()?;
2645        let row_id = row_ids_iter.next().expect_ok()?;
2646        if num_tokens_in_doc == 0 {
2647            for _ in query_tokens {
2648                query_token_counts_iter.next().expect_ok()?;
2649            }
2650            continue;
2651        }
2652        let doc_norm = K1 * (1.0 - B + B * num_tokens_in_doc as f32 / scorer.avg_doc_length());
2653        let mut score = 0.0;
2654        for token in query_tokens {
2655            let freq = query_token_counts_iter.next().expect_ok()? as f32;
2656            let idf = idf(scorer.num_docs_containing_token(token), scorer.num_docs());
2657            score += idf * (freq * (K1 + 1.0) / (freq + doc_norm));
2658        }
2659        if score > 0.0 {
2660            row_ids_builder.append_value(row_id);
2661            scores_builder.append_value(score);
2662        }
2663    }
2664
2665    let row_ids = row_ids_builder.finish();
2666    let scores = scores_builder.finish();
2667    let batch = RecordBatch::try_new(
2668        FTS_SCHEMA.clone(),
2669        vec![Arc::new(row_ids) as ArrayRef, Arc::new(scores) as ArrayRef],
2670    )?;
2671    Ok(batch)
2672}
2673
2674pub async fn flat_bm25_search_stream(
2675    input: SendableRecordBatchStream,
2676    doc_col: String,
2677    query: String,
2678    index: &Option<InvertedIndex>,
2679    target_batch_size: usize,
2680) -> DataFusionResult<SendableRecordBatchStream> {
2681    let mut tokenizer = match index {
2682        Some(index) => index.tokenizer(),
2683        None => Box::new(TextTokenizer::new(
2684            tantivy::tokenizer::TextAnalyzer::builder(
2685                tantivy::tokenizer::SimpleTokenizer::default(),
2686            )
2687            .build(),
2688        )),
2689    };
2690    let query_tokens = Arc::new(collect_query_tokens(&query, &mut tokenizer));
2691
2692    let input_schema = input.schema();
2693    let doc_col_idx = input_schema.index_of(&doc_col)?;
2694
2695    // Accumulate small batches until this threshold before dispatching a task.
2696    const ACCUMULATE_BYTES: usize = 256 * 1024;
2697    // Slice oversized batches down to roughly this size.
2698    const SLICE_BYTES: usize = 512 * 1024;
2699
2700    // Phase 1 - rechunk the input stream into appropriately sized chunks.  Tokenization is
2701    // fairly CPU-intensive, and we don't need too much data to justify a new thread task.
2702    let chunked = lance_arrow::stream::rechunk_stream_by_size(
2703        input,
2704        input_schema,
2705        ACCUMULATE_BYTES,
2706        SLICE_BYTES,
2707    );
2708
2709    // Phase 2 - For each row we need to know the total number of tokens and the count of each
2710    // of the query tokens.  For example, if the query is "book" and the row is "the book shop"
2711    // and we are tokenizing with a whitespace tokenizer, we need to know that there are 3 tokens
2712    // and the token book appears once.
2713    let counted_input =
2714        tokenize_and_count(chunked, tokenizer, query_tokens.clone(), doc_col_idx).await?;
2715
2716    // Phase 3 - Calculate final scores (this is fairly cheap, probably don't need to parallelize)
2717    let scorer = initialize_scorer(index, query_tokens.as_ref(), &counted_input);
2718    let scores = flat_bm25_score(query_tokens.as_ref(), &counted_input, &scorer)?;
2719
2720    // Finally we emit batches according to the target batch size
2721    let num_out_batches = scores.num_rows().div_ceil(target_batch_size);
2722    let mut batches = Vec::with_capacity(num_out_batches);
2723    for i in 0..num_out_batches {
2724        let start = i * target_batch_size;
2725        let len = (scores.num_rows() - start).min(target_batch_size);
2726        batches.push(Ok(scores.slice(start, len)));
2727    }
2728    Ok(Box::pin(RecordBatchStreamAdapter::new(
2729        FTS_SCHEMA.clone(),
2730        stream::iter(batches),
2731    )))
2732}
2733
2734pub fn is_phrase_query(query: &str) -> bool {
2735    query.starts_with('\"') && query.ends_with('\"')
2736}
2737
2738#[cfg(test)]
2739mod tests {
2740    use crate::scalar::inverted::lance_tokenizer::DocType;
2741    use lance_core::cache::LanceCache;
2742    use lance_core::utils::tempfile::TempObjDir;
2743    use lance_io::object_store::ObjectStore;
2744
2745    use crate::metrics::NoOpMetricsCollector;
2746    use crate::prefilter::NoFilter;
2747    use crate::scalar::inverted::builder::{InnerBuilder, PositionRecorder, inverted_list_schema};
2748    use crate::scalar::inverted::encoding::decompress_posting_list;
2749    use crate::scalar::inverted::query::{FtsSearchParams, Operator};
2750    use crate::scalar::lance_format::LanceIndexStore;
2751    use arrow::array::AsArray;
2752    use arrow::datatypes::{Float32Type, UInt32Type};
2753
2754    use super::*;
2755
2756    #[tokio::test]
2757    async fn test_posting_builder_remap() {
2758        let mut builder = PostingListBuilder::new(false);
2759        let n = BLOCK_SIZE + 3;
2760        for i in 0..n {
2761            builder.add(i as u32, PositionRecorder::Count(1));
2762        }
2763        let removed = vec![5, 7];
2764        builder.remap(&removed);
2765
2766        let mut expected = PostingListBuilder::new(false);
2767        for i in 0..n - removed.len() {
2768            expected.add(i as u32, PositionRecorder::Count(1));
2769        }
2770        assert_eq!(builder.doc_ids, expected.doc_ids);
2771        assert_eq!(builder.frequencies, expected.frequencies);
2772
2773        // BLOCK_SIZE + 3 elements should be reduced to BLOCK_SIZE + 1,
2774        // there are still 2 blocks.
2775        let batch = builder.to_batch(vec![1.0, 2.0]).unwrap();
2776        let (doc_ids, freqs) = decompress_posting_list(
2777            (n - removed.len()) as u32,
2778            batch[POSTING_COL]
2779                .as_list::<i32>()
2780                .value(0)
2781                .as_binary::<i64>(),
2782        )
2783        .unwrap();
2784        assert!(
2785            doc_ids
2786                .iter()
2787                .zip(expected.doc_ids.iter())
2788                .all(|(a, b)| a == b)
2789        );
2790        assert!(
2791            freqs
2792                .iter()
2793                .zip(expected.frequencies.iter())
2794                .all(|(a, b)| a == b)
2795        );
2796    }
2797
2798    #[test]
2799    fn test_posting_list_batch_matches_docset_scoring() {
2800        let mut docs = DocSet::default();
2801        let num_docs = BLOCK_SIZE + 3;
2802        for doc_id in 0..num_docs as u32 {
2803            docs.append(doc_id as u64, doc_id % 7 + 1);
2804        }
2805
2806        let doc_ids = (0..num_docs as u32).collect::<Vec<_>>();
2807        let freqs = doc_ids
2808            .iter()
2809            .map(|doc_id| doc_id % 5 + 1)
2810            .collect::<Vec<_>>();
2811
2812        let mut builder_scores = PostingListBuilder::new(false);
2813        let mut builder_docs = PostingListBuilder::new(false);
2814        for (&doc_id, &freq) in doc_ids.iter().zip(freqs.iter()) {
2815            builder_scores.add(doc_id, PositionRecorder::Count(freq));
2816            builder_docs.add(doc_id, PositionRecorder::Count(freq));
2817        }
2818
2819        let block_max_scores = docs.calculate_block_max_scores(doc_ids.iter(), freqs.iter());
2820        let batch_scores = builder_scores.to_batch(block_max_scores).unwrap();
2821        let batch_docs = builder_docs
2822            .to_batch_with_docs(&docs, inverted_list_schema(false))
2823            .unwrap();
2824
2825        let scores_posting = batch_scores[POSTING_COL].as_list::<i32>().value(0);
2826        let scores_posting = scores_posting.as_binary::<i64>();
2827        let docs_posting = batch_docs[POSTING_COL].as_list::<i32>().value(0);
2828        let docs_posting = docs_posting.as_binary::<i64>();
2829        assert_eq!(scores_posting, docs_posting);
2830
2831        let score_left = batch_scores[MAX_SCORE_COL]
2832            .as_primitive::<Float32Type>()
2833            .value(0);
2834        let score_right = batch_docs[MAX_SCORE_COL]
2835            .as_primitive::<Float32Type>()
2836            .value(0);
2837        assert!((score_left - score_right).abs() < 1e-6);
2838
2839        let len_left = batch_scores[LENGTH_COL]
2840            .as_primitive::<UInt32Type>()
2841            .value(0);
2842        let len_right = batch_docs[LENGTH_COL].as_primitive::<UInt32Type>().value(0);
2843        assert_eq!(len_left, len_right);
2844    }
2845
2846    #[tokio::test]
2847    async fn test_remap_to_empty_posting_list() {
2848        let tmpdir = TempObjDir::default();
2849        let store = Arc::new(LanceIndexStore::new(
2850            ObjectStore::local().into(),
2851            tmpdir.clone(),
2852            Arc::new(LanceCache::no_cache()),
2853        ));
2854
2855        let mut builder = InnerBuilder::new(0, false, TokenSetFormat::default());
2856
2857        // index of docs:
2858        // 0: lance
2859        // 1: lake lake
2860        // 2: lake lake lake
2861        builder.tokens.add("lance".to_owned());
2862        builder.tokens.add("lake".to_owned());
2863        builder.posting_lists.push(PostingListBuilder::new(false));
2864        builder.posting_lists.push(PostingListBuilder::new(false));
2865        builder.posting_lists[0].add(0, PositionRecorder::Count(1));
2866        builder.posting_lists[1].add(1, PositionRecorder::Count(2));
2867        builder.posting_lists[1].add(2, PositionRecorder::Count(3));
2868        builder.docs.append(0, 1);
2869        builder.docs.append(1, 1);
2870        builder.docs.append(2, 1);
2871        builder.write(store.as_ref()).await.unwrap();
2872
2873        let index = InvertedPartition::load(
2874            store.clone(),
2875            0,
2876            None,
2877            &LanceCache::no_cache(),
2878            TokenSetFormat::default(),
2879        )
2880        .await
2881        .unwrap();
2882        let mut builder = index.into_builder().await.unwrap();
2883
2884        let mapping = HashMap::from([(0, None), (2, Some(3))]);
2885        builder.remap(&mapping).await.unwrap();
2886
2887        // after remap, the doc 0 is removed, and the doc 2 is updated to 3
2888        assert_eq!(builder.tokens.len(), 1);
2889        assert_eq!(builder.tokens.get("lake"), Some(0));
2890        assert_eq!(builder.posting_lists.len(), 1);
2891        assert_eq!(builder.posting_lists[0].len(), 2);
2892        assert_eq!(builder.docs.len(), 2);
2893        assert_eq!(builder.docs.row_id(0), 1);
2894        assert_eq!(builder.docs.row_id(1), 3);
2895
2896        builder.write(store.as_ref()).await.unwrap();
2897
2898        // remap to delete all docs
2899        let mapping = HashMap::from([(1, None), (3, None)]);
2900        builder.remap(&mapping).await.unwrap();
2901
2902        assert_eq!(builder.tokens.len(), 0);
2903        assert_eq!(builder.posting_lists.len(), 0);
2904        assert_eq!(builder.docs.len(), 0);
2905
2906        builder.write(store.as_ref()).await.unwrap();
2907    }
2908
2909    #[tokio::test]
2910    async fn test_posting_cache_conflict_across_partitions() {
2911        let tmpdir = TempObjDir::default();
2912        let store = Arc::new(LanceIndexStore::new(
2913            ObjectStore::local().into(),
2914            tmpdir.clone(),
2915            Arc::new(LanceCache::no_cache()),
2916        ));
2917
2918        // Create first partition with one token and posting list length 1
2919        let mut builder1 = InnerBuilder::new(0, false, TokenSetFormat::default());
2920        builder1.tokens.add("test".to_owned());
2921        builder1.posting_lists.push(PostingListBuilder::new(false));
2922        builder1.posting_lists[0].add(0, PositionRecorder::Count(1));
2923        builder1.docs.append(100, 1); // row_id=100, num_tokens=1
2924        builder1.write(store.as_ref()).await.unwrap();
2925
2926        // Create second partition with one token and posting list length 4
2927        let mut builder2 = InnerBuilder::new(1, false, TokenSetFormat::default());
2928        builder2.tokens.add("test".to_owned()); // Use same token to test cache prefix fix
2929        builder2.posting_lists.push(PostingListBuilder::new(false));
2930        builder2.posting_lists[0].add(0, PositionRecorder::Count(2));
2931        builder2.posting_lists[0].add(1, PositionRecorder::Count(1));
2932        builder2.posting_lists[0].add(2, PositionRecorder::Count(3));
2933        builder2.posting_lists[0].add(3, PositionRecorder::Count(1));
2934        builder2.docs.append(200, 2); // row_id=200, num_tokens=2
2935        builder2.docs.append(201, 1); // row_id=201, num_tokens=1
2936        builder2.docs.append(202, 3); // row_id=202, num_tokens=3
2937        builder2.docs.append(203, 1); // row_id=203, num_tokens=1
2938        builder2.write(store.as_ref()).await.unwrap();
2939
2940        // Create metadata file with both partitions
2941        let metadata = std::collections::HashMap::from_iter(vec![
2942            (
2943                "partitions".to_owned(),
2944                serde_json::to_string(&vec![0u64, 1u64]).unwrap(),
2945            ),
2946            (
2947                "params".to_owned(),
2948                serde_json::to_string(&InvertedIndexParams::default()).unwrap(),
2949            ),
2950            (
2951                TOKEN_SET_FORMAT_KEY.to_owned(),
2952                TokenSetFormat::default().to_string(),
2953            ),
2954        ]);
2955        let mut writer = store
2956            .new_index_file(METADATA_FILE, Arc::new(arrow_schema::Schema::empty()))
2957            .await
2958            .unwrap();
2959        writer.finish_with_metadata(metadata).await.unwrap();
2960
2961        // Load the inverted index
2962        let cache = Arc::new(LanceCache::with_capacity(4096));
2963        let index = InvertedIndex::load(store.clone(), None, cache.as_ref())
2964            .await
2965            .unwrap();
2966
2967        // Verify the index structure
2968        assert_eq!(index.partitions.len(), 2);
2969        assert_eq!(index.partitions[0].tokens.len(), 1);
2970        assert_eq!(index.partitions[1].tokens.len(), 1);
2971
2972        // Verify the partitions were loaded correctly
2973
2974        // Verify posting list lengths (note: partition order may differ from creation order)
2975        // Verify based on actual loading order
2976        if index.partitions[0].id() == 0 {
2977            // If partition[0] is ID=0, then it should have 1 document
2978            assert_eq!(index.partitions[0].inverted_list.posting_len(0), 1);
2979            assert_eq!(index.partitions[1].inverted_list.posting_len(0), 4);
2980            assert_eq!(index.partitions[0].docs.len(), 1);
2981            assert_eq!(index.partitions[1].docs.len(), 4);
2982        } else {
2983            // If partition[0] is ID=1, then it should have 4 documents
2984            assert_eq!(index.partitions[0].inverted_list.posting_len(0), 4);
2985            assert_eq!(index.partitions[1].inverted_list.posting_len(0), 1);
2986            assert_eq!(index.partitions[0].docs.len(), 4);
2987            assert_eq!(index.partitions[1].docs.len(), 1);
2988        }
2989
2990        // Prewarm the inverted index (this loads posting lists into cache)
2991        index.prewarm().await.unwrap();
2992
2993        let tokens = Arc::new(Tokens::new(vec!["test".to_string()], DocType::Text));
2994        let params = Arc::new(FtsSearchParams::new().with_limit(Some(10)));
2995        let prefilter = Arc::new(NoFilter);
2996        let metrics = Arc::new(NoOpMetricsCollector);
2997
2998        let (row_ids, scores) = index
2999            .bm25_search(tokens, params, Operator::Or, prefilter, metrics)
3000            .await
3001            .unwrap();
3002
3003        // Verify that we got search results
3004        // Expected to find 5 documents: 1 from first partition, 4 from second partition
3005        assert_eq!(row_ids.len(), 5, "row_ids: {:?}", row_ids);
3006        assert!(!row_ids.is_empty(), "Should find at least some documents");
3007        assert_eq!(row_ids.len(), scores.len());
3008
3009        // All scores should be positive since all documents contain the search token
3010        for &score in &scores {
3011            assert!(score > 0.0, "All scores should be positive");
3012        }
3013
3014        // Check that we got results from both partitions
3015        assert!(
3016            row_ids.contains(&100),
3017            "Should contain row_id from partition 0"
3018        );
3019        assert!(
3020            row_ids.iter().any(|&id| id >= 200),
3021            "Should contain row_id from partition 1"
3022        );
3023    }
3024
3025    #[test]
3026    fn test_block_max_scores_capacity_matches_block_count() {
3027        let mut docs = DocSet::default();
3028        let num_docs = BLOCK_SIZE * 3 + 7;
3029        let doc_ids = (0..num_docs as u32).collect::<Vec<_>>();
3030        for doc_id in &doc_ids {
3031            docs.append(*doc_id as u64, 1);
3032        }
3033
3034        let freqs = vec![1_u32; doc_ids.len()];
3035        let block_max_scores = docs.calculate_block_max_scores(doc_ids.iter(), freqs.iter());
3036        let expected_blocks = doc_ids.len().div_ceil(BLOCK_SIZE);
3037
3038        assert_eq!(block_max_scores.len(), expected_blocks);
3039        assert_eq!(block_max_scores.capacity(), expected_blocks);
3040    }
3041
3042    #[tokio::test]
3043    async fn test_bm25_search_uses_global_idf() {
3044        let tmpdir = TempObjDir::default();
3045        let store = Arc::new(LanceIndexStore::new(
3046            ObjectStore::local().into(),
3047            tmpdir.clone(),
3048            Arc::new(LanceCache::no_cache()),
3049        ));
3050
3051        // Partition 0: 3 docs, only one contains "alpha".
3052        let mut builder0 = InnerBuilder::new(0, false, TokenSetFormat::default());
3053        builder0.tokens.add("alpha".to_owned());
3054        builder0.tokens.add("beta".to_owned());
3055        builder0.posting_lists.push(PostingListBuilder::new(false));
3056        builder0.posting_lists.push(PostingListBuilder::new(false));
3057        builder0.posting_lists[0].add(0, PositionRecorder::Count(1));
3058        builder0.posting_lists[1].add(1, PositionRecorder::Count(1));
3059        builder0.posting_lists[1].add(2, PositionRecorder::Count(1));
3060        builder0.docs.append(100, 1);
3061        builder0.docs.append(101, 1);
3062        builder0.docs.append(102, 1);
3063        builder0.write(store.as_ref()).await.unwrap();
3064
3065        // Partition 1: 1 doc, contains "alpha".
3066        let mut builder1 = InnerBuilder::new(1, false, TokenSetFormat::default());
3067        builder1.tokens.add("alpha".to_owned());
3068        builder1.posting_lists.push(PostingListBuilder::new(false));
3069        builder1.posting_lists[0].add(0, PositionRecorder::Count(1));
3070        builder1.docs.append(200, 1);
3071        builder1.write(store.as_ref()).await.unwrap();
3072
3073        let metadata = std::collections::HashMap::from_iter(vec![
3074            (
3075                "partitions".to_owned(),
3076                serde_json::to_string(&vec![0u64, 1u64]).unwrap(),
3077            ),
3078            (
3079                "params".to_owned(),
3080                serde_json::to_string(&InvertedIndexParams::default()).unwrap(),
3081            ),
3082            (
3083                TOKEN_SET_FORMAT_KEY.to_owned(),
3084                TokenSetFormat::default().to_string(),
3085            ),
3086        ]);
3087        let mut writer = store
3088            .new_index_file(METADATA_FILE, Arc::new(arrow_schema::Schema::empty()))
3089            .await
3090            .unwrap();
3091        writer.finish_with_metadata(metadata).await.unwrap();
3092
3093        let cache = Arc::new(LanceCache::with_capacity(4096));
3094        let index = InvertedIndex::load(store.clone(), None, cache.as_ref())
3095            .await
3096            .unwrap();
3097
3098        let tokens = Arc::new(Tokens::new(vec!["alpha".to_string()], DocType::Text));
3099        let params = Arc::new(FtsSearchParams::new().with_limit(Some(10)));
3100        let prefilter = Arc::new(NoFilter);
3101        let metrics = Arc::new(NoOpMetricsCollector);
3102
3103        let (row_ids, scores) = index
3104            .bm25_search(tokens, params, Operator::Or, prefilter, metrics)
3105            .await
3106            .unwrap();
3107
3108        assert_eq!(row_ids.len(), 2);
3109        assert!(row_ids.contains(&100));
3110        assert!(row_ids.contains(&200));
3111        assert_eq!(row_ids.len(), scores.len());
3112
3113        let expected_idf = idf(2, 4);
3114        for score in scores {
3115            assert!(
3116                (score - expected_idf).abs() < 1e-6,
3117                "score: {}, expected: {}",
3118                score,
3119                expected_idf
3120            );
3121        }
3122    }
3123}