1use aspero_core::{Error, Filter, Page, ProviderId, SearchMode, SearchQuery, Usage};
4
5use crate::{Engine, EngineError, Served};
6
7#[derive(Debug, Clone)]
8pub struct SearchReport {
9 pub provider: ProviderId,
10 pub page: Page,
11 pub ignored_filters: Vec<Filter>,
13 pub usage: Option<Usage>,
14 pub served: Served,
15}
16
17impl Engine {
18 pub async fn search(
21 &self,
22 source: Option<&str>,
23 query: &SearchQuery,
24 ) -> Result<SearchReport, EngineError> {
25 let citation = query.mode == SearchMode::Citation;
26 let entry = match source {
27 Some(name) => self
28 .entries
29 .iter()
30 .find(|e| e.provider.id().as_str() == name)
31 .ok_or_else(|| EngineError::UnknownProvider(name.to_owned()))?,
32 None => self
33 .entries
34 .iter()
35 .find(|e| {
36 let caps = e.provider.capabilities();
37 caps.search && (!citation || caps.citation_search)
38 })
39 .ok_or(EngineError::NoCapableProvider(if citation {
40 "citation search"
41 } else {
42 "search"
43 }))?,
44 };
45 let provider = entry.provider.id();
46 let caps = entry.provider.capabilities();
47 let refuse = |what| EngineError::Provider {
48 provider: provider.clone(),
49 error: Error::Unsupported(what),
50 };
51 if !caps.search {
52 return Err(refuse("search"));
53 }
54 if citation && !caps.citation_search {
55 return Err(refuse("citation search"));
56 }
57
58 let ignored_filters: Vec<Filter> = query
59 .filters()
60 .into_iter()
61 .filter(|f| !caps.search_filters.contains(f))
62 .collect();
63 let mut sent = query.clone();
64 for filter in &ignored_filters {
65 match filter {
66 Filter::Years => sent.years = None,
67 Filter::WorkType => sent.work_type = None,
68 Filter::OpenAccess => sent.open_access = None,
69 _ => {}
70 }
71 }
72
73 let report = |page, usage, served| SearchReport {
74 provider: provider.clone(),
75 page,
76 ignored_filters: ignored_filters.clone(),
77 usage,
78 served,
79 };
80 let cached = self.cached_search(&provider, &sent);
81 if self.options.offline {
82 return match cached {
83 Some((page, fresh)) => Ok(report(
84 page,
85 None,
86 if fresh { Served::Cache } else { Served::Stale },
87 )),
88 None => Err(EngineError::Provider {
89 provider: provider.clone(),
90 error: Error::Offline,
91 }),
92 };
93 }
94 if let Some((page, true)) = cached {
95 tracing::info!(target: "aspero::cache", provider = %provider, text = %sent.text, "search served from cache");
96 return Ok(report(page, None, Served::Cache));
97 }
98 match self.call(entry, || entry.provider.search(&sent)).await {
99 Ok(resp) => {
100 self.remember_search(&provider, &sent, &resp.value);
101 Ok(report(resp.value, resp.usage, Served::Live))
102 }
103 Err(error) => match cached {
104 Some((page, _)) => Ok(report(page, None, Served::Stale)),
105 None => Err(EngineError::Provider {
106 provider: provider.clone(),
107 error,
108 }),
109 },
110 }
111 }
112
113 fn cached_search(&self, provider: &ProviderId, query: &SearchQuery) -> Option<(Page, bool)> {
115 let entry = self.cache.as_ref()?.get_search(provider, query).ok()??;
116 let fresh = entry.is_fresh((self.clock)(), self.cache_policy.searches);
117 Some((entry.value, fresh))
118 }
119
120 fn remember_search(&self, provider: &ProviderId, query: &SearchQuery, page: &Page) {
121 if let Some(cache) = &self.cache {
122 let _ = cache.put_search(provider, query, page, (self.clock)());
123 }
124 }
125}