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                _ => {}
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    /// A cached page and whether it is still fresh.
114    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}