Skip to main content

backbone_search/
lib.rs

1//! Backbone Framework Search Module
2//!
3//! Provides search functionality with Elasticsearch and Algolia support.
4//!
5//! ## Features
6//!
7//! - **Elasticsearch Backend** (`elasticsearch` feature, off by default): Full-text search with advanced queries
8//! - **Algolia Backend**: Fast search-as-a-service integration
9//! - **Index Management**: Create, update, delete search indices
10//! - **Document Operations**: Index, update, delete documents
11//! - **Advanced Search**: Full-text, fuzzy, faceted, and geo search
12//! - **Analytics**: Search analytics and performance metrics
13//! - **Async/Await**: Full async support with tokio
14//! - **Multi-index Support**: Search across multiple indices
15//!
16//! ## Quick Start
17//!
18//! ```ignore
19//! use backbone_search::{SearchService, ElasticsearchSearch, SearchQuery, SearchDocument};
20//!
21//! // Elasticsearch search service
22//! let search = ElasticsearchSearch::new("http://localhost:9200").await?;
23//!
24//! // Create index
25//! search.create_index("products", None).await?;
26//!
27//! // Index document
28//! let doc = SearchDocument::builder()
29//!     .id("product-1")
30//!     .content("Wireless headphones with noise cancellation")
31//!     .field("category", "electronics")
32//!     .field("price", 299.99)
33//!     .build();
34//!
35//! search.index_document("products", doc).await?;
36//!
37//! // Search documents
38//! let query = SearchQuery::builder()
39//!     .text("headphones")
40//!     .filter("category", "electronics")
41//!     .build();
42//!
43//! let results = search.search("products", query).await?;
44//! ```
45
46#[cfg(feature = "elasticsearch")]
47pub mod elasticsearch;
48pub mod algolia;
49pub mod traits;
50pub mod types;
51
52pub use traits::*;
53pub use types::*;
54#[cfg(feature = "elasticsearch")]
55pub use elasticsearch::*;
56pub use algolia::*;
57
58/// Search module version
59pub const VERSION: &str = env!("CARGO_PKG_VERSION");
60
61/// Default index name
62pub const DEFAULT_INDEX_NAME: &str = "default";
63
64/// Maximum document size (100MB for Elasticsearch)
65pub const MAX_DOCUMENT_SIZE: usize = 100 * 1024 * 1024;
66
67/// Default search limit
68pub const DEFAULT_SEARCH_LIMIT: usize = 100;
69
70/// Maximum search limit
71pub const MAX_SEARCH_LIMIT: usize = 10000;
72
73/// Search error types
74#[derive(thiserror::Error, Debug)]
75pub enum SearchError {
76    #[error("Elasticsearch connection error: {0}")]
77    ElasticsearchConnection(String),
78
79    #[error("Elasticsearch operation error: {0}")]
80    ElasticsearchOperation(String),
81
82    #[error("Algolia API error: {0}")]
83    AlgoliaError(String),
84
85    #[error("Index not found: {0}")]
86    IndexNotFound(String),
87
88    #[error("Document not found: {0}")]
89    DocumentNotFound(String),
90
91    #[error("Invalid query: {0}")]
92    InvalidQuery(String),
93
94    #[error("Document serialization error: {0}")]
95    Serialization(String),
96
97    #[error("Document deserialization error: {0}")]
98    Deserialization(String),
99
100    #[error("Document too large: {size} bytes (max: {max} bytes)")]
101    DocumentTooLarge { size: usize, max: usize },
102
103    #[error("Invalid index name: {0}")]
104    InvalidIndexName(String),
105
106    #[error("Configuration error: {0}")]
107    ConfigError(String),
108
109    #[error("Network error: {0}")]
110    NetworkError(String),
111
112    #[error("Search timeout error")]
113    TimeoutError,
114
115    #[error("Search rate limit exceeded")]
116    RateLimitExceeded,
117
118    #[error("Search error: {0}")]
119    Other(String),
120}
121
122/// Result type for search operations
123pub type SearchResult<T> = Result<T, SearchError>;
124
125/// Search configuration
126#[derive(Debug, Clone)]
127pub struct SearchConfig {
128    /// Default search limit
129    pub default_limit: usize,
130
131    /// Maximum search limit
132    pub max_limit: usize,
133
134    /// Enable search analytics
135    pub enable_analytics: bool,
136
137    /// Search timeout in milliseconds
138    pub timeout_ms: u64,
139
140    /// Enable fuzzy search by default
141    pub enable_fuzzy_search: bool,
142
143    /// Fuzziness level (0.0 to 1.0)
144    pub fuzziness: f32,
145
146    /// Minimum should match for multi-term queries
147    pub minimum_should_match: String,
148
149    /// Enable highlighting
150    pub enable_highlighting: bool,
151
152    /// Highlight pre tag
153    pub highlight_pre_tag: String,
154
155    /// Highlight post tag
156    pub highlight_post_tag: String,
157
158    /// Enable snippet extraction
159    pub enable_snippets: bool,
160
161    /// Snippet length
162    pub snippet_length: usize,
163
164    /// Enable search suggestions
165    pub enable_suggestions: bool,
166
167    /// Suggestion limit
168    pub suggestion_limit: usize,
169
170    /// Enable geospatial search
171    pub enable_geo_search: bool,
172
173    /// Default coordinate reference system
174    pub default_crs: String,
175}
176
177impl Default for SearchConfig {
178    fn default() -> Self {
179        Self {
180            default_limit: DEFAULT_SEARCH_LIMIT,
181            max_limit: MAX_SEARCH_LIMIT,
182            enable_analytics: true,
183            timeout_ms: 5000,
184            enable_fuzzy_search: true,
185            fuzziness: 0.7,
186            minimum_should_match: "75%".to_string(),
187            enable_highlighting: true,
188            highlight_pre_tag: "<mark>".to_string(),
189            highlight_post_tag: "</mark>".to_string(),
190            enable_snippets: true,
191            snippet_length: 200,
192            enable_suggestions: true,
193            suggestion_limit: 10,
194            enable_geo_search: false,
195            default_crs: "EPSG4326".to_string(),
196        }
197    }
198}
199
200/// Search backend types
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum SearchBackend {
203    Elasticsearch,
204    Algolia,
205}
206
207impl SearchBackend {
208    /// Get backend name
209    pub fn name(&self) -> &'static str {
210        match self {
211            Self::Elasticsearch => "elasticsearch",
212            Self::Algolia => "algolia",
213        }
214    }
215}
216
217/// Search statistics
218#[derive(Debug, Clone)]
219pub struct SearchStats {
220    /// Total number of queries
221    pub total_queries: u64,
222
223    /// Total number of documents indexed
224    pub total_documents: u64,
225
226    /// Average query time in milliseconds
227    pub avg_query_time_ms: f64,
228
229    /// Queries per second
230    pub queries_per_second: f64,
231
232    /// Index size in bytes
233    pub index_size_bytes: Option<u64>,
234
235    /// Number of indices
236    pub total_indices: u64,
237
238    /// Last query timestamp
239    pub last_query_at: Option<chrono::DateTime<chrono::Utc>>,
240
241    /// Backend-specific statistics
242    pub backend_stats: std::collections::HashMap<String, serde_json::Value>,
243}
244
245impl Default for SearchStats {
246    fn default() -> Self {
247        Self {
248            total_queries: 0,
249            total_documents: 0,
250            avg_query_time_ms: 0.0,
251            queries_per_second: 0.0,
252            index_size_bytes: None,
253            total_indices: 0,
254            last_query_at: None,
255            backend_stats: std::collections::HashMap::new(),
256        }
257    }
258}
259
260impl SearchStats {
261    /// Update statistics with query time
262    pub fn update_with_query(&mut self, query_time_ms: u64) {
263        self.total_queries += 1;
264
265        // Update average query time
266        let total_time = self.avg_query_time_ms * (self.total_queries - 1) as f64 + query_time_ms as f64;
267        self.avg_query_time_ms = total_time / self.total_queries as f64;
268
269        // Update last query timestamp
270        use chrono::Utc;
271        self.last_query_at = Some(Utc::now());
272    }
273
274    /// Calculate queries per second
275    pub fn update_queries_per_second(&mut self, time_window_seconds: u64) {
276        if time_window_seconds > 0 {
277            self.queries_per_second = self.total_queries as f64 / time_window_seconds as f64;
278        }
279    }
280}