firecrawl_sdk/
search.rs

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    /// The URL of the search result
14    pub url: String,
15    /// The title of the search result
16    pub title: String,
17    /// A brief description or snippet of the search result
18    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    /// Maximum number of results to return (default: 5)
27    pub limit: Option<u32>,
28
29    /// Language code for search results (default: en)
30    pub lang: Option<String>,
31
32    /// Country code for search results (default: us)
33    pub country: Option<String>,
34
35    /// Time-based search filter
36    pub tbs: Option<String>,
37
38    /// Search filter
39    pub filter: Option<String>,
40
41    /// Location settings for search
42    pub location: Option<LocationOptions>,
43
44    /// Options for scraping search results
45    pub scrape_options: Option<ScrapeOptions>,
46
47    /// This field is not in the schema, so we skip it for schema generation
48    #[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    /// Country code for geolocation
57    pub country: Option<String>,
58
59    /// Language codes for content
60    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 message when success is false
77    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    /// Performs a web search using the Firecrawl API.
91    ///
92    /// # Arguments
93    ///
94    /// * `query` - The search query string
95    /// * `options` - Optional search configuration
96    ///
97    /// # Returns
98    ///
99    /// Returns a Result containing a vector of SearchResult on success, or a FirecrawlError on failure.
100    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        // JSON to deserialize
148        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        // Expected struct after deserialization
163        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        // Deserialize JSON to struct
180        let deserialized: SearchRequestBody = serde_json::from_str(json_str).unwrap();
181
182        // Compare the deserialized struct with the expected struct directly
183        assert_eq!(deserialized, expected);
184    }
185}