lc/search/
serpapi.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use super::{SearchResult, SearchResults};
6
7#[derive(Debug, Serialize, Deserialize)]
8pub struct SerpApiRequest {
9    pub engine: String,
10    pub q: String,
11    pub num: Option<usize>,
12}
13
14#[derive(Debug, Serialize, Deserialize)]
15pub struct SerpApiResponse {
16    pub organic_results: Option<Vec<SerpApiOrganicResult>>,
17    pub answer_box: Option<SerpApiAnswerBox>,
18    pub knowledge_graph: Option<SerpApiKnowledgeGraph>,
19    pub search_metadata: Option<SerpApiSearchMetadata>,
20    pub search_parameters: Option<SerpApiSearchParameters>,
21    pub search_information: Option<SerpApiSearchInformation>,
22}
23
24#[derive(Debug, Serialize, Deserialize)]
25pub struct SerpApiOrganicResult {
26    pub position: Option<u32>,
27    pub title: Option<String>,
28    pub link: Option<String>,
29    pub displayed_link: Option<String>,
30    pub snippet: Option<String>,
31    pub date: Option<String>,
32    pub cached_page_link: Option<String>,
33    pub related_pages_link: Option<String>,
34    // SerpApi sometimes returns sitelinks as an object or array, so we'll skip it for now
35    #[serde(skip_deserializing)]
36    pub sitelinks: Option<Vec<SerpApiSitelink>>,
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40pub struct SerpApiSitelink {
41    pub title: Option<String>,
42    pub link: Option<String>,
43}
44
45#[derive(Debug, Serialize, Deserialize)]
46pub struct SerpApiAnswerBox {
47    #[serde(rename = "type")]
48    pub answer_type: Option<String>,
49    pub title: Option<String>,
50    pub answer: Option<String>,
51    pub snippet: Option<String>,
52    pub link: Option<String>,
53    pub displayed_link: Option<String>,
54}
55
56#[derive(Debug, Serialize, Deserialize)]
57pub struct SerpApiKnowledgeGraph {
58    pub title: Option<String>,
59    #[serde(rename = "type")]
60    pub kg_type: Option<String>,
61    pub description: Option<String>,
62    pub source: Option<SerpApiKnowledgeGraphSource>,
63}
64
65#[derive(Debug, Serialize, Deserialize)]
66pub struct SerpApiKnowledgeGraphSource {
67    pub name: Option<String>,
68    pub link: Option<String>,
69}
70
71#[derive(Debug, Serialize, Deserialize)]
72pub struct SerpApiSearchMetadata {
73    pub id: Option<String>,
74    pub status: Option<String>,
75    pub json_endpoint: Option<String>,
76    pub created_at: Option<String>,
77    pub processed_at: Option<String>,
78    pub google_url: Option<String>,
79    pub raw_html_file: Option<String>,
80    pub total_time_taken: Option<f64>,
81}
82
83#[derive(Debug, Serialize, Deserialize)]
84pub struct SerpApiSearchParameters {
85    pub engine: Option<String>,
86    pub q: Option<String>,
87    pub google_domain: Option<String>,
88    pub hl: Option<String>,
89    pub gl: Option<String>,
90    pub device: Option<String>,
91}
92
93#[derive(Debug, Serialize, Deserialize)]
94pub struct SerpApiSearchInformation {
95    pub organic_results_state: Option<String>,
96    pub query_displayed: Option<String>,
97    pub total_results: Option<u64>,
98    pub time_taken_displayed: Option<f64>,
99}
100
101pub struct SerpApiProvider {
102    pub url: String,
103    pub headers: HashMap<String, String>,
104}
105
106impl SerpApiProvider {
107    pub fn new(url: String, headers: HashMap<String, String>) -> Self {
108        Self { url, headers }
109    }
110
111    pub async fn search(&self, query: &str, count: Option<usize>) -> Result<SearchResults> {
112        let client = reqwest::Client::new();
113
114        // Build query parameters
115        let mut params = vec![("engine", "google".to_string()), ("q", query.to_string())];
116
117        if let Some(num) = count {
118            params.push(("num", num.to_string()));
119        }
120
121        // Add API key from headers
122        if let Some(api_key) = self.headers.get("api_key") {
123            params.push(("api_key", api_key.clone()));
124        }
125
126        crate::debug_log!(
127            "SerpApi: Making GET request to {} with params: {:?}",
128            self.url,
129            params
130        );
131
132        let response = client.get(&self.url).query(&params).send().await?;
133
134        let status = response.status();
135        crate::debug_log!("SerpApi: Received response with status: {}", status);
136
137        if !status.is_success() {
138            let error_text = response.text().await.unwrap_or_default();
139            crate::debug_log!("SerpApi: Error response: {}", error_text);
140            anyhow::bail!(
141                "SerpApi request failed with status {}: {}",
142                status,
143                error_text
144            );
145        }
146
147        let response_text = response.text().await?;
148        crate::debug_log!(
149            "SerpApi: Response body length: {} bytes",
150            response_text.len()
151        );
152
153        let serpapi_response: SerpApiResponse = serde_json::from_str(&response_text)
154            .map_err(|e| anyhow::anyhow!("Failed to parse SerpApi response: {}", e))?;
155
156        crate::debug_log!("SerpApi: Successfully parsed response");
157
158        // Convert to our standard format
159        let mut results = Vec::new();
160
161        if let Some(organic_results) = serpapi_response.organic_results {
162            crate::debug_log!(
163                "SerpApi: Processing {} organic results",
164                organic_results.len()
165            );
166
167            for result in organic_results {
168                if let (Some(title), Some(url)) = (result.title, result.link) {
169                    let search_result = SearchResult {
170                        title,
171                        url,
172                        snippet: result.snippet.unwrap_or_default(),
173                        published_date: result.date,
174                        author: None,
175                        score: None,
176                    };
177                    results.push(search_result);
178                }
179            }
180        }
181
182        crate::debug_log!(
183            "SerpApi: Converted {} results to standard format",
184            results.len()
185        );
186
187        // Extract total results and search time from metadata if available
188        let total_results = serpapi_response
189            .search_information
190            .and_then(|info| info.total_results);
191        let search_time_ms = serpapi_response
192            .search_metadata
193            .and_then(|meta| meta.total_time_taken)
194            .map(|time| (time * 1000.0) as u64);
195
196        Ok(SearchResults {
197            query: query.to_string(),
198            provider: "SerpApi".to_string(),
199            results,
200            total_results,
201            search_time_ms,
202        })
203    }
204}
205
206/// Search function that matches the interface used by other providers
207pub async fn search(
208    provider_config: &super::SearchProviderConfig,
209    query: &str,
210    count: Option<usize>,
211) -> anyhow::Result<super::SearchResults> {
212    let provider =
213        SerpApiProvider::new(provider_config.url.clone(), provider_config.headers.clone());
214
215    provider.search(query, count).await
216}