Skip to main content

nodedb_fts/index/
fieldnorm.rs

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