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