1use crate::{db, quantize, MemoryError};
8use serde::{Deserialize, Serialize};
9use stack_ids::{ContentDigest, DigestBuilder};
10
11const PROFILE_SCHEMA_V1: &str = "vector_codec_profile_v1";
12const ARTIFACT_SCHEMA_V1: &str = "vector_artifact_v1";
13
14fn b3_digest(bytes: &[u8]) -> String {
15 format!("blake3:{}", ContentDigest::compute(bytes).hex())
16}
17
18fn b3_json_digest<T: Serialize>(domain: &str, value: &T) -> String {
19 let mut builder = DigestBuilder::new();
20 builder.update_str(domain).separator();
21 match builder.update_json(value) {
22 Ok(_) => format!("blake3:{}", builder.finalize().hex()),
23 Err(_) => b3_digest(format!("{domain}:digest-fallback").as_bytes()),
24 }
25}
26
27fn dim_u32(dim: usize) -> Result<u32, MemoryError> {
28 u32::try_from(dim).map_err(|_| MemoryError::InvalidConfig {
29 field: "embedding.dimensions",
30 reason: format!("dimension {dim} does not fit vector codec profile u32"),
31 })
32}
33
34fn dim_usize(dim: u32) -> usize {
35 dim as usize
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct VectorCodecProfileV1 {
41 pub schema_version: String,
43 pub codec: String,
45 pub dim: u32,
47 pub bits: u8,
49 pub projections: Option<u32>,
51 pub seed: Option<u64>,
53 pub codec_version: String,
55 pub scoring_semantics: String,
57 pub normalization: String,
59}
60
61impl VectorCodecProfileV1 {
62 pub fn raw_f32(dim: usize) -> Result<Self, MemoryError> {
64 Ok(Self {
65 schema_version: PROFILE_SCHEMA_V1.into(),
66 codec: "raw_f32".into(),
67 dim: dim_u32(dim)?,
68 bits: 32,
69 projections: None,
70 seed: None,
71 codec_version: "1".into(),
72 scoring_semantics: "cosine_on_decoded_f32".into(),
73 normalization: "caller_supplied".into(),
74 })
75 }
76
77 pub fn sq8(dim: usize) -> Result<Self, MemoryError> {
79 Ok(Self {
80 schema_version: PROFILE_SCHEMA_V1.into(),
81 codec: "sq8".into(),
82 dim: dim_u32(dim)?,
83 bits: 8,
84 projections: None,
85 seed: None,
86 codec_version: "1".into(),
87 scoring_semantics: "cosine_on_dequantized_f32".into(),
88 normalization: "caller_supplied".into(),
89 })
90 }
91
92 #[cfg(feature = "turbo-quant-codec")]
94 pub fn turbo_quant(
95 dim: usize,
96 bits: u8,
97 projections: usize,
98 seed: u64,
99 ) -> Result<Self, MemoryError> {
100 Ok(Self {
101 schema_version: PROFILE_SCHEMA_V1.into(),
102 codec: "turbo_quant".into(),
103 dim: dim_u32(dim)?,
104 bits,
105 projections: Some(dim_u32(projections)?),
106 seed: Some(seed),
107 codec_version: "turbo-quant:0.2.0-alpha.1".into(),
108 scoring_semantics: "inner_product_estimate".into(),
109 normalization: "caller_supplied".into(),
110 })
111 }
112
113 #[cfg(feature = "fib-quant-codec")]
115 pub fn fib_quant(dim: usize) -> Result<Self, MemoryError> {
116 Ok(Self {
117 schema_version: PROFILE_SCHEMA_V1.into(),
118 codec: "fib_quant".into(),
119 dim: dim_u32(dim)?,
120 bits: 8,
121 projections: None,
122 seed: None,
123 codec_version: "fib-quant:0.1.0-beta.3".into(),
124 scoring_semantics: "inner_product_estimate".into(),
125 normalization: "caller_supplied".into(),
126 })
127 }
128
129 pub fn digest(&self) -> String {
131 b3_json_digest(PROFILE_SCHEMA_V1, self)
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137pub struct VectorArtifactV1 {
138 pub schema_version: String,
140 pub profile: VectorCodecProfileV1,
142 pub profile_digest: String,
144 #[serde(default)]
146 pub artifact_digest: String,
147 pub encoded: Vec<u8>,
149}
150
151impl VectorArtifactV1 {
152 pub fn new(profile: VectorCodecProfileV1, encoded: Vec<u8>) -> Self {
154 let profile_digest = profile.digest();
155 let artifact_digest = b3_digest(&encoded);
156 Self {
157 schema_version: ARTIFACT_SCHEMA_V1.into(),
158 profile,
159 profile_digest,
160 artifact_digest,
161 encoded,
162 }
163 }
164
165 pub fn encoded_digest(&self) -> String {
167 b3_digest(&self.encoded)
168 }
169}
170
171pub trait VectorCodec: Send + Sync {
178 fn profile(&self) -> &VectorCodecProfileV1;
180
181 fn capabilities(&self) -> CodecCapabilityInfo {
183 CodecCapabilityInfo::default()
184 }
185
186 fn encode(&self, vector: &[f32]) -> Result<VectorArtifactV1, MemoryError>;
188
189 fn decode(&self, artifact: &VectorArtifactV1) -> Result<Vec<f32>, MemoryError>;
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
195pub struct CodecCapabilityInfo {
196 pub can_score_inner_product: bool,
198 pub is_lossless: bool,
200}
201
202fn validate_artifact_profile(
203 expected: &VectorCodecProfileV1,
204 artifact: &VectorArtifactV1,
205) -> Result<(), MemoryError> {
206 let artifact_profile_digest = artifact.profile.digest();
207 if artifact.profile_digest != artifact_profile_digest {
208 return Err(MemoryError::VectorCodecProfileMismatch {
209 expected_digest: artifact_profile_digest,
210 actual_digest: artifact.profile_digest.clone(),
211 });
212 }
213
214 let expected_digest = expected.digest();
215 if artifact.profile_digest != expected_digest {
216 return Err(MemoryError::VectorCodecProfileMismatch {
217 expected_digest,
218 actual_digest: artifact.profile_digest.clone(),
219 });
220 }
221
222 let encoded_digest = artifact.encoded_digest();
223 if !artifact.artifact_digest.is_empty() && artifact.artifact_digest != encoded_digest {
224 return Err(MemoryError::CorruptData {
225 table: "vector_artifacts",
226 row_id: artifact.profile_digest.clone(),
227 detail: format!(
228 "encoded artifact digest mismatch: expected {}, got {}",
229 artifact.artifact_digest, encoded_digest
230 ),
231 });
232 }
233
234 Ok(())
235}
236
237#[derive(Debug, Clone)]
239pub struct RawF32Codec {
240 profile: VectorCodecProfileV1,
241}
242
243impl RawF32Codec {
244 pub fn new(dim: usize) -> Result<Self, MemoryError> {
246 Ok(Self {
247 profile: VectorCodecProfileV1::raw_f32(dim)?,
248 })
249 }
250}
251
252impl VectorCodec for RawF32Codec {
253 fn profile(&self) -> &VectorCodecProfileV1 {
254 &self.profile
255 }
256
257 fn capabilities(&self) -> CodecCapabilityInfo {
258 CodecCapabilityInfo {
259 can_score_inner_product: false,
260 is_lossless: true,
261 }
262 }
263
264 fn encode(&self, vector: &[f32]) -> Result<VectorArtifactV1, MemoryError> {
265 db::validate_embedding(vector, dim_usize(self.profile.dim))?;
266 Ok(VectorArtifactV1::new(
267 self.profile.clone(),
268 db::encode_f32_le(vector),
269 ))
270 }
271
272 fn decode(&self, artifact: &VectorArtifactV1) -> Result<Vec<f32>, MemoryError> {
273 validate_artifact_profile(&self.profile, artifact)?;
274 db::decode_f32_le(&artifact.encoded, dim_usize(self.profile.dim))
275 }
276}
277
278#[derive(Debug, Clone)]
280pub struct Sq8Codec {
281 profile: VectorCodecProfileV1,
282}
283
284impl Sq8Codec {
285 pub fn new(dim: usize) -> Result<Self, MemoryError> {
287 Ok(Self {
288 profile: VectorCodecProfileV1::sq8(dim)?,
289 })
290 }
291}
292
293impl VectorCodec for Sq8Codec {
294 fn profile(&self) -> &VectorCodecProfileV1 {
295 &self.profile
296 }
297
298 fn capabilities(&self) -> CodecCapabilityInfo {
299 CodecCapabilityInfo {
300 can_score_inner_product: false,
301 is_lossless: false,
302 }
303 }
304
305 fn encode(&self, vector: &[f32]) -> Result<VectorArtifactV1, MemoryError> {
306 db::validate_embedding(vector, dim_usize(self.profile.dim))?;
307 let quantized = quantize::Quantizer::new(dim_usize(self.profile.dim)).quantize(vector)?;
308 Ok(VectorArtifactV1::new(
309 self.profile.clone(),
310 quantize::pack_quantized(&quantized),
311 ))
312 }
313
314 fn decode(&self, artifact: &VectorArtifactV1) -> Result<Vec<f32>, MemoryError> {
315 validate_artifact_profile(&self.profile, artifact)?;
316 let quantized = quantize::unpack_quantized(&artifact.encoded, dim_usize(self.profile.dim))?;
317 let decoded = quantize::Quantizer::new(dim_usize(self.profile.dim)).dequantize(&quantized);
318 db::validate_embedding(&decoded, dim_usize(self.profile.dim))?;
319 Ok(decoded)
320 }
321}
322
323#[cfg(feature = "per-dim-codec")]
324#[derive(Debug, Clone)]
325pub struct PerDimCodec {
326 profile: VectorCodecProfileV1,
327 scorer: compressed_scorer::PerDimScorer,
328}
329
330#[cfg(feature = "per-dim-codec")]
331impl PerDimCodec {
332 pub fn new(dim: usize, bits: u8) -> Result<Self, MemoryError> {
333 let scorer = compressed_scorer::PerDimScorer::new(dim, bits as u32)
334 .map_err(|e| MemoryError::QuantizationError(format!("per-dim: {e}")))?;
335 Ok(Self {
336 profile: VectorCodecProfileV1 {
337 schema_version: PROFILE_SCHEMA_V1.into(),
338 codec: "per_dim".into(),
339 dim: dim_u32(dim)?,
340 bits,
341 projections: None,
342 seed: None,
343 codec_version: "compressed-scorer:0.1.0".into(),
344 scoring_semantics: "inner_product_estimate".into(),
345 normalization: "caller_supplied".into(),
346 },
347 scorer,
348 })
349 }
350 pub fn profile(&self) -> &VectorCodecProfileV1 {
351 &self.profile
352 }
353 pub fn config_json(&self) -> Result<String, MemoryError> {
354 serde_json::to_string(
355 &serde_json::json!({"dim": self.profile.dim, "bits": self.profile.bits}),
356 )
357 .map_err(|e| MemoryError::Other(format!("serialize per-dim config: {e}")))
358 }
359
360 pub fn prepare_query(
361 &self,
362 query: &[f32],
363 ) -> Result<compressed_scorer::PerDimPrepared, MemoryError> {
364 use compressed_scorer::CompressedScorer;
365 self.scorer
366 .prepare_query(query)
367 .map_err(|e| MemoryError::QuantizationError(format!("per-dim: {e}")))
368 }
369 pub fn score_inner_product_prepared(
370 &self,
371 artifact: &VectorArtifactV1,
372 prepared: &compressed_scorer::PerDimPrepared,
373 ) -> Result<f32, MemoryError> {
374 use compressed_scorer::CompressedScorer;
375 validate_artifact_profile(&self.profile, artifact)?;
376 let c = self.decode_compressed(artifact)?;
377 self.scorer
378 .score_prepared(prepared, &c)
379 .map_err(|e| MemoryError::QuantizationError(format!("per-dim: {e}")))
380 }
381 fn decode_compressed(
382 &self,
383 artifact: &VectorArtifactV1,
384 ) -> Result<compressed_scorer::PerDimCompressed, MemoryError> {
385 validate_artifact_profile(&self.profile, artifact)?;
386 if artifact.encoded.len() < 4 {
387 return Err(MemoryError::CorruptData {
388 table: "vector_artifacts",
389 row_id: artifact.profile_digest.clone(),
390 detail: "per-dim artifact too short".into(),
391 });
392 }
393 Ok(compressed_scorer::PerDimCompressed {
394 norm: f32::from_le_bytes(artifact.encoded[..4].try_into().unwrap()),
395 codes: artifact.encoded[4..].to_vec(),
396 })
397 }
398}
399
400#[cfg(feature = "per-dim-codec")]
401impl VectorCodec for PerDimCodec {
402 fn profile(&self) -> &VectorCodecProfileV1 {
403 &self.profile
404 }
405 fn capabilities(&self) -> CodecCapabilityInfo {
406 CodecCapabilityInfo {
407 can_score_inner_product: true,
408 is_lossless: false,
409 }
410 }
411 fn encode(&self, vector: &[f32]) -> Result<VectorArtifactV1, MemoryError> {
412 use compressed_scorer::CompressedScorer;
413 db::validate_embedding(vector, dim_usize(self.profile.dim))?;
414 let c = self
415 .scorer
416 .compress(vector)
417 .map_err(|e| MemoryError::QuantizationError(format!("per-dim: {e}")))?;
418 let mut bytes = c.norm.to_le_bytes().to_vec();
419 bytes.extend_from_slice(&c.codes);
420 Ok(VectorArtifactV1::new(self.profile.clone(), bytes))
421 }
422 fn decode(&self, artifact: &VectorArtifactV1) -> Result<Vec<f32>, MemoryError> {
423 use compressed_scorer::CompressedScorer;
424 self.scorer
425 .decode(&self.decode_compressed(artifact)?)
426 .map_err(|e| MemoryError::QuantizationError(format!("per-dim: {e}")))
427 }
428}
429
430#[cfg(feature = "turbo-quant-codec")]
431fn map_turbo_error(err: turbo_quant::TurboQuantError) -> MemoryError {
432 MemoryError::QuantizationError(format!("turbo-quant: {err}"))
433}
434
435#[cfg(feature = "turbo-quant-codec")]
437#[derive(Debug, Clone)]
438pub struct TurboQuantCodec {
439 profile: VectorCodecProfileV1,
440 quantizer: turbo_quant::TurboQuantizer,
441}
442
443#[cfg(feature = "turbo-quant-codec")]
444impl TurboQuantCodec {
445 pub fn new(dim: usize, bits: u8, projections: usize, seed: u64) -> Result<Self, MemoryError> {
447 let quantizer = turbo_quant::TurboQuantizer::new(dim, bits, projections, seed)
448 .map_err(map_turbo_error)?;
449 Ok(Self {
450 profile: VectorCodecProfileV1::turbo_quant(dim, bits, projections, seed)?,
451 quantizer,
452 })
453 }
454
455 fn decode_code(
456 &self,
457 artifact: &VectorArtifactV1,
458 ) -> Result<turbo_quant::TurboCode, MemoryError> {
459 validate_artifact_profile(&self.profile, artifact)?;
460 self.quantizer
461 .decode_code_from_bytes(&artifact.encoded)
462 .map_err(map_turbo_error)
463 }
464
465 pub fn score_inner_product(
467 &self,
468 artifact: &VectorArtifactV1,
469 query: &[f32],
470 ) -> Result<f32, MemoryError> {
471 db::validate_embedding(query, dim_usize(self.profile.dim))?;
472 validate_artifact_profile(&self.profile, artifact)?;
473 self.quantizer
474 .score_inner_product_from_bytes(&artifact.encoded, query)
475 .map_err(map_turbo_error)
476 }
477
478 pub fn prepare_query(
480 &self,
481 query: &[f32],
482 ) -> Result<turbo_quant::TurboProjectedQuery, MemoryError> {
483 db::validate_embedding(query, dim_usize(self.profile.dim))?;
484 self.quantizer.prepare_query(query).map_err(map_turbo_error)
485 }
486
487 pub fn score_inner_product_prepared(
489 &self,
490 artifact: &VectorArtifactV1,
491 prepared: &turbo_quant::TurboProjectedQuery,
492 ) -> Result<f32, MemoryError> {
493 validate_artifact_profile(&self.profile, artifact)?;
494 let code = self.decode_code(artifact)?;
495 self.quantizer
496 .inner_product_estimate_prepared(&code, prepared)
497 .map_err(map_turbo_error)
498 }
499
500 pub fn score_l2(&self, artifact: &VectorArtifactV1, query: &[f32]) -> Result<f32, MemoryError> {
502 db::validate_embedding(query, dim_usize(self.profile.dim))?;
503 validate_artifact_profile(&self.profile, artifact)?;
504 let code = self.decode_code(artifact)?;
505 self.quantizer
506 .l2_distance_estimate(&code, query)
507 .map_err(map_turbo_error)
508 }
509}
510
511#[cfg(feature = "turbo-quant-codec")]
512impl VectorCodec for TurboQuantCodec {
513 fn profile(&self) -> &VectorCodecProfileV1 {
514 &self.profile
515 }
516
517 fn capabilities(&self) -> CodecCapabilityInfo {
518 CodecCapabilityInfo {
519 can_score_inner_product: true,
520 is_lossless: false,
521 }
522 }
523
524 fn encode(&self, vector: &[f32]) -> Result<VectorArtifactV1, MemoryError> {
525 db::validate_embedding(vector, dim_usize(self.profile.dim))?;
526 let encoded = self
527 .quantizer
528 .encode_to_bytes(vector)
529 .map_err(map_turbo_error)?;
530 Ok(VectorArtifactV1::new(self.profile.clone(), encoded))
531 }
532
533 fn decode(&self, artifact: &VectorArtifactV1) -> Result<Vec<f32>, MemoryError> {
534 let code = self.decode_code(artifact)?;
535 let decoded = self
536 .quantizer
537 .decode_approximate(&code)
538 .map_err(map_turbo_error)?;
539 db::validate_embedding(&decoded, dim_usize(self.profile.dim))?;
540 Ok(decoded)
541 }
542}
543
544#[cfg(feature = "fib-quant-codec")]
546#[derive(Debug, Clone)]
547pub struct FibQuantPreparedQuery {
548 vector: Vec<f32>,
549}
550
551#[cfg(feature = "fib-quant-codec")]
553#[derive(Debug, Clone)]
554pub struct FibQuantCodec {
555 profile: VectorCodecProfileV1,
556 quantizer: fib_quant::FibQuantizer,
557}
558
559#[cfg(feature = "fib-quant-codec")]
560fn map_fib_error(err: fib_quant::FibQuantError) -> MemoryError {
561 MemoryError::QuantizationError(format!("fib-quant: {err}"))
562}
563
564#[cfg(feature = "fib-quant-codec")]
565impl FibQuantCodec {
566 pub fn codebook(&self) -> &fib_quant::FibCodebookV1 {
567 self.quantizer.codebook()
568 }
569
570 pub fn gram_scorer(&self) -> Result<crate::scoring::fib_scorer::FibGramScorer, MemoryError> {
571 crate::scoring::fib_scorer::FibGramScorer::from_quantizer(&self.quantizer)
572 }
573
574 pub fn encode_code(&self, vector: &[f32]) -> Result<fib_quant::FibCodeV1, MemoryError> {
575 db::validate_embedding(vector, dim_usize(self.profile.dim))?;
576 self.quantizer.encode(vector).map_err(map_fib_error)
577 }
578
579 pub fn code_from_artifact(
580 &self,
581 artifact: &VectorArtifactV1,
582 ) -> Result<fib_quant::FibCodeV1, MemoryError> {
583 validate_artifact_profile(&self.profile, artifact)?;
584 serde_json::from_slice(&artifact.encoded)
585 .map_err(|e| MemoryError::QuantizationError(format!("fib-quant decode: {e}")))
586 }
587
588 pub fn from_codebook(codebook: fib_quant::FibCodebookV1) -> Result<Self, MemoryError> {
589 let quantizer = fib_quant::FibQuantizer::from_codebook(codebook).map_err(map_fib_error)?;
590 let dim = quantizer.profile().ambient_dim as usize;
591 Ok(Self {
592 profile: VectorCodecProfileV1::fib_quant(dim)?,
593 quantizer,
594 })
595 }
596
597 pub fn new(
598 dim: usize,
599 block_count: usize,
600 gram_table_size: usize,
601 ) -> Result<Self, MemoryError> {
602 let fib_profile =
603 fib_quant::FibQuantProfileV1::paper_default(dim, block_count, gram_table_size, 0)
604 .map_err(map_fib_error)?;
605 let quantizer = fib_quant::FibQuantizer::new(fib_profile).map_err(map_fib_error)?;
606 Ok(Self {
607 profile: VectorCodecProfileV1::fib_quant(dim)?,
608 quantizer,
609 })
610 }
611
612 pub fn prepare_query(&self, query: &[f32]) -> Result<FibQuantPreparedQuery, MemoryError> {
613 db::validate_embedding(query, dim_usize(self.profile.dim))?;
614 Ok(FibQuantPreparedQuery {
615 vector: query.to_vec(),
616 })
617 }
618
619 pub fn score_inner_product_prepared(
620 &self,
621 artifact: &VectorArtifactV1,
622 prepared: &FibQuantPreparedQuery,
623 ) -> Result<f32, MemoryError> {
624 let decoded = self.decode(artifact)?;
625 let score = fib_quant::metrics::cosine_similarity(&prepared.vector, &decoded)
626 .map_err(map_fib_error)?;
627 Ok(score as f32)
628 }
629
630 pub fn score_inner_product(
631 &self,
632 artifact: &VectorArtifactV1,
633 query: &[f32],
634 ) -> Result<f32, MemoryError> {
635 let prepared = self.prepare_query(query)?;
636 self.score_inner_product_prepared(artifact, &prepared)
637 }
638}
639
640#[cfg(feature = "fib-quant-codec")]
641impl VectorCodec for FibQuantCodec {
642 fn profile(&self) -> &VectorCodecProfileV1 {
643 &self.profile
644 }
645 fn capabilities(&self) -> CodecCapabilityInfo {
646 CodecCapabilityInfo {
647 can_score_inner_product: true,
648 is_lossless: false,
649 }
650 }
651
652 fn encode(&self, vector: &[f32]) -> Result<VectorArtifactV1, MemoryError> {
653 db::validate_embedding(vector, dim_usize(self.profile.dim))?;
654 let code = self.encode_code(vector)?;
655 let encoded = serde_json::to_vec(&code)
656 .map_err(|e| MemoryError::QuantizationError(format!("fib-quant encode: {e}")))?;
657 Ok(VectorArtifactV1::new(self.profile.clone(), encoded))
658 }
659
660 fn decode(&self, artifact: &VectorArtifactV1) -> Result<Vec<f32>, MemoryError> {
661 validate_artifact_profile(&self.profile, artifact)?;
662 let code = self.code_from_artifact(artifact)?;
663 let decoded = self.quantizer.decode(&code).map_err(map_fib_error)?;
664 db::validate_embedding(&decoded, dim_usize(self.profile.dim))?;
665 Ok(decoded)
666 }
667}