1use 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#[derive(Debug, Clone, Default)]
17pub struct WebSearchConfig {
18 pub providers: Vec<String>,
20 pub google_api_key: Option<String>,
22 pub google_cx: Option<String>,
24 pub bing_api_key: Option<String>,
26 pub weights: HashMap<String, f64>,
28 pub merge_strategy: MergeStrategy,
30}
31
32impl WebSearchConfig {
33 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
46pub 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 pub fn new() -> Self {
58 Self::with_config(WebSearchConfig::from_env())
59 }
60
61 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 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 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 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 pub fn get_available_providers(&self) -> Vec<String> {
179 self.providers.keys().cloned().collect()
180 }
181
182 pub fn get_registry(&self) -> &[RegistryEntry] {
184 &self.registry
185 }
186
187 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 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 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#[derive(Debug, Clone, serde::Serialize)]
243#[serde(rename_all = "camelCase")]
244pub struct ProviderStatus {
245 pub enabled: bool,
247 pub weight: f64,
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub category: Option<String>,
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub label: Option<String>,
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub cors_readable: Option<bool>,
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub access: Option<String>,
261}