Skip to main content

web_search/
search.rs

1//! Web Search Engine - main entry point
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use tokio::sync::RwLock;
7
8use crate::error::SearchError;
9use crate::merger::{merge_results, MergeOptions, MergeStrategy};
10use crate::providers::{
11    build_providers, get_default_provider_ids, get_registry, BuildConfig, RegistryEntry,
12    SearchOptions, SearchProvider, SearchResult,
13};
14
15/// Configuration for the web search engine
16#[derive(Debug, Clone, Default)]
17pub struct WebSearchConfig {
18    /// Providers to use by default
19    pub providers: Vec<String>,
20    /// Google API key
21    pub google_api_key: Option<String>,
22    /// Google Custom Search Engine ID
23    pub google_cx: Option<String>,
24    /// Bing API key
25    pub bing_api_key: Option<String>,
26    /// Default weights for providers
27    pub weights: HashMap<String, f64>,
28    /// Default merge strategy
29    pub merge_strategy: MergeStrategy,
30}
31
32impl WebSearchConfig {
33    /// Create config from environment variables
34    pub fn from_env() -> Self {
35        Self {
36            providers: get_default_provider_ids(),
37            google_api_key: std::env::var("GOOGLE_API_KEY").ok(),
38            google_cx: std::env::var("GOOGLE_CX").ok(),
39            bing_api_key: std::env::var("BING_API_KEY").ok(),
40            weights: HashMap::new(),
41            merge_strategy: MergeStrategy::Rrf,
42        }
43    }
44}
45
46/// Web Search Engine
47pub struct WebSearchEngine {
48    providers: HashMap<String, Arc<RwLock<Box<dyn SearchProvider>>>>,
49    registry: Vec<RegistryEntry>,
50    default_providers: Vec<String>,
51    default_weights: HashMap<String, f64>,
52    default_strategy: MergeStrategy,
53}
54
55impl WebSearchEngine {
56    /// Create a new web search engine with default configuration
57    pub fn new() -> Self {
58        Self::with_config(WebSearchConfig::from_env())
59    }
60
61    /// Create a new web search engine with custom configuration.
62    ///
63    /// Providers are instantiated from the typed registry (the single source of
64    /// truth), so every catalogued engine — class-based, descriptor-driven, and
65    /// web-capture-backed — is available for selection.
66    pub fn with_config(config: WebSearchConfig) -> Self {
67        let mut providers: HashMap<String, Arc<RwLock<Box<dyn SearchProvider>>>> = HashMap::new();
68
69        let build_config = BuildConfig {
70            google_api_key: config.google_api_key,
71            google_cx: config.google_cx,
72            bing_api_key: config.bing_api_key,
73        };
74
75        for (id, provider) in build_providers(&build_config) {
76            providers.insert(id, Arc::new(RwLock::new(provider)));
77        }
78
79        Self {
80            providers,
81            registry: get_registry(),
82            default_providers: config.providers,
83            default_weights: config.weights,
84            default_strategy: config.merge_strategy,
85        }
86    }
87
88    /// Search across multiple providers
89    pub async fn search(
90        &self,
91        query: &str,
92        options: SearchOptions,
93    ) -> Result<Vec<SearchResult>, SearchError> {
94        self.search_with_options(query, options, None, None).await
95    }
96
97    /// Search with additional merge options
98    pub async fn search_with_options(
99        &self,
100        query: &str,
101        options: SearchOptions,
102        providers: Option<Vec<String>>,
103        merge_options: Option<MergeOptions>,
104    ) -> Result<Vec<SearchResult>, SearchError> {
105        if query.is_empty() {
106            return Ok(Vec::new());
107        }
108
109        let providers_to_use = providers.unwrap_or_else(|| self.default_providers.clone());
110        let merge_opts = merge_options.unwrap_or_else(|| MergeOptions {
111            strategy: self.default_strategy,
112            weights: self.default_weights.clone(),
113            rrf_k: None,
114            remove_duplicates: true,
115        });
116
117        let mut handles = Vec::new();
118
119        for provider_name in &providers_to_use {
120            let provider = self.providers.get(provider_name).cloned();
121            if provider.is_none() {
122                continue;
123            }
124
125            let provider = provider.unwrap();
126            let query = query.to_string();
127            let opts = options.clone();
128            let name = provider_name.clone();
129
130            handles.push(tokio::spawn(async move {
131                let provider = provider.read().await;
132                if !provider.is_available() {
133                    return (name, Vec::new());
134                }
135                match provider.search(&query, &opts).await {
136                    Ok(results) => (name, results),
137                    Err(e) => {
138                        tracing::error!("Provider {} failed: {}", name, e);
139                        (name, Vec::new())
140                    }
141                }
142            }));
143        }
144
145        let mut results_by_provider = HashMap::new();
146
147        for handle in handles {
148            if let Ok((name, results)) = handle.await {
149                results_by_provider.insert(name, results);
150            }
151        }
152
153        Ok(merge_results(&results_by_provider, &merge_opts))
154    }
155
156    /// Search with a single provider
157    pub async fn search_single(
158        &self,
159        query: &str,
160        provider_name: &str,
161        options: SearchOptions,
162    ) -> Result<Vec<SearchResult>, SearchError> {
163        let provider = self
164            .providers
165            .get(provider_name)
166            .ok_or_else(|| SearchError::UnknownProvider(provider_name.to_string()))?;
167
168        let provider = provider.read().await;
169
170        if !provider.is_available() {
171            return Err(SearchError::ProviderDisabled(provider_name.to_string()));
172        }
173
174        provider.search(query, &options).await
175    }
176
177    /// Get available provider names
178    pub fn get_available_providers(&self) -> Vec<String> {
179        self.providers.keys().cloned().collect()
180    }
181
182    /// Get the full provider registry (metadata for every known provider).
183    pub fn get_registry(&self) -> &[RegistryEntry] {
184        &self.registry
185    }
186
187    /// Get provider status, enriched with registry metadata (category, label,
188    /// CORS readability, access mechanism) so callers see the same shape the
189    /// JavaScript implementation exposes.
190    pub async fn get_provider_status(&self) -> HashMap<String, ProviderStatus> {
191        let mut status = HashMap::new();
192
193        for (name, provider) in &self.providers {
194            let p = provider.read().await;
195            let meta = self.registry.iter().find(|e| &e.id == name);
196            status.insert(
197                name.clone(),
198                ProviderStatus {
199                    enabled: p.is_available(),
200                    weight: p.weight(),
201                    category: meta.map(|m| m.category.clone()),
202                    label: meta.map(|m| m.label.clone()),
203                    cors_readable: meta.map(|m| m.cors_readable),
204                    access: meta.map(|m| m.access.clone()),
205                },
206            );
207        }
208
209        status
210    }
211
212    /// Set provider weight
213    pub async fn set_provider_weight(&self, name: &str, weight: f64) -> Result<(), SearchError> {
214        let provider = self
215            .providers
216            .get(name)
217            .ok_or_else(|| SearchError::UnknownProvider(name.to_string()))?;
218
219        provider.write().await.set_weight(weight);
220        Ok(())
221    }
222
223    /// Enable or disable a provider
224    pub async fn set_provider_enabled(&self, name: &str, enabled: bool) -> Result<(), SearchError> {
225        let provider = self
226            .providers
227            .get(name)
228            .ok_or_else(|| SearchError::UnknownProvider(name.to_string()))?;
229
230        provider.write().await.set_enabled(enabled);
231        Ok(())
232    }
233}
234
235impl Default for WebSearchEngine {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241/// Provider status information, enriched with registry metadata.
242#[derive(Debug, Clone, serde::Serialize)]
243#[serde(rename_all = "camelCase")]
244pub struct ProviderStatus {
245    /// Whether the provider is enabled
246    pub enabled: bool,
247    /// Provider weight for reranking
248    pub weight: f64,
249    /// Provider category (one of the registry categories)
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub category: Option<String>,
252    /// Human-readable label
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub label: Option<String>,
255    /// Whether the endpoint is browser-CORS readable
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub cors_readable: Option<bool>,
258    /// How results are obtained (api, html, hybrid, component, ...)
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub access: Option<String>,
261}