Skip to main content

aspero_engine/
search.rs

1//! Searching one provider.
2
3use 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    /// Query filters the provider can't apply; they were not sent.
12    pub ignored_filters: Vec<Filter>,
13    pub usage: Option<Usage>,
14    pub served: Served,
15}
16
17impl Engine {
18    /// Searches one provider: `source` if given, otherwise the first that
19    /// supports the query's mode.
20    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    /// A cached page and whether it is still fresh.
115    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}