1use super::{
2 CollectionUuid, Metadata, MetadataValueConversionError, SegmentScope,
3 SegmentScopeConversionError,
4};
5use crate::{chroma_proto, DatabaseUuid};
6use chroma_error::{ChromaError, ErrorCodes};
7use std::{collections::HashMap, str::FromStr};
8use thiserror::Error;
9use tonic::Status;
10use uuid::Uuid;
11
12pub const USER_ID_TO_OFFSET_ID: &str = "user_id_to_offset_id";
13pub const OFFSET_ID_TO_USER_ID: &str = "offset_id_to_user_id";
14pub const OFFSET_ID_TO_DATA: &str = "offset_id_to_data";
15pub const MAX_OFFSET_ID: &str = "max_offset_id";
16pub const USER_ID_BLOOM_FILTER: &str = "user_id_bloom_filter";
17
18pub const FULL_TEXT_PLS: &str = "full_text_pls";
19pub const STRING_METADATA: &str = "string_metadata";
20pub const BOOL_METADATA: &str = "bool_metadata";
21pub const F32_METADATA: &str = "f32_metadata";
22pub const U32_METADATA: &str = "u32_metadata";
23
24pub const SPARSE_MAX: &str = "sparse_max";
25pub const SPARSE_OFFSET_VALUE: &str = "sparse_offset_value";
26
27pub const HNSW_PATH: &str = "hnsw_path";
28pub const VERSION_MAP_PATH: &str = "version_map_path";
29pub const POSTING_LIST_PATH: &str = "posting_list_path";
30pub const MAX_HEAD_ID_BF_PATH: &str = "max_head_id_path";
31
32pub const QUANTIZED_SPANN_CLUSTER: &str = "quantized_spann_cluster";
33pub const QUANTIZED_SPANN_SCALAR_METADATA: &str = "quantized_spann_scalar_metadata";
34pub const QUANTIZED_SPANN_EMBEDDING_METADATA: &str = "quantized_spann_embedding_metadata";
35pub const QUANTIZED_SPANN_RAW_CENTROID: &str = "quantized_spann_raw_centroid";
36pub const QUANTIZED_SPANN_QUANTIZED_CENTROID: &str = "quantized_spann_quantized_centroid";
37
38#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
40pub struct SegmentUuid(pub Uuid);
41
42impl SegmentUuid {
43 pub fn new() -> Self {
44 SegmentUuid(Uuid::new_v4())
45 }
46}
47
48impl FromStr for SegmentUuid {
49 type Err = SegmentConversionError;
50
51 fn from_str(s: &str) -> Result<Self, SegmentConversionError> {
52 match Uuid::parse_str(s) {
53 Ok(uuid) => Ok(SegmentUuid(uuid)),
54 Err(_) => Err(SegmentConversionError::InvalidUuid),
55 }
56 }
57}
58
59impl std::fmt::Display for SegmentUuid {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 write!(f, "{}", self.0)
62 }
63}
64
65#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
66pub enum SegmentType {
67 BlockfileMetadata,
68 BlockfileRecord,
69 HnswDistributed,
70 HnswLocalMemory,
71 HnswLocalPersisted,
72 Sqlite,
73 Spann,
74 QuantizedSpann,
75}
76
77impl From<SegmentType> for String {
78 fn from(segment_type: SegmentType) -> String {
79 match segment_type {
80 SegmentType::BlockfileMetadata => "urn:chroma:segment/metadata/blockfile".to_string(),
81 SegmentType::BlockfileRecord => "urn:chroma:segment/record/blockfile".to_string(),
82 SegmentType::HnswDistributed => {
83 "urn:chroma:segment/vector/hnsw-distributed".to_string()
84 }
85 SegmentType::HnswLocalMemory => {
86 "urn:chroma:segment/vector/hnsw-local-memory".to_string()
87 }
88 SegmentType::HnswLocalPersisted => {
89 "urn:chroma:segment/vector/hnsw-local-persisted".to_string()
90 }
91 SegmentType::Spann => "urn:chroma:segment/vector/spann".to_string(),
92 SegmentType::QuantizedSpann => "urn:chroma:segment/vector/quantized-spann".to_string(),
93 SegmentType::Sqlite => "urn:chroma:segment/metadata/sqlite".to_string(),
94 }
95 }
96}
97
98impl TryFrom<&str> for SegmentType {
99 type Error = SegmentConversionError;
100
101 fn try_from(segment_type: &str) -> Result<Self, Self::Error> {
102 match segment_type {
103 "urn:chroma:segment/metadata/blockfile" => Ok(SegmentType::BlockfileMetadata),
104 "urn:chroma:segment/record/blockfile" => Ok(SegmentType::BlockfileRecord),
105 "urn:chroma:segment/vector/hnsw-distributed" => Ok(SegmentType::HnswDistributed),
106 "urn:chroma:segment/vector/hnsw-local-memory" => Ok(SegmentType::HnswLocalMemory),
107 "urn:chroma:segment/vector/hnsw-local-persisted" => Ok(Self::HnswLocalPersisted),
108 "urn:chroma:segment/vector/spann" => Ok(SegmentType::Spann),
109 "urn:chroma:segment/vector/quantized-spann" => Ok(SegmentType::QuantizedSpann),
110 "urn:chroma:segment/metadata/sqlite" => Ok(SegmentType::Sqlite),
111 _ => Err(SegmentConversionError::InvalidSegmentType),
112 }
113 }
114}
115
116#[derive(Clone, Debug, PartialEq)]
117pub struct Segment {
118 pub id: SegmentUuid,
119 pub r#type: SegmentType,
120 pub scope: SegmentScope,
121 pub collection: CollectionUuid,
122 pub metadata: Option<Metadata>,
123 pub file_path: HashMap<String, Vec<String>>,
124}
125
126impl Segment {
127 pub fn get_shards(&self) -> Result<Vec<SegmentShard>, SegmentShardError> {
129 if self.file_path.is_empty() {
131 let vec = vec![self.new_shard()];
132 return Ok(vec);
133 }
134
135 let counts: std::collections::HashSet<usize> =
137 self.file_path.values().map(|v| v.len()).collect();
138 if counts.len() > 1 {
139 let first_key = self.file_path.keys().next().unwrap();
140 let expected = self.file_path[first_key].len();
141 return Err(SegmentShardError::MismatchedShardCounts {
142 key: format!("segment {}", self.id),
143 actual: counts.into_iter().next().unwrap(),
144 expected,
145 });
146 }
147
148 let num_shards = self
150 .file_path
151 .values()
152 .next()
153 .map(|paths| paths.len())
154 .unwrap_or(0);
155
156 let shards: Result<Vec<SegmentShard>, SegmentShardError> = (0..num_shards)
158 .map(|shard_index| SegmentShard::try_from((self, shard_index as u32)))
159 .collect();
160
161 shards
162 }
163
164 pub fn prefetch_supported(&self) -> bool {
165 matches!(
166 self.r#type,
167 SegmentType::BlockfileMetadata
168 | SegmentType::BlockfileRecord
169 | SegmentType::QuantizedSpann
170 | SegmentType::Spann
171 )
172 }
173
174 pub fn filepaths_to_prefetch(&self, shard_index: Option<u32>) -> Vec<String> {
178 let mut res = Vec::new();
179 match self.r#type {
180 SegmentType::QuantizedSpann => {
181 for key in [
182 QUANTIZED_SPANN_CLUSTER,
183 QUANTIZED_SPANN_EMBEDDING_METADATA,
184 QUANTIZED_SPANN_SCALAR_METADATA,
185 ] {
186 if let Some(paths) = self.file_path.get(key) {
187 if let Some(path) = match shard_index {
188 Some(index) => paths.get(index as usize),
189 None => paths.last(),
190 } {
191 res.push(path.clone());
192 }
193 }
194 }
195 }
196 SegmentType::Spann => {
197 if let Some(pl_path) = self.file_path.get(POSTING_LIST_PATH) {
198 if let Some(path) = match shard_index {
199 Some(index) => pl_path.get(index as usize),
200 None => pl_path.last(),
201 } {
202 res.push(path.clone());
203 }
204 }
205 }
206 SegmentType::BlockfileMetadata | SegmentType::BlockfileRecord => {
207 for (key, paths) in &self.file_path {
208 if key == USER_ID_BLOOM_FILTER {
209 continue;
210 }
211 if let Some(path) = match shard_index {
212 Some(index) => paths.get(index as usize),
213 None => paths.last(),
214 } {
215 res.push(path.clone());
216 }
217 }
218 }
219 _ => {}
220 }
221 res
222 }
223
224 pub fn num_shards(&self) -> Result<usize, SegmentShardError> {
228 let mut values = self.file_path.values();
229 let num_shards = match values.next() {
230 Some(paths) => paths.len().max(1),
231 None => return Ok(1),
232 };
233 for (key, paths) in &self.file_path {
234 if paths.len() != num_shards {
235 return Err(SegmentShardError::MismatchedShardCounts {
236 key: key.clone(),
237 actual: paths.len(),
238 expected: num_shards,
239 });
240 }
241 }
242 Ok(num_shards)
243 }
244
245 pub fn extract_prefix_and_id(path: &str) -> Result<(&str, uuid::Uuid), uuid::Error> {
246 let (prefix, id) = match path.rfind('/') {
247 Some(pos) => (&path[..pos], &path[pos + 1..]),
248 None => ("", path),
249 };
250 match Uuid::try_parse(id) {
251 Ok(uid) => Ok((prefix, uid)),
252 Err(e) => Err(e),
253 }
254 }
255
256 pub fn construct_prefix_path(&self, tenant: &str, database_id: &DatabaseUuid) -> String {
257 Self::construct_prefix_path_impl(tenant, database_id, &self.collection, &self.id)
258 }
259
260 fn construct_prefix_path_impl(
261 tenant: &str,
262 database_id: &DatabaseUuid,
263 collection: &CollectionUuid,
264 segment_id: &SegmentUuid,
265 ) -> String {
266 format!(
267 "tenant/{}/database/{}/collection/{}/segment/{}",
268 tenant, database_id, collection, segment_id
269 )
270 }
271}
272
273#[derive(Clone, Debug, PartialEq)]
274pub struct SegmentShard {
275 pub id: SegmentUuid,
276 pub r#type: SegmentType,
277 pub scope: SegmentScope,
278 pub collection: CollectionUuid,
279 pub metadata: Option<Metadata>,
280 pub file_path: HashMap<String, String>,
281}
282
283impl SegmentShard {
284 pub fn construct_prefix_path(&self, tenant: &str, database_id: &DatabaseUuid) -> String {
285 Segment::construct_prefix_path_impl(tenant, database_id, &self.collection, &self.id)
286 }
287}
288
289#[derive(Error, Debug)]
290pub enum SegmentShardError {
291 #[error("Empty path vector for key '{0}'")]
292 EmptyPathVector(String),
293 #[error("Empty path string for key '{0}'")]
294 EmptyPathString(String),
295 #[error("Shard index {index} out of bounds for key '{key}' (len {len})")]
296 ShardIndexOutOfBounds { key: String, index: u32, len: usize },
297 #[error("Mismatched shard counts: key '{key}' has {actual} entries, expected {expected}")]
298 MismatchedShardCounts {
299 key: String,
300 actual: usize,
301 expected: usize,
302 },
303 #[error("No shards found")]
304 EmptyShards,
305}
306
307impl ChromaError for SegmentShardError {
308 fn code(&self) -> ErrorCodes {
309 ErrorCodes::Internal
310 }
311}
312
313impl TryFrom<(&Segment, u32)> for SegmentShard {
314 type Error = SegmentShardError;
315
316 fn try_from((segment, shard_index): (&Segment, u32)) -> Result<Self, Self::Error> {
317 let mut file_path = HashMap::new();
318 for (key, paths) in &segment.file_path {
319 match paths.get(shard_index as usize) {
320 Some(path) if path.is_empty() => {
321 return Err(SegmentShardError::EmptyPathString(key.clone()));
322 }
323 Some(path) => {
324 file_path.insert(key.clone(), path.clone());
325 }
326 None if paths.is_empty() => {
327 return Err(SegmentShardError::EmptyPathVector(key.clone()));
328 }
329 None => {
330 return Err(SegmentShardError::ShardIndexOutOfBounds {
331 key: key.clone(),
332 index: shard_index,
333 len: paths.len(),
334 });
335 }
336 }
337 }
338
339 Ok(SegmentShard {
340 id: segment.id,
341 r#type: segment.r#type,
342 scope: segment.scope.clone(),
343 collection: segment.collection,
344 metadata: segment.metadata.clone(),
345 file_path,
346 })
347 }
348}
349
350impl Segment {
351 pub fn new_shard(&self) -> SegmentShard {
352 SegmentShard {
353 id: self.id,
354 r#type: self.r#type,
355 scope: self.scope.clone(),
356 collection: self.collection,
357 metadata: self.metadata.clone(),
358 file_path: HashMap::new(),
359 }
360 }
361}
362
363#[derive(Error, Debug)]
364pub enum SegmentConversionError {
365 #[error("Invalid UUID")]
366 InvalidUuid,
367 #[error(transparent)]
368 MetadataValueConversionError(#[from] MetadataValueConversionError),
369 #[error(transparent)]
370 SegmentScopeConversionError(#[from] SegmentScopeConversionError),
371 #[error("Invalid segment type")]
372 InvalidSegmentType,
373}
374
375impl ChromaError for SegmentConversionError {
376 fn code(&self) -> ErrorCodes {
377 match self {
378 SegmentConversionError::InvalidUuid => ErrorCodes::InvalidArgument,
379 SegmentConversionError::InvalidSegmentType => ErrorCodes::InvalidArgument,
380 SegmentConversionError::SegmentScopeConversionError(e) => e.code(),
381 SegmentConversionError::MetadataValueConversionError(e) => e.code(),
382 }
383 }
384}
385
386impl From<SegmentConversionError> for Status {
387 fn from(value: SegmentConversionError) -> Self {
388 Status::invalid_argument(value.to_string())
389 }
390}
391
392impl TryFrom<chroma_proto::Segment> for Segment {
393 type Error = SegmentConversionError;
394
395 fn try_from(proto_segment: chroma_proto::Segment) -> Result<Self, Self::Error> {
396 let mut proto_segment = proto_segment;
397
398 let segment_uuid = match SegmentUuid::from_str(&proto_segment.id) {
399 Ok(uuid) => uuid,
400 Err(_) => return Err(SegmentConversionError::InvalidUuid),
401 };
402 let collection_uuid = match Uuid::try_parse(&proto_segment.collection) {
403 Ok(uuid) => uuid,
404 Err(_) => return Err(SegmentConversionError::InvalidUuid),
405 };
406 let collection_uuid = CollectionUuid(collection_uuid);
407 let segment_metadata: Option<Metadata> = match proto_segment.metadata {
408 Some(proto_metadata) => match proto_metadata.try_into() {
409 Ok(metadata) => Some(metadata),
410 Err(e) => return Err(SegmentConversionError::MetadataValueConversionError(e)),
411 },
412 None => None,
413 };
414 let scope: SegmentScope = match proto_segment.scope.try_into() {
415 Ok(scope) => scope,
416 Err(e) => return Err(SegmentConversionError::SegmentScopeConversionError(e)),
417 };
418
419 let segment_type: SegmentType = proto_segment.r#type.as_str().try_into()?;
420
421 let mut file_paths = HashMap::new();
422 let drain = proto_segment.file_paths.drain();
423 for (key, value) in drain {
424 file_paths.insert(key, value.paths);
425 }
426
427 Ok(Segment {
428 id: segment_uuid,
429 r#type: segment_type,
430 scope,
431 collection: collection_uuid,
432 metadata: segment_metadata,
433 file_path: file_paths,
434 })
435 }
436}
437
438impl From<Segment> for chroma_proto::Segment {
439 fn from(value: Segment) -> Self {
440 Self {
441 id: value.id.0.to_string(),
442 r#type: value.r#type.into(),
443 scope: chroma_proto::SegmentScope::from(value.scope) as i32,
444 collection: value.collection.0.to_string(),
445 metadata: value.metadata.map(Into::into),
446 file_paths: value
447 .file_path
448 .into_iter()
449 .map(|(name, paths)| (name, chroma_proto::FilePaths { paths }))
450 .collect(),
451 }
452 }
453}
454
455pub fn test_segment(collection_uuid: CollectionUuid, scope: SegmentScope) -> Segment {
456 let r#type = match scope {
457 SegmentScope::METADATA => SegmentType::BlockfileMetadata,
458 SegmentScope::RECORD => SegmentType::BlockfileRecord,
459 SegmentScope::VECTOR => SegmentType::HnswDistributed,
460 SegmentScope::SQLITE => unimplemented!("Sqlite segment is not implemented"),
461 };
462 Segment {
463 id: SegmentUuid::new(),
464 r#type,
465 scope,
466 collection: collection_uuid,
467 metadata: None,
468 file_path: HashMap::new(),
469 }
470}
471
472#[cfg(test)]
473mod tests {
474
475 use super::*;
476 use crate::MetadataValue;
477
478 #[test]
479 fn test_segment_try_from() {
480 let mut metadata = chroma_proto::UpdateMetadata {
481 metadata: HashMap::new(),
482 };
483 metadata.metadata.insert(
484 "foo".to_string(),
485 chroma_proto::UpdateMetadataValue {
486 value: Some(chroma_proto::update_metadata_value::Value::IntValue(42)),
487 },
488 );
489 let proto_segment = chroma_proto::Segment {
490 id: "00000000-0000-0000-0000-000000000000".to_string(),
491 r#type: "urn:chroma:segment/vector/hnsw-distributed".to_string(),
492 scope: chroma_proto::SegmentScope::Vector as i32,
493 collection: "00000000-0000-0000-0000-000000000000".to_string(),
494 metadata: Some(metadata),
495 file_paths: HashMap::new(),
496 };
497 let converted_segment: Segment = proto_segment.try_into().unwrap();
498 assert_eq!(converted_segment.id, SegmentUuid(Uuid::nil()));
499 assert_eq!(converted_segment.r#type, SegmentType::HnswDistributed);
500 assert_eq!(converted_segment.scope, SegmentScope::VECTOR);
501 assert_eq!(converted_segment.collection, CollectionUuid(Uuid::nil()));
502 let metadata = converted_segment.metadata.unwrap();
503 assert_eq!(metadata.len(), 1);
504 assert_eq!(metadata.get("foo").unwrap(), &MetadataValue::Int(42));
505 }
506
507 #[test]
508 fn test_segment_construct_prefix_path() {
509 let segment = Segment {
510 id: SegmentUuid(Uuid::nil()),
511 r#type: SegmentType::BlockfileMetadata,
512 scope: SegmentScope::METADATA,
513 collection: CollectionUuid(Uuid::nil()),
514 metadata: None,
515 file_path: HashMap::new(),
516 };
517 let tenant = "test_tenant";
518 let database_id = &DatabaseUuid(Uuid::nil());
519 let prefix_path = segment.construct_prefix_path(tenant, database_id);
520 assert_eq!(
521 prefix_path,
522 "tenant/test_tenant/database/00000000-0000-0000-0000-000000000000/collection/00000000-0000-0000-0000-000000000000/segment/00000000-0000-0000-0000-000000000000"
523 );
524 }
525
526 #[test]
527 fn test_segment_extract_prefix_and_id() {
528 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";
529 let (prefix, id) =
530 Segment::extract_prefix_and_id(path).expect("Failed to extract prefix and id");
531 assert_eq!(
532 prefix,
533 "tenant/test_tenant/database/00000000-0000-0000-0000-000000000000/collection/00000000-0000-0000-0000-000000000000/segment/00000000-0000-0000-0000-000000000000"
534 );
535 assert_eq!(
536 id,
537 Uuid::from_str("00000000-0000-0000-0000-000000000001").expect("Cannot happen")
538 );
539 }
540
541 #[test]
542 fn test_segment_extract_prefix_and_id_legacy() {
543 let path = "00000000-0000-0000-0000-000000000001";
544 let (prefix, id) =
545 Segment::extract_prefix_and_id(path).expect("Failed to extract prefix and id");
546 assert_eq!(prefix, "");
547 assert_eq!(
548 id,
549 Uuid::from_str("00000000-0000-0000-0000-000000000001").expect("Cannot happen")
550 );
551 }
552
553 #[test]
554 fn test_num_shards_empty_file_path() {
555 let segment = Segment {
556 id: SegmentUuid(Uuid::nil()),
557 r#type: SegmentType::BlockfileRecord,
558 scope: SegmentScope::RECORD,
559 collection: CollectionUuid(Uuid::nil()),
560 metadata: None,
561 file_path: HashMap::new(),
562 };
563 assert_eq!(segment.num_shards().unwrap(), 1);
564 }
565
566 #[test]
567 fn test_num_shards_single_shard() {
568 let mut file_path = HashMap::new();
569 file_path.insert("key_a".to_string(), vec!["path_a_0".to_string()]);
570 file_path.insert("key_b".to_string(), vec!["path_b_0".to_string()]);
571 let segment = Segment {
572 id: SegmentUuid(Uuid::nil()),
573 r#type: SegmentType::BlockfileRecord,
574 scope: SegmentScope::RECORD,
575 collection: CollectionUuid(Uuid::nil()),
576 metadata: None,
577 file_path,
578 };
579 assert_eq!(segment.num_shards().unwrap(), 1);
580 }
581
582 #[test]
583 fn test_num_shards_multi_shard() {
584 let mut file_path = HashMap::new();
585 file_path.insert(
586 "key_a".to_string(),
587 vec!["a0".to_string(), "a1".to_string(), "a2".to_string()],
588 );
589 file_path.insert(
590 "key_b".to_string(),
591 vec!["b0".to_string(), "b1".to_string(), "b2".to_string()],
592 );
593 let segment = Segment {
594 id: SegmentUuid(Uuid::nil()),
595 r#type: SegmentType::BlockfileRecord,
596 scope: SegmentScope::RECORD,
597 collection: CollectionUuid(Uuid::nil()),
598 metadata: None,
599 file_path,
600 };
601 assert_eq!(segment.num_shards().unwrap(), 3);
602 }
603
604 #[test]
605 fn test_num_shards_mismatched_lengths() {
606 let mut file_path = HashMap::new();
607 file_path.insert(
608 "key_a".to_string(),
609 vec!["a0".to_string(), "a1".to_string()],
610 );
611 file_path.insert(
612 "key_b".to_string(),
613 vec!["b0".to_string(), "b1".to_string(), "b2".to_string()],
614 );
615 let segment = Segment {
616 id: SegmentUuid(Uuid::nil()),
617 r#type: SegmentType::BlockfileRecord,
618 scope: SegmentScope::RECORD,
619 collection: CollectionUuid(Uuid::nil()),
620 metadata: None,
621 file_path,
622 };
623 let err = segment.num_shards().unwrap_err();
624 assert!(matches!(
625 err,
626 SegmentShardError::MismatchedShardCounts { .. }
627 ));
628 }
629}