Skip to main content

websearch/providers/
google.rs

1//! Google Custom Search API provider
2
3use 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/// Google Custom Search API response types
12#[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/// Google Custom Search configuration
46#[derive(Debug, Clone)]
47pub struct GoogleConfig {
48    /// Google API key
49    pub api_key: String,
50    /// Custom Search Engine ID
51    pub cx: String,
52    /// Base URL for the API
53    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/// Google Custom Search provider
91#[derive(Debug)]
92pub struct GoogleProvider {
93    config: GoogleConfig,
94    http_client: HttpClient,
95}
96
97impl GoogleProvider {
98    /// Create a new Google provider with API key and Search Engine ID
99    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    /// Create a new Google provider with custom configuration
115    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    /// Build the search URL with parameters
125    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        // Add max results (Google limits to 10 per request)
133        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        // Add pagination
139        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        // Add language
146        if let Some(language) = &options.language {
147            params.insert("lr".to_string(), format!("lang_{language}"));
148        }
149
150        // Add region
151        if let Some(region) = &options.region {
152            params.insert("gl".to_string(), region.clone());
153        }
154
155        // Add safe search
156        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        // Log request if debugging is enabled
176        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        // Make the request
185        let response: GoogleSearchResponse = self.http_client.get_json(&url).await?;
186
187        // Log response if debugging is enabled
188        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        // Convert Google results to standard format
201        let results = if let Some(items) = response.items {
202            items
203                .into_iter()
204                .map(|item| {
205                    // Extract published date from metadata if available
206                    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()); // Hide API key
239        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}