1use crate::backend::StorageBackend;
7use crate::backend::table_names;
8use crate::backend::types::ScanRequest;
9#[cfg(feature = "lance-backend")]
10use crate::storage::inverted_index::InvertedIndex;
11#[cfg(feature = "lance-backend")]
12use crate::storage::sparse_index::SparseVectorIndex;
13use crate::storage::vertex::VertexDataset;
14use anyhow::{Result, anyhow};
15use chrono::{DateTime, Utc};
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18#[cfg(feature = "lance-backend")]
19use std::collections::HashSet;
20use std::sync::Arc;
21#[cfg(feature = "lance-backend")]
22use tracing::{debug, info, instrument, warn};
23use uni_common::core::id::Vid;
24#[cfg(feature = "lance-backend")]
25use uni_common::core::schema::IndexDefinition;
26use uni_common::core::schema::SchemaManager;
27#[cfg(feature = "lance-backend")]
28use uni_common::core::schema::{
29 DistanceMetric, FullTextIndexConfig, InvertedIndexConfig, JsonFtsIndexConfig,
30 ScalarIndexConfig, ScalarIndexType, SparseVectorIndexConfig, VectorIndexConfig,
31 VectorIndexType,
32};
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub enum IndexRebuildStatus {
37 Pending,
39 InProgress,
41 Completed,
43 Failed,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct IndexRebuildTask {
50 pub id: String,
52 pub label: String,
54 pub status: IndexRebuildStatus,
56 pub created_at: DateTime<Utc>,
58 pub started_at: Option<DateTime<Utc>>,
60 pub completed_at: Option<DateTime<Utc>>,
62 pub error: Option<String>,
64 pub retry_count: u32,
66}
67
68fn resolve_vector_dim(t: &uni_common::DataType) -> Option<usize> {
73 match t {
74 uni_common::DataType::Vector { dimensions } => Some(*dimensions),
75 uni_common::DataType::List(inner) => resolve_vector_dim(inner),
76 _ => None,
77 }
78}
79
80#[cfg(feature = "lance-backend")]
90fn to_backend_vector_params(
91 metric: DistanceMetric,
92 index_type: &VectorIndexType,
93) -> Result<crate::backend::types::VectorIndexParams> {
94 use crate::backend::types::{VectorIndexKind, VectorIndexParams};
95 let backend_metric = match metric {
98 DistanceMetric::L2 => crate::backend::types::DistanceMetric::L2,
99 DistanceMetric::Cosine => crate::backend::types::DistanceMetric::Cosine,
100 DistanceMetric::Dot => crate::backend::types::DistanceMetric::Dot,
101 DistanceMetric::L1 => {
103 return Err(anyhow!(
104 "L1/Manhattan distance does not support an ANN vector index — L1 columns \
105 are searched exact/brute-force. Declare the column without a vector index \
106 (or use cosine/l2/dot if you need ANN)."
107 ));
108 }
109 DistanceMetric::Hamming | DistanceMetric::Jaccard => {
112 return Err(anyhow!(
113 "{metric:?} distance does not support a vector index — it applies to \
114 BinaryVector columns and is computed exactly via \
115 VECTOR_DISTANCE(a, b, 'hamming'|'jaccard'). Declare the column without a \
116 vector index."
117 ));
118 }
119 other => return Err(anyhow!("Unsupported vector index metric: {:?}", other)),
120 };
121 let kind = match index_type {
122 VectorIndexType::Flat => VectorIndexKind::Flat,
123 VectorIndexType::IvfFlat { num_partitions } => VectorIndexKind::IvfFlat {
124 num_partitions: *num_partitions,
125 },
126 VectorIndexType::IvfPq {
127 num_partitions,
128 num_sub_vectors,
129 bits_per_subvector,
130 } => VectorIndexKind::IvfPq {
131 num_partitions: *num_partitions,
132 num_sub_vectors: *num_sub_vectors,
133 num_bits: *bits_per_subvector,
134 },
135 VectorIndexType::IvfSq { num_partitions } => VectorIndexKind::IvfSq {
136 num_partitions: *num_partitions,
137 },
138 VectorIndexType::IvfRq {
139 num_partitions,
140 num_bits,
141 } => VectorIndexKind::IvfRq {
142 num_partitions: *num_partitions,
143 num_bits: *num_bits,
144 },
145 VectorIndexType::HnswFlat {
146 m,
147 ef_construction,
148 num_partitions,
149 } => VectorIndexKind::HnswFlat {
150 m: *m,
151 ef_construction: *ef_construction,
152 num_partitions: num_partitions.unwrap_or(1),
153 },
154 VectorIndexType::HnswSq {
155 m,
156 ef_construction,
157 num_partitions,
158 } => VectorIndexKind::HnswSq {
159 m: *m,
160 ef_construction: *ef_construction,
161 num_partitions: num_partitions.unwrap_or(1),
162 },
163 VectorIndexType::HnswPq {
164 m,
165 ef_construction,
166 num_sub_vectors,
167 num_partitions,
168 } => VectorIndexKind::HnswPq {
169 m: *m,
170 ef_construction: *ef_construction,
171 num_sub_vectors: *num_sub_vectors,
172 num_partitions: num_partitions.unwrap_or(1),
173 },
174 VectorIndexType::Muvera { .. } => {
175 return Err(anyhow!(
176 "MUVERA must be resolved to its inner index type before the physical build"
177 ));
178 }
179 other => return Err(anyhow!("Unsupported vector index type: {:?}", other)),
180 };
181 Ok(VectorIndexParams {
182 metric: backend_metric,
183 kind,
184 })
185}
186
187pub struct IndexManager {
189 base_uri: String,
190 schema_manager: Arc<SchemaManager>,
191 backend: Option<Arc<dyn StorageBackend>>,
195}
196
197impl std::fmt::Debug for IndexManager {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 f.debug_struct("IndexManager")
200 .field("base_uri", &self.base_uri)
201 .finish_non_exhaustive()
202 }
203}
204
205impl IndexManager {
206 pub fn new(base_uri: &str, schema_manager: Arc<SchemaManager>) -> Self {
209 Self {
210 base_uri: base_uri.to_string(),
211 schema_manager,
212 backend: None,
213 }
214 }
215
216 pub fn with_backend(mut self, backend: Arc<dyn StorageBackend>) -> Self {
218 self.backend = Some(backend);
219 self
220 }
221
222 #[cfg(feature = "lance-backend")]
232 async fn postings_write_guard(
233 &self,
234 postings_path: &str,
235 ) -> Option<crate::backend::traits::TableWriteGuard> {
236 match self.backend.as_ref() {
237 Some(backend) => Some(backend.lock_table_for_write(postings_path).await),
238 None => None,
239 }
240 }
241
242 #[cfg(feature = "lance-backend")]
244 #[instrument(skip(self), level = "info")]
245 pub async fn create_inverted_index(&self, config: InvertedIndexConfig) -> Result<()> {
246 let label = &config.label;
247 let property = &config.property;
248 info!(
249 "Creating Inverted Index '{}' on {}.{}",
250 config.name, label, property
251 );
252
253 let schema = self.schema_manager.schema();
254 if !schema.labels.contains_key(label) {
255 return Err(anyhow!("Label '{}' not found", label));
256 }
257
258 let postings_path = format!("{}/indexes/{}/{}_inverted", self.base_uri, label, property);
261 let _postings_guard = self.postings_write_guard(&postings_path).await;
262
263 let mut index = InvertedIndex::new(&self.base_uri, config.clone()).await?;
264
265 let table = table_names::vertex_table_name(label);
271 if let Some(backend) = self.backend.as_ref() {
272 if backend.table_exists(&table).await? {
273 let batches = backend.scan(ScanRequest::all(&table)).await?;
274 index
275 .build_from_batches(&batches, |n| info!("Indexed {} terms", n))
276 .await?;
277 } else {
278 debug!(
279 "Table '{}' not flushed yet; creating empty inverted index (populated on flush)",
280 table
281 );
282 }
283 } else {
284 warn!(
285 "No storage backend available; inverted index '{}' left empty (populated on flush)",
286 config.name
287 );
288 }
289
290 self.schema_manager
291 .add_index(IndexDefinition::Inverted(config))?;
292 self.schema_manager.save().await?;
293
294 Ok(())
295 }
296
297 #[cfg(feature = "lance-backend")]
307 #[instrument(skip(self), level = "info")]
308 pub async fn create_vector_index(&self, config: VectorIndexConfig) -> Result<()> {
309 self.create_vector_index_inner(config, false).await
310 }
311
312 #[cfg(feature = "lance-backend")]
324 async fn create_vector_index_inner(
325 &self,
326 config: VectorIndexConfig,
327 force_backfill: bool,
328 ) -> Result<()> {
329 if let VectorIndexType::Muvera { inner, .. } = &config.index_type {
330 self.prepare_muvera_fde(&config, force_backfill).await?;
332 let inner_cfg = VectorIndexConfig {
333 name: config.name.clone(),
334 label: config.label.clone(),
335 property: crate::storage::muvera_index::fde_derived_column(&config.name),
336 index_type: (**inner).clone(),
337 metric: DistanceMetric::Dot,
338 embedding_config: None,
339 metadata: config.metadata.clone(),
340 };
341 self.build_physical_vector_index(&inner_cfg).await?;
342 } else {
343 self.build_physical_vector_index(&config).await?;
344 }
345 self.schema_manager
346 .add_index(IndexDefinition::Vector(config))?;
347 self.schema_manager.save().await?;
348 Ok(())
349 }
350
351 #[cfg(feature = "lance-backend")]
366 async fn prepare_muvera_fde(
367 &self,
368 config: &VectorIndexConfig,
369 force_backfill: bool,
370 ) -> Result<()> {
371 use crate::storage::muvera_index::fde_spec_for_config;
372
373 let schema = self.schema_manager.schema();
374 let Some(spec) = fde_spec_for_config(&schema, config) else {
375 return Ok(());
376 };
377 spec.params.validate()?;
378
379 let newly_added = self.schema_manager.add_internal_property(
383 &spec.label,
384 &spec.derived_col,
385 uni_common::DataType::Vector {
386 dimensions: spec.params.fde_dim(),
387 },
388 true,
389 )?;
390
391 if !newly_added && !force_backfill {
395 return Ok(());
396 }
397
398 if let Err(e) = self.backfill_fde_column(&spec).await {
410 if newly_added {
411 let _ = self
412 .schema_manager
413 .drop_property(&spec.label, &spec.derived_col);
414 }
415 return Err(e);
416 }
417 Ok(())
418 }
419
420 #[cfg(feature = "lance-backend")]
427 async fn backfill_fde_column(
428 &self,
429 spec: &crate::storage::muvera_index::FdeSpec,
430 ) -> Result<()> {
431 use crate::storage::muvera_index::splice_fde_batch;
432
433 let Some(backend) = self.backend.as_ref() else {
434 return Ok(());
435 };
436 let table = table_names::vertex_table_name(&spec.label);
437 if !backend.table_exists(&table).await? {
441 return Ok(());
442 }
443
444 let schema = self.schema_manager.schema();
445 let label_id = schema
446 .label_id_by_name(&spec.label)
447 .ok_or_else(|| anyhow!("MUVERA: label '{}' not found", spec.label))?;
448 let target_schema =
451 VertexDataset::new(&self.base_uri, &spec.label, label_id).get_arrow_schema(&schema)?;
452 let source_dt = schema
453 .properties
454 .get(&spec.label)
455 .and_then(|p| p.get(&spec.source_prop))
456 .map(|m| m.r#type.clone());
457 let encoder = uni_common::muvera::FdeEncoder::new(&spec.params)?;
458
459 let _table_guard = backend.lock_table_for_write(&table).await;
468
469 let batches = backend.scan(ScanRequest::all(&table)).await?;
470 let mut new_batches = Vec::with_capacity(batches.len());
471 for batch in &batches {
472 new_batches.push(splice_fde_batch(
473 batch,
474 &target_schema,
475 spec,
476 &encoder,
477 source_dt.as_ref(),
478 )?);
479 }
480 backend
481 .replace_table_atomic(&table, new_batches, target_schema)
482 .await?;
483 Ok(())
484 }
485
486 #[cfg(feature = "lance-backend")]
491 async fn build_physical_vector_index(&self, config: &VectorIndexConfig) -> Result<()> {
492 let label = &config.label;
493 let property = &config.property;
494 info!(
495 "Creating vector index '{}' on {}.{}",
496 config.name, label, property
497 );
498
499 let schema = self.schema_manager.schema();
500 if !schema.labels.contains_key(label) {
501 return Err(anyhow!("Label '{}' not found", label));
502 }
503
504 if matches!(config.metric, DistanceMetric::L1) {
509 info!(
510 "Vector index '{}' uses L1/Manhattan — no physical ANN index; \
511 searched exact/brute-force",
512 config.name
513 );
514 return Ok(());
515 }
516
517 let prop_dim = schema
522 .properties
523 .get(label)
524 .and_then(|props| props.get(property))
525 .and_then(|meta| resolve_vector_dim(&meta.r#type));
526 let pq_sub = match &config.index_type {
527 VectorIndexType::IvfPq {
528 num_sub_vectors, ..
529 }
530 | VectorIndexType::HnswPq {
531 num_sub_vectors, ..
532 } => Some(*num_sub_vectors as usize),
533 _ => None,
534 };
535 if let (Some(dim), Some(sub)) = (prop_dim, pq_sub)
541 && sub != 0
542 && dim >= sub
543 && dim % sub != 0
544 {
545 return Err(anyhow!(
546 "Vector index '{}': PQ num_sub_vectors ({}) must divide the embedding dimension ({})",
547 config.name,
548 sub,
549 dim
550 ));
551 }
552
553 let params = to_backend_vector_params(config.metric.clone(), &config.index_type)?;
554 let table = table_names::vertex_table_name(label);
555
556 let Some(backend) = self.backend.as_ref() else {
557 warn!(
558 "No storage backend; physical vector index '{}' deferred until a flush",
559 config.name
560 );
561 return Ok(());
562 };
563
564 if backend.table_exists(&table).await? {
569 info!(
570 "Building physical vector index '{}' on '{}'",
571 config.name, table
572 );
573 if let Err(e) = backend
574 .create_vector_index(&table, property, &config.name, params)
575 .await
576 {
577 warn!(
578 "Failed to build physical vector index '{}' (column may be empty): {}",
579 config.name, e
580 );
581 } else {
582 info!("Vector index '{}' created", config.name);
583 }
584 } else {
585 debug!(
586 "Label '{}' not flushed yet; physical vector index '{}' built on next flush",
587 label, config.name
588 );
589 }
590
591 Ok(())
592 }
593
594 #[cfg(feature = "lance-backend")]
596 #[instrument(skip(self), level = "info")]
597 pub async fn create_scalar_index(&self, config: ScalarIndexConfig) -> Result<()> {
598 let label = &config.label;
599 let properties = &config.properties;
600 info!(
601 "Creating scalar index '{}' on {}.{:?}",
602 config.name, label, properties
603 );
604
605 let schema = self.schema_manager.schema();
606 if !schema.labels.contains_key(label) {
607 return Err(anyhow!("Label '{}' not found", label));
608 }
609
610 let columns: Vec<&str> = properties.iter().map(|s| s.as_str()).collect();
611 let backend_idx_type = match config.index_type {
615 ScalarIndexType::Bitmap => crate::backend::types::ScalarIndexType::Bitmap,
616 ScalarIndexType::LabelList => crate::backend::types::ScalarIndexType::LabelList,
617 _ => crate::backend::types::ScalarIndexType::BTree,
618 };
619 let table = table_names::vertex_table_name(label);
620
621 if let Some(backend) = self.backend.as_ref() {
622 if backend.table_exists(&table).await? {
623 info!(
624 "Building physical scalar index '{}' on '{}'",
625 config.name, table
626 );
627 if let Err(e) = backend
628 .create_scalar_index(&table, &columns, backend_idx_type, Some(&config.name))
629 .await
630 {
631 warn!(
632 "Failed to build physical scalar index '{}' (table may be empty): {}",
633 config.name, e
634 );
635 } else {
636 info!("Scalar index '{}' created", config.name);
637 }
638 } else {
639 debug!(
640 "Label '{}' not flushed yet; physical scalar index '{}' built on next flush",
641 label, config.name
642 );
643 }
644 } else {
645 warn!(
646 "No storage backend; physical scalar index '{}' deferred until a flush",
647 config.name
648 );
649 }
650
651 self.schema_manager
652 .add_index(IndexDefinition::Scalar(config))?;
653 self.schema_manager.save().await?;
654
655 Ok(())
656 }
657
658 #[cfg(feature = "lance-backend")]
660 #[instrument(skip(self), level = "info")]
661 pub async fn create_fts_index(&self, config: FullTextIndexConfig) -> Result<()> {
662 let label = &config.label;
663 info!(
664 "Creating FTS index '{}' on {}.{:?}",
665 config.name, label, config.properties
666 );
667
668 let schema = self.schema_manager.schema();
669 if !schema.labels.contains_key(label) {
670 return Err(anyhow!("Label '{}' not found", label));
671 }
672
673 let columns: Vec<&str> = config.properties.iter().map(|s| s.as_str()).collect();
674 let table = table_names::vertex_table_name(label);
675
676 if let Some(backend) = self.backend.as_ref() {
677 if backend.table_exists(&table).await? {
678 info!(
679 "Building physical FTS index '{}' on '{}'",
680 config.name, table
681 );
682 if let Err(e) = crate::backend::fts_analyzer::to_inverted_params(
686 &config.tokenizer,
687 config.with_positions,
688 ) {
689 return Err(anyhow!(
690 "Invalid tokenizer/analyzer config for FTS index '{}': {}",
691 config.name,
692 e
693 ));
694 }
695 if let Err(e) = backend
696 .create_fts_index(
697 &table,
698 &columns,
699 Some(&config.name),
700 &config.tokenizer,
701 config.with_positions,
702 )
703 .await
704 {
705 warn!(
706 "Failed to build physical FTS index '{}' (table may be empty): {}",
707 config.name, e
708 );
709 } else {
710 info!("FTS index '{}' created", config.name);
711 }
712 } else {
713 debug!(
714 "Label '{}' not flushed yet; physical FTS index '{}' built on next flush",
715 label, config.name
716 );
717 }
718 } else {
719 warn!(
720 "No storage backend; physical FTS index '{}' deferred until a flush",
721 config.name
722 );
723 }
724
725 self.schema_manager
726 .add_index(IndexDefinition::FullText(config))?;
727 self.schema_manager.save().await?;
728
729 Ok(())
730 }
731
732 #[cfg(feature = "lance-backend")]
737 #[instrument(skip(self), level = "info")]
738 pub async fn create_json_fts_index(&self, config: JsonFtsIndexConfig) -> Result<()> {
739 let label = &config.label;
740 let column = &config.column;
741 info!(
742 "Creating JSON FTS index '{}' on {}.{}",
743 config.name, label, column
744 );
745
746 let schema = self.schema_manager.schema();
747 if !schema.labels.contains_key(label) {
748 return Err(anyhow!("Label '{}' not found", label));
749 }
750
751 let table = table_names::vertex_table_name(label);
752
753 if let Some(backend) = self.backend.as_ref() {
754 if backend.table_exists(&table).await? {
755 info!(
756 "Building physical JSON FTS index '{}' on '{}'",
757 config.name, table
758 );
759 if let Err(e) = backend
760 .create_fts_index(
761 &table,
762 &[column.as_str()],
763 Some(&config.name),
764 &uni_common::core::schema::TokenizerConfig::Standard,
767 config.with_positions,
768 )
769 .await
770 {
771 warn!(
772 "Failed to build physical JSON FTS index '{}' (table may be empty): {}",
773 config.name, e
774 );
775 } else {
776 info!("JSON FTS index '{}' created", config.name);
777 }
778 } else {
779 debug!(
780 "Label '{}' not flushed yet; physical JSON FTS index '{}' built on next flush",
781 label, config.name
782 );
783 }
784 } else {
785 warn!(
786 "No storage backend; physical JSON FTS index '{}' deferred until a flush",
787 config.name
788 );
789 }
790
791 self.schema_manager
792 .add_index(IndexDefinition::JsonFullText(config))?;
793 self.schema_manager.save().await?;
794
795 Ok(())
796 }
797
798 #[cfg(feature = "lance-backend")]
800 #[instrument(skip(self), level = "info")]
801 pub async fn drop_index(&self, name: &str) -> Result<()> {
802 info!("Dropping index '{}'", name);
803
804 let idx_def = self
805 .schema_manager
806 .get_index(name)
807 .ok_or_else(|| anyhow!("Index '{}' not found in schema", name))?;
808
809 let label = idx_def.label();
813 let table = table_names::vertex_table_name(label);
814 if let Some(backend) = self.backend.as_ref() {
815 if let Err(e) = backend.drop_index(&table, name).await {
816 warn!(
817 "Physical index drop for '{}' returned error (non-fatal): {}",
818 name, e
819 );
820 } else {
821 info!("Physical index '{}' dropped from '{}'", name, table);
822 }
823 }
824
825 self.schema_manager.remove_index(name)?;
826 self.schema_manager.save().await?;
827 Ok(())
828 }
829
830 #[cfg(feature = "lance-backend")]
832 #[instrument(skip(self), level = "info")]
833 pub async fn rebuild_indexes_for_label(&self, label: &str) -> Result<()> {
834 info!("Rebuilding all indexes for label '{}'", label);
835 let schema = self.schema_manager.schema();
836
837 let indexes: Vec<_> = schema
839 .indexes
840 .iter()
841 .filter(|idx| idx.label() == label)
842 .cloned()
843 .collect();
844
845 for index in indexes {
846 match index {
847 IndexDefinition::Vector(cfg) => self.create_vector_index_inner(cfg, true).await?,
851 IndexDefinition::Scalar(cfg) => self.create_scalar_index(cfg).await?,
852 IndexDefinition::FullText(cfg) => self.create_fts_index(cfg).await?,
853 IndexDefinition::Inverted(cfg) => self.create_inverted_index(cfg).await?,
854 IndexDefinition::JsonFullText(cfg) => self.create_json_fts_index(cfg).await?,
855 IndexDefinition::Sparse(cfg) => self.create_sparse_vector_index(cfg).await?,
856 _ => warn!("Unknown index type encountered during rebuild, skipping"),
857 }
858 }
859 Ok(())
860 }
861
862 #[cfg(feature = "lance-backend")]
864 pub async fn create_composite_index(&self, label: &str, properties: &[String]) -> Result<()> {
865 let schema = self.schema_manager.schema();
866 if !schema.labels.contains_key(label) {
867 return Err(anyhow!("Label '{}' not found", label));
868 }
869
870 let index_name = format!("{}_{}_composite", label, properties.join("_"));
872 let columns: Vec<&str> = properties.iter().map(|s| s.as_str()).collect();
873 let table = table_names::vertex_table_name(label);
874
875 if let Some(backend) = self.backend.as_ref() {
876 if backend.table_exists(&table).await? {
877 info!("Building composite index '{}' on '{}'", index_name, table);
878 if let Err(e) = backend
879 .create_scalar_index(
880 &table,
881 &columns,
882 crate::backend::types::ScalarIndexType::BTree,
883 Some(&index_name),
884 )
885 .await
886 {
887 warn!(
888 "Failed to build composite index '{}' (table may be empty): {}",
889 index_name, e
890 );
891 } else {
892 info!("Composite index '{}' created", index_name);
893 }
894
895 let config = ScalarIndexConfig {
896 name: index_name,
897 label: label.to_string(),
898 properties: properties.to_vec(),
899 index_type: uni_common::core::schema::ScalarIndexType::BTree,
900 where_clause: None,
901 metadata: Default::default(),
902 };
903 self.schema_manager
904 .add_index(IndexDefinition::Scalar(config))?;
905 self.schema_manager.save().await?;
906 } else {
907 debug!(
908 "Label '{}' not flushed yet; composite index for {:?} built on next flush",
909 label, properties
910 );
911 }
912 } else {
913 warn!(
914 "No storage backend; composite index for {:?} deferred until a flush",
915 properties
916 );
917 }
918
919 Ok(())
920 }
921
922 #[cfg(feature = "lance-backend")]
931 #[instrument(skip(self, added, removed), level = "info", fields(
932 label = %config.label,
933 property = %config.property
934 ))]
935 pub async fn update_inverted_index_incremental(
936 &self,
937 config: &InvertedIndexConfig,
938 added: &HashMap<Vid, Vec<String>>,
939 removed: &HashSet<Vid>,
940 ) -> Result<()> {
941 info!(
942 added = added.len(),
943 removed = removed.len(),
944 "Incrementally updating inverted index"
945 );
946
947 let postings_path = format!(
950 "{}/indexes/{}/{}_inverted",
951 self.base_uri, config.label, config.property
952 );
953 let _postings_guard = self.postings_write_guard(&postings_path).await;
954
955 let mut index = InvertedIndex::new(&self.base_uri, config.clone()).await?;
956 index.apply_incremental_updates(added, removed).await
957 }
958
959 #[cfg(feature = "lance-backend")]
963 #[instrument(skip(self), level = "info")]
964 pub async fn create_sparse_vector_index(&self, config: SparseVectorIndexConfig) -> Result<()> {
965 let label = &config.label;
966 let property = &config.property;
967 info!(
968 "Creating Sparse Vector Index '{}' on {}.{}",
969 config.name, label, property
970 );
971
972 let schema = self.schema_manager.schema();
973 if !schema.labels.contains_key(label) {
974 return Err(anyhow!("Label '{}' not found", label));
975 }
976
977 let postings_path = format!("{}/indexes/{}/{}_sparse", self.base_uri, label, property);
980 let _postings_guard = self.postings_write_guard(&postings_path).await;
981
982 let mut index = SparseVectorIndex::new(&self.base_uri, config.clone()).await?;
983
984 let table = table_names::vertex_table_name(label);
988 if let Some(backend) = self.backend.as_ref() {
989 if backend.table_exists(&table).await? {
992 let batches = backend.scan(ScanRequest::all(&table)).await?;
993 index
994 .build_from_batches(&batches, |n| debug!("Indexed {} sparse docs", n))
995 .await?;
996 } else {
997 debug!(
998 "Table '{}' not flushed yet; creating empty sparse index (populated on flush)",
999 table
1000 );
1001 }
1002 } else {
1003 warn!(
1004 "No storage backend available; sparse index '{}' left empty (populated on flush)",
1005 config.name
1006 );
1007 }
1008
1009 self.schema_manager
1010 .add_index(IndexDefinition::Sparse(config))?;
1011 self.schema_manager.save().await?;
1012
1013 Ok(())
1014 }
1015
1016 #[cfg(feature = "lance-backend")]
1019 #[instrument(skip(self, added, removed), level = "info", fields(
1020 label = %config.label,
1021 property = %config.property
1022 ))]
1023 pub async fn update_sparse_vector_index_incremental(
1024 &self,
1025 config: &SparseVectorIndexConfig,
1026 added: &HashMap<Vid, Vec<(u32, f32)>>,
1027 removed: &HashSet<Vid>,
1028 ) -> Result<()> {
1029 info!(
1030 added = added.len(),
1031 removed = removed.len(),
1032 "Incrementally updating sparse vector index"
1033 );
1034 let postings_path = format!(
1037 "{}/indexes/{}/{}_sparse",
1038 self.base_uri, config.label, config.property
1039 );
1040 let _postings_guard = self.postings_write_guard(&postings_path).await;
1041
1042 let mut index = SparseVectorIndex::new(&self.base_uri, config.clone()).await?;
1043 index.apply_incremental_updates(added, removed).await
1044 }
1045
1046 #[cfg(feature = "lance-backend")]
1049 pub async fn sparse_vector_index(
1050 &self,
1051 label: &str,
1052 property: &str,
1053 ) -> Result<SparseVectorIndex> {
1054 let schema = self.schema_manager.schema();
1055 let config = schema
1056 .indexes
1057 .iter()
1058 .find_map(|idx| match idx {
1059 IndexDefinition::Sparse(cfg) if cfg.label == label && cfg.property == property => {
1060 Some(cfg.clone())
1061 }
1062 _ => None,
1063 })
1064 .ok_or_else(|| anyhow!("No sparse vector index found for {}.{}", label, property))?;
1065 SparseVectorIndex::new(&self.base_uri, config).await
1066 }
1067}