Skip to main content

nodedb_fts/
posting.rs

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