sparrow/tools/
web_search.rs1use std::process::Command;
4
5pub fn web_search(query: &str, max_results: usize) -> anyhow::Result<Vec<SearchResult>> {
7 let output = Command::new("curl")
9 .args([
10 "-s",
11 "-A",
12 "sparrow/0.5",
13 "--max-time",
14 "10",
15 &format!(
16 "https://api.duckduckgo.com/?q={}&format=json&no_html=1",
17 urlencoding(query)
18 ),
19 ])
20 .output()?;
21
22 if output.status.success() {
23 let json: serde_json::Value = serde_json::from_slice(&output.stdout)?;
24 return parse_duckduckgo(&json, max_results);
25 }
26
27 if let Ok(output) = Command::new("curl")
29 .args([
30 "-s",
31 "--max-time",
32 "10",
33 &format!(
34 "https://searx.be/search?q={}&format=json",
35 urlencoding(query)
36 ),
37 ])
38 .output()
39 {
40 if output.status.success() {
41 let json: serde_json::Value = serde_json::from_slice(&output.stdout)?;
42 return parse_searx(&json, max_results);
43 }
44 }
45
46 Ok(vec![SearchResult {
48 title: format!("Search: {}", query),
49 url: format!("https://duckduckgo.com/?q={}", urlencoding(query)),
50 snippet: "No results. Open the URL in your browser to search manually.".into(),
51 }])
52}
53
54#[derive(Debug, Clone, serde::Serialize)]
55pub struct SearchResult {
56 pub title: String,
57 pub url: String,
58 pub snippet: String,
59}
60
61fn parse_duckduckgo(json: &serde_json::Value, max: usize) -> anyhow::Result<Vec<SearchResult>> {
62 let mut results = Vec::new();
63 if let Some(items) = json.get("RelatedTopics").and_then(|t| t.as_array()) {
64 for item in items.iter().take(max) {
65 if let (Some(text), Some(url)) = (
66 item.get("Text").and_then(|t| t.as_str()),
67 item.get("FirstURL").and_then(|u| u.as_str()),
68 ) {
69 results.push(SearchResult {
70 title: text.chars().take(80).collect(),
71 url: url.to_string(),
72 snippet: text.to_string(),
73 });
74 }
75 }
76 }
77 Ok(results)
78}
79
80fn parse_searx(json: &serde_json::Value, max: usize) -> anyhow::Result<Vec<SearchResult>> {
81 let mut results = Vec::new();
82 if let Some(items) = json.get("results").and_then(|r| r.as_array()) {
83 for item in items.iter().take(max) {
84 results.push(SearchResult {
85 title: item
86 .get("title")
87 .and_then(|t| t.as_str())
88 .unwrap_or("")
89 .to_string(),
90 url: item
91 .get("url")
92 .and_then(|u| u.as_str())
93 .unwrap_or("")
94 .to_string(),
95 snippet: item
96 .get("content")
97 .or(item.get("snippet"))
98 .and_then(|s| s.as_str())
99 .unwrap_or("")
100 .to_string(),
101 });
102 }
103 }
104 Ok(results)
105}
106
107fn urlencoding(s: &str) -> String {
108 s.replace(' ', "+")
109 .replace('&', "%26")
110 .replace('=', "%3D")
111 .replace('#', "%23")
112}