1use std::collections::HashMap;
16use std::sync::Arc;
17use std::sync::atomic::AtomicUsize;
18
19use api::v1::SemanticType;
20use arrow_schema::extension::ExtensionType;
21use common_telemetry::warn;
22use datatypes::arrow::array::{Array, LargeStringArray, StringArray};
23use datatypes::arrow::datatypes::DataType;
24use datatypes::arrow::record_batch::RecordBatch;
25use datatypes::extension::json::Json2ExtensionType;
26use datatypes::schema::{FulltextAnalyzer, FulltextBackend};
27use datatypes::vectors::VectorRef;
28use index::fulltext_index::create::{
29 BloomFilterFulltextIndexCreator, FulltextIndexCreator, TantivyFulltextIndexCreator,
30};
31use index::fulltext_index::{Analyzer, Config};
32use index::target::IndexTarget;
33use puffin::blob_metadata::CompressionCodec;
34use puffin::puffin_manager::PutOptions;
35use snafu::{OptionExt, ResultExt, ensure};
36use store_api::metadata::RegionMetadataRef;
37use store_api::storage::{ConcreteDataType, FileId, RegionId};
38
39use crate::error::{
40 CastVectorSnafu, ComputeArrowSnafu, CreateFulltextCreatorSnafu, DataTypeMismatchSnafu,
41 FulltextFinishSnafu, FulltextPushTextSnafu, IndexOptionsSnafu, InvalidRecordBatchSnafu,
42 OperateAbortedIndexSnafu, Result,
43};
44use crate::read::Batch;
45use crate::sst::index::column::extract_json_type_hint_values;
46use crate::sst::index::fulltext_index::{INDEX_BLOB_TYPE_BLOOM, INDEX_BLOB_TYPE_TANTIVY};
47use crate::sst::index::intermediate::{
48 IntermediateLocation, IntermediateManager, TempFileProvider,
49};
50use crate::sst::index::puffin_manager::SstPuffinWriter;
51use crate::sst::index::statistics::{ByteCount, RowCount, Statistics};
52use crate::sst::index::{IndexBuildType, TYPE_FULLTEXT_INDEX};
53
54pub struct FulltextIndexer {
56 creators: HashMap<IndexTarget, SingleCreator>,
58 aborted: bool,
60 stats: Statistics,
62}
63
64impl FulltextIndexer {
65 pub async fn new(
67 region_id: &RegionId,
68 sst_file_id: &FileId,
69 intermediate_manager: &IntermediateManager,
70 metadata: &RegionMetadataRef,
71 compress: bool,
72 mem_limit: usize,
73 build_type: IndexBuildType,
74 ) -> Result<Option<Self>> {
75 let mut creators = HashMap::new();
76 let global_memory_usage = Arc::new(AtomicUsize::new(0));
77
78 for column in &metadata.column_metadatas {
79 if column.semantic_type == SemanticType::Tag {
83 common_telemetry::debug!(
84 "Skip creating fulltext index for tag column {}",
85 column.column_schema.name
86 );
87 continue;
88 }
89
90 let options = column
91 .column_schema
92 .fulltext_options()
93 .context(IndexOptionsSnafu {
94 column_name: &column.column_schema.name,
95 })?;
96
97 let mut targets = Vec::new();
98 if let Some(options) = options.filter(|options| options.enable) {
99 targets.push((IndexTarget::ColumnId(column.column_id), options));
100 }
101 if matches!(build_type, IndexBuildType::Flush | IndexBuildType::Compact)
105 && column.column_schema.data_type.is_json2()
106 && let Some(extension) = column
107 .column_schema
108 .extension_type::<Json2ExtensionType>()
109 .context(IndexOptionsSnafu {
110 column_name: &column.column_schema.name,
111 })?
112 {
113 for hint in extension.metadata().json_settings().type_hints() {
114 let Some(options) = hint.fulltext_index.as_ref().filter(|o| o.enable) else {
115 continue;
116 };
117 match IndexTarget::new_json_path(column.column_id, hint.path.clone()) {
118 Ok(target) => targets.push((target, options.clone())),
119 Err(err) => {
120 warn!(
121 err;
122 "Skip invalid JSON fulltext index target, column_id: {}, path: {:?}",
123 column.column_id,
124 hint.path
125 );
126 }
127 }
128 }
129 }
130 for (target, options) in targets {
131 let intm_path =
134 intermediate_manager.fulltext_path(region_id, sst_file_id, column.column_id);
135
136 let config = Config {
137 analyzer: match options.analyzer {
138 FulltextAnalyzer::English => Analyzer::English,
139 FulltextAnalyzer::Chinese => Analyzer::Chinese,
140 },
141 case_sensitive: options.case_sensitive,
142 };
143
144 let inner = match options.backend {
145 FulltextBackend::Tantivy => {
146 let creator =
147 TantivyFulltextIndexCreator::new(&intm_path, config, mem_limit)
148 .await
149 .context(CreateFulltextCreatorSnafu)?;
150 AltFulltextCreator::Tantivy(creator)
151 }
152 FulltextBackend::Bloom => {
153 let temp_file_provider = Arc::new(TempFileProvider::new(
154 IntermediateLocation::new(&metadata.region_id, sst_file_id),
155 intermediate_manager.clone(),
156 ));
157 let creator = BloomFilterFulltextIndexCreator::new(
158 config,
159 options.granularity as _,
160 options.false_positive_rate(),
161 temp_file_provider,
162 global_memory_usage.clone(),
163 Some(mem_limit),
164 );
165 AltFulltextCreator::Bloom(creator)
166 }
167 };
168
169 creators.insert(
170 target.clone(),
171 SingleCreator {
172 target,
173 column_name: column.column_schema.name.clone(),
174 inner,
175 compress,
176 },
177 );
178 }
179 }
180 Ok((!creators.is_empty()).then(move || Self {
181 creators,
182 aborted: false,
183 stats: Statistics::new(TYPE_FULLTEXT_INDEX),
184 }))
185 }
186
187 pub async fn update(&mut self, batch: &mut Batch) -> Result<()> {
189 ensure!(!self.aborted, OperateAbortedIndexSnafu);
190
191 if let Err(update_err) = self.do_update(batch).await {
192 if let Err(err) = self.do_abort().await {
193 if cfg!(any(test, feature = "test")) {
194 panic!("Failed to abort index creator, err: {err}");
195 } else {
196 warn!(err; "Failed to abort index creator");
197 }
198 }
199 return Err(update_err);
200 }
201
202 Ok(())
203 }
204
205 pub async fn update_flat(&mut self, batch: &RecordBatch) -> Result<()> {
207 ensure!(!self.aborted, OperateAbortedIndexSnafu);
208
209 if batch.num_rows() == 0 {
210 return Ok(());
211 }
212
213 if let Err(update_err) = self.do_update_flat(batch).await {
214 if let Err(err) = self.do_abort().await {
215 if cfg!(any(test, feature = "test")) {
216 panic!("Failed to abort index creator, err: {err}");
217 } else {
218 warn!(err; "Failed to abort index creator");
219 }
220 }
221 return Err(update_err);
222 }
223
224 Ok(())
225 }
226
227 pub async fn finish(
229 &mut self,
230 puffin_writer: &mut SstPuffinWriter,
231 ) -> Result<(RowCount, ByteCount)> {
232 ensure!(!self.aborted, OperateAbortedIndexSnafu);
233
234 match self.do_finish(puffin_writer).await {
235 Ok(()) => Ok((self.stats.row_count(), self.stats.byte_count())),
236 Err(finish_err) => {
237 if let Err(err) = self.do_abort().await {
238 if cfg!(any(test, feature = "test")) {
239 panic!("Failed to abort index creator, err: {err}");
240 } else {
241 warn!(err; "Failed to abort index creator");
242 }
243 }
244 Err(finish_err)
245 }
246 }
247 }
248
249 pub async fn abort(&mut self) -> Result<()> {
251 if self.aborted {
252 return Ok(());
253 }
254
255 self.do_abort().await
256 }
257
258 pub fn memory_usage(&self) -> usize {
260 self.creators.values().map(|c| c.inner.memory_usage()).sum()
261 }
262
263 pub fn indexed_targets(&self) -> impl Iterator<Item = &IndexTarget> {
265 self.creators.keys()
266 }
267
268 pub fn bloom_indexed_targets(&self) -> impl Iterator<Item = &IndexTarget> {
270 self.creators
271 .values()
272 .filter(|c| matches!(c.inner, AltFulltextCreator::Bloom(_)))
273 .map(|c| &c.target)
274 }
275}
276
277impl FulltextIndexer {
278 async fn do_update(&mut self, batch: &mut Batch) -> Result<()> {
279 let mut guard = self.stats.record_update();
280 guard.inc_row_count(batch.num_rows());
281
282 for creator in self.creators.values_mut() {
283 creator.update(batch).await?;
284 }
285
286 Ok(())
287 }
288
289 async fn do_update_flat(&mut self, batch: &RecordBatch) -> Result<()> {
290 let mut guard = self.stats.record_update();
291 guard.inc_row_count(batch.num_rows());
292
293 for creator in self.creators.values_mut() {
294 creator.update_flat(batch).await?;
295 }
296
297 Ok(())
298 }
299
300 async fn do_finish(&mut self, puffin_writer: &mut SstPuffinWriter) -> Result<()> {
301 let mut guard = self.stats.record_finish();
302
303 let mut written_bytes = 0;
304 for creator in self.creators.values_mut() {
305 written_bytes += creator.finish(puffin_writer).await?;
306 }
307
308 guard.inc_byte_count(written_bytes);
309 Ok(())
310 }
311
312 async fn do_abort(&mut self) -> Result<()> {
313 let _guard = self.stats.record_cleanup();
314
315 self.aborted = true;
316
317 for (_, mut creator) in self.creators.drain() {
318 creator.abort().await?;
319 }
320
321 Ok(())
322 }
323}
324
325struct SingleCreator {
327 target: IndexTarget,
329 column_name: String,
331 inner: AltFulltextCreator,
333 compress: bool,
335}
336
337impl SingleCreator {
338 async fn update(&mut self, batch: &mut Batch) -> Result<()> {
339 let column_id = match &self.target {
340 IndexTarget::ColumnId(id) => *id,
341 IndexTarget::JsonPath(path) => {
342 let root = batch
343 .field_col_value(path.column_id())
344 .with_context(|| InvalidRecordBatchSnafu {
345 reason: format!("Missing JSON fulltext column {}", path.column_id()),
346 })?
347 .data
348 .to_arrow_array();
349 let values = extract_json_type_hint_values(&root, path.path())?;
350 return self.push_json_values(values).await;
351 }
352 };
353 let text_column = batch.fields().iter().find(|c| c.column_id == column_id);
354 match text_column {
355 Some(column) => {
356 let data = column
357 .data
358 .cast(&ConcreteDataType::string_datatype())
359 .context(CastVectorSnafu {
360 from: column.data.data_type(),
361 to: ConcreteDataType::string_datatype(),
362 })?;
363
364 for i in 0..batch.num_rows() {
365 let data = data.get_ref(i);
366 let text = data
367 .try_into_string()
368 .context(DataTypeMismatchSnafu)?
369 .unwrap_or_default();
370 self.inner.push_text(text).await?;
371 }
372 }
373 _ => {
374 for _ in 0..batch.num_rows() {
378 self.inner.push_text("").await?;
379 }
380 }
381 }
382
383 Ok(())
384 }
385
386 async fn update_flat(&mut self, batch: &RecordBatch) -> Result<()> {
387 if let IndexTarget::JsonPath(path) = &self.target {
388 let root = batch.column_by_name(&self.column_name).with_context(|| {
389 InvalidRecordBatchSnafu {
390 reason: format!("Missing JSON fulltext column {}", self.column_name),
391 }
392 })?;
393 let values = extract_json_type_hint_values(root, path.path())?;
394 return self.push_json_values(values).await;
395 }
396 if let Some(column_array) = batch.column_by_name(&self.column_name) {
398 match column_array.data_type() {
401 DataType::Utf8 => {
402 let string_array = column_array.as_any().downcast_ref::<StringArray>().unwrap();
403 for text_opt in string_array.iter() {
404 let text = text_opt.unwrap_or_default();
405 self.inner.push_text(text).await?;
406 }
407 }
408 DataType::LargeUtf8 => {
409 let large_string_array = column_array
410 .as_any()
411 .downcast_ref::<LargeStringArray>()
412 .unwrap();
413 for text_opt in large_string_array.iter() {
414 let text = text_opt.unwrap_or_default();
415 self.inner.push_text(text).await?;
416 }
417 }
418 _ => {
419 let array = datatypes::arrow::compute::cast(column_array, &DataType::Utf8)
421 .context(ComputeArrowSnafu)?;
422 let string_array = array.as_any().downcast_ref::<StringArray>().unwrap();
423 for text_opt in string_array.iter() {
424 let text = text_opt.unwrap_or_default();
425 self.inner.push_text(text).await?;
426 }
427 }
428 }
429 } else {
430 for _ in 0..batch.num_rows() {
434 self.inner.push_text("").await?;
435 }
436 }
437
438 Ok(())
439 }
440
441 async fn push_json_values(&mut self, values: VectorRef) -> Result<()> {
442 for row in 0..values.len() {
443 let text = values
444 .get_ref(row)
445 .try_into_string()
446 .context(DataTypeMismatchSnafu)?
447 .unwrap_or_default();
448 self.inner.push_text(text).await?;
449 }
450 Ok(())
451 }
452
453 async fn finish(&mut self, puffin_writer: &mut SstPuffinWriter) -> Result<ByteCount> {
454 let options = PutOptions {
455 compression: self.compress.then_some(CompressionCodec::Zstd),
456 };
457 self.inner
458 .finish(puffin_writer, &self.target, options)
459 .await
460 }
461
462 async fn abort(&mut self) -> Result<()> {
463 self.inner.abort(&self.target).await;
464 Ok(())
465 }
466}
467
468#[allow(dead_code, clippy::large_enum_variant)]
469enum AltFulltextCreator {
471 Tantivy(TantivyFulltextIndexCreator),
472 Bloom(BloomFilterFulltextIndexCreator),
473}
474
475impl AltFulltextCreator {
476 async fn push_text(&mut self, text: &str) -> Result<()> {
477 match self {
478 Self::Tantivy(creator) => creator.push_text(text).await.context(FulltextPushTextSnafu),
479 Self::Bloom(creator) => creator.push_text(text).await.context(FulltextPushTextSnafu),
480 }
481 }
482
483 fn memory_usage(&self) -> usize {
484 match self {
485 Self::Tantivy(creator) => creator.memory_usage(),
486 Self::Bloom(creator) => creator.memory_usage(),
487 }
488 }
489
490 async fn finish(
491 &mut self,
492 puffin_writer: &mut SstPuffinWriter,
493 target: &IndexTarget,
494 put_options: PutOptions,
495 ) -> Result<ByteCount> {
496 let target_key = target.encode();
497 match self {
498 Self::Tantivy(creator) => {
499 let blob_key = format!("{INDEX_BLOB_TYPE_TANTIVY}-{target_key}");
500 creator
501 .finish(puffin_writer, &blob_key, put_options)
502 .await
503 .context(FulltextFinishSnafu)
504 }
505 Self::Bloom(creator) => {
506 let blob_key = format!("{INDEX_BLOB_TYPE_BLOOM}-{target_key}");
507 creator
508 .finish(puffin_writer, &blob_key, put_options)
509 .await
510 .context(FulltextFinishSnafu)
511 }
512 }
513 }
514
515 async fn abort(&mut self, target: &IndexTarget) {
516 match self {
517 Self::Tantivy(creator) => {
518 if let Err(err) = creator.abort().await {
519 warn!(err; "Failed to abort the fulltext index creator in the Tantivy flavor, target: {:?}", target);
520 }
521 }
522 Self::Bloom(creator) => {
523 if let Err(err) = creator.abort().await {
524 warn!(err; "Failed to abort the fulltext index creator in the Bloom Filter flavor, target: {:?}", target);
525 }
526 }
527 }
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use std::collections::{BTreeMap, BTreeSet};
534 use std::sync::Arc;
535
536 use api::v1::SemanticType;
537 use common_base::BitVec;
538 use datatypes::arrow::array::{ArrayRef, StructArray};
539 use datatypes::arrow::buffer::NullBuffer;
540 use datatypes::arrow::datatypes::{Field, Schema};
541 use datatypes::data_type::DataType;
542 use datatypes::extension::json::{Json2ExtensionType, JsonMetadata};
543 use datatypes::json::{JsonSettings, JsonTypeHint};
544 use datatypes::schema::{ColumnSchema, FulltextAnalyzer, FulltextOptions};
545 use datatypes::types::json_type::{JsonNativeType, JsonObjectType};
546 use datatypes::vectors::{Helper, UInt8Vector, UInt64Vector};
547 use futures::FutureExt;
548 use futures::future::BoxFuture;
549 use index::fulltext_index::search::RowId;
550 use object_store::ObjectStore;
551 use object_store::services::Memory;
552 use puffin::puffin_manager::{PuffinManager, PuffinWriter};
553 use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder, RegionMetadataRef};
554 use store_api::region_request::PathType;
555 use store_api::storage::{ColumnId, ConcreteDataType, FileId, RegionId};
556
557 use super::*;
558 use crate::access_layer::RegionFilePathFactory;
559 use crate::read::{Batch, BatchColumn};
560 use crate::sst::file::{RegionFileId, RegionIndexId};
561 use crate::sst::index::fulltext_index::applier::FulltextIndexApplier;
562 use crate::sst::index::fulltext_index::applier::builder::{
563 FulltextQuery, FulltextRequest, FulltextTerm,
564 };
565 use crate::sst::index::puffin_manager::PuffinManagerFactory;
566
567 #[tokio::test]
568 async fn test_json_fulltext_build_and_prune_both_formats() {
569 for backend in [FulltextBackend::Bloom, FulltextBackend::Tantivy] {
570 for (flat, build_type) in [
571 (false, IndexBuildType::Flush),
572 (false, IndexBuildType::Compact),
573 (true, IndexBuildType::Flush),
574 (true, IndexBuildType::Compact),
575 ] {
576 let (dir, factory) =
577 PuffinManagerFactory::new_for_test_async("json_fulltext").await;
578 let options = FulltextOptions::new(
579 true,
580 FulltextAnalyzer::English,
581 false,
582 backend.clone(),
583 1,
584 0.01,
585 )
586 .unwrap();
587 let hints = ["message", "other/\".field"].map(|name| JsonTypeHint {
588 path: vec![name.into()],
589 data_type: ConcreteDataType::string_datatype(),
590 inverted_index: false,
591 skipping_index: None,
592 fulltext_index: Some(options.clone()),
593 });
594 let mut column = ColumnSchema::new(
595 "attrs",
596 ConcreteDataType::json2(JsonNativeType::Object(JsonObjectType::new())),
597 true,
598 );
599 column.with_extension_type(&Json2ExtensionType::new(Arc::new(JsonMetadata::new(
600 JsonSettings::try_new(hints.to_vec(), Some(0)).unwrap(),
601 ))));
602 let mut builder = RegionMetadataBuilder::new(RegionId::new(1, 2));
603 builder.push_column_metadata(ColumnMetadata {
604 column_schema: column,
605 semantic_type: SemanticType::Field,
606 column_id: 1,
607 });
608 builder.push_column_metadata(ColumnMetadata {
609 column_schema: ColumnSchema::new(
610 "ts",
611 ConcreteDataType::timestamp_millisecond_datatype(),
612 false,
613 ),
614 semantic_type: SemanticType::Timestamp,
615 column_id: 2,
616 });
617 let metadata = Arc::new(builder.build().unwrap());
618 let file_id = FileId::random();
619 let manager = new_intm_mgr(dir.path().to_string_lossy()).await;
620 for rebuild_type in [IndexBuildType::Manual, IndexBuildType::SchemaChange] {
621 assert!(
622 FulltextIndexer::new(
623 &metadata.region_id,
624 &file_id,
625 &manager,
626 &metadata,
627 false,
628 1024,
629 rebuild_type,
630 )
631 .await
632 .unwrap()
633 .is_none()
634 );
635 }
636 let mut creator = FulltextIndexer::new(
637 &metadata.region_id,
638 &file_id,
639 &manager,
640 &metadata,
641 false,
642 1024,
643 build_type,
644 )
645 .await
646 .unwrap()
647 .unwrap();
648 let messages: ArrayRef = Arc::new(StringArray::from(vec![
649 Some("alpha"),
650 None,
651 Some("beta"),
652 Some("alpha"),
653 ]));
654 let other: ArrayRef = Arc::new(StringArray::from(vec![
655 Some("beta"),
656 None,
657 Some("alpha"),
658 Some(""),
659 ]));
660 let root: ArrayRef = Arc::new(StructArray::new(
661 vec![
662 Field::new("message", messages.data_type().clone(), true),
663 Field::new("other/\".field", other.data_type().clone(), true),
664 ]
665 .into(),
666 vec![messages, other],
667 Some(NullBuffer::from(vec![true, false, true, true])),
668 ));
669 for start in [0, 2] {
670 let array = root.slice(start, 2);
671 if flat {
672 let batch = RecordBatch::try_new(
673 Arc::new(Schema::new(vec![Field::new(
674 "attrs",
675 array.data_type().clone(),
676 true,
677 )])),
678 vec![array],
679 )
680 .unwrap();
681 creator.update_flat(&batch).await.unwrap();
682 } else {
683 let mut batch = Batch::new(
684 vec![],
685 Arc::new(UInt64Vector::from_vec(vec![0, 1])),
686 Arc::new(UInt64Vector::from_vec(vec![0, 0])),
687 Arc::new(UInt8Vector::from_vec(vec![1, 1])),
688 vec![BatchColumn {
689 column_id: 1,
690 data: Helper::try_into_vector(array).unwrap(),
691 }],
692 )
693 .unwrap();
694 creator.update(&mut batch).await.unwrap();
695 }
696 }
697 assert_eq!(creator.indexed_targets().count(), hints.len());
698 assert!(
699 creator
700 .indexed_targets()
701 .all(|target| matches!(target, IndexTarget::JsonPath(_)))
702 );
703 let bloom_targets: BTreeSet<_> = creator.bloom_indexed_targets().collect();
704 if backend == FulltextBackend::Bloom {
705 assert_eq!(bloom_targets, creator.indexed_targets().collect());
706 } else {
707 assert!(bloom_targets.is_empty());
708 }
709 let store = mock_object_store();
710 let puffin = factory.build(
711 store.clone(),
712 RegionFilePathFactory::new("json_test".into(), PathType::Bare),
713 );
714 let index_id =
715 RegionIndexId::new(RegionFileId::new(metadata.region_id, file_id), 0);
716 let mut writer = puffin.writer(&index_id).await.unwrap();
717 let (rows, bytes) = creator.finish(&mut writer).await.unwrap();
718 assert_eq!(rows, 4);
719 assert!(bytes > 0);
720 writer.finish().await.unwrap();
721 let cache = Arc::new(
722 crate::cache::index::bloom_filter_index::BloomFilterIndexCache::new(
723 1024 * 1024,
724 1024 * 1024,
725 1024,
726 ),
727 );
728 for (path, term, expected) in [
729 ("message", "alpha", vec![0, 3]),
730 ("other/\".field", "alpha", vec![2]),
731 ("message", "hidden", vec![]),
732 ("message", "absent", vec![]),
733 ] {
734 let target = IndexTarget::new_json_path(1, vec![path.into()]).unwrap();
735 let request = match backend {
736 FulltextBackend::Tantivy => FulltextRequest {
737 queries: vec![FulltextQuery(term.into())],
738 ..Default::default()
739 },
740 FulltextBackend::Bloom => FulltextRequest {
741 terms: vec![FulltextTerm {
742 col_lowered: false,
743 term: term.into(),
744 }],
745 ..Default::default()
746 },
747 };
748 let applier = FulltextIndexApplier::new(
749 "json_test".into(),
750 PathType::Bare,
751 store.clone(),
752 BTreeMap::from([
753 (target, request.clone()),
754 (
755 IndexTarget::new_json_path(1, vec!["missing".into()]).unwrap(),
756 request,
757 ),
758 ]),
759 factory.clone(),
760 &metadata,
761 )
762 .with_bloom_filter_cache(Some(cache.clone()));
763 let requests = applier.compatible_requests_for_sst(&metadata).unwrap();
764 assert_eq!(requests.len(), 1);
765 for change in 0..3 {
766 let mut changed_hints = hints.to_vec();
767 for hint in &mut changed_hints {
768 let options = hint.fulltext_index.as_mut().unwrap();
769 match change {
770 0 => options.analyzer = FulltextAnalyzer::Chinese,
771 1 => options.case_sensitive = true,
772 _ => {
773 options.backend = match backend {
774 FulltextBackend::Bloom => FulltextBackend::Tantivy,
775 FulltextBackend::Tantivy => FulltextBackend::Bloom,
776 }
777 }
778 }
779 }
780 let mut changed = (*metadata).clone();
781 changed.column_metadatas[0]
782 .column_schema
783 .with_extension_type(&Json2ExtensionType::new(Arc::new(
784 JsonMetadata::new(
785 JsonSettings::try_new(changed_hints, Some(0)).unwrap(),
786 ),
787 )));
788 assert_eq!(
789 applier.compatible_requests_for_sst(&changed).is_some(),
790 change == 0 && backend == FulltextBackend::Bloom,
791 );
792 }
793 assert!(
794 applier
795 .compatible_requests_for_sst(&mock_region_metadata(backend.clone()))
796 .is_none()
797 );
798 for _ in 0..2 {
799 match backend {
800 FulltextBackend::Tantivy => {
801 let rows = applier
802 .apply_fine(index_id, None, &requests, None)
803 .await
804 .unwrap()
805 .unwrap();
806 assert_eq!(
807 rows.into_iter().collect::<Vec<_>>(),
808 expected.iter().map(|v| *v as RowId).collect::<Vec<_>>()
809 );
810 }
811 FulltextBackend::Bloom => {
812 let ranges = applier
813 .apply_coarse(
814 index_id,
815 None,
816 [(4, true)].into_iter(),
817 &requests,
818 None,
819 )
820 .await
821 .unwrap()
822 .unwrap();
823 let rows: Vec<_> = ranges[0].1.iter().cloned().flatten().collect();
824 assert!(expected.iter().all(|row| rows.contains(row)));
826 if expected.is_empty() {
827 assert!(
828 rows.len() < 4,
829 "path={path}, term={term}, flat={flat}, rows={rows:?}"
830 );
831 }
832 }
833 }
834 }
835 }
836 }
837 }
838 }
839
840 fn mock_object_store() -> ObjectStore {
841 ObjectStore::new(Memory::default()).unwrap()
842 }
843
844 async fn new_intm_mgr(path: impl AsRef<str>) -> IntermediateManager {
845 IntermediateManager::init_fs(path).await.unwrap()
846 }
847
848 fn mock_region_metadata(backend: FulltextBackend) -> RegionMetadataRef {
849 let mut builder = RegionMetadataBuilder::new(RegionId::new(1, 2));
850 builder
851 .push_column_metadata(ColumnMetadata {
852 column_schema: ColumnSchema::new(
853 "text_english_case_sensitive",
854 ConcreteDataType::string_datatype(),
855 true,
856 )
857 .with_fulltext_options(FulltextOptions::new_unchecked(
858 true,
859 FulltextAnalyzer::English,
860 true,
861 backend.clone(),
862 1,
863 0.01,
864 ))
865 .unwrap(),
866 semantic_type: SemanticType::Field,
867 column_id: 1,
868 })
869 .push_column_metadata(ColumnMetadata {
870 column_schema: ColumnSchema::new(
871 "text_english_case_insensitive",
872 ConcreteDataType::string_datatype(),
873 true,
874 )
875 .with_fulltext_options(FulltextOptions::new_unchecked(
876 true,
877 FulltextAnalyzer::English,
878 false,
879 backend.clone(),
880 1,
881 0.01,
882 ))
883 .unwrap(),
884 semantic_type: SemanticType::Field,
885 column_id: 2,
886 })
887 .push_column_metadata(ColumnMetadata {
888 column_schema: ColumnSchema::new(
889 "text_chinese",
890 ConcreteDataType::string_datatype(),
891 true,
892 )
893 .with_fulltext_options(FulltextOptions::new_unchecked(
894 true,
895 FulltextAnalyzer::Chinese,
896 false,
897 backend.clone(),
898 1,
899 0.01,
900 ))
901 .unwrap(),
902 semantic_type: SemanticType::Field,
903 column_id: 3,
904 })
905 .push_column_metadata(ColumnMetadata {
906 column_schema: ColumnSchema::new(
907 "ts",
908 ConcreteDataType::timestamp_millisecond_datatype(),
909 false,
910 ),
911 semantic_type: SemanticType::Timestamp,
912 column_id: 4,
913 });
914
915 Arc::new(builder.build().unwrap())
916 }
917
918 fn new_batch(
919 rows: &[(
920 Option<&str>, Option<&str>, Option<&str>, )],
924 ) -> Batch {
925 let mut vec_english_sensitive =
926 ConcreteDataType::string_datatype().create_mutable_vector(0);
927 let mut vec_english_insensitive =
928 ConcreteDataType::string_datatype().create_mutable_vector(0);
929 let mut vec_chinese = ConcreteDataType::string_datatype().create_mutable_vector(0);
930
931 for (text_english_case_sensitive, text_english_case_insensitive, text_chinese) in rows {
932 match text_english_case_sensitive {
933 Some(s) => vec_english_sensitive.push_value_ref(&(*s).into()),
934 None => vec_english_sensitive.push_null(),
935 }
936 match text_english_case_insensitive {
937 Some(s) => vec_english_insensitive.push_value_ref(&(*s).into()),
938 None => vec_english_insensitive.push_null(),
939 }
940 match text_chinese {
941 Some(s) => vec_chinese.push_value_ref(&(*s).into()),
942 None => vec_chinese.push_null(),
943 }
944 }
945
946 let num_rows = vec_english_sensitive.len();
947 Batch::new(
948 vec![],
949 Arc::new(UInt64Vector::from_iter_values(
950 (0..num_rows).map(|n| n as u64),
951 )),
952 Arc::new(UInt64Vector::from_iter_values(std::iter::repeat_n(
953 0, num_rows,
954 ))),
955 Arc::new(UInt8Vector::from_iter_values(std::iter::repeat_n(
956 1, num_rows,
957 ))),
958 vec![
959 BatchColumn {
960 column_id: 1,
961 data: vec_english_sensitive.to_vector(),
962 },
963 BatchColumn {
964 column_id: 2,
965 data: vec_english_insensitive.to_vector(),
966 },
967 BatchColumn {
968 column_id: 3,
969 data: vec_chinese.to_vector(),
970 },
971 ],
972 )
973 .unwrap()
974 }
975
976 async fn build_fulltext_applier_factory(
985 prefix: &str,
986 backend: FulltextBackend,
987 rows: &[(
988 Option<&str>, Option<&str>, Option<&str>, )],
992 ) -> impl Fn(
993 Vec<(ColumnId, &str)>,
994 Vec<(ColumnId, Vec<(bool, &str)>)>,
995 Option<BitVec>,
996 ) -> BoxFuture<'static, Option<BTreeSet<RowId>>> {
997 let (d, factory) = PuffinManagerFactory::new_for_test_async(prefix).await;
998 let table_dir = "table0".to_string();
999 let sst_file_id = FileId::random();
1000 let object_store = mock_object_store();
1001 let region_metadata = mock_region_metadata(backend.clone());
1002 let intm_mgr = new_intm_mgr(d.path().to_string_lossy()).await;
1003
1004 let mut indexer = FulltextIndexer::new(
1005 ®ion_metadata.region_id,
1006 &sst_file_id,
1007 &intm_mgr,
1008 ®ion_metadata,
1009 true,
1010 1024,
1011 IndexBuildType::Flush,
1012 )
1013 .await
1014 .unwrap()
1015 .unwrap();
1016
1017 let mut batch = new_batch(rows);
1018 indexer.update(&mut batch).await.unwrap();
1019
1020 let puffin_manager = factory.build(
1021 object_store.clone(),
1022 RegionFilePathFactory::new(table_dir.clone(), PathType::Bare),
1023 );
1024 let region_file_id = RegionFileId::new(region_metadata.region_id, sst_file_id);
1025 let index_id = RegionIndexId::new(region_file_id, 0);
1026 let mut writer = puffin_manager.writer(&index_id).await.unwrap();
1027 let _ = indexer.finish(&mut writer).await.unwrap();
1028 writer.finish().await.unwrap();
1029
1030 move |queries: Vec<(ColumnId, &str)>,
1031 terms_requests: Vec<(ColumnId, Vec<(bool, &str)>)>,
1032 coarse_mask: Option<BitVec>| {
1033 let _d = &d;
1034 let table_dir = table_dir.clone();
1035 let object_store = object_store.clone();
1036 let factory = factory.clone();
1037
1038 let mut requests: BTreeMap<IndexTarget, FulltextRequest> = BTreeMap::new();
1039
1040 for (column_id, query) in queries {
1042 requests
1043 .entry(IndexTarget::ColumnId(column_id))
1044 .or_default()
1045 .queries
1046 .push(FulltextQuery(query.to_string()));
1047 }
1048
1049 for (column_id, terms) in terms_requests {
1051 let fulltext_terms = terms
1052 .into_iter()
1053 .map(|(col_lowered, term)| FulltextTerm {
1054 col_lowered,
1055 term: term.to_string(),
1056 })
1057 .collect::<Vec<_>>();
1058
1059 requests
1060 .entry(IndexTarget::ColumnId(column_id))
1061 .or_default()
1062 .terms
1063 .extend(fulltext_terms);
1064 }
1065
1066 let applier = FulltextIndexApplier::new(
1067 table_dir,
1068 PathType::Bare,
1069 object_store,
1070 requests,
1071 factory,
1072 ®ion_metadata,
1073 );
1074
1075 let requests = applier
1076 .compatible_requests_for_sst(®ion_metadata)
1077 .unwrap();
1078 let backend = backend.clone();
1079 async move {
1080 match backend {
1081 FulltextBackend::Tantivy => applier
1082 .apply_fine(index_id, None, &requests, None)
1083 .await
1084 .unwrap(),
1085 FulltextBackend::Bloom => {
1086 let coarse_mask = coarse_mask.unwrap_or_default();
1087 let row_groups = (0..coarse_mask.len()).map(|i| (1, coarse_mask[i]));
1088 let resp = applier
1090 .apply_coarse(index_id, None, row_groups, &requests, None)
1091 .await
1092 .unwrap();
1093 resp.map(|r| {
1094 r.into_iter()
1095 .filter(|(_, ranges)| !ranges.is_empty())
1096 .map(|(row_group_id, _)| row_group_id as RowId)
1097 .collect()
1098 })
1099 }
1100 }
1101 }
1102 .boxed()
1103 }
1104 }
1105
1106 fn rows(row_ids: impl IntoIterator<Item = RowId>) -> BTreeSet<RowId> {
1107 row_ids.into_iter().collect()
1108 }
1109
1110 #[tokio::test]
1111 async fn test_fulltext_bloom_shared_memory_spills() {
1112 let dir = common_test_util::temp_dir::create_temp_dir("shared_fulltext_memory");
1113 let metadata = mock_region_metadata(FulltextBackend::Bloom);
1114 let manager = new_intm_mgr(dir.path().to_string_lossy()).await;
1115 let mut indexer = FulltextIndexer::new(
1116 &metadata.region_id,
1117 &FileId::random(),
1118 &manager,
1119 &metadata,
1120 false,
1121 2 * 1024 * 1024,
1122 IndexBuildType::Flush,
1123 )
1124 .await
1125 .unwrap()
1126 .unwrap();
1127 for creator in indexer.creators.values_mut() {
1130 for row in 0..20_000 {
1131 creator.inner.push_text(&format!("row{row}")).await.unwrap();
1132 }
1133 }
1134 let mut directories = vec![dir.path().join("__intm")];
1135 let mut spilled = false;
1136 while let Some(directory) = directories.pop() {
1137 for entry in std::fs::read_dir(directory).unwrap() {
1138 let entry = entry.unwrap();
1139 if entry.file_type().unwrap().is_dir() {
1140 directories.push(entry.path());
1141 } else {
1142 spilled = true;
1143 }
1144 }
1145 }
1146 assert!(spilled, "combined memory usage must trigger spilling");
1147 indexer.abort().await.unwrap();
1148 }
1149
1150 #[tokio::test]
1151 async fn test_fulltext_index_basic_case_sensitive_tantivy() {
1152 let applier_factory = build_fulltext_applier_factory(
1153 "test_fulltext_index_basic_case_sensitive_tantivy_",
1154 FulltextBackend::Tantivy,
1155 &[
1156 (Some("hello"), None, None),
1157 (Some("world"), None, None),
1158 (None, None, None),
1159 (Some("Hello, World"), None, None),
1160 ],
1161 )
1162 .await;
1163
1164 let row_ids = applier_factory(vec![(1, "hello")], vec![], None).await;
1165 assert_eq!(row_ids, Some(rows([0])));
1166
1167 let row_ids = applier_factory(vec![(1, "world")], vec![], None).await;
1168 assert_eq!(row_ids, Some(rows([1])));
1169
1170 let row_ids = applier_factory(vec![(1, "Hello")], vec![], None).await;
1171 assert_eq!(row_ids, Some(rows([3])));
1172
1173 let row_ids = applier_factory(vec![(1, "World")], vec![], None).await;
1174 assert_eq!(row_ids, Some(rows([3])));
1175
1176 let row_ids = applier_factory(vec![], vec![(1, vec![(false, "hello")])], None).await;
1178 assert_eq!(row_ids, None);
1179 }
1180
1181 #[tokio::test]
1182 async fn test_fulltext_index_basic_case_sensitive_bloom() {
1183 let applier_factory = build_fulltext_applier_factory(
1184 "test_fulltext_index_basic_case_sensitive_bloom_",
1185 FulltextBackend::Bloom,
1186 &[
1187 (Some("hello"), None, None),
1188 (Some("world"), None, None),
1189 (None, None, None),
1190 (Some("Hello, World"), None, None),
1191 ],
1192 )
1193 .await;
1194
1195 let row_ids = applier_factory(
1196 vec![],
1197 vec![(1, vec![(false, "hello")])],
1198 Some(BitVec::from_slice(&[0b1111])),
1199 )
1200 .await;
1201 assert_eq!(row_ids, Some(rows([0])));
1202
1203 let row_ids = applier_factory(
1204 vec![],
1205 vec![(1, vec![(false, "hello")])],
1206 Some(BitVec::from_slice(&[0b1110])), )
1208 .await;
1209 assert_eq!(row_ids, Some(rows([])));
1210
1211 let row_ids = applier_factory(
1212 vec![],
1213 vec![(1, vec![(true, "hello")])],
1214 Some(BitVec::from_slice(&[0b1111])),
1215 )
1216 .await;
1217 assert_eq!(row_ids, None);
1218
1219 let row_ids = applier_factory(
1220 vec![],
1221 vec![(1, vec![(false, "world")])],
1222 Some(BitVec::from_slice(&[0b1111])),
1223 )
1224 .await;
1225 assert_eq!(row_ids, Some(rows([1])));
1226
1227 let row_ids = applier_factory(
1228 vec![],
1229 vec![(1, vec![(false, "world")])],
1230 Some(BitVec::from_slice(&[0b1101])), )
1232 .await;
1233 assert_eq!(row_ids, Some(rows([])));
1234
1235 let row_ids = applier_factory(
1236 vec![],
1237 vec![(1, vec![(true, "world")])],
1238 Some(BitVec::from_slice(&[0b1111])),
1239 )
1240 .await;
1241 assert_eq!(row_ids, None);
1242
1243 let row_ids = applier_factory(
1244 vec![],
1245 vec![(1, vec![(false, "Hello")])],
1246 Some(BitVec::from_slice(&[0b1111])),
1247 )
1248 .await;
1249 assert_eq!(row_ids, Some(rows([3])));
1250
1251 let row_ids = applier_factory(
1252 vec![],
1253 vec![(1, vec![(false, "Hello")])],
1254 Some(BitVec::from_slice(&[0b0111])), )
1256 .await;
1257 assert_eq!(row_ids, Some(rows([])));
1258
1259 let row_ids = applier_factory(
1260 vec![],
1261 vec![(1, vec![(true, "Hello")])],
1262 Some(BitVec::from_slice(&[0b1111])),
1263 )
1264 .await;
1265 assert_eq!(row_ids, None);
1266
1267 let row_ids = applier_factory(
1268 vec![],
1269 vec![(1, vec![(false, "Hello, World")])],
1270 Some(BitVec::from_slice(&[0b1111])),
1271 )
1272 .await;
1273 assert_eq!(row_ids, Some(rows([3])));
1274
1275 let row_ids = applier_factory(
1276 vec![],
1277 vec![(1, vec![(false, "Hello, World")])],
1278 Some(BitVec::from_slice(&[0b0111])), )
1280 .await;
1281 assert_eq!(row_ids, Some(rows([])));
1282
1283 let row_ids = applier_factory(
1284 vec![],
1285 vec![(1, vec![(true, "Hello, World")])],
1286 Some(BitVec::from_slice(&[0b1111])),
1287 )
1288 .await;
1289 assert_eq!(row_ids, None);
1290 }
1291
1292 #[tokio::test]
1293 async fn test_fulltext_index_basic_case_insensitive_tantivy() {
1294 let applier_factory = build_fulltext_applier_factory(
1295 "test_fulltext_index_basic_case_insensitive_tantivy_",
1296 FulltextBackend::Tantivy,
1297 &[
1298 (None, Some("hello"), None),
1299 (None, None, None),
1300 (None, Some("world"), None),
1301 (None, Some("Hello, World"), None),
1302 ],
1303 )
1304 .await;
1305
1306 let row_ids = applier_factory(vec![(2, "hello")], vec![], None).await;
1307 assert_eq!(row_ids, Some(rows([0, 3])));
1308
1309 let row_ids = applier_factory(vec![(2, "world")], vec![], None).await;
1310 assert_eq!(row_ids, Some(rows([2, 3])));
1311
1312 let row_ids = applier_factory(vec![(2, "Hello")], vec![], None).await;
1313 assert_eq!(row_ids, Some(rows([0, 3])));
1314
1315 let row_ids = applier_factory(vec![(2, "World")], vec![], None).await;
1316 assert_eq!(row_ids, Some(rows([2, 3])));
1317 }
1318
1319 #[tokio::test]
1320 async fn test_fulltext_index_basic_case_insensitive_bloom() {
1321 let applier_factory = build_fulltext_applier_factory(
1322 "test_fulltext_index_basic_case_insensitive_bloom_",
1323 FulltextBackend::Bloom,
1324 &[
1325 (None, Some("hello"), None),
1326 (None, None, None),
1327 (None, Some("world"), None),
1328 (None, Some("Hello, World"), None),
1329 ],
1330 )
1331 .await;
1332
1333 let row_ids = applier_factory(
1334 vec![],
1335 vec![(2, vec![(false, "hello")])],
1336 Some(BitVec::from_slice(&[0b1111])),
1337 )
1338 .await;
1339 assert_eq!(row_ids, Some(rows([0, 3])));
1340
1341 let row_ids = applier_factory(
1342 vec![],
1343 vec![(2, vec![(false, "hello")])],
1344 Some(BitVec::from_slice(&[0b1110])), )
1346 .await;
1347 assert_eq!(row_ids, Some(rows([3])));
1348
1349 let row_ids = applier_factory(
1350 vec![],
1351 vec![(2, vec![(false, "world")])],
1352 Some(BitVec::from_slice(&[0b1111])),
1353 )
1354 .await;
1355 assert_eq!(row_ids, Some(rows([2, 3])));
1356
1357 let row_ids = applier_factory(
1358 vec![],
1359 vec![(2, vec![(false, "world")])],
1360 Some(BitVec::from_slice(&[0b1011])), )
1362 .await;
1363 assert_eq!(row_ids, Some(rows([3])));
1364
1365 let row_ids = applier_factory(
1366 vec![],
1367 vec![(2, vec![(false, "Hello")])],
1368 Some(BitVec::from_slice(&[0b1111])),
1369 )
1370 .await;
1371 assert_eq!(row_ids, Some(rows([0, 3])));
1372
1373 let row_ids = applier_factory(
1374 vec![],
1375 vec![(2, vec![(false, "Hello")])],
1376 Some(BitVec::from_slice(&[0b0111])), )
1378 .await;
1379 assert_eq!(row_ids, Some(rows([0])));
1380
1381 let row_ids = applier_factory(
1382 vec![],
1383 vec![(2, vec![(false, "World")])],
1384 Some(BitVec::from_slice(&[0b1111])),
1385 )
1386 .await;
1387 assert_eq!(row_ids, Some(rows([2, 3])));
1388
1389 let row_ids = applier_factory(
1390 vec![],
1391 vec![(2, vec![(false, "World")])],
1392 Some(BitVec::from_slice(&[0b0111])), )
1394 .await;
1395 assert_eq!(row_ids, Some(rows([2])));
1396
1397 let row_ids = applier_factory(
1398 vec![],
1399 vec![(2, vec![(true, "hello")])],
1400 Some(BitVec::from_slice(&[0b1111])),
1401 )
1402 .await;
1403 assert_eq!(row_ids, Some(rows([0, 3])));
1404 }
1405
1406 #[tokio::test]
1407 async fn test_fulltext_index_basic_chinese_tantivy() {
1408 let applier_factory = build_fulltext_applier_factory(
1409 "test_fulltext_index_basic_chinese_tantivy_",
1410 FulltextBackend::Tantivy,
1411 &[
1412 (None, None, Some("你好")),
1413 (None, None, None),
1414 (None, None, Some("世界")),
1415 (None, None, Some("你好,世界")),
1416 ],
1417 )
1418 .await;
1419
1420 let row_ids = applier_factory(vec![(3, "你好")], vec![], None).await;
1421 assert_eq!(row_ids, Some(rows([0, 3])));
1422
1423 let row_ids = applier_factory(vec![(3, "世界")], vec![], None).await;
1424 assert_eq!(row_ids, Some(rows([2, 3])));
1425 }
1426
1427 #[tokio::test]
1428 async fn test_fulltext_index_basic_chinese_bloom() {
1429 let applier_factory = build_fulltext_applier_factory(
1430 "test_fulltext_index_basic_chinese_bloom_",
1431 FulltextBackend::Bloom,
1432 &[
1433 (None, None, Some("你好")),
1434 (None, None, None),
1435 (None, None, Some("世界")),
1436 (None, None, Some("你好,世界")),
1437 ],
1438 )
1439 .await;
1440
1441 let row_ids = applier_factory(
1442 vec![],
1443 vec![(3, vec![(false, "你好")])],
1444 Some(BitVec::from_slice(&[0b1111])),
1445 )
1446 .await;
1447 assert_eq!(row_ids, Some(rows([0, 3])));
1448
1449 let row_ids = applier_factory(
1450 vec![],
1451 vec![(3, vec![(false, "你好")])],
1452 Some(BitVec::from_slice(&[0b1110])), )
1454 .await;
1455 assert_eq!(row_ids, Some(rows([3])));
1456
1457 let row_ids = applier_factory(
1458 vec![],
1459 vec![(3, vec![(false, "世界")])],
1460 Some(BitVec::from_slice(&[0b1111])),
1461 )
1462 .await;
1463 assert_eq!(row_ids, Some(rows([2, 3])));
1464
1465 let row_ids = applier_factory(
1466 vec![],
1467 vec![(3, vec![(false, "世界")])],
1468 Some(BitVec::from_slice(&[0b1011])), )
1470 .await;
1471 assert_eq!(row_ids, Some(rows([3])));
1472 }
1473
1474 #[tokio::test]
1475 async fn test_fulltext_index_multi_terms_case_sensitive_bloom() {
1476 let applier_factory = build_fulltext_applier_factory(
1477 "test_fulltext_index_multi_terms_case_sensitive_bloom_",
1478 FulltextBackend::Bloom,
1479 &[
1480 (Some("Hello"), None, None),
1481 (Some("World"), None, None),
1482 (None, None, None),
1483 (Some("Hello, World"), None, None),
1484 ],
1485 )
1486 .await;
1487
1488 let row_ids = applier_factory(
1489 vec![],
1490 vec![(1, vec![(false, "hello"), (false, "world")])],
1491 Some(BitVec::from_slice(&[0b1111])),
1492 )
1493 .await;
1494 assert_eq!(row_ids, Some(rows([])));
1495
1496 let row_ids = applier_factory(
1497 vec![],
1498 vec![(1, vec![(false, "Hello"), (false, "World")])],
1499 Some(BitVec::from_slice(&[0b1111])),
1500 )
1501 .await;
1502 assert_eq!(row_ids, Some(rows([3])));
1503
1504 let row_ids = applier_factory(
1505 vec![],
1506 vec![(1, vec![(true, "Hello"), (false, "World")])],
1507 Some(BitVec::from_slice(&[0b1111])),
1508 )
1509 .await;
1510 assert_eq!(row_ids, Some(rows([1, 3])));
1511
1512 let row_ids = applier_factory(
1513 vec![],
1514 vec![(1, vec![(false, "Hello"), (true, "World")])],
1515 Some(BitVec::from_slice(&[0b1111])),
1516 )
1517 .await;
1518 assert_eq!(row_ids, Some(rows([0, 3])));
1519
1520 let row_ids = applier_factory(
1521 vec![],
1522 vec![(1, vec![(true, "Hello"), (true, "World")])],
1523 Some(BitVec::from_slice(&[0b1111])),
1524 )
1525 .await;
1526 assert_eq!(row_ids, None);
1527 }
1528
1529 #[tokio::test]
1530 async fn test_fulltext_index_multi_terms_case_insensitive_bloom() {
1531 let applier_factory = build_fulltext_applier_factory(
1532 "test_fulltext_index_multi_terms_case_insensitive_bloom_",
1533 FulltextBackend::Bloom,
1534 &[
1535 (None, Some("hello"), None),
1536 (None, None, None),
1537 (None, Some("world"), None),
1538 (None, Some("Hello, World"), None),
1539 ],
1540 )
1541 .await;
1542
1543 let row_ids = applier_factory(
1544 vec![],
1545 vec![(2, vec![(false, "hello"), (false, "world")])],
1546 Some(BitVec::from_slice(&[0b1111])),
1547 )
1548 .await;
1549 assert_eq!(row_ids, Some(rows([3])));
1550
1551 let row_ids = applier_factory(
1552 vec![],
1553 vec![(2, vec![(true, "hello"), (false, "world")])],
1554 Some(BitVec::from_slice(&[0b1111])),
1555 )
1556 .await;
1557 assert_eq!(row_ids, Some(rows([3])));
1558
1559 let row_ids = applier_factory(
1560 vec![],
1561 vec![(2, vec![(false, "hello"), (true, "world")])],
1562 Some(BitVec::from_slice(&[0b1111])),
1563 )
1564 .await;
1565 assert_eq!(row_ids, Some(rows([3])));
1566
1567 let row_ids = applier_factory(
1568 vec![],
1569 vec![(2, vec![(true, "hello"), (true, "world")])],
1570 Some(BitVec::from_slice(&[0b1111])),
1571 )
1572 .await;
1573 assert_eq!(row_ids, Some(rows([3])));
1574 }
1575
1576 #[tokio::test]
1577 async fn test_fulltext_index_multi_columns_tantivy() {
1578 let applier_factory = build_fulltext_applier_factory(
1579 "test_fulltext_index_multi_columns_tantivy_",
1580 FulltextBackend::Tantivy,
1581 &[
1582 (Some("Hello"), None, Some("你好")),
1583 (Some("World"), Some("world"), None),
1584 (None, Some("World"), Some("世界")),
1585 (
1586 Some("Hello, World"),
1587 Some("Hello, World"),
1588 Some("你好,世界"),
1589 ),
1590 ],
1591 )
1592 .await;
1593
1594 let row_ids = applier_factory(vec![(1, "Hello"), (3, "你好")], vec![], None).await;
1595 assert_eq!(row_ids, Some(rows([0, 3])));
1596
1597 let row_ids = applier_factory(vec![(1, "World"), (2, "World")], vec![], None).await;
1598 assert_eq!(row_ids, Some(rows([1, 3])));
1599 }
1600
1601 #[tokio::test]
1602 async fn test_fulltext_index_multi_columns_bloom() {
1603 let applier_factory = build_fulltext_applier_factory(
1604 "test_fulltext_index_multi_columns_bloom_",
1605 FulltextBackend::Bloom,
1606 &[
1607 (Some("Hello"), None, Some("你好")),
1608 (Some("World"), Some("world"), None),
1609 (None, Some("World"), Some("世界")),
1610 (
1611 Some("Hello, World"),
1612 Some("Hello, World"),
1613 Some("你好,世界"),
1614 ),
1615 ],
1616 )
1617 .await;
1618
1619 let row_ids = applier_factory(
1620 vec![],
1621 vec![
1622 (1, vec![(false, "Hello")]),
1623 (2, vec![(false, "world")]),
1624 (3, vec![(false, "你好")]),
1625 ],
1626 Some(BitVec::from_slice(&[0b1111])),
1627 )
1628 .await;
1629 assert_eq!(row_ids, Some(rows([3])));
1630
1631 let row_ids = applier_factory(
1632 vec![],
1633 vec![(1, vec![(false, "World")]), (2, vec![(false, "World")])],
1634 Some(BitVec::from_slice(&[0b1111])),
1635 )
1636 .await;
1637 assert_eq!(row_ids, Some(rows([1, 3])));
1638 }
1639}