1use super::{
2 CollectionUuid, Metadata, MetadataValueConversionError, SegmentScope,
3 SegmentScopeConversionError,
4};
5use crate::collection_schema::Schema;
6use crate::{chroma_proto, DatabaseUuid};
7use chroma_error::{ChromaError, ErrorCodes};
8use chroma_proto::CollectionVersionInfo;
9use std::{collections::HashMap, str::FromStr};
10use thiserror::Error;
11use tonic::Status;
12use uuid::Uuid;
13
14pub const USER_ID_TO_OFFSET_ID: &str = "user_id_to_offset_id";
15pub const OFFSET_ID_TO_USER_ID: &str = "offset_id_to_user_id";
16pub const OFFSET_ID_TO_DATA: &str = "offset_id_to_data";
17pub const MAX_OFFSET_ID: &str = "max_offset_id";
18pub const USER_ID_BLOOM_FILTER: &str = "user_id_bloom_filter";
19
20pub const FULL_TEXT_PLS: &str = "full_text_pls";
21pub const FULL_TEXT_TOKEN: &str = "full_text_token";
22pub const STRING_METADATA: &str = "string_metadata";
23pub const BOOL_METADATA: &str = "bool_metadata";
24pub const F32_METADATA: &str = "f32_metadata";
25pub const U32_METADATA: &str = "u32_metadata";
26
27pub const SPARSE_MAX: &str = "sparse_max";
28pub const SPARSE_OFFSET_VALUE: &str = "sparse_offset_value";
29pub const SPARSE_POSTING: &str = "sparse_posting";
30
31pub const SPARSE_FILE_PATH_DELIMITER: &str = "::";
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum SparseFilePathKind {
41 Posting,
43 Max,
45 OffsetValue,
47}
48
49pub fn sparse_posting_key(metadata_key: &str) -> String {
51 format!("{SPARSE_POSTING}{SPARSE_FILE_PATH_DELIMITER}{metadata_key}")
52}
53
54pub fn sparse_max_key(metadata_key: &str) -> String {
56 format!("{SPARSE_MAX}{SPARSE_FILE_PATH_DELIMITER}{metadata_key}")
57}
58
59pub fn sparse_offset_value_key(metadata_key: &str) -> String {
61 format!("{SPARSE_OFFSET_VALUE}{SPARSE_FILE_PATH_DELIMITER}{metadata_key}")
62}
63
64pub fn parse_sparse_file_path_key(name: &str) -> Option<(SparseFilePathKind, Option<String>)> {
70 match name {
71 SPARSE_POSTING => return Some((SparseFilePathKind::Posting, None)),
72 SPARSE_MAX => return Some((SparseFilePathKind::Max, None)),
73 SPARSE_OFFSET_VALUE => return Some((SparseFilePathKind::OffsetValue, None)),
74 _ => {}
75 }
76 for (family, kind) in [
79 (SPARSE_POSTING, SparseFilePathKind::Posting),
80 (SPARSE_OFFSET_VALUE, SparseFilePathKind::OffsetValue),
81 (SPARSE_MAX, SparseFilePathKind::Max),
82 ] {
83 let with_delimiter = format!("{family}{SPARSE_FILE_PATH_DELIMITER}");
84 if let Some(metadata_key) = name.strip_prefix(&with_delimiter) {
85 return Some((kind, Some(metadata_key.to_string())));
86 }
87 }
88 None
89}
90
91pub const HNSW_PATH: &str = "hnsw_path";
92pub const VERSION_MAP_PATH: &str = "version_map_path";
93pub const POSTING_LIST_PATH: &str = "posting_list_path";
94pub const MAX_HEAD_ID_BF_PATH: &str = "max_head_id_path";
95
96pub const QUANTIZED_SPANN_CLUSTER: &str = "quantized_spann_cluster";
97pub const QUANTIZED_SPANN_SCALAR_METADATA: &str = "quantized_spann_scalar_metadata";
98pub const QUANTIZED_SPANN_EMBEDDING_METADATA: &str = "quantized_spann_embedding_metadata";
99pub const QUANTIZED_SPANN_RAW_CENTROID: &str = "quantized_spann_raw_centroid";
100pub const QUANTIZED_SPANN_QUANTIZED_CENTROID: &str = "quantized_spann_quantized_centroid";
101
102#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
104pub struct SegmentUuid(pub Uuid);
105
106impl SegmentUuid {
107 pub fn new() -> Self {
108 SegmentUuid(Uuid::new_v4())
109 }
110}
111
112impl FromStr for SegmentUuid {
113 type Err = SegmentConversionError;
114
115 fn from_str(s: &str) -> Result<Self, SegmentConversionError> {
116 match Uuid::parse_str(s) {
117 Ok(uuid) => Ok(SegmentUuid(uuid)),
118 Err(_) => Err(SegmentConversionError::InvalidUuid),
119 }
120 }
121}
122
123impl std::fmt::Display for SegmentUuid {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 write!(f, "{}", self.0)
126 }
127}
128
129#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
130pub enum SegmentType {
131 BlockfileMetadata,
132 BlockfileRecord,
133 HnswDistributed,
134 HnswLocalMemory,
135 HnswLocalPersisted,
136 Sqlite,
137 Spann,
138 QuantizedSpann,
139}
140
141impl From<SegmentType> for String {
142 fn from(segment_type: SegmentType) -> String {
143 match segment_type {
144 SegmentType::BlockfileMetadata => "urn:chroma:segment/metadata/blockfile".to_string(),
145 SegmentType::BlockfileRecord => "urn:chroma:segment/record/blockfile".to_string(),
146 SegmentType::HnswDistributed => {
147 "urn:chroma:segment/vector/hnsw-distributed".to_string()
148 }
149 SegmentType::HnswLocalMemory => {
150 "urn:chroma:segment/vector/hnsw-local-memory".to_string()
151 }
152 SegmentType::HnswLocalPersisted => {
153 "urn:chroma:segment/vector/hnsw-local-persisted".to_string()
154 }
155 SegmentType::Spann => "urn:chroma:segment/vector/spann".to_string(),
156 SegmentType::QuantizedSpann => "urn:chroma:segment/vector/quantized-spann".to_string(),
157 SegmentType::Sqlite => "urn:chroma:segment/metadata/sqlite".to_string(),
158 }
159 }
160}
161
162impl TryFrom<&str> for SegmentType {
163 type Error = SegmentConversionError;
164
165 fn try_from(segment_type: &str) -> Result<Self, Self::Error> {
166 match segment_type {
167 "urn:chroma:segment/metadata/blockfile" => Ok(SegmentType::BlockfileMetadata),
168 "urn:chroma:segment/record/blockfile" => Ok(SegmentType::BlockfileRecord),
169 "urn:chroma:segment/vector/hnsw-distributed" => Ok(SegmentType::HnswDistributed),
170 "urn:chroma:segment/vector/hnsw-local-memory" => Ok(SegmentType::HnswLocalMemory),
171 "urn:chroma:segment/vector/hnsw-local-persisted" => Ok(Self::HnswLocalPersisted),
172 "urn:chroma:segment/vector/spann" => Ok(SegmentType::Spann),
173 "urn:chroma:segment/vector/quantized-spann" => Ok(SegmentType::QuantizedSpann),
174 "urn:chroma:segment/metadata/sqlite" => Ok(SegmentType::Sqlite),
175 _ => Err(SegmentConversionError::InvalidSegmentType),
176 }
177 }
178}
179
180#[derive(Clone, Debug, PartialEq)]
181pub struct Segment {
182 pub id: SegmentUuid,
183 pub r#type: SegmentType,
184 pub scope: SegmentScope,
185 pub collection: CollectionUuid,
186 pub metadata: Option<Metadata>,
187 pub file_path: HashMap<String, Vec<String>>,
188}
189
190impl Segment {
191 pub fn empty_segment(&self) -> Self {
194 let mut segment = self.clone();
195 segment.file_path.clear();
196 segment
197 }
198
199 pub fn historical_segment_for_version(
200 &self,
201 version: &CollectionVersionInfo,
202 segment_id: SegmentUuid,
203 ) -> Result<Self, String> {
204 let mut historical_segment = self.clone();
205 let all_segments_info = version.segment_info.as_ref().ok_or_else(|| {
206 format!(
207 "Invariant violation: collection version {} is missing segment info",
208 version.version
209 )
210 })?;
211
212 let segment_info = all_segments_info
213 .segment_compaction_info
214 .iter()
215 .find(|segment_info| segment_info.segment_id == segment_id.to_string())
216 .ok_or_else(|| {
217 format!(
218 "Invariant violation: collection version {} is missing segment {}",
219 version.version, segment_id
220 )
221 })?;
222
223 historical_segment.file_path = segment_info
224 .file_paths
225 .iter()
226 .map(|(key, value)| (key.clone(), value.paths.clone()))
227 .collect::<HashMap<_, _>>();
228 Ok(historical_segment)
229 }
230
231 pub fn get_shards(&self) -> Result<Vec<SegmentShard>, SegmentShardError> {
233 let num_shards = self.num_shards()?;
234
235 let shards: Result<Vec<SegmentShard>, SegmentShardError> = (0..num_shards)
237 .map(|shard_index| SegmentShard::try_from((self, shard_index as u32)))
238 .collect();
239
240 shards
241 }
242
243 pub fn prefetch_supported(&self) -> bool {
244 matches!(
245 self.r#type,
246 SegmentType::BlockfileMetadata
247 | SegmentType::BlockfileRecord
248 | SegmentType::QuantizedSpann
249 | SegmentType::Spann
250 )
251 }
252
253 pub fn filepaths_to_prefetch(&self, shard_index: Option<u32>) -> Vec<String> {
257 let mut res = Vec::new();
258 match self.r#type {
259 SegmentType::QuantizedSpann => {
260 for key in [
261 QUANTIZED_SPANN_CLUSTER,
262 QUANTIZED_SPANN_EMBEDDING_METADATA,
263 QUANTIZED_SPANN_SCALAR_METADATA,
264 ] {
265 if let Some(paths) = self.file_path.get(key) {
266 if let Some(path) = match shard_index {
267 Some(index) => paths.get(index as usize),
268 None => paths.last(),
269 } {
270 res.push(path.clone());
271 }
272 }
273 }
274 }
275 SegmentType::Spann => {
276 if let Some(pl_path) = self.file_path.get(POSTING_LIST_PATH) {
277 if let Some(path) = match shard_index {
278 Some(index) => pl_path.get(index as usize),
279 None => pl_path.last(),
280 } {
281 res.push(path.clone());
282 }
283 }
284 }
285 SegmentType::BlockfileMetadata | SegmentType::BlockfileRecord => {
286 for (key, paths) in &self.file_path {
287 if key == USER_ID_BLOOM_FILTER {
288 continue;
289 }
290 if let Some(path) = match shard_index {
291 Some(index) => paths.get(index as usize),
292 None => paths.last(),
293 } {
294 res.push(path.clone());
295 }
296 }
297 }
298 _ => {}
299 }
300 res
301 }
302
303 pub fn num_shards(&self) -> Result<usize, SegmentShardError> {
307 let mut values = self.file_path.values();
308 let num_shards = match values.next() {
309 Some(paths) => paths.len().max(1),
310 None => return Ok(1),
311 };
312 for (key, paths) in &self.file_path {
313 if paths.len() != num_shards {
314 return Err(SegmentShardError::MismatchedShardCounts {
315 key: key.clone(),
316 actual: paths.len(),
317 expected: num_shards,
318 });
319 }
320 }
321 Ok(num_shards)
322 }
323
324 pub fn clear_shard_file_paths(&mut self, shard_index: u32) {
328 let shard_idx = shard_index as usize;
329 for (_, paths) in self.file_path.iter_mut() {
330 if paths.len() > shard_idx {
331 paths[shard_idx].clear();
332 }
333 }
334 if self
335 .file_path
336 .values()
337 .all(|paths| paths.iter().all(|p| p.is_empty()))
338 {
339 self.file_path.clear();
340 }
341 }
342
343 pub fn matches_schema(&self, schema: &Schema) -> Result<(), SchemaMismatchError> {
351 if self.file_path.is_empty() {
352 return Ok(());
353 }
354 self.num_shards().map_err(SchemaMismatchError::ShardError)?;
356
357 match self.scope {
358 SegmentScope::VECTOR => self.check_vector_consistency(schema),
359 SegmentScope::METADATA => self.check_metadata_consistency(schema),
360 SegmentScope::RECORD => self.check_record_consistency(),
361 SegmentScope::SQLITE => Ok(()),
362 }
363 }
364
365 fn mismatch(&self, detail: String) -> SchemaMismatchError {
366 SchemaMismatchError::Mismatch {
367 segment_id: self.id,
368 scope: self.scope.clone(),
369 detail,
370 }
371 }
372
373 fn require_keys(&self, required: &[&str], denied: &[&str]) -> Result<(), SchemaMismatchError> {
376 let has_required: Vec<&str> = required
377 .iter()
378 .filter(|k| self.file_path.contains_key(**k))
379 .copied()
380 .collect();
381 let has_denied: Vec<&str> = denied
382 .iter()
383 .filter(|k| self.file_path.contains_key(**k))
384 .copied()
385 .collect();
386
387 if has_required.is_empty() && has_denied.is_empty() {
389 return Ok(());
390 }
391
392 if !has_denied.is_empty() {
394 return Err(self.mismatch(format!(
395 "unexpected keys present: {}",
396 has_denied.join(", ")
397 )));
398 }
399
400 if has_required.len() != required.len() {
402 let missing: Vec<&str> = required
403 .iter()
404 .filter(|k| !self.file_path.contains_key(**k))
405 .copied()
406 .collect();
407 return Err(self.mismatch(format!("missing required keys: {}", missing.join(", "))));
408 }
409
410 Ok(())
411 }
412
413 fn check_vector_consistency(&self, schema: &Schema) -> Result<(), SchemaMismatchError> {
414 if schema.get_spann_config().is_none() {
415 return Ok(());
416 }
417
418 let spann_keys: &[&str] = &[
419 HNSW_PATH,
420 VERSION_MAP_PATH,
421 POSTING_LIST_PATH,
422 MAX_HEAD_ID_BF_PATH,
423 ];
424 let qspann_keys: &[&str] = &[
425 QUANTIZED_SPANN_CLUSTER,
426 QUANTIZED_SPANN_SCALAR_METADATA,
427 QUANTIZED_SPANN_EMBEDDING_METADATA,
428 QUANTIZED_SPANN_RAW_CENTROID,
429 QUANTIZED_SPANN_QUANTIZED_CENTROID,
430 ];
431
432 if schema.is_quantization_enabled() {
433 self.require_keys(qspann_keys, spann_keys)
434 } else {
435 self.require_keys(spann_keys, qspann_keys)
436 }
437 }
438
439 fn check_metadata_consistency(&self, schema: &Schema) -> Result<(), SchemaMismatchError> {
440 let base_keys: &[&str] = &[STRING_METADATA, BOOL_METADATA, F32_METADATA, U32_METADATA];
441 self.require_keys(base_keys, &[])?;
442
443 if schema.is_fts_enabled() {
444 if schema.is_token_bitmap_fts_enabled() {
445 self.require_keys(&[FULL_TEXT_TOKEN], &[FULL_TEXT_PLS])?;
446 } else {
447 self.require_keys(&[FULL_TEXT_PLS], &[FULL_TEXT_TOKEN])?;
448 }
449 }
450
451 self.check_sparse_consistency(schema)
452 }
453
454 fn check_sparse_consistency(&self, schema: &Schema) -> Result<(), SchemaMismatchError> {
468 if schema.is_sparse_index_enabled() {
469 let has_legacy = self.file_path.contains_key(SPARSE_MAX)
470 || self.file_path.contains_key(SPARSE_OFFSET_VALUE)
471 || self.file_path.contains_key(SPARSE_POSTING);
472 if has_legacy {
473 let wand_keys: &[&str] = &[SPARSE_MAX, SPARSE_OFFSET_VALUE];
474 let maxscore_keys: &[&str] = &[SPARSE_POSTING];
475 if schema.is_maxscore_enabled() {
476 self.require_keys(maxscore_keys, wand_keys)?;
477 } else {
478 self.require_keys(wand_keys, maxscore_keys)?;
479 }
480 }
481 }
482
483 for key in schema.enabled_sparse_keys() {
484 let max_key = sparse_max_key(&key);
485 let offset_value_key = sparse_offset_value_key(&key);
486 let posting_key = sparse_posting_key(&key);
487
488 let has_per_key = self.file_path.contains_key(&max_key)
489 || self.file_path.contains_key(&offset_value_key)
490 || self.file_path.contains_key(&posting_key);
491 if !has_per_key {
492 continue;
493 }
494
495 let wand_keys: &[&str] = &[max_key.as_str(), offset_value_key.as_str()];
496 let maxscore_keys: &[&str] = &[posting_key.as_str()];
497 if schema.is_key_maxscore_enabled(&key) {
498 self.require_keys(maxscore_keys, wand_keys)?;
499 } else {
500 self.require_keys(wand_keys, maxscore_keys)?;
501 }
502 }
503
504 Ok(())
505 }
506
507 fn check_record_consistency(&self) -> Result<(), SchemaMismatchError> {
508 let required: &[&str] = &[
509 USER_ID_TO_OFFSET_ID,
510 OFFSET_ID_TO_USER_ID,
511 OFFSET_ID_TO_DATA,
512 MAX_OFFSET_ID,
513 ];
514 self.require_keys(required, &[])
515 }
516
517 pub fn extract_prefix_and_id(path: &str) -> Result<(&str, uuid::Uuid), uuid::Error> {
518 let (prefix, id) = match path.rfind('/') {
519 Some(pos) => (&path[..pos], &path[pos + 1..]),
520 None => ("", path),
521 };
522 match Uuid::try_parse(id) {
523 Ok(uid) => Ok((prefix, uid)),
524 Err(e) => Err(e),
525 }
526 }
527
528 pub fn construct_prefix_path(&self, tenant: &str, database_id: &DatabaseUuid) -> String {
529 Self::construct_prefix_path_impl(tenant, database_id, &self.collection, &self.id)
530 }
531
532 fn construct_prefix_path_impl(
533 tenant: &str,
534 database_id: &DatabaseUuid,
535 collection: &CollectionUuid,
536 segment_id: &SegmentUuid,
537 ) -> String {
538 format!(
539 "tenant/{}/database/{}/collection/{}/segment/{}",
540 tenant, database_id, collection, segment_id
541 )
542 }
543}
544
545#[derive(Clone, Debug, PartialEq)]
546pub struct SegmentShard {
547 pub id: SegmentUuid,
548 pub r#type: SegmentType,
549 pub scope: SegmentScope,
550 pub collection: CollectionUuid,
551 pub metadata: Option<Metadata>,
552 pub file_path: HashMap<String, String>,
553}
554
555impl SegmentShard {
556 pub fn construct_prefix_path(&self, tenant: &str, database_id: &DatabaseUuid) -> String {
557 Segment::construct_prefix_path_impl(tenant, database_id, &self.collection, &self.id)
558 }
559}
560
561#[derive(Error, Debug)]
562pub enum SegmentShardError {
563 #[error("Empty path vector for key '{0}'")]
564 EmptyPathVector(String),
565 #[error("Empty path string for key '{0}'")]
566 EmptyPathString(String),
567 #[error("Shard index {index} out of bounds for key '{key}' (len {len})")]
568 ShardIndexOutOfBounds { key: String, index: u32, len: usize },
569 #[error("Mismatched shard counts: key '{key}' has {actual} entries, expected {expected}")]
570 MismatchedShardCounts {
571 key: String,
572 actual: usize,
573 expected: usize,
574 },
575 #[error("No shards found")]
576 EmptyShards,
577}
578
579impl ChromaError for SegmentShardError {
580 fn code(&self) -> ErrorCodes {
581 ErrorCodes::Internal
582 }
583}
584
585#[derive(Error, Debug)]
588pub enum SchemaMismatchError {
589 #[error("Schema/file_path mismatch on segment {segment_id} (scope {scope:?}): {detail}")]
590 Mismatch {
591 segment_id: SegmentUuid,
592 scope: SegmentScope,
593 detail: String,
594 },
595 #[error("Error inspecting shards: {0}")]
596 ShardError(SegmentShardError),
597}
598
599impl ChromaError for SchemaMismatchError {
600 fn code(&self) -> ErrorCodes {
601 match self {
602 Self::Mismatch { .. } => ErrorCodes::FailedPrecondition,
603 Self::ShardError(e) => e.code(),
604 }
605 }
606}
607
608impl TryFrom<(&Segment, u32)> for SegmentShard {
609 type Error = SegmentShardError;
610
611 fn try_from((segment, shard_index): (&Segment, u32)) -> Result<Self, Self::Error> {
612 let mut file_path = HashMap::new();
613 for (key, paths) in &segment.file_path {
616 match paths.get(shard_index as usize) {
617 Some(path) => {
618 if !path.is_empty() {
621 file_path.insert(key.clone(), path.clone());
622 }
623 }
624 None if paths.is_empty() => {
625 return Err(SegmentShardError::EmptyPathVector(key.clone()));
626 }
627 None => {
628 return Err(SegmentShardError::ShardIndexOutOfBounds {
629 key: key.clone(),
630 index: shard_index,
631 len: paths.len(),
632 });
633 }
634 }
635 }
636
637 Ok(SegmentShard {
638 id: segment.id,
639 r#type: segment.r#type,
640 scope: segment.scope.clone(),
641 collection: segment.collection,
642 metadata: segment.metadata.clone(),
643 file_path,
644 })
645 }
646}
647
648impl Segment {
649 pub fn new_shard(&self) -> SegmentShard {
650 SegmentShard {
651 id: self.id,
652 r#type: self.r#type,
653 scope: self.scope.clone(),
654 collection: self.collection,
655 metadata: self.metadata.clone(),
656 file_path: HashMap::new(),
657 }
658 }
659}
660
661#[derive(Error, Debug)]
662pub enum SegmentConversionError {
663 #[error("Invalid UUID")]
664 InvalidUuid,
665 #[error(transparent)]
666 MetadataValueConversionError(#[from] MetadataValueConversionError),
667 #[error(transparent)]
668 SegmentScopeConversionError(#[from] SegmentScopeConversionError),
669 #[error("Invalid segment type")]
670 InvalidSegmentType,
671}
672
673impl ChromaError for SegmentConversionError {
674 fn code(&self) -> ErrorCodes {
675 match self {
676 SegmentConversionError::InvalidUuid => ErrorCodes::InvalidArgument,
677 SegmentConversionError::InvalidSegmentType => ErrorCodes::InvalidArgument,
678 SegmentConversionError::SegmentScopeConversionError(e) => e.code(),
679 SegmentConversionError::MetadataValueConversionError(e) => e.code(),
680 }
681 }
682}
683
684impl From<SegmentConversionError> for Status {
685 fn from(value: SegmentConversionError) -> Self {
686 Status::invalid_argument(value.to_string())
687 }
688}
689
690impl TryFrom<chroma_proto::Segment> for Segment {
691 type Error = SegmentConversionError;
692
693 fn try_from(proto_segment: chroma_proto::Segment) -> Result<Self, Self::Error> {
694 let mut proto_segment = proto_segment;
695
696 let segment_uuid = match SegmentUuid::from_str(&proto_segment.id) {
697 Ok(uuid) => uuid,
698 Err(_) => return Err(SegmentConversionError::InvalidUuid),
699 };
700 let collection_uuid = match Uuid::try_parse(&proto_segment.collection) {
701 Ok(uuid) => uuid,
702 Err(_) => return Err(SegmentConversionError::InvalidUuid),
703 };
704 let collection_uuid = CollectionUuid(collection_uuid);
705 let segment_metadata: Option<Metadata> = match proto_segment.metadata {
706 Some(proto_metadata) => match proto_metadata.try_into() {
707 Ok(metadata) => Some(metadata),
708 Err(e) => return Err(SegmentConversionError::MetadataValueConversionError(e)),
709 },
710 None => None,
711 };
712 let scope: SegmentScope = match proto_segment.scope.try_into() {
713 Ok(scope) => scope,
714 Err(e) => return Err(SegmentConversionError::SegmentScopeConversionError(e)),
715 };
716
717 let segment_type: SegmentType = proto_segment.r#type.as_str().try_into()?;
718
719 let mut file_paths = HashMap::new();
720 let drain = proto_segment.file_paths.drain();
721 for (key, value) in drain {
722 file_paths.insert(key, value.paths);
723 }
724
725 Ok(Segment {
726 id: segment_uuid,
727 r#type: segment_type,
728 scope,
729 collection: collection_uuid,
730 metadata: segment_metadata,
731 file_path: file_paths,
732 })
733 }
734}
735
736impl From<Segment> for chroma_proto::Segment {
737 fn from(value: Segment) -> Self {
738 Self {
739 id: value.id.0.to_string(),
740 r#type: value.r#type.into(),
741 scope: chroma_proto::SegmentScope::from(value.scope) as i32,
742 collection: value.collection.0.to_string(),
743 metadata: value.metadata.map(Into::into),
744 file_paths: value
745 .file_path
746 .into_iter()
747 .map(|(name, paths)| (name, chroma_proto::FilePaths { paths }))
748 .collect(),
749 }
750 }
751}
752
753pub fn test_segment(collection_uuid: CollectionUuid, scope: SegmentScope) -> Segment {
754 let r#type = match scope {
755 SegmentScope::METADATA => SegmentType::BlockfileMetadata,
756 SegmentScope::RECORD => SegmentType::BlockfileRecord,
757 SegmentScope::VECTOR => SegmentType::HnswDistributed,
758 SegmentScope::SQLITE => unimplemented!("Sqlite segment is not implemented"),
759 };
760 Segment {
761 id: SegmentUuid::new(),
762 r#type,
763 scope,
764 collection: collection_uuid,
765 metadata: None,
766 file_path: HashMap::new(),
767 }
768}
769
770#[cfg(test)]
771mod tests {
772
773 use super::*;
774 use crate::MetadataValue;
775
776 #[test]
777 fn test_segment_try_from() {
778 let mut metadata = chroma_proto::UpdateMetadata {
779 metadata: HashMap::new(),
780 };
781 metadata.metadata.insert(
782 "foo".to_string(),
783 chroma_proto::UpdateMetadataValue {
784 value: Some(chroma_proto::update_metadata_value::Value::IntValue(42)),
785 },
786 );
787 let proto_segment = chroma_proto::Segment {
788 id: "00000000-0000-0000-0000-000000000000".to_string(),
789 r#type: "urn:chroma:segment/vector/hnsw-distributed".to_string(),
790 scope: chroma_proto::SegmentScope::Vector as i32,
791 collection: "00000000-0000-0000-0000-000000000000".to_string(),
792 metadata: Some(metadata),
793 file_paths: HashMap::new(),
794 };
795 let converted_segment: Segment = proto_segment.try_into().unwrap();
796 assert_eq!(converted_segment.id, SegmentUuid(Uuid::nil()));
797 assert_eq!(converted_segment.r#type, SegmentType::HnswDistributed);
798 assert_eq!(converted_segment.scope, SegmentScope::VECTOR);
799 assert_eq!(converted_segment.collection, CollectionUuid(Uuid::nil()));
800 let metadata = converted_segment.metadata.unwrap();
801 assert_eq!(metadata.len(), 1);
802 assert_eq!(metadata.get("foo").unwrap(), &MetadataValue::Int(42));
803 }
804
805 #[test]
806 fn test_segment_construct_prefix_path() {
807 let segment = Segment {
808 id: SegmentUuid(Uuid::nil()),
809 r#type: SegmentType::BlockfileMetadata,
810 scope: SegmentScope::METADATA,
811 collection: CollectionUuid(Uuid::nil()),
812 metadata: None,
813 file_path: HashMap::new(),
814 };
815 let tenant = "test_tenant";
816 let database_id = &DatabaseUuid(Uuid::nil());
817 let prefix_path = segment.construct_prefix_path(tenant, database_id);
818 assert_eq!(
819 prefix_path,
820 "tenant/test_tenant/database/00000000-0000-0000-0000-000000000000/collection/00000000-0000-0000-0000-000000000000/segment/00000000-0000-0000-0000-000000000000"
821 );
822 }
823
824 #[test]
825 fn test_segment_extract_prefix_and_id() {
826 let path = "tenant/test_tenant/database/00000000-0000-0000-0000-000000000000/collection/00000000-0000-0000-0000-000000000000/segment/00000000-0000-0000-0000-000000000000/00000000-0000-0000-0000-000000000001";
827 let (prefix, id) =
828 Segment::extract_prefix_and_id(path).expect("Failed to extract prefix and id");
829 assert_eq!(
830 prefix,
831 "tenant/test_tenant/database/00000000-0000-0000-0000-000000000000/collection/00000000-0000-0000-0000-000000000000/segment/00000000-0000-0000-0000-000000000000"
832 );
833 assert_eq!(
834 id,
835 Uuid::from_str("00000000-0000-0000-0000-000000000001").expect("Cannot happen")
836 );
837 }
838
839 #[test]
840 fn test_segment_extract_prefix_and_id_legacy() {
841 let path = "00000000-0000-0000-0000-000000000001";
842 let (prefix, id) =
843 Segment::extract_prefix_and_id(path).expect("Failed to extract prefix and id");
844 assert_eq!(prefix, "");
845 assert_eq!(
846 id,
847 Uuid::from_str("00000000-0000-0000-0000-000000000001").expect("Cannot happen")
848 );
849 }
850
851 #[test]
852 fn test_num_shards_empty_file_path() {
853 let segment = Segment {
854 id: SegmentUuid(Uuid::nil()),
855 r#type: SegmentType::BlockfileRecord,
856 scope: SegmentScope::RECORD,
857 collection: CollectionUuid(Uuid::nil()),
858 metadata: None,
859 file_path: HashMap::new(),
860 };
861 assert_eq!(segment.num_shards().unwrap(), 1);
862 }
863
864 #[test]
865 fn test_num_shards_single_shard() {
866 let mut file_path = HashMap::new();
867 file_path.insert("key_a".to_string(), vec!["path_a_0".to_string()]);
868 file_path.insert("key_b".to_string(), vec!["path_b_0".to_string()]);
869 let segment = Segment {
870 id: SegmentUuid(Uuid::nil()),
871 r#type: SegmentType::BlockfileRecord,
872 scope: SegmentScope::RECORD,
873 collection: CollectionUuid(Uuid::nil()),
874 metadata: None,
875 file_path,
876 };
877 assert_eq!(segment.num_shards().unwrap(), 1);
878 }
879
880 #[test]
881 fn test_num_shards_multi_shard() {
882 let mut file_path = HashMap::new();
883 file_path.insert(
884 "key_a".to_string(),
885 vec!["a0".to_string(), "a1".to_string(), "a2".to_string()],
886 );
887 file_path.insert(
888 "key_b".to_string(),
889 vec!["b0".to_string(), "b1".to_string(), "b2".to_string()],
890 );
891 let segment = Segment {
892 id: SegmentUuid(Uuid::nil()),
893 r#type: SegmentType::BlockfileRecord,
894 scope: SegmentScope::RECORD,
895 collection: CollectionUuid(Uuid::nil()),
896 metadata: None,
897 file_path,
898 };
899 assert_eq!(segment.num_shards().unwrap(), 3);
900 }
901
902 #[test]
903 fn test_num_shards_mismatched_lengths() {
904 let mut file_path = HashMap::new();
905 file_path.insert(
906 "key_a".to_string(),
907 vec!["a0".to_string(), "a1".to_string()],
908 );
909 file_path.insert(
910 "key_b".to_string(),
911 vec!["b0".to_string(), "b1".to_string(), "b2".to_string()],
912 );
913 let segment = Segment {
914 id: SegmentUuid(Uuid::nil()),
915 r#type: SegmentType::BlockfileRecord,
916 scope: SegmentScope::RECORD,
917 collection: CollectionUuid(Uuid::nil()),
918 metadata: None,
919 file_path,
920 };
921 let err = segment.num_shards().unwrap_err();
922 assert!(matches!(
923 err,
924 SegmentShardError::MismatchedShardCounts { .. }
925 ));
926 }
927
928 #[test]
929 fn test_clear_shard_file_paths() {
930 let mut file_path = HashMap::new();
932 file_path.insert(
933 "metadata".to_string(),
934 vec![
935 "/path/to/shard0/metadata".to_string(),
936 "/path/to/shard1/metadata".to_string(),
937 "/path/to/shard2/metadata".to_string(),
938 ],
939 );
940 file_path.insert(
941 "index".to_string(),
942 vec![
943 "/path/to/shard0/index".to_string(),
944 "/path/to/shard1/index".to_string(),
945 "/path/to/shard2/index".to_string(),
946 ],
947 );
948 file_path.insert(
949 "data".to_string(),
950 vec![
951 "/path/to/shard0/data".to_string(),
952 "/path/to/shard1/data".to_string(),
953 "/path/to/shard2/data".to_string(),
954 ],
955 );
956
957 let mut segment = Segment {
958 id: SegmentUuid(Uuid::nil()),
959 r#type: SegmentType::BlockfileRecord,
960 scope: SegmentScope::RECORD,
961 collection: CollectionUuid(Uuid::nil()),
962 metadata: None,
963 file_path,
964 };
965
966 segment.clear_shard_file_paths(1);
968
969 assert_eq!(
971 segment.file_path.get("metadata").unwrap()[0],
972 "/path/to/shard0/metadata"
973 );
974 assert_eq!(
975 segment.file_path.get("metadata").unwrap()[2],
976 "/path/to/shard2/metadata"
977 );
978 assert_eq!(
979 segment.file_path.get("index").unwrap()[0],
980 "/path/to/shard0/index"
981 );
982 assert_eq!(
983 segment.file_path.get("index").unwrap()[2],
984 "/path/to/shard2/index"
985 );
986 assert_eq!(
987 segment.file_path.get("data").unwrap()[0],
988 "/path/to/shard0/data"
989 );
990 assert_eq!(
991 segment.file_path.get("data").unwrap()[2],
992 "/path/to/shard2/data"
993 );
994
995 assert_eq!(segment.file_path.get("metadata").unwrap()[1], "");
997 assert_eq!(segment.file_path.get("index").unwrap()[1], "");
998 assert_eq!(segment.file_path.get("data").unwrap()[1], "");
999
1000 segment.clear_shard_file_paths(5);
1002
1003 assert_eq!(
1005 segment.file_path.get("metadata").unwrap()[0],
1006 "/path/to/shard0/metadata"
1007 );
1008 }
1009
1010 use crate::collection_configuration::KnnIndex;
1013 use crate::collection_schema::{
1014 Quantization, Schema, SparseIndexAlgorithm, SparseVectorIndexConfig, SparseVectorIndexType,
1015 SparseVectorValueType,
1016 };
1017
1018 fn spann_schema() -> Schema {
1020 Schema::new_default(KnnIndex::Spann)
1021 }
1022
1023 fn quantized_spann_schema() -> Schema {
1025 let mut schema = Schema::new_default(KnnIndex::Spann);
1026 if let Some(spann_config) = schema.get_spann_config_mut() {
1027 spann_config.quantize = Quantization::FourBitRabitQWithUSearch;
1028 }
1029 schema
1030 }
1031
1032 fn hnsw_schema() -> Schema {
1034 Schema::new_default(KnnIndex::Hnsw)
1035 }
1036
1037 fn sparse_wand_schema() -> Schema {
1039 let mut schema = Schema::new_default(KnnIndex::Hnsw);
1040 schema
1041 .keys
1042 .entry("sparse_key".to_string())
1043 .or_default()
1044 .sparse_vector = Some(SparseVectorValueType {
1045 sparse_vector_index: Some(SparseVectorIndexType {
1046 enabled: true,
1047 config: SparseVectorIndexConfig {
1048 embedding_function: None,
1049 source_key: None,
1050 bm25: None,
1051 algorithm: SparseIndexAlgorithm::Wand,
1052 },
1053 }),
1054 });
1055 schema
1056 }
1057
1058 fn sparse_maxscore_schema() -> Schema {
1060 let mut schema = sparse_wand_schema();
1061 schema.set_sparse_algorithm(SparseIndexAlgorithm::MaxScore);
1062 schema
1063 }
1064
1065 fn make_vector_segment(file_path: HashMap<String, Vec<String>>) -> Segment {
1066 Segment {
1067 id: SegmentUuid(Uuid::nil()),
1068 r#type: SegmentType::Spann,
1069 scope: SegmentScope::VECTOR,
1070 collection: CollectionUuid(Uuid::nil()),
1071 metadata: None,
1072 file_path,
1073 }
1074 }
1075
1076 fn make_metadata_segment(file_path: HashMap<String, Vec<String>>) -> Segment {
1077 Segment {
1078 id: SegmentUuid(Uuid::nil()),
1079 r#type: SegmentType::BlockfileMetadata,
1080 scope: SegmentScope::METADATA,
1081 collection: CollectionUuid(Uuid::nil()),
1082 metadata: None,
1083 file_path,
1084 }
1085 }
1086
1087 #[test]
1088 fn test_matches_schema_empty_file_path() {
1089 let seg = make_vector_segment(HashMap::new());
1090 assert!(seg.matches_schema(&spann_schema()).is_ok());
1091 assert!(seg.matches_schema(&quantized_spann_schema()).is_ok());
1092 assert!(seg.matches_schema(&hnsw_schema()).is_ok());
1093 }
1094
1095 #[test]
1096 fn test_matches_schema_spann_matches_spann() {
1097 let mut fp = HashMap::new();
1098 fp.insert(HNSW_PATH.to_string(), vec!["path/a".to_string()]);
1099 fp.insert(VERSION_MAP_PATH.to_string(), vec!["path/b".to_string()]);
1100 fp.insert(POSTING_LIST_PATH.to_string(), vec!["path/c".to_string()]);
1101 fp.insert(MAX_HEAD_ID_BF_PATH.to_string(), vec!["path/d".to_string()]);
1102 let seg = make_vector_segment(fp);
1103 assert!(seg.matches_schema(&spann_schema()).is_ok());
1104 }
1105
1106 #[test]
1107 fn test_matches_schema_qspann_matches_qspann() {
1108 let mut fp = HashMap::new();
1109 fp.insert(
1110 QUANTIZED_SPANN_CLUSTER.to_string(),
1111 vec!["path/a".to_string()],
1112 );
1113 fp.insert(
1114 QUANTIZED_SPANN_SCALAR_METADATA.to_string(),
1115 vec!["path/b".to_string()],
1116 );
1117 fp.insert(
1118 QUANTIZED_SPANN_EMBEDDING_METADATA.to_string(),
1119 vec!["path/c".to_string()],
1120 );
1121 fp.insert(
1122 QUANTIZED_SPANN_RAW_CENTROID.to_string(),
1123 vec!["path/d".to_string()],
1124 );
1125 fp.insert(
1126 QUANTIZED_SPANN_QUANTIZED_CENTROID.to_string(),
1127 vec!["path/e".to_string()],
1128 );
1129 let seg = make_vector_segment(fp);
1130 assert!(seg.matches_schema(&quantized_spann_schema()).is_ok());
1131 }
1132
1133 #[test]
1134 fn test_matches_schema_spann_on_disk_qspann_in_schema() {
1135 let mut fp = HashMap::new();
1136 fp.insert(HNSW_PATH.to_string(), vec!["path/a".to_string()]);
1137 fp.insert(VERSION_MAP_PATH.to_string(), vec!["path/b".to_string()]);
1138 fp.insert(POSTING_LIST_PATH.to_string(), vec!["path/c".to_string()]);
1139 fp.insert(MAX_HEAD_ID_BF_PATH.to_string(), vec!["path/d".to_string()]);
1140 let seg = make_vector_segment(fp);
1141 let err = seg.matches_schema(&quantized_spann_schema()).unwrap_err();
1142 assert!(matches!(err, SchemaMismatchError::Mismatch { .. }));
1143 }
1144
1145 #[test]
1146 fn test_matches_schema_qspann_on_disk_spann_in_schema() {
1147 let mut fp = HashMap::new();
1148 fp.insert(
1149 QUANTIZED_SPANN_CLUSTER.to_string(),
1150 vec!["path/a".to_string()],
1151 );
1152 fp.insert(
1153 QUANTIZED_SPANN_SCALAR_METADATA.to_string(),
1154 vec!["path/b".to_string()],
1155 );
1156 fp.insert(
1157 QUANTIZED_SPANN_EMBEDDING_METADATA.to_string(),
1158 vec!["path/c".to_string()],
1159 );
1160 fp.insert(
1161 QUANTIZED_SPANN_RAW_CENTROID.to_string(),
1162 vec!["path/d".to_string()],
1163 );
1164 fp.insert(
1165 QUANTIZED_SPANN_QUANTIZED_CENTROID.to_string(),
1166 vec!["path/e".to_string()],
1167 );
1168 let seg = make_vector_segment(fp);
1169 let err = seg.matches_schema(&spann_schema()).unwrap_err();
1170 assert!(matches!(err, SchemaMismatchError::Mismatch { .. }));
1171 }
1172
1173 #[test]
1174 fn test_matches_schema_hnsw_schema_always_ok() {
1175 let mut fp = HashMap::new();
1177 fp.insert(HNSW_PATH.to_string(), vec!["path/a".to_string()]);
1178 let seg = make_vector_segment(fp);
1179 assert!(seg.matches_schema(&hnsw_schema()).is_ok());
1180 }
1181
1182 fn make_record_segment(file_path: HashMap<String, Vec<String>>) -> Segment {
1183 Segment {
1184 id: SegmentUuid(Uuid::nil()),
1185 r#type: SegmentType::BlockfileRecord,
1186 scope: SegmentScope::RECORD,
1187 collection: CollectionUuid(Uuid::nil()),
1188 metadata: None,
1189 file_path,
1190 }
1191 }
1192
1193 #[test]
1194 fn test_matches_schema_record_empty() {
1195 let seg = make_record_segment(HashMap::new());
1196 assert!(seg.matches_schema(&spann_schema()).is_ok());
1197 }
1198
1199 #[test]
1200 fn test_matches_schema_record_complete() {
1201 let mut fp = HashMap::new();
1202 fp.insert(USER_ID_TO_OFFSET_ID.to_string(), vec!["a".to_string()]);
1203 fp.insert(OFFSET_ID_TO_USER_ID.to_string(), vec!["b".to_string()]);
1204 fp.insert(OFFSET_ID_TO_DATA.to_string(), vec!["c".to_string()]);
1205 fp.insert(MAX_OFFSET_ID.to_string(), vec!["d".to_string()]);
1206 let seg = make_record_segment(fp);
1207 assert!(seg.matches_schema(&spann_schema()).is_ok());
1208 }
1209
1210 #[test]
1211 fn test_matches_schema_record_missing_key() {
1212 let mut fp = HashMap::new();
1213 fp.insert(USER_ID_TO_OFFSET_ID.to_string(), vec!["a".to_string()]);
1214 fp.insert(OFFSET_ID_TO_USER_ID.to_string(), vec!["b".to_string()]);
1215 fp.insert(OFFSET_ID_TO_DATA.to_string(), vec!["c".to_string()]);
1216 let seg = make_record_segment(fp);
1218 let err = seg.matches_schema(&spann_schema()).unwrap_err();
1219 assert!(matches!(err, SchemaMismatchError::Mismatch { .. }));
1220 }
1221
1222 #[test]
1223 fn test_matches_schema_sparse_wand_matches_wand() {
1224 let mut fp = HashMap::new();
1225 fp.insert(SPARSE_MAX.to_string(), vec!["path/a".to_string()]);
1226 fp.insert(SPARSE_OFFSET_VALUE.to_string(), vec!["path/b".to_string()]);
1227 let seg = make_metadata_segment(fp);
1228 assert!(seg.matches_schema(&sparse_wand_schema()).is_ok());
1229 }
1230
1231 #[test]
1232 fn test_matches_schema_sparse_maxscore_matches_maxscore() {
1233 let mut fp = HashMap::new();
1234 fp.insert(SPARSE_POSTING.to_string(), vec!["path/a".to_string()]);
1235 let seg = make_metadata_segment(fp);
1236 assert!(seg.matches_schema(&sparse_maxscore_schema()).is_ok());
1237 }
1238
1239 #[test]
1240 fn test_matches_schema_sparse_wand_on_disk_maxscore_in_schema() {
1241 let mut fp = HashMap::new();
1242 fp.insert(SPARSE_MAX.to_string(), vec!["path/a".to_string()]);
1243 fp.insert(SPARSE_OFFSET_VALUE.to_string(), vec!["path/b".to_string()]);
1244 let seg = make_metadata_segment(fp);
1245 let err = seg.matches_schema(&sparse_maxscore_schema()).unwrap_err();
1246 assert!(matches!(err, SchemaMismatchError::Mismatch { .. }));
1247 }
1248
1249 #[test]
1250 fn test_matches_schema_sparse_maxscore_on_disk_wand_in_schema() {
1251 let mut fp = HashMap::new();
1252 fp.insert(SPARSE_POSTING.to_string(), vec!["path/a".to_string()]);
1253 let seg = make_metadata_segment(fp);
1254 let err = seg.matches_schema(&sparse_wand_schema()).unwrap_err();
1255 assert!(matches!(err, SchemaMismatchError::Mismatch { .. }));
1256 }
1257
1258 #[test]
1259 fn test_matches_schema_sparse_empty_file_path() {
1260 let seg = make_metadata_segment(HashMap::new());
1261 assert!(seg.matches_schema(&sparse_maxscore_schema()).is_ok());
1262 assert!(seg.matches_schema(&sparse_wand_schema()).is_ok());
1263 }
1264
1265 #[test]
1266 fn test_matches_schema_sparse_disabled_index_no_check() {
1267 let schema = hnsw_schema();
1269 assert!(!schema.is_sparse_index_enabled());
1270 let mut fp = HashMap::new();
1271 fp.insert(SPARSE_POSTING.to_string(), vec!["path/a".to_string()]);
1272 let seg = make_metadata_segment(fp);
1273 assert!(seg.matches_schema(&schema).is_ok());
1274 }
1275
1276 #[test]
1277 fn test_matches_schema_sparse_both_keys_present() {
1278 let mut fp = HashMap::new();
1280 fp.insert(SPARSE_POSTING.to_string(), vec!["path/a".to_string()]);
1281 fp.insert(SPARSE_MAX.to_string(), vec!["path/b".to_string()]);
1282 fp.insert(SPARSE_OFFSET_VALUE.to_string(), vec!["path/c".to_string()]);
1283 let seg = make_metadata_segment(fp);
1284 let err = seg.matches_schema(&sparse_maxscore_schema()).unwrap_err();
1285 assert!(matches!(err, SchemaMismatchError::Mismatch { .. }));
1286 }
1287
1288 #[test]
1289 fn test_matches_schema_sparse_per_key_wand_matches_wand() {
1290 let mut fp = HashMap::new();
1291 fp.insert(sparse_max_key("sparse_key"), vec!["path/a".to_string()]);
1292 fp.insert(
1293 sparse_offset_value_key("sparse_key"),
1294 vec!["path/b".to_string()],
1295 );
1296 let seg = make_metadata_segment(fp);
1297 assert!(seg.matches_schema(&sparse_wand_schema()).is_ok());
1298 }
1299
1300 #[test]
1301 fn test_matches_schema_sparse_per_key_maxscore_matches_maxscore() {
1302 let mut fp = HashMap::new();
1303 fp.insert(sparse_posting_key("sparse_key"), vec!["path/a".to_string()]);
1304 let seg = make_metadata_segment(fp);
1305 assert!(seg.matches_schema(&sparse_maxscore_schema()).is_ok());
1306 }
1307
1308 #[test]
1309 fn test_matches_schema_sparse_per_key_algorithm_mismatch() {
1310 let mut fp = HashMap::new();
1312 fp.insert(sparse_max_key("sparse_key"), vec!["path/a".to_string()]);
1313 fp.insert(
1314 sparse_offset_value_key("sparse_key"),
1315 vec!["path/b".to_string()],
1316 );
1317 let seg = make_metadata_segment(fp);
1318 let err = seg.matches_schema(&sparse_maxscore_schema()).unwrap_err();
1319 assert!(matches!(err, SchemaMismatchError::Mismatch { .. }));
1320 }
1321
1322 #[test]
1323 fn test_matches_schema_sparse_per_key_incomplete_wand_pair() {
1324 let mut fp = HashMap::new();
1326 fp.insert(sparse_max_key("sparse_key"), vec!["path/a".to_string()]);
1327 let seg = make_metadata_segment(fp);
1328 let err = seg.matches_schema(&sparse_wand_schema()).unwrap_err();
1329 assert!(matches!(err, SchemaMismatchError::Mismatch { .. }));
1330 }
1331}