1use serde::{Deserialize, Serialize};
2
3#[cfg(feature = "mcp-tool")]
4use schemars::JsonSchema;
5
6use crate::{
7 API_VERSION, FirecrawlApp, FirecrawlError, error::FirecrawlAPIError, scrape::ScrapeOptions,
8};
9
10#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
11#[cfg_attr(feature = "mcp-tool", derive(JsonSchema))]
12pub struct SearchResult {
13 pub url: String,
15 pub title: String,
17 pub description: String,
19}
20
21#[serde_with::skip_serializing_none]
22#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
23#[cfg_attr(feature = "mcp-tool", derive(JsonSchema))]
24#[serde(rename_all = "camelCase")]
25pub struct SearchOptions {
26 pub limit: Option<u32>,
28
29 pub lang: Option<String>,
31
32 pub country: Option<String>,
34
35 pub tbs: Option<String>,
37
38 pub filter: Option<String>,
40
41 pub location: Option<LocationOptions>,
43
44 pub scrape_options: Option<ScrapeOptions>,
46
47 #[cfg_attr(feature = "mcp-tool", schemars(skip))]
49 pub max_results: Option<usize>,
50}
51
52#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
53#[cfg_attr(feature = "mcp-tool", derive(JsonSchema))]
54#[serde(rename_all = "camelCase")]
55pub struct LocationOptions {
56 pub country: Option<String>,
58
59 pub languages: Option<Vec<String>>,
61}
62
63#[derive(Deserialize, Serialize, Debug, Default, PartialEq)]
64#[serde(rename_all = "camelCase")]
65pub struct SearchRequestBody {
66 pub query: String,
67 #[serde(flatten)]
68 pub options: SearchOptions,
69}
70
71#[derive(Deserialize, Serialize, Debug, Default)]
72#[serde(rename_all = "camelCase")]
73struct SearchResponse {
74 success: bool,
75 data: Option<Vec<SearchResult>>,
76 error: Option<String>,
78}
79
80#[derive(Deserialize, Serialize, Debug, Default, PartialEq)]
81#[cfg_attr(feature = "mcp-tool", derive(JsonSchema))]
82#[serde(rename_all = "camelCase")]
83pub struct SearchInput {
84 pub query: String,
85 #[serde(flatten)]
86 pub options: SearchOptions,
87}
88
89impl FirecrawlApp {
90 pub async fn search(
101 &self,
102 query: impl AsRef<str>,
103 options: impl Into<Option<SearchOptions>>,
104 ) -> Result<Vec<SearchResult>, FirecrawlError> {
105 let body = SearchRequestBody {
106 query: query.as_ref().to_string(),
107 options: options.into().unwrap_or_default(),
108 };
109
110 let headers = self.prepare_headers(None);
111
112 let response = self
113 .client
114 .post(format!("{}/{}/search", self.api_url, API_VERSION))
115 .headers(headers)
116 .json(&body)
117 .send()
118 .await
119 .map_err(|e| {
120 FirecrawlError::HttpError(format!("Searching for {:?}", query.as_ref()), e)
121 })?;
122
123 let response = self
124 .handle_response::<SearchResponse>(response, "search")
125 .await?;
126
127 if !response.success {
128 return Err(FirecrawlError::APIError(
129 "search request failed".to_string(),
130 FirecrawlAPIError {
131 error: response.error.unwrap_or_default(),
132 details: None,
133 },
134 ));
135 }
136
137 Ok(response.data.unwrap_or_default())
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_search_request_deserialization() {
147 let json_str = r#"{
149 "query": "test query",
150 "limit": 5,
151 "tbs": "qdr:d",
152 "lang": "en",
153 "country": "us",
154 "location": {
155 "country": "us",
156 "languages": ["en"]
157 },
158 "timeout": 60000,
159 "scrapeOptions": {}
160 }"#;
161
162 let expected = SearchRequestBody {
164 query: "test query".to_string(),
165 options: SearchOptions {
166 limit: Some(5),
167 tbs: Some("qdr:d".to_string()),
168 lang: Some("en".to_string()),
169 country: Some("us".to_string()),
170 location: Some(LocationOptions {
171 country: Some("us".to_string()),
172 languages: Some(vec!["en".to_string()]),
173 }),
174 scrape_options: Some(ScrapeOptions::default()),
175 ..Default::default()
176 },
177 };
178
179 let deserialized: SearchRequestBody = serde_json::from_str(json_str).unwrap();
181
182 assert_eq!(deserialized, expected);
184 }
185}