Skip to main content

nodedb_fts/
posting.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Core types for the full-text search engine.
4//!
5//! These types are shared between Origin (redb-backed) and Lite (in-memory)
6//! deployments, ensuring identical scoring semantics across all tiers.
7
8use nodedb_types::Surrogate;
9
10/// A single posting entry for a term in a document.
11///
12/// Records the document ID, how many times the term appears, and the
13/// token positions (for phrase matching and proximity boost).
14#[derive(
15    serde::Serialize,
16    serde::Deserialize,
17    zerompk::ToMessagePack,
18    zerompk::FromMessagePack,
19    Clone,
20    Debug,
21)]
22pub struct Posting {
23    pub doc_id: Surrogate,
24    pub term_freq: u32,
25    pub positions: Vec<u32>,
26}
27
28/// Boolean query mode for multi-term searches.
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum QueryMode {
32    /// All query terms must match (intersection). Default.
33    #[default]
34    And,
35    /// Any query term can match (union).
36    Or,
37}
38
39impl From<nodedb_types::text_search::QueryMode> for QueryMode {
40    fn from(mode: nodedb_types::text_search::QueryMode) -> Self {
41        match mode {
42            nodedb_types::text_search::QueryMode::And => Self::And,
43            nodedb_types::text_search::QueryMode::Or => Self::Or,
44        }
45    }
46}
47
48/// A scored search result from the inverted index.
49#[derive(Debug, Clone)]
50pub struct TextSearchResult {
51    pub doc_id: Surrogate,
52    pub score: f32,
53    /// Whether any result came from fuzzy matching.
54    pub fuzzy: bool,
55}
56
57/// A character-level offset of a matched term in the original text.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct MatchOffset {
60    pub start: usize,
61    pub end: usize,
62    pub term: String,
63}
64
65/// BM25 parameters.
66#[derive(Debug, Clone, Copy)]
67pub struct Bm25Params {
68    /// Term frequency saturation. Default: 1.2.
69    pub k1: f32,
70    /// Length normalization. Default: 0.75.
71    pub b: f32,
72}
73
74impl Default for Bm25Params {
75    fn default() -> Self {
76        Self { k1: 1.2, b: 0.75 }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn default_query_mode_is_and() {
86        assert_eq!(QueryMode::default(), QueryMode::And);
87    }
88
89    #[test]
90    fn public_query_mode_maps_to_the_same_mode() {
91        use nodedb_types::text_search::QueryMode as Public;
92        assert_eq!(QueryMode::from(Public::And), QueryMode::And);
93        assert_eq!(QueryMode::from(Public::Or), QueryMode::Or);
94    }
95
96    #[test]
97    fn default_bm25_params() {
98        let p = Bm25Params::default();
99        assert!((p.k1 - 1.2).abs() < f32::EPSILON);
100        assert!((p.b - 0.75).abs() < f32::EPSILON);
101    }
102
103    #[test]
104    fn posting_fields() {
105        let posting = Posting {
106            doc_id: Surrogate(1),
107            term_freq: 3,
108            positions: vec![0, 5, 12],
109        };
110        assert_eq!(posting.doc_id, Surrogate(1));
111        assert_eq!(posting.term_freq, 3);
112        assert_eq!(posting.positions, vec![0, 5, 12]);
113    }
114}