1use std::fmt::{Debug, Display};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
7use std::{
8 cmp::{Reverse, min},
9 collections::BinaryHeap,
10};
11use std::{collections::HashMap, ops::Range};
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
81pub 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
105pub 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 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(|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 pub fn partition_count(&self) -> usize {
242 self.partitions.len()
243 }
244
245 #[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 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 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 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 #[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 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 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(¶ms)?;
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 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 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 #[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 return Ok(Vec::new());
792 }
793 }
794 if token_ids.is_empty() {
795 return Ok(Vec::new());
796 }
797 if !is_phrase_query {
798 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 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 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#[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#[derive(Debug, Clone, Default, DeepSizeOf)]
910pub struct TokenSet {
911 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 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 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 offsets: Option<Vec<usize>>,
1205
1206 max_scores: Option<Vec<f32>>,
1209
1210 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 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 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 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 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#[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#[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 Self::Plain(posting) => {
1615 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>, }
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 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 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 continue;
1978 }
1979 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#[derive(Debug, Clone, Default, DeepSizeOf)]
2155pub struct DocSet {
2156 row_ids: Vec<u64>,
2157 num_tokens: Vec<u32>,
2158 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 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 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 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 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 #[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 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 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
2444const BYTES_ACCUMULATED_WARNING_THRESHOLD: u64 = 1024 * 1024 * 1024; async 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
2556fn 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 const ACCUMULATE_BYTES: usize = 256 * 1024;
2697 const SLICE_BYTES: usize = 512 * 1024;
2699
2700 let chunked = lance_arrow::stream::rechunk_stream_by_size(
2703 input,
2704 input_schema,
2705 ACCUMULATE_BYTES,
2706 SLICE_BYTES,
2707 );
2708
2709 let counted_input =
2714 tokenize_and_count(chunked, tokenizer, query_tokens.clone(), doc_col_idx).await?;
2715
2716 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 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 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 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 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 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 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); builder1.write(store.as_ref()).await.unwrap();
2925
2926 let mut builder2 = InnerBuilder::new(1, false, TokenSetFormat::default());
2928 builder2.tokens.add("test".to_owned()); 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); builder2.docs.append(201, 1); builder2.docs.append(202, 3); builder2.docs.append(203, 1); builder2.write(store.as_ref()).await.unwrap();
2939
2940 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 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 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 if index.partitions[0].id() == 0 {
2977 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 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 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 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 for &score in &scores {
3011 assert!(score > 0.0, "All scores should be positive");
3012 }
3013
3014 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 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 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}