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**: 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
46pub mod elasticsearch;
47pub mod algolia;
48pub mod traits;
49pub mod types;
50
51pub use traits::*;
52pub use types::*;
53pub use elasticsearch::*;
54pub use algolia::*;
55
56/// Search module version
57pub const VERSION: &str = env!("CARGO_PKG_VERSION");
58
59/// Default index name
60pub const DEFAULT_INDEX_NAME: &str = "default";
61
62/// Maximum document size (100MB for Elasticsearch)
63pub const MAX_DOCUMENT_SIZE: usize = 100 * 1024 * 1024;
64
65/// Default search limit
66pub const DEFAULT_SEARCH_LIMIT: usize = 100;
67
68/// Maximum search limit
69pub const MAX_SEARCH_LIMIT: usize = 10000;
70
71/// Search error types
72#[derive(thiserror::Error, Debug)]
73pub enum SearchError {
74    #[error("Elasticsearch connection error: {0}")]
75    ElasticsearchConnection(String),
76
77    #[error("Elasticsearch operation error: {0}")]
78    ElasticsearchOperation(String),
79
80    #[error("Algolia API error: {0}")]
81    AlgoliaError(String),
82
83    #[error("Index not found: {0}")]
84    IndexNotFound(String),
85
86    #[error("Document not found: {0}")]
87    DocumentNotFound(String),
88
89    #[error("Invalid query: {0}")]
90    InvalidQuery(String),
91
92    #[error("Document serialization error: {0}")]
93    Serialization(String),
94
95    #[error("Document deserialization error: {0}")]
96    Deserialization(String),
97
98    #[error("Document too large: {size} bytes (max: {max} bytes)")]
99    DocumentTooLarge { size: usize, max: usize },
100
101    #[error("Invalid index name: {0}")]
102    InvalidIndexName(String),
103
104    #[error("Configuration error: {0}")]
105    ConfigError(String),
106
107    #[error("Network error: {0}")]
108    NetworkError(String),
109
110    #[error("Search timeout error")]
111    TimeoutError,
112
113    #[error("Search rate limit exceeded")]
114    RateLimitExceeded,
115
116    #[error("Search error: {0}")]
117    Other(String),
118}
119
120/// Result type for search operations
121pub type SearchResult<T> = Result<T, SearchError>;
122
123/// Search configuration
124#[derive(Debug, Clone)]
125pub struct SearchConfig {
126    /// Default search limit
127    pub default_limit: usize,
128
129    /// Maximum search limit
130    pub max_limit: usize,
131
132    /// Enable search analytics
133    pub enable_analytics: bool,
134
135    /// Search timeout in milliseconds
136    pub timeout_ms: u64,
137
138    /// Enable fuzzy search by default
139    pub enable_fuzzy_search: bool,
140
141    /// Fuzziness level (0.0 to 1.0)
142    pub fuzziness: f32,
143
144    /// Minimum should match for multi-term queries
145    pub minimum_should_match: String,
146
147    /// Enable highlighting
148    pub enable_highlighting: bool,
149
150    /// Highlight pre tag
151    pub highlight_pre_tag: String,
152
153    /// Highlight post tag
154    pub highlight_post_tag: String,
155
156    /// Enable snippet extraction
157    pub enable_snippets: bool,
158
159    /// Snippet length
160    pub snippet_length: usize,
161
162    /// Enable search suggestions
163    pub enable_suggestions: bool,
164
165    /// Suggestion limit
166    pub suggestion_limit: usize,
167
168    /// Enable geospatial search
169    pub enable_geo_search: bool,
170
171    /// Default coordinate reference system
172    pub default_crs: String,
173}
174
175impl Default for SearchConfig {
176    fn default() -> Self {
177        Self {
178            default_limit: DEFAULT_SEARCH_LIMIT,
179            max_limit: MAX_SEARCH_LIMIT,
180            enable_analytics: true,
181            timeout_ms: 5000,
182            enable_fuzzy_search: true,
183            fuzziness: 0.7,
184            minimum_should_match: "75%".to_string(),
185            enable_highlighting: true,
186            highlight_pre_tag: "<mark>".to_string(),
187            highlight_post_tag: "</mark>".to_string(),
188            enable_snippets: true,
189            snippet_length: 200,
190            enable_suggestions: true,
191            suggestion_limit: 10,
192            enable_geo_search: false,
193            default_crs: "EPSG4326".to_string(),
194        }
195    }
196}
197
198/// Search backend types
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum SearchBackend {
201    Elasticsearch,
202    Algolia,
203}
204
205impl SearchBackend {
206    /// Get backend name
207    pub fn name(&self) -> &'static str {
208        match self {
209            Self::Elasticsearch => "elasticsearch",
210            Self::Algolia => "algolia",
211        }
212    }
213}
214
215/// Search statistics
216#[derive(Debug, Clone)]
217pub struct SearchStats {
218    /// Total number of queries
219    pub total_queries: u64,
220
221    /// Total number of documents indexed
222    pub total_documents: u64,
223
224    /// Average query time in milliseconds
225    pub avg_query_time_ms: f64,
226
227    /// Queries per second
228    pub queries_per_second: f64,
229
230    /// Index size in bytes
231    pub index_size_bytes: Option<u64>,
232
233    /// Number of indices
234    pub total_indices: u64,
235
236    /// Last query timestamp
237    pub last_query_at: Option<chrono::DateTime<chrono::Utc>>,
238
239    /// Backend-specific statistics
240    pub backend_stats: std::collections::HashMap<String, serde_json::Value>,
241}
242
243impl Default for SearchStats {
244    fn default() -> Self {
245        Self {
246            total_queries: 0,
247            total_documents: 0,
248            avg_query_time_ms: 0.0,
249            queries_per_second: 0.0,
250            index_size_bytes: None,
251            total_indices: 0,
252            last_query_at: None,
253            backend_stats: std::collections::HashMap::new(),
254        }
255    }
256}
257
258impl SearchStats {
259    /// Update statistics with query time
260    pub fn update_with_query(&mut self, query_time_ms: u64) {
261        self.total_queries += 1;
262
263        // Update average query time
264        let total_time = self.avg_query_time_ms * (self.total_queries - 1) as f64 + query_time_ms as f64;
265        self.avg_query_time_ms = total_time / self.total_queries as f64;
266
267        // Update last query timestamp
268        use chrono::Utc;
269        self.last_query_at = Some(Utc::now());
270    }
271
272    /// Calculate queries per second
273    pub fn update_queries_per_second(&mut self, time_window_seconds: u64) {
274        if time_window_seconds > 0 {
275            self.queries_per_second = self.total_queries as f64 / time_window_seconds as f64;
276        }
277    }
278}