memstead_base/search_index/
schema.rs1use std::collections::{BTreeMap, BTreeSet};
14use std::sync::Arc;
15
16use memstead_schema::{Filterable, Schema};
17use tantivy::schema::{
18 Field, STORED, STRING, Schema as TantivySchema, SchemaBuilder, TextFieldIndexing, TextOptions,
19};
20
21use super::tokenizer::MEMSTEAD_TOKENIZER;
22
23#[derive(Clone, Debug)]
25pub struct IndexFields {
26 pub schema: TantivySchema,
27 pub id: Field,
28 pub mem: Field,
29 pub entity_type: Field,
30 pub title: Field,
31 pub sections: BTreeMap<String, Field>,
34 pub metadata: BTreeMap<String, Field>,
37 pub metadata_text: Field,
46}
47
48impl IndexFields {
49 pub fn build(mem_schema: Option<&Arc<Schema>>) -> Self {
55 let mut builder = SchemaBuilder::new();
56
57 let id = builder.add_text_field("id", STRING | STORED);
60 let mem = builder.add_text_field("mem", STRING | STORED);
61 let entity_type = builder.add_text_field("entity_type", STRING | STORED);
62 let title = builder.add_text_field("title", text_options());
63 let metadata_text = builder.add_text_field("metadata", text_options());
64
65 let section_keys: BTreeSet<String> = match mem_schema {
68 Some(schema) => schema
69 .types
70 .values()
71 .flat_map(|t| t.sections.iter().map(|s| s.key.clone()))
72 .collect(),
73 None => BTreeSet::new(),
74 };
75 let mut sections = BTreeMap::new();
76 for key in section_keys {
77 let f = builder.add_text_field(&format!("section_{key}"), text_options());
78 sections.insert(key, f);
79 }
80
81 let filterable_keys: BTreeSet<String> = match mem_schema {
85 Some(schema) => schema
86 .types
87 .values()
88 .flat_map(|t| t.metadata_fields.iter())
89 .filter(|f| matches!(f.filterable, Filterable::Equality | Filterable::Range))
90 .map(|f| f.key.clone())
91 .collect(),
92 None => BTreeSet::new(),
93 };
94 let mut metadata = BTreeMap::new();
95 for key in filterable_keys {
96 let f = builder.add_text_field(&format!("meta_{key}"), STRING);
97 metadata.insert(key, f);
98 }
99
100 let schema = builder.build();
101
102 Self {
103 schema,
104 id,
105 mem,
106 entity_type,
107 title,
108 sections,
109 metadata,
110 metadata_text,
111 }
112 }
113}
114
115fn text_options() -> TextOptions {
118 TextOptions::default().set_indexing_options(
119 TextFieldIndexing::default()
120 .set_tokenizer(MEMSTEAD_TOKENIZER)
121 .set_index_option(tantivy::schema::IndexRecordOption::WithFreqsAndPositions),
122 )
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use memstead_schema::Schema;
129
130 #[test]
131 fn build_without_schema_still_has_fixed_fields() {
132 let fields = IndexFields::build(None);
133 assert!(fields.schema.get_field("id").is_ok());
134 assert!(fields.schema.get_field("mem").is_ok());
135 assert!(fields.schema.get_field("title").is_ok());
136 assert!(fields.sections.is_empty());
137 assert!(fields.metadata.is_empty());
138 }
139
140 #[test]
141 fn build_with_default_schema_emits_section_fields() {
142 let schema = Schema::builtin_default();
143 let fields = IndexFields::build(Some(&schema));
144 assert!(fields.sections.contains_key("identity"));
147 assert!(fields.sections.contains_key("purpose"));
148 assert!(fields.schema.get_field("section_identity").is_ok());
149 }
150
151 #[test]
152 fn build_with_default_schema_emits_filterable_metadata() {
153 let schema = Schema::builtin_default();
154 let fields = IndexFields::build(Some(&schema));
155 assert!(
159 !fields.metadata.is_empty(),
160 "default schema should expose at least one filterable metadata field"
161 );
162 }
163}