Skip to main content

nodedb_fts/index/
fieldnorm.rs

1//! Fieldnorm storage: SmallFloat-encoded document lengths per collection.
2//!
3//! Stores a compact `Vec<u8>` array indexed by u32 doc_id. Each byte is
4//! a SmallFloat-encoded document length. Persisted as metadata blob via
5//! the backend's `read_meta`/`write_meta`.
6
7use crate::backend::FtsBackend;
8use crate::codec::smallfloat;
9use crate::index::FtsIndex;
10
11impl<B: FtsBackend> FtsIndex<B> {
12    /// Get the fieldnorm (SmallFloat-encoded doc length) for a doc.
13    ///
14    /// Returns the decoded approximate u32 length, or `None` if not stored.
15    pub fn read_fieldnorm(&self, collection: &str, doc_id: u32) -> Result<Option<u32>, B::Error> {
16        let meta_key = format!("{collection}:fieldnorms");
17        let data = self.backend.read_meta(&meta_key)?;
18        match data {
19            Some(bytes) if (doc_id as usize) < bytes.len() => {
20                Ok(Some(smallfloat::decode(bytes[doc_id as usize])))
21            }
22            _ => Ok(None),
23        }
24    }
25
26    /// Write a fieldnorm byte for a doc_id. Grows the array if needed.
27    pub fn write_fieldnorm(
28        &self,
29        collection: &str,
30        doc_id: u32,
31        doc_length: u32,
32    ) -> Result<(), B::Error> {
33        let meta_key = format!("{collection}:fieldnorms");
34        let mut data = self.backend.read_meta(&meta_key)?.unwrap_or_default();
35
36        let idx = doc_id as usize;
37        if idx >= data.len() {
38            data.resize(idx + 1, 0);
39        }
40        data[idx] = smallfloat::encode(doc_length);
41
42        self.backend.write_meta(&meta_key, &data)
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use crate::backend::memory::MemoryBackend;
49    use crate::codec::smallfloat;
50    use crate::index::FtsIndex;
51
52    #[test]
53    fn fieldnorm_roundtrip() {
54        let idx = FtsIndex::new(MemoryBackend::new());
55        idx.write_fieldnorm("col", 0, 100).unwrap();
56        idx.write_fieldnorm("col", 5, 50).unwrap();
57
58        let norm0 = idx.read_fieldnorm("col", 0).unwrap().unwrap();
59        let norm5 = idx.read_fieldnorm("col", 5).unwrap().unwrap();
60
61        // SmallFloat roundtrip within tolerance.
62        assert!(norm0 <= 100);
63        assert!(norm5 <= 50);
64        assert_eq!(norm0, smallfloat::decode(smallfloat::encode(100)));
65        assert_eq!(norm5, smallfloat::decode(smallfloat::encode(50)));
66    }
67
68    #[test]
69    fn fieldnorm_missing_doc() {
70        let idx = FtsIndex::new(MemoryBackend::new());
71        assert_eq!(idx.read_fieldnorm("col", 99).unwrap(), None);
72    }
73
74    #[test]
75    fn fieldnorm_overwrite() {
76        let idx = FtsIndex::new(MemoryBackend::new());
77        idx.write_fieldnorm("col", 0, 100).unwrap();
78        idx.write_fieldnorm("col", 0, 200).unwrap();
79
80        let norm = idx.read_fieldnorm("col", 0).unwrap().unwrap();
81        assert_eq!(norm, smallfloat::decode(smallfloat::encode(200)));
82    }
83}