diff --git a/Cargo.lock b/Cargo.lock index 19acee93..2f1b91b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -846,6 +846,7 @@ dependencies = [ "rand 0.10.1", "rand_chacha 0.10.0", "rayon", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 43111e50..8e7ec30c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ all-features = false [dependencies] rayon = "1.10" +serde = { version = "1.0", features = ["derive"], optional = true } [dev-dependencies] # Test/bench corpora are seeded with these; not needed by the library. @@ -78,6 +79,7 @@ rand_chacha = "0.10" # target takes the scalar fallback. `experimental` exposes MultiBucketBitmap # (research scaffold), kept off the stable surface. experimental = [] +serde = ["dep:serde"] # `test-utils` exposes internal dispatch probes used by the crate's own integration # tests (e.g. the allocation-free guarantee check). Gated off the default surface # because these helpers are not part of the public API and carry no semver guarantee. diff --git a/docs/msrv-and-features.md b/docs/msrv-and-features.md index 3700aa84..02dfa48a 100644 --- a/docs/msrv-and-features.md +++ b/docs/msrv-and-features.md @@ -19,7 +19,7 @@ any migration note. | Surface | Default features | Stable default-off features | Optional dependency features | Experimental/internal features | | --- | --- | --- | --- | --- | -| `ordvec` | none | none | none | `experimental` exposes `MultiBucketBitmap`; `test-utils` is repo-test-only and has no public stability promise. | +| `ordvec` | none | `serde` derives `Serialize`/`Deserialize` for `SearchResults` only | `serde` | `experimental` exposes `MultiBucketBitmap`; `test-utils` is repo-test-only and has no public stability promise. | | `ordvec-manifest` | none | none | `cli`, `sqlite`, `sqlite-bundled` | none | | `ordvec-python` | n/a | n/a | n/a | n/a | | `ordvec-manifest-python` | n/a | n/a | n/a | n/a | diff --git a/src/bitmap.rs b/src/bitmap.rs index 0ac18459..46e527b4 100644 --- a/src/bitmap.rs +++ b/src/bitmap.rs @@ -56,6 +56,17 @@ pub struct Bitmap { bitmaps: Vec, } +impl std::fmt::Debug for Bitmap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Bitmap") + .field("dim", &self.dim) + .field("n_top", &self.n_top) + .field("n_vectors", &self.n_vectors) + .field("bytes_per_vector", &self.bytes_per_vec()) + .finish() + } +} + impl Bitmap { pub fn validate_params(dim: usize, n_top: usize) -> Result<(), OrdvecError> { if dim == 0 { diff --git a/src/fastscan.rs b/src/fastscan.rs index 86b26f7d..b856da4d 100644 --- a/src/fastscan.rs +++ b/src/fastscan.rs @@ -534,6 +534,15 @@ pub struct RankQuantFastscan { packed_fs: Vec, } +impl std::fmt::Debug for RankQuantFastscan { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RankQuantFastscan") + .field("dim", &self.dim) + .field("n_vectors", &self.n_vectors) + .finish() + } +} + impl RankQuantFastscan { /// Construct an empty FastScan b=2 index. /// diff --git a/src/lib.rs b/src/lib.rs index 1b131390..6f7bdf5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -312,7 +312,93 @@ pub struct SearchResults { pub k: usize, } +#[cfg(feature = "serde")] +#[derive(serde::Deserialize, serde::Serialize)] +struct SearchResultsSerdeRepr { + scores: Vec, + indices: Vec, + nq: u64, + k: u64, +} + +#[cfg(feature = "serde")] +impl serde::Serialize for SearchResults { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct as _; + + let mut state = serializer.serialize_struct("SearchResults", 4)?; + state.serialize_field("scores", &self.scores)?; + state.serialize_field("indices", &self.indices)?; + state.serialize_field("nq", &(self.nq as u64))?; + state.serialize_field("k", &(self.k as u64))?; + state.end() + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for SearchResults { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let repr = ::deserialize(deserializer)?; + SearchResults::from_serde_repr(repr).map_err(serde::de::Error::custom) + } +} + +impl fmt::Debug for SearchResults { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SearchResults") + .field("nq", &self.nq) + .field("k", &self.k) + .field("scores_len", &self.scores.len()) + .field("indices_len", &self.indices.len()) + .finish() + } +} + impl SearchResults { + #[cfg(feature = "serde")] + fn from_serde_repr(repr: SearchResultsSerdeRepr) -> Result { + let nq = usize::try_from(repr.nq) + .map_err(|_| "SearchResults nq does not fit usize".to_string())?; + let k = usize::try_from(repr.k) + .map_err(|_| "SearchResults k does not fit usize".to_string())?; + let expected_len = nq + .checked_mul(k) + .ok_or_else(|| "SearchResults nq * k overflows usize".to_string())?; + if repr.scores.len() != expected_len { + return Err(format!( + "SearchResults scores length {} does not match nq * k {}", + repr.scores.len(), + expected_len + )); + } + if repr.indices.len() != expected_len { + return Err(format!( + "SearchResults indices length {} does not match nq * k {}", + repr.indices.len(), + expected_len + )); + } + if repr.scores.len() != repr.indices.len() { + return Err(format!( + "SearchResults scores length {} does not match indices length {}", + repr.scores.len(), + repr.indices.len() + )); + } + Ok(Self { + scores: repr.scores, + indices: repr.indices, + nq, + k, + }) + } + pub fn scores_for_query(&self, qi: usize) -> &[f32] { &self.scores[qi * self.k..(qi + 1) * self.k] } @@ -321,3 +407,613 @@ impl SearchResults { &self.indices[qi * self.k..(qi + 1) * self.k] } } + +#[cfg(all(test, feature = "serde"))] +mod search_results_serde_tests { + use super::{SearchResults, SearchResultsSerdeRepr}; + use serde::de::{self, DeserializeSeed, IntoDeserializer, SeqAccess, Visitor}; + use serde::ser::{Impossible, SerializeSeq, SerializeStruct}; + use serde::Deserialize as _; + use serde::Serialize as _; + use std::fmt; + + fn repr(scores: Vec, indices: Vec, nq: u64, k: u64) -> SearchResultsSerdeRepr { + SearchResultsSerdeRepr { + scores, + indices, + nq, + k, + } + } + + #[derive(Debug)] + struct TestSerError(String); + + impl fmt::Display for TestSerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } + } + + impl std::error::Error for TestSerError {} + + impl serde::ser::Error for TestSerError { + fn custom(msg: T) -> Self { + Self(msg.to_string()) + } + } + + macro_rules! unsupported_serializer_common { + () => { + fn serialize_bool(self, _v: bool) -> Result { + Err(serde::ser::Error::custom("unsupported bool")) + } + fn serialize_i8(self, _v: i8) -> Result { + Err(serde::ser::Error::custom("unsupported i8")) + } + fn serialize_i16(self, _v: i16) -> Result { + Err(serde::ser::Error::custom("unsupported i16")) + } + fn serialize_i32(self, _v: i32) -> Result { + Err(serde::ser::Error::custom("unsupported i32")) + } + fn serialize_i128(self, _v: i128) -> Result { + Err(serde::ser::Error::custom("unsupported i128")) + } + fn serialize_u128(self, _v: u128) -> Result { + Err(serde::ser::Error::custom("unsupported u128")) + } + fn serialize_f64(self, _v: f64) -> Result { + Err(serde::ser::Error::custom("unsupported f64")) + } + fn serialize_char(self, _v: char) -> Result { + Err(serde::ser::Error::custom("unsupported char")) + } + fn serialize_str(self, _v: &str) -> Result { + Err(serde::ser::Error::custom("unsupported str")) + } + fn serialize_bytes(self, _v: &[u8]) -> Result { + Err(serde::ser::Error::custom("unsupported bytes")) + } + fn serialize_none(self) -> Result { + Err(serde::ser::Error::custom("unsupported none")) + } + fn serialize_some( + self, + _value: &T, + ) -> Result { + Err(serde::ser::Error::custom("unsupported some")) + } + fn serialize_unit(self) -> Result { + Err(serde::ser::Error::custom("unsupported unit")) + } + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Err(serde::ser::Error::custom("unsupported unit struct")) + } + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + Err(serde::ser::Error::custom("unsupported unit variant")) + } + fn serialize_newtype_struct( + self, + _name: &'static str, + _value: &T, + ) -> Result { + Err(serde::ser::Error::custom("unsupported newtype struct")) + } + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result { + Err(serde::ser::Error::custom("unsupported newtype variant")) + } + fn serialize_tuple(self, _len: usize) -> Result { + Err(serde::ser::Error::custom("unsupported tuple")) + } + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(serde::ser::Error::custom("unsupported tuple struct")) + } + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(serde::ser::Error::custom("unsupported tuple variant")) + } + fn serialize_map(self, _len: Option) -> Result { + Err(serde::ser::Error::custom("unsupported map")) + } + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(serde::ser::Error::custom("unsupported struct variant")) + } + }; + } + + macro_rules! unsupported_i64_serializer { + () => { + fn serialize_i64(self, _v: i64) -> Result { + Err(serde::ser::Error::custom("unsupported i64")) + } + }; + } + + macro_rules! unsupported_f32_serializer { + () => { + fn serialize_f32(self, _v: f32) -> Result { + Err(serde::ser::Error::custom("unsupported f32")) + } + }; + } + + macro_rules! unsupported_unsigned_serializers { + () => { + fn serialize_u8(self, _v: u8) -> Result { + Err(serde::ser::Error::custom("unsupported u8")) + } + fn serialize_u16(self, _v: u16) -> Result { + Err(serde::ser::Error::custom("unsupported u16")) + } + fn serialize_u32(self, _v: u32) -> Result { + Err(serde::ser::Error::custom("unsupported u32")) + } + fn serialize_u64(self, _v: u64) -> Result { + Err(serde::ser::Error::custom("unsupported u64")) + } + }; + } + + macro_rules! unsupported_seq_serializer { + () => { + fn serialize_seq(self, _len: Option) -> Result { + Err(serde::ser::Error::custom("unsupported seq")) + } + }; + } + + macro_rules! unsupported_struct_serializer { + () => { + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(serde::ser::Error::custom("unsupported struct")) + } + }; + } + + struct SearchResultsSerializer; + + impl serde::Serializer for SearchResultsSerializer { + type Ok = SearchResultsSerdeRepr; + type Error = TestSerError; + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = SearchResultsStructSerializer; + type SerializeStructVariant = Impossible; + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(SearchResultsStructSerializer::default()) + } + + unsupported_serializer_common! {} + unsupported_i64_serializer! {} + unsupported_f32_serializer! {} + unsupported_unsigned_serializers! {} + unsupported_seq_serializer! {} + } + + #[derive(Default)] + struct SearchResultsStructSerializer { + scores: Option>, + indices: Option>, + nq: Option, + k: Option, + } + + impl SerializeStruct for SearchResultsStructSerializer { + type Ok = SearchResultsSerdeRepr; + type Error = TestSerError; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> { + match key { + "scores" => self.scores = Some(value.serialize(F32VecSerializer)?), + "indices" => self.indices = Some(value.serialize(I64VecSerializer)?), + "nq" => self.nq = Some(value.serialize(U64Serializer)?), + "k" => self.k = Some(value.serialize(U64Serializer)?), + _ => { + return Err(serde::ser::Error::custom(format!( + "unexpected SearchResults field {key}" + ))); + } + } + Ok(()) + } + + fn end(self) -> Result { + Ok(SearchResultsSerdeRepr { + scores: self + .scores + .ok_or_else(|| serde::ser::Error::custom("missing scores"))?, + indices: self + .indices + .ok_or_else(|| serde::ser::Error::custom("missing indices"))?, + nq: self + .nq + .ok_or_else(|| serde::ser::Error::custom("missing nq"))?, + k: self + .k + .ok_or_else(|| serde::ser::Error::custom("missing k"))?, + }) + } + } + + struct F32VecSerializer; + struct I64VecSerializer; + struct U64Serializer; + struct F32ValueSerializer; + struct I64ValueSerializer; + + impl serde::Serializer for F32VecSerializer { + type Ok = Vec; + type Error = TestSerError; + type SerializeSeq = F32SeqSerializer; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + + fn serialize_seq(self, len: Option) -> Result { + Ok(F32SeqSerializer { + values: Vec::with_capacity(len.unwrap_or(0)), + }) + } + + unsupported_serializer_common! {} + unsupported_i64_serializer! {} + unsupported_f32_serializer! {} + unsupported_unsigned_serializers! {} + unsupported_struct_serializer! {} + } + + impl serde::Serializer for I64VecSerializer { + type Ok = Vec; + type Error = TestSerError; + type SerializeSeq = I64SeqSerializer; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + + fn serialize_seq(self, len: Option) -> Result { + Ok(I64SeqSerializer { + values: Vec::with_capacity(len.unwrap_or(0)), + }) + } + + unsupported_serializer_common! {} + unsupported_i64_serializer! {} + unsupported_f32_serializer! {} + unsupported_unsigned_serializers! {} + unsupported_struct_serializer! {} + } + + struct F32SeqSerializer { + values: Vec, + } + + impl SerializeSeq for F32SeqSerializer { + type Ok = Vec; + type Error = TestSerError; + + fn serialize_element( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + self.values.push(value.serialize(F32ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(self.values) + } + } + + struct I64SeqSerializer { + values: Vec, + } + + impl SerializeSeq for I64SeqSerializer { + type Ok = Vec; + type Error = TestSerError; + + fn serialize_element( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + self.values.push(value.serialize(I64ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(self.values) + } + } + + impl serde::Serializer for F32ValueSerializer { + type Ok = f32; + type Error = TestSerError; + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + + fn serialize_f32(self, value: f32) -> Result { + Ok(value) + } + + unsupported_serializer_common! {} + unsupported_i64_serializer! {} + unsupported_unsigned_serializers! {} + unsupported_seq_serializer! {} + unsupported_struct_serializer! {} + } + + impl serde::Serializer for I64ValueSerializer { + type Ok = i64; + type Error = TestSerError; + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + + fn serialize_i64(self, value: i64) -> Result { + Ok(value) + } + + unsupported_serializer_common! {} + unsupported_f32_serializer! {} + unsupported_unsigned_serializers! {} + unsupported_seq_serializer! {} + unsupported_struct_serializer! {} + } + + impl serde::Serializer for U64Serializer { + type Ok = u64; + type Error = TestSerError; + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + + fn serialize_u8(self, value: u8) -> Result { + Ok(u64::from(value)) + } + + fn serialize_u16(self, value: u16) -> Result { + Ok(u64::from(value)) + } + + fn serialize_u32(self, value: u32) -> Result { + Ok(u64::from(value)) + } + + fn serialize_u64(self, value: u64) -> Result { + Ok(value) + } + + unsupported_serializer_common! {} + unsupported_i64_serializer! {} + unsupported_f32_serializer! {} + unsupported_seq_serializer! {} + unsupported_struct_serializer! {} + } + + enum TestValue { + F32s(Vec), + I64s(Vec), + U64(u64), + } + + struct VecSeq { + iter: std::vec::IntoIter, + } + + impl<'de, T> SeqAccess<'de> for VecSeq + where + T: IntoDeserializer<'de, de::value::Error>, + { + type Error = de::value::Error; + + fn next_element_seed(&mut self, seed: S) -> Result, Self::Error> + where + S: DeserializeSeed<'de>, + { + self.iter + .next() + .map(|value| seed.deserialize(value.into_deserializer())) + .transpose() + } + } + + impl<'de> serde::Deserializer<'de> for TestValue { + type Error = de::value::Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match self { + Self::F32s(values) => visitor.visit_seq(VecSeq { + iter: values.into_iter(), + }), + Self::I64s(values) => visitor.visit_seq(VecSeq { + iter: values.into_iter(), + }), + Self::U64(value) => visitor.visit_u64(value), + } + } + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 u8 u16 u32 u64 u128 f32 f64 char str string + bytes byte_buf option unit unit_struct newtype_struct seq tuple + tuple_struct map struct enum identifier ignored_any + } + } + + struct ReprDeserializer { + values: std::vec::IntoIter, + } + + impl ReprDeserializer { + fn new(repr: SearchResultsSerdeRepr) -> Self { + Self { + values: vec![ + TestValue::F32s(repr.scores), + TestValue::I64s(repr.indices), + TestValue::U64(repr.nq), + TestValue::U64(repr.k), + ] + .into_iter(), + } + } + } + + impl<'de> SeqAccess<'de> for ReprDeserializer { + type Error = de::value::Error; + + fn next_element_seed(&mut self, seed: S) -> Result, Self::Error> + where + S: DeserializeSeed<'de>, + { + self.values + .next() + .map(|value| seed.deserialize(value)) + .transpose() + } + } + + impl<'de> serde::Deserializer<'de> for ReprDeserializer { + type Error = de::value::Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_seq(self) + } + + fn deserialize_struct( + self, + _name: &'static str, + _fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_seq(self) + } + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 u8 u16 u32 u64 u128 f32 f64 char str string + bytes byte_buf option unit unit_struct newtype_struct seq tuple + tuple_struct map enum identifier ignored_any + } + } + + fn deserialize(repr: SearchResultsSerdeRepr) -> Result { + SearchResults::deserialize(ReprDeserializer::new(repr)) + } + + fn roundtrip(results: &SearchResults) -> SearchResults { + let encoded = results + .serialize(SearchResultsSerializer) + .expect("serialize SearchResults"); + deserialize(encoded).expect("deserialize SearchResults") + } + + #[test] + fn search_results_roundtrips_through_in_memory_serde_format() { + let results = SearchResults { + scores: vec![1.0, 0.25, -0.5, 0.0], + indices: vec![10, 2, -1, 7], + nq: 2, + k: 2, + }; + + let decoded = roundtrip(&results); + assert_eq!(decoded.scores, results.scores); + assert_eq!(decoded.indices, results.indices); + assert_eq!(decoded.nq, results.nq); + assert_eq!(decoded.k, results.k); + } + + #[test] + fn search_results_deserialize_accepts_valid_shape() { + let results = deserialize(repr(vec![1.0, 0.5], vec![7, 3], 1, 2)).unwrap(); + assert_eq!(results.scores_for_query(0), &[1.0, 0.5]); + assert_eq!(results.indices_for_query(0), &[7, 3]); + } + + #[test] + fn search_results_deserialize_rejects_invalid_lengths() { + let invalid = [ + repr(vec![1.0], vec![7], 1, 2), + repr(vec![1.0, 0.5], vec![7], 1, 2), + repr(vec![1.0], vec![7, 3], 1, 2), + ]; + for repr in invalid { + assert!(deserialize(repr).is_err()); + } + } + + #[test] + fn search_results_deserialize_rejects_shape_overflow() { + let repr = repr(Vec::new(), Vec::new(), u64::MAX, 2); + assert!(deserialize(repr).is_err()); + } +} diff --git a/src/quant.rs b/src/quant.rs index 070fa197..4e44e361 100644 --- a/src/quant.rs +++ b/src/quant.rs @@ -217,6 +217,17 @@ pub struct RankQuant { pub(crate) packed: Vec, } +impl std::fmt::Debug for RankQuant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RankQuant") + .field("dim", &self.dim) + .field("bits", &self.bits) + .field("n_vectors", &self.n_vectors) + .field("capability", &self.capability) + .finish() + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TwoStageCandidatePolicy { pub min_candidates: usize, diff --git a/src/rank.rs b/src/rank.rs index 902557db..94dbe400 100644 --- a/src/rank.rs +++ b/src/rank.rs @@ -337,6 +337,15 @@ pub struct Rank { ranks: Vec, } +impl std::fmt::Debug for Rank { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Rank") + .field("dim", &self.dim) + .field("n_vectors", &self.n_vectors) + .finish() + } +} + impl Rank { pub fn new(dim: usize) -> Self { assert!(dim >= 2, "dim must be >= 2"); diff --git a/src/sign_bitmap.rs b/src/sign_bitmap.rs index 4c7b7899..7d6bbcc6 100644 --- a/src/sign_bitmap.rs +++ b/src/sign_bitmap.rs @@ -98,6 +98,16 @@ pub struct SignBitmap { bitmaps: Vec, } +impl std::fmt::Debug for SignBitmap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SignBitmap") + .field("dim", &self.dim) + .field("n_vectors", &self.n_vectors) + .field("bytes_per_vector", &self.bytes_per_vec()) + .finish() + } +} + impl SignBitmap { pub fn validate_dim(dim: usize) -> Result<(), OrdvecError> { if dim == 0 { @@ -1073,8 +1083,8 @@ mod tests { fn load_rejects_bad_magic() { let tmp = std::env::temp_dir().join("ordvec_sign_bitmap_bad_magic.tvsb"); std::fs::write(&tmp, b"BAD!\x01\x00\x00\x01\x00\x00\x00\x00\x00").expect("write tmp"); - // SignBitmap does not derive Debug (matches the convention of - // other rank-mode types), so unwrap_err / expect_err do not apply; + // SignBitmap implements a params-only Debug that intentionally avoids + // dumping packed buffers, so keep this explicit match for the error arm; // use a match to inspect the Err arm instead. match SignBitmap::load(&tmp) { Ok(_) => { diff --git a/tests/debug_serde.rs b/tests/debug_serde.rs new file mode 100644 index 00000000..c3fd6598 --- /dev/null +++ b/tests/debug_serde.rs @@ -0,0 +1,56 @@ +use ordvec::{Bitmap, Rank, RankQuant, RankQuantFastscan, SearchResults, SignBitmap}; + +fn assert_param_debug(debug: &str, type_name: &str) { + assert!(debug.contains(type_name), "{type_name} missing: {debug}"); + assert!(debug.contains("dim"), "{type_name} missing dim: {debug}"); + assert!( + !debug.contains("packed") + && !debug.contains("ranks") + && !debug.contains("bitmaps") + && !debug.contains("scores:") + && !debug.contains("indices:"), + "{type_name} debug output leaks storage fields: {debug}" + ); +} + +#[test] +fn public_types_debug_prints_shape_not_storage() { + let rank = Rank::new(8); + assert_param_debug(&format!("{rank:?}"), "Rank"); + + let rq = RankQuant::new(64, 2); + let rq_dbg = format!("{rq:?}"); + assert_param_debug(&rq_dbg, "RankQuant"); + assert!(rq_dbg.contains("bits")); + + let bitmap = Bitmap::new(64, 16); + let bitmap_dbg = format!("{bitmap:?}"); + assert_param_debug(&bitmap_dbg, "Bitmap"); + assert!(bitmap_dbg.contains("n_top")); + + let sign = SignBitmap::new(64); + assert_param_debug(&format!("{sign:?}"), "SignBitmap"); + + let fastscan = RankQuantFastscan::new(64); + assert_param_debug(&format!("{fastscan:?}"), "RankQuantFastscan"); + + let results = SearchResults { + scores: vec![0.5, 0.25], + indices: vec![7, 3], + nq: 1, + k: 2, + }; + let results_dbg = format!("{results:?}"); + assert!(results_dbg.contains("SearchResults")); + assert!(results_dbg.contains("scores_len")); + assert!(results_dbg.contains("indices_len")); + assert!(!results_dbg.contains("scores:") && !results_dbg.contains("indices:")); +} + +#[cfg(feature = "serde")] +#[test] +fn search_results_implements_serde_traits_with_serde_feature() { + fn assert_serde serde::Deserialize<'de>>() {} + + assert_serde::(); +}