websearch/providers/
google.rs1use crate::{
4 error::{SearchError, SearchResult},
5 types::{ProviderConfig, SearchOptions, SearchProvider, SearchResult as SearchResultType},
6 utils::{debug, http::HttpClient},
7};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Deserialize, Serialize)]
13struct GoogleSearchItem {
14 title: String,
15 link: String,
16 #[serde(rename = "displayLink")]
17 display_link: String,
18 snippet: String,
19 #[serde(default)]
20 pagemap: Option<GooglePageMap>,
21}
22
23#[derive(Debug, Deserialize, Serialize)]
24struct GooglePageMap {
25 #[serde(default)]
26 metatags: Option<Vec<HashMap<String, String>>>,
27}
28
29#[derive(Debug, Deserialize)]
30struct GoogleSearchResponse {
31 #[serde(default)]
32 items: Option<Vec<GoogleSearchItem>>,
33 #[serde(rename = "searchInformation")]
34 search_information: Option<GoogleSearchInfo>,
35}
36
37#[derive(Debug, Deserialize)]
38struct GoogleSearchInfo {
39 #[serde(rename = "totalResults")]
40 total_results: String,
41 #[serde(rename = "searchTime")]
42 search_time: f64,
43}
44
45#[derive(Debug, Clone)]
47pub struct GoogleConfig {
48 pub api_key: String,
50 pub cx: String,
52 pub base_url: String,
54}
55
56impl Default for GoogleConfig {
57 fn default() -> Self {
58 Self {
59 api_key: String::new(),
60 cx: String::new(),
61 base_url: "https://www.googleapis.com/customsearch/v1".to_string(),
62 }
63 }
64}
65
66impl ProviderConfig for GoogleConfig {
67 fn validate(&self) -> Result<(), SearchError> {
68 if self.api_key.is_empty() {
69 return Err(SearchError::ConfigError(
70 "Google API key is required".to_string(),
71 ));
72 }
73 if self.cx.is_empty() {
74 return Err(SearchError::ConfigError(
75 "Google Search Engine ID (cx) is required".to_string(),
76 ));
77 }
78 Ok(())
79 }
80
81 fn base_url(&self) -> &str {
82 &self.base_url
83 }
84
85 fn api_key(&self) -> Option<&str> {
86 Some(&self.api_key)
87 }
88}
89
90#[derive(Debug)]
92pub struct GoogleProvider {
93 config: GoogleConfig,
94 http_client: HttpClient,
95}
96
97impl GoogleProvider {
98 pub fn new(api_key: &str, cx: &str) -> SearchResult<Self> {
100 let config = GoogleConfig {
101 api_key: api_key.to_string(),
102 cx: cx.to_string(),
103 ..Default::default()
104 };
105
106 config.validate()?;
107
108 Ok(Self {
109 config,
110 http_client: HttpClient::new(),
111 })
112 }
113
114 pub fn with_config(config: GoogleConfig) -> SearchResult<Self> {
116 config.validate()?;
117
118 Ok(Self {
119 config,
120 http_client: HttpClient::new(),
121 })
122 }
123
124 fn build_search_url(&self, options: &SearchOptions) -> SearchResult<String> {
126 let mut params = HashMap::new();
127
128 params.insert("key".to_string(), self.config.api_key.clone());
129 params.insert("cx".to_string(), self.config.cx.clone());
130 params.insert("q".to_string(), options.query.clone());
131
132 if let Some(max_results) = options.max_results {
134 let num = if max_results > 10 { 10 } else { max_results };
135 params.insert("num".to_string(), num.to_string());
136 }
137
138 if let Some(page) = options.page {
140 let max_results = options.max_results.unwrap_or(10);
141 let start = (page - 1) * max_results + 1;
142 params.insert("start".to_string(), start.to_string());
143 }
144
145 if let Some(language) = &options.language {
147 params.insert("lr".to_string(), format!("lang_{language}"));
148 }
149
150 if let Some(region) = &options.region {
152 params.insert("gl".to_string(), region.clone());
153 }
154
155 if let Some(safe_search) = &options.safe_search {
157 let safe = match safe_search.to_string().as_str() {
158 "off" => "off",
159 _ => "active",
160 };
161 params.insert("safe".to_string(), safe.to_string());
162 }
163
164 crate::utils::http::build_url(&self.config.base_url, params)
165 }
166}
167
168#[async_trait::async_trait]
169impl SearchProvider for GoogleProvider {
170 fn name(&self) -> &str {
171 "google"
172 }
173
174 async fn search(&self, options: &SearchOptions) -> SearchResult<Vec<SearchResultType>> {
175 debug::log_request(
177 &options.debug,
178 "Google Search request",
179 &format!("query: {}", options.query),
180 );
181
182 let url = self.build_search_url(options)?;
183
184 let response: GoogleSearchResponse = self.http_client.get_json(&url).await?;
186
187 debug::log_response(
189 &options.debug,
190 &format!(
191 "Google Search returned {} results",
192 response
193 .items
194 .as_ref()
195 .map(|items| items.len())
196 .unwrap_or(0)
197 ),
198 );
199
200 let results = if let Some(items) = response.items {
202 items
203 .into_iter()
204 .map(|item| {
205 let published_date = item
207 .pagemap
208 .as_ref()
209 .and_then(|pm| pm.metatags.as_ref())
210 .and_then(|tags| tags.first())
211 .and_then(|meta| {
212 meta.get("article:published_time")
213 .or_else(|| meta.get("date"))
214 .or_else(|| meta.get("og:updated_time"))
215 })
216 .cloned();
217
218 SearchResultType {
219 url: item.link.clone(),
220 title: item.title.clone(),
221 snippet: Some(item.snippet.clone()),
222 domain: Some(item.display_link.clone()),
223 published_date,
224 provider: Some("google".to_string()),
225 raw: serde_json::to_value(&item).ok(),
226 }
227 })
228 .collect()
229 } else {
230 Vec::new()
231 };
232
233 Ok(results)
234 }
235
236 fn config(&self) -> HashMap<String, String> {
237 let mut config = HashMap::new();
238 config.insert("api_key".to_string(), "***".to_string()); config.insert("cx".to_string(), self.config.cx.clone());
240 config.insert("base_url".to_string(), self.config.base_url.clone());
241 config
242 }
243}