1use async_trait::async_trait;
4use serde::Deserialize;
5
6use super::base::{SearchOptions, SearchProvider, SearchResult};
7use crate::error::SearchError;
8
9#[derive(Debug, Deserialize)]
11#[serde(rename_all = "camelCase")]
12struct BingApiResponse {
13 web_pages: Option<BingWebPages>,
14}
15
16#[derive(Debug, Deserialize)]
17struct BingWebPages {
18 value: Vec<BingWebPage>,
19}
20
21#[derive(Debug, Deserialize)]
22struct BingWebPage {
23 name: String,
24 url: String,
25 snippet: Option<String>,
26}
27
28#[derive(Debug, Clone, Default)]
30pub struct BingConfig {
31 pub api_key: Option<String>,
33}
34
35pub struct BingProvider {
37 name: String,
38 enabled: bool,
39 weight: f64,
40 client: reqwest::Client,
41 config: BingConfig,
42 api_url: String,
43}
44
45impl BingProvider {
46 pub fn new(config: BingConfig) -> Self {
48 Self {
49 name: "bing".to_string(),
50 enabled: true,
51 weight: 1.0,
52 client: reqwest::Client::builder()
53 .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
54 .build()
55 .expect("Failed to create HTTP client"),
56 config,
57 api_url: "https://api.bing.microsoft.com/v7.0/search".to_string(),
58 }
59 }
60
61 pub fn from_env() -> Self {
63 Self::new(BingConfig {
64 api_key: std::env::var("BING_API_KEY").ok(),
65 })
66 }
67
68 pub fn has_api_credentials(&self) -> bool {
70 self.config.api_key.is_some()
71 }
72
73 async fn search_with_api(
74 &self,
75 query: &str,
76 options: &SearchOptions,
77 ) -> Result<Vec<SearchResult>, SearchError> {
78 let api_key = self.config.api_key.as_ref().unwrap();
79 let limit = options.limit.unwrap_or(10).min(50);
80
81 let mut url = format!(
82 "{}?q={}&count={}&responseFilter=Webpages",
83 self.api_url,
84 urlencoding::encode(query),
85 limit
86 );
87
88 if let Some(ref region) = options.region {
89 let lang = options.language.as_deref().unwrap_or("en");
90 url.push_str(&format!("&mkt={}-{}", lang, region.to_uppercase()));
91 }
92
93 let safe_search = match options.safe_search {
94 Some(true) => "Strict",
95 Some(false) => "Off",
96 None => "Moderate",
97 };
98 url.push_str(&format!("&safeSearch={}", safe_search));
99
100 let response = self
101 .client
102 .get(&url)
103 .header("Ocp-Apim-Subscription-Key", api_key)
104 .send()
105 .await?;
106
107 if !response.status().is_success() {
108 let error_text = response.text().await.unwrap_or_default();
109 return Err(SearchError::ApiError {
110 provider: self.name.clone(),
111 message: error_text,
112 });
113 }
114
115 let api_response: BingApiResponse = response.json().await?;
116
117 let results = api_response
118 .web_pages
119 .map(|wp| wp.value)
120 .unwrap_or_default()
121 .into_iter()
122 .enumerate()
123 .map(|(i, page)| SearchResult {
124 title: page.name,
125 url: page.url,
126 snippet: page.snippet.unwrap_or_default(),
127 source: self.name.clone(),
128 rank: i + 1,
129 score: None,
130 sources: None,
131 })
132 .collect();
133
134 Ok(results)
135 }
136
137 async fn search_with_scraping(
138 &self,
139 query: &str,
140 options: &SearchOptions,
141 ) -> Result<Vec<SearchResult>, SearchError> {
142 let limit = options.limit.unwrap_or(10);
143 let mut url = format!(
144 "https://www.bing.com/search?q={}&count={}",
145 urlencoding::encode(query),
146 limit.min(30)
147 );
148
149 if let Some(ref region) = options.region {
150 url.push_str(&format!("&cc={}", region.to_uppercase()));
151 }
152
153 let response = self
154 .client
155 .get(&url)
156 .header(
157 "Accept",
158 "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
159 )
160 .header("Accept-Language", "en-US,en;q=0.5")
161 .send()
162 .await?;
163
164 if !response.status().is_success() {
165 return Err(SearchError::ApiError {
166 provider: self.name.clone(),
167 message: format!("HTTP {}", response.status()),
168 });
169 }
170
171 let html = response.text().await?;
172 Ok(self.parse_scraped_results(&html, limit))
173 }
174
175 fn parse_scraped_results(&self, html: &str, limit: usize) -> Vec<SearchResult> {
176 use scraper::{Html, Selector};
177
178 let document = Html::parse_document(html);
179 let mut results = Vec::new();
180
181 let algo_selector = Selector::parse(".b_algo").unwrap();
182 let link_selector = Selector::parse("a").unwrap();
183 let h2_selector = Selector::parse("h2").unwrap();
184 let snippet_selector = Selector::parse("p").unwrap();
185
186 for element in document.select(&algo_selector) {
187 if results.len() >= limit {
188 break;
189 }
190
191 let link = element.select(&link_selector).next();
192 let h2 = element.select(&h2_selector).next();
193 let snippet = element.select(&snippet_selector).next();
194
195 if let Some(link_elem) = link {
196 let url = link_elem.value().attr("href").unwrap_or_default();
197 if url.is_empty() || url.contains("bing.com") || url.starts_with('/') {
198 continue;
199 }
200
201 let title = h2
202 .map(|h| h.text().collect::<String>())
203 .unwrap_or_else(|| link_elem.text().collect::<String>())
204 .trim()
205 .to_string();
206
207 let snippet_text = snippet
208 .map(|s| s.text().collect::<String>())
209 .unwrap_or_default()
210 .trim()
211 .to_string();
212
213 if title.is_empty() {
214 continue;
215 }
216
217 results.push(SearchResult {
218 title,
219 url: url.to_string(),
220 snippet: snippet_text,
221 source: self.name.clone(),
222 rank: results.len() + 1,
223 score: None,
224 sources: None,
225 });
226 }
227 }
228
229 results
230 }
231}
232
233impl Default for BingProvider {
234 fn default() -> Self {
235 Self::from_env()
236 }
237}
238
239#[async_trait]
240impl SearchProvider for BingProvider {
241 fn name(&self) -> &str {
242 &self.name
243 }
244
245 fn is_available(&self) -> bool {
246 self.enabled
247 }
248
249 fn weight(&self) -> f64 {
250 self.weight
251 }
252
253 fn set_weight(&mut self, weight: f64) {
254 self.weight = weight.clamp(0.0, 1.0);
255 }
256
257 fn set_enabled(&mut self, enabled: bool) {
258 self.enabled = enabled;
259 }
260
261 async fn search(
262 &self,
263 query: &str,
264 options: &SearchOptions,
265 ) -> Result<Vec<SearchResult>, SearchError> {
266 if query.is_empty() {
267 return Ok(Vec::new());
268 }
269
270 if self.has_api_credentials() {
271 match self.search_with_api(query, options).await {
272 Ok(results) => return Ok(results),
273 Err(e) => {
274 tracing::warn!("Bing API search failed, falling back to scraping: {}", e);
275 }
276 }
277 }
278
279 self.search_with_scraping(query, options).await
280 }
281}