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 Filter::Place => sent.place = None,
70 _ => {}
71 }
72 }
73
74 let report = |page, usage, served| SearchReport {
75 provider: provider.clone(),
76 page,
77 ignored_filters: ignored_filters.clone(),
78 usage,
79 served,
80 };
81 let cached = self.cached_search(&provider, &sent);
82 if self.options.offline {
83 return match cached {
84 Some((page, fresh)) => Ok(report(
85 page,
86 None,
87 if fresh { Served::Cache } else { Served::Stale },
88 )),
89 None => Err(EngineError::Provider {
90 provider: provider.clone(),
91 error: Error::Offline,
92 }),
93 };
94 }
95 if let Some((page, true)) = cached {
96 tracing::info!(target: "aspero::cache", provider = %provider, text = %sent.text, "search served from cache");
97 return Ok(report(page, None, Served::Cache));
98 }
99 match self.call(entry, || entry.provider.search(&sent)).await {
100 Ok(resp) => {
101 self.remember_search(&provider, &sent, &resp.value);
102 Ok(report(resp.value, resp.usage, Served::Live))
103 }
104 Err(error) => match cached {
105 Some((page, _)) => Ok(report(page, None, Served::Stale)),
106 None => Err(EngineError::Provider {
107 provider: provider.clone(),
108 error,
109 }),
110 },
111 }
112 }
113
114 fn cached_search(&self, provider: &ProviderId, query: &SearchQuery) -> Option<(Page, bool)> {
116 let entry = self.cache.as_ref()?.get_search(provider, query).ok()??;
117 let fresh = entry.is_fresh((self.clock)(), self.cache_policy.searches);
118 Some((entry.value, fresh))
119 }
120
121 fn remember_search(&self, provider: &ProviderId, query: &SearchQuery, page: &Page) {
122 if let Some(cache) = &self.cache {
123 let _ = cache.put_search(provider, query, page, (self.clock)());
124 }
125 }
126}