Skip to main content

semantic_memory/
vector_codec.rs

1//! Vector codec profile and artifact boundary.
2//!
3//! Codecs are derived acceleration/transport helpers only. SQLite memory rows
4//! and raw f32 embeddings remain authoritative and every artifact must carry a
5//! profile digest that can be checked before decoding.
6
7use 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/// Stable identity for a vector codec configuration.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct VectorCodecProfileV1 {
41    /// Stable schema marker for this profile format.
42    pub schema_version: String,
43    /// Codec implementation identifier.
44    pub codec: String,
45    /// Vector dimensionality. Persisted as fixed-width `u32`, never `usize`.
46    pub dim: u32,
47    /// Effective encoded bits per scalar, when applicable.
48    pub bits: u8,
49    /// Number of projection vectors, reserved for future derived codecs.
50    pub projections: Option<u32>,
51    /// Codec seed, reserved for randomized derived codecs.
52    pub seed: Option<u64>,
53    /// Codec wire/algorithm version.
54    pub codec_version: String,
55    /// Declared scoring semantics for decoded/reference comparison.
56    pub scoring_semantics: String,
57    /// Declared vector normalization contract.
58    pub normalization: String,
59}
60
61impl VectorCodecProfileV1 {
62    /// Build the reference raw f32 profile.
63    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    /// Build the existing per-vector scalar quantization profile.
78    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    /// Build a TurboQuant profile.
93    #[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    /// Build a FibQuant profile.
114    #[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    /// Stable digest over the explicit profile fields.
130    pub fn digest(&self) -> String {
131        b3_json_digest(PROFILE_SCHEMA_V1, self)
132    }
133}
134
135/// Persistable encoded vector plus the profile identity required to decode it.
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137pub struct VectorArtifactV1 {
138    /// Stable schema marker for this artifact format.
139    pub schema_version: String,
140    /// Full codec profile used to produce `encoded`.
141    pub profile: VectorCodecProfileV1,
142    /// Digest of `profile`; checked before decode.
143    pub profile_digest: String,
144    /// Digest of `encoded`; checked before decode when present.
145    #[serde(default)]
146    pub artifact_digest: String,
147    /// Codec-owned encoded bytes.
148    pub encoded: Vec<u8>,
149}
150
151impl VectorArtifactV1 {
152    /// Construct a new artifact and stamp the profile digest.
153    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    /// Stable digest over encoded artifact bytes.
166    pub fn encoded_digest(&self) -> String {
167        b3_digest(&self.encoded)
168    }
169}
170
171/// INT-001: Object-safe vector codec boundary for derived vector artifacts.
172///
173/// This trait is the semantic-memory consumer-facing boundary. It should
174/// eventually be replaced by direct consumption of the canonical
175/// `quant_codec_core::VectorCodec` trait. Until then, implementations of
176/// this trait should also implement the canonical trait where practical.
177pub trait VectorCodec: Send + Sync {
178    /// Codec profile identity.
179    fn profile(&self) -> &VectorCodecProfileV1;
180
181    /// INT-001: Codec capabilities — what operations this codec supports.
182    fn capabilities(&self) -> CodecCapabilityInfo {
183        CodecCapabilityInfo::default()
184    }
185
186    /// Encode a raw f32 vector into a byte artifact.
187    fn encode(&self, vector: &[f32]) -> Result<VectorArtifactV1, MemoryError>;
188
189    /// Decode an artifact back to f32 for reference scoring or differential tests.
190    fn decode(&self, artifact: &VectorArtifactV1) -> Result<Vec<f32>, MemoryError>;
191}
192
193/// INT-001: Codec capability information for semantic-memory consumers.
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
195pub struct CodecCapabilityInfo {
196    /// Codec can estimate inner product from compressed form.
197    pub can_score_inner_product: bool,
198    /// Codec is lossless (raw f32 representation).
199    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/// Reference codec that stores raw little-endian f32 bytes.
238#[derive(Debug, Clone)]
239pub struct RawF32Codec {
240    profile: VectorCodecProfileV1,
241}
242
243impl RawF32Codec {
244    /// Create a raw f32 codec for `dim` dimensions.
245    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/// Codec wrapper around the existing per-vector SQ8 quantization path.
279#[derive(Debug, Clone)]
280pub struct Sq8Codec {
281    profile: VectorCodecProfileV1,
282}
283
284impl Sq8Codec {
285    /// Create an SQ8 codec for `dim` dimensions.
286    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/// Optional TurboQuant codec backend.
436#[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    /// Create a TurboQuant codec profile.
446    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    /// Estimate inner product between a raw query vector and a TurboQuant artifact.
466    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    /// Prepare a query once for scoring many TurboQuant artifacts.
479    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    /// Estimate inner product using a pre-projected query.
488    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    /// Estimate squared L2 distance between a raw query vector and a TurboQuant artifact.
501    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/// A query prepared for repeated FibQuant scoring.
545#[cfg(feature = "fib-quant-codec")]
546#[derive(Debug, Clone)]
547pub struct FibQuantPreparedQuery {
548    vector: Vec<f32>,
549}
550
551/// Optional FibQuant codec backend.
552#[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}