Skip to main content

luci/mapping/
mapping.rs

1use crate::mapping::field_type::FieldType;
2
3/// A single field's mapping: its name, type, and indexing flags.
4///
5/// Flags control which index structures are built for this field:
6///
7/// - `stored`: include in the document store (retrievable via `_source`)
8/// - `indexed`: add to the inverted index (searchable)
9/// - `doc_values`: create columnar storage (sortable, aggregatable)
10/// - `norms`: store field norms for [[best-matching-25|BM25]] scoring
11///
12/// See [[architecture-api-surface#Schema Definition]] and [[architecture-indexing-pipeline]].
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct FieldMapping {
15    /// Field name as it appears in documents.
16    pub name: String,
17    /// Data type.
18    pub field_type: FieldType,
19    /// Whether to include this field's value in the document store.
20    pub stored: bool,
21    /// Whether to add this field to the inverted index for search.
22    pub indexed: bool,
23    /// Whether to create columnar (doc_values) storage for sorting and
24    /// aggregations.
25    pub doc_values: bool,
26    /// Whether to store field length norms for BM25 scoring.
27    /// Only meaningful for `Text` fields.
28    pub norms: bool,
29    /// Analyzer name for `Text` fields. `None` uses the default analyzer.
30    pub analyzer: Option<String>,
31    /// Search-time analyzer name. If set, queries against this field use this
32    /// analyzer instead of `analyzer`. See [[feature-analysis-pipeline]].
33    pub search_analyzer: Option<String>,
34    /// If this is a multi-field sub-field, the name of the parent field.
35    /// The parent's source value is routed to this sub-field during indexing.
36    /// `None` for top-level fields. See [[feature-mapping-multi-fields]].
37    pub parent_field: Option<String>,
38    /// Copy this field's value to target fields at index time.
39    /// See [[feature-mapping-copy-to]].
40    pub copy_to: Vec<String>,
41}
42
43impl FieldMapping {
44    /// Create a mapping with sensible defaults for the given field type.
45    ///
46    /// Default flags per type:
47    ///
48    /// | Type | stored | indexed | doc_values | norms |
49    /// |------|--------|---------|------------|-------|
50    /// | Text | true | true | false | true |
51    /// | Keyword | true | true | true | false |
52    /// | Numeric/Boolean/Date | true | true | true | false |
53    pub fn new(name: impl Into<String>, field_type: FieldType) -> Self {
54        let norms = field_type == FieldType::Text;
55        let is_vector = field_type.is_dense_vector();
56        let is_geo = matches!(field_type, FieldType::GeoPoint | FieldType::GeoShape);
57        let doc_values = !matches!(field_type, FieldType::Text) && !is_vector && !is_geo;
58
59        Self {
60            name: name.into(),
61            field_type,
62            stored: !is_vector && !is_geo, // vectors and geo excluded from source by default
63            indexed: true,
64            doc_values,
65            norms,
66            analyzer: None,
67            search_analyzer: None,
68            parent_field: None,
69            copy_to: Vec::new(),
70        }
71    }
72
73    /// Set the `stored` flag.
74    pub fn stored(mut self, stored: bool) -> Self {
75        self.stored = stored;
76        self
77    }
78
79    /// Set the `indexed` flag.
80    pub fn indexed(mut self, indexed: bool) -> Self {
81        self.indexed = indexed;
82        self
83    }
84
85    /// Set the `doc_values` flag.
86    pub fn doc_values(mut self, doc_values: bool) -> Self {
87        self.doc_values = doc_values;
88        self
89    }
90
91    /// Set the `norms` flag.
92    pub fn norms(mut self, norms: bool) -> Self {
93        self.norms = norms;
94        self
95    }
96
97    /// Set the analyzer name (only meaningful for `Text` fields).
98    pub fn analyzer(mut self, analyzer: impl Into<String>) -> Self {
99        self.analyzer = Some(analyzer.into());
100        self
101    }
102
103    /// Set the search-time analyzer name (only meaningful for `Text` fields).
104    /// See [[feature-analysis-pipeline]].
105    pub fn search_analyzer(mut self, analyzer: impl Into<String>) -> Self {
106        self.search_analyzer = Some(analyzer.into());
107        self
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn text_defaults() {
117        let m = FieldMapping::new("title", FieldType::Text);
118        assert!(m.stored);
119        assert!(m.indexed);
120        assert!(!m.doc_values);
121        assert!(m.norms);
122        assert!(m.analyzer.is_none());
123    }
124
125    #[test]
126    fn keyword_defaults() {
127        let m = FieldMapping::new("status", FieldType::Keyword);
128        assert!(m.stored);
129        assert!(m.indexed);
130        assert!(m.doc_values);
131        assert!(!m.norms);
132    }
133
134    #[test]
135    fn numeric_defaults() {
136        for ft in [
137            FieldType::Integer,
138            FieldType::Long,
139            FieldType::Float,
140            FieldType::Double,
141        ] {
142            let m = FieldMapping::new("val", ft);
143            assert!(m.stored);
144            assert!(m.indexed);
145            assert!(m.doc_values);
146            assert!(!m.norms);
147        }
148    }
149
150    #[test]
151    fn boolean_defaults() {
152        let m = FieldMapping::new("active", FieldType::Boolean);
153        assert!(m.doc_values);
154        assert!(!m.norms);
155    }
156
157    #[test]
158    fn date_defaults() {
159        let m = FieldMapping::new("created", FieldType::Date);
160        assert!(m.doc_values);
161        assert!(!m.norms);
162    }
163
164    #[test]
165    fn builder_chaining() {
166        let m = FieldMapping::new("body", FieldType::Text)
167            .stored(false)
168            .norms(false)
169            .analyzer("whitespace");
170        assert!(!m.stored);
171        assert!(!m.norms);
172        assert_eq!(m.analyzer.as_deref(), Some("whitespace"));
173    }
174}