Skip to main content

use_search_index/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5
6macro_rules! string_newtype {
7    ($(#[$meta:meta])* $name:ident) => {
8        $(#[$meta])*
9        #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
10        pub struct $name(String);
11
12        impl $name {
13            /// Creates a new string-backed primitive.
14            pub fn new(value: impl Into<String>) -> Self {
15                Self(value.into())
16            }
17
18            /// Returns the stored string value.
19            pub fn as_str(&self) -> &str {
20                &self.0
21            }
22        }
23
24        impl AsRef<str> for $name {
25            fn as_ref(&self) -> &str {
26                self.as_str()
27            }
28        }
29
30        impl From<String> for $name {
31            fn from(value: String) -> Self {
32                Self::new(value)
33            }
34        }
35
36        impl From<&str> for $name {
37            fn from(value: &str) -> Self {
38                Self::new(value)
39            }
40        }
41
42        impl fmt::Display for $name {
43            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44                formatter.write_str(self.as_str())
45            }
46        }
47    };
48}
49
50string_newtype! {
51    /// A search index name.
52    IndexName
53}
54string_newtype! {
55    /// A search index document identifier.
56    SearchDocumentId
57}
58string_newtype! {
59    /// A searchable field name.
60    SearchField
61}
62string_newtype! {
63    /// A search term.
64    SearchTerm
65}
66string_newtype! {
67    /// A search analyzer label.
68    SearchAnalyzer
69}
70string_newtype! {
71    /// A search filter label or expression shape.
72    SearchFilter
73}
74
75/// A modeled search-index document.
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct SearchIndexDocument {
78    id: SearchDocumentId,
79    fields: Vec<(SearchField, String)>,
80}
81
82impl SearchIndexDocument {
83    /// Creates an empty search-index document.
84    pub fn new(id: SearchDocumentId) -> Self {
85        Self {
86            id,
87            fields: Vec::new(),
88        }
89    }
90
91    /// Adds a field value.
92    pub fn with_field(mut self, field: SearchField, value: impl Into<String>) -> Self {
93        self.fields.push((field, value.into()));
94        self
95    }
96
97    /// Returns the document identifier.
98    pub const fn id(&self) -> &SearchDocumentId {
99        &self.id
100    }
101
102    /// Returns field values.
103    pub fn fields(&self) -> &[(SearchField, String)] {
104        &self.fields
105    }
106}
107
108/// A conservative search query shape label.
109#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
110pub enum SearchQueryShape {
111    Term,
112    Phrase,
113    Boolean,
114    Vector,
115    Hybrid,
116    #[default]
117    Unknown,
118}
119
120impl SearchQueryShape {
121    /// Returns a stable lowercase label.
122    pub const fn as_str(self) -> &'static str {
123        match self {
124            Self::Term => "term",
125            Self::Phrase => "phrase",
126            Self::Boolean => "boolean",
127            Self::Vector => "vector",
128            Self::Hybrid => "hybrid",
129            Self::Unknown => "unknown",
130        }
131    }
132}
133
134impl fmt::Display for SearchQueryShape {
135    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136        formatter.write_str(self.as_str())
137    }
138}
139
140/// A sort instruction for search results.
141#[derive(Clone, Debug, Eq, PartialEq)]
142pub struct SearchSort {
143    field: SearchField,
144    descending: bool,
145}
146
147impl SearchSort {
148    /// Creates an ascending sort.
149    pub const fn ascending(field: SearchField) -> Self {
150        Self {
151            field,
152            descending: false,
153        }
154    }
155
156    /// Creates a descending sort.
157    pub const fn descending(field: SearchField) -> Self {
158        Self {
159            field,
160            descending: true,
161        }
162    }
163
164    /// Returns the sort field.
165    pub const fn field(&self) -> &SearchField {
166        &self.field
167    }
168
169    /// Returns whether the sort is descending.
170    pub const fn is_descending(&self) -> bool {
171        self.descending
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::{
178        IndexName, SearchAnalyzer, SearchDocumentId, SearchField, SearchFilter,
179        SearchIndexDocument, SearchQueryShape, SearchSort, SearchTerm,
180    };
181
182    #[test]
183    fn constructs_search_labels() {
184        assert_eq!(IndexName::new("reviews").to_string(), "reviews");
185        assert_eq!(SearchTerm::new("plumber").as_ref(), "plumber");
186        assert_eq!(SearchAnalyzer::new("standard").as_str(), "standard");
187        assert_eq!(SearchFilter::new("rating >= 4").to_string(), "rating >= 4");
188    }
189
190    #[test]
191    fn builds_documents_and_sorts() {
192        let document = SearchIndexDocument::new(SearchDocumentId::new("review_1"))
193            .with_field(SearchField::new("title"), "Great service");
194        let sort = SearchSort::descending(SearchField::new("rating"));
195
196        assert_eq!(document.fields().len(), 1);
197        assert!(sort.is_descending());
198        assert_eq!(SearchQueryShape::Hybrid.to_string(), "hybrid");
199    }
200}