#[cfg(feature = "search_bing")]
mod bing;
#[cfg(feature = "search_brave")]
mod brave;
#[cfg(feature = "search_serper")]
mod serper;
#[cfg(feature = "search_tavily")]
mod tavily;
#[cfg(feature = "search_bing")]
pub use bing::BingProvider;
#[cfg(feature = "search_brave")]
pub use brave::BraveProvider;
#[cfg(feature = "search_serper")]
pub use serper::SerperProvider;
#[cfg(feature = "search_tavily")]
pub use tavily::TavilyProvider;
use crate::config::SearchOptions;
use crate::error::SearchError;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[async_trait]
pub trait SearchProvider: Send + Sync {
async fn search(
&self,
query: &str,
options: &SearchOptions,
client: &reqwest::Client,
) -> Result<SearchResults, SearchError>;
fn provider_name(&self) -> &'static str;
fn is_configured(&self) -> bool;
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SearchResults {
pub query: String,
pub results: Vec<SearchResult>,
pub total_results: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
impl SearchResults {
pub fn new(query: impl Into<String>) -> Self {
Self {
query: query.into(),
results: Vec::new(),
total_results: None,
metadata: None,
}
}
pub fn push(&mut self, result: SearchResult) {
self.results.push(result);
}
pub fn urls(&self) -> Vec<&str> {
self.results.iter().map(|r| r.url.as_str()).collect()
}
pub fn is_empty(&self) -> bool {
self.results.is_empty()
}
pub fn len(&self) -> usize {
self.results.len()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SearchResult {
pub title: String,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub snippet: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date: Option<String>,
pub position: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub score: Option<f32>,
}
impl SearchResult {
pub fn new(title: impl Into<String>, url: impl Into<String>, position: usize) -> Self {
Self {
title: title.into(),
url: url.into(),
snippet: None,
date: None,
position,
score: None,
}
}
pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
self.snippet = Some(snippet.into());
self
}
pub fn with_date(mut self, date: impl Into<String>) -> Self {
self.date = Some(date.into());
self
}
pub fn with_score(mut self, score: f32) -> Self {
self.score = Some(score);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_search_results() {
let mut results = SearchResults::new("test query");
results.push(SearchResult::new("Title 1", "https://example1.com", 1));
results.push(SearchResult::new("Title 2", "https://example2.com", 2));
assert_eq!(results.len(), 2);
assert!(!results.is_empty());
assert_eq!(
results.urls(),
vec!["https://example1.com", "https://example2.com"]
);
}
#[test]
fn test_search_result_builder() {
let result = SearchResult::new("Test Title", "https://example.com", 1)
.with_snippet("Test snippet")
.with_score(0.95);
assert_eq!(result.title, "Test Title");
assert_eq!(result.url, "https://example.com");
assert_eq!(result.position, 1);
assert_eq!(result.snippet.as_deref(), Some("Test snippet"));
assert_eq!(result.score, Some(0.95));
}
}