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 #[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 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 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(¶ms).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 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 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
206pub 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}