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
39/// A scored search result from the inverted index.
40#[derive(Debug, Clone)]
41pub struct TextSearchResult {
42    pub doc_id: Surrogate,
43    pub score: f32,
44    /// Whether any result came from fuzzy matching.
45    pub fuzzy: bool,
46}
47
48/// A character-level offset of a matched term in the original text.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct MatchOffset {
51    pub start: usize,
52    pub end: usize,
53    pub term: String,
54}
55
56/// BM25 parameters.
57#[derive(Debug, Clone, Copy)]
58pub struct Bm25Params {
59    /// Term frequency saturation. Default: 1.2.
60    pub k1: f32,
61    /// Length normalization. Default: 0.75.
62    pub b: f32,
63}
64
65impl Default for Bm25Params {
66    fn default() -> Self {
67        Self { k1: 1.2, b: 0.75 }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn default_query_mode_is_and() {
77        assert_eq!(QueryMode::default(), QueryMode::And);
78    }
79
80    #[test]
81    fn default_bm25_params() {
82        let p = Bm25Params::default();
83        assert!((p.k1 - 1.2).abs() < f32::EPSILON);
84        assert!((p.b - 0.75).abs() < f32::EPSILON);
85    }
86
87    #[test]
88    fn posting_fields() {
89        let posting = Posting {
90            doc_id: Surrogate(1),
91            term_freq: 3,
92            positions: vec![0, 5, 12],
93        };
94        assert_eq!(posting.doc_id, Surrogate(1));
95        assert_eq!(posting.term_freq, 3);
96        assert_eq!(posting.positions, vec![0, 5, 12]);
97    }
98}