#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Field {
Title,
Author,
Abstract,
Keyword,
}
impl Field {
pub fn name(self) -> &'static str {
match self {
Field::Title => "title_t",
Field::Author => "authFullName_t",
Field::Abstract => "abstract_t",
Field::Keyword => "keyword_t",
}
}
}
#[derive(Clone, Debug)]
pub struct SearchQuery {
q: String,
fields: Vec<String>,
rows: Option<u32>,
start: Option<u32>,
sort: Option<String>,
facet_fields: Vec<String>,
}
impl SearchQuery {
#[must_use]
pub fn basic(query: impl Into<String>) -> Self {
SearchQuery {
q: query.into(),
fields: Vec::new(),
rows: None,
start: None,
sort: None,
facet_fields: Vec::new(),
}
}
#[must_use]
pub fn field(field: &str, value: &str) -> Self {
Self::basic(format!("{field}:{value}"))
}
#[must_use]
pub fn in_field(field: Field, value: &str) -> Self {
let words: Vec<&str> = value.split_whitespace().collect();
if words.is_empty() {
Self::basic(value.to_owned())
} else {
Self::field_terms(field.name(), words)
}
}
#[must_use]
pub fn field_terms<I, S>(field: &str, terms: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let joined = terms
.into_iter()
.map(|t| t.as_ref().to_owned())
.collect::<Vec<_>>()
.join(" ");
Self::basic(format!("{field}:({joined})"))
}
#[must_use]
pub fn field_all_terms<I, S>(field: &str, terms: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let joined = terms
.into_iter()
.map(|t| t.as_ref().to_owned())
.collect::<Vec<_>>()
.join(" AND ");
Self::basic(format!("{field}:({joined})"))
}
#[must_use]
pub fn proximity(field: &str, phrase: &str, distance: u32) -> Self {
Self::basic(format!("{field}:\"{phrase}\"~{distance}"))
}
#[must_use]
pub fn fields<I, S>(mut self, fields: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.fields = fields.into_iter().map(|f| f.as_ref().to_owned()).collect();
self
}
#[must_use]
pub fn rows(mut self, rows: u32) -> Self {
self.rows = Some(rows);
self
}
#[must_use]
pub fn start(mut self, start: u32) -> Self {
self.start = Some(start);
self
}
#[must_use]
pub fn page(mut self, page: u32, per_page: u32) -> Self {
self.rows = Some(per_page);
self.start = Some(page * per_page);
self
}
#[must_use]
pub fn sort(mut self, sort: impl Into<String>) -> Self {
self.sort = Some(sort.into());
self
}
#[must_use]
pub fn facet(mut self, field: impl Into<String>) -> Self {
self.facet_fields.push(field.into());
self
}
#[must_use]
pub fn to_params(&self) -> Vec<(String, String)> {
let mut params = vec![
("q".to_owned(), self.q.clone()),
("wt".to_owned(), "json".to_owned()),
];
if !self.fields.is_empty() {
params.push(("fl".to_owned(), self.fields.join(",")));
}
if let Some(rows) = self.rows {
params.push(("rows".to_owned(), rows.to_string()));
}
if let Some(start) = self.start {
params.push(("start".to_owned(), start.to_string()));
}
if let Some(sort) = &self.sort {
params.push(("sort".to_owned(), sort.clone()));
}
if !self.facet_fields.is_empty() {
params.push(("facet".to_owned(), "true".to_owned()));
for field in &self.facet_fields {
params.push(("facet.field".to_owned(), field.clone()));
}
}
params
}
}
#[cfg(test)]
mod tests {
use super::*;
fn param<'a>(params: &'a [(String, String)], key: &str) -> Option<&'a str> {
params
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
#[test]
fn basic_query() {
let params = SearchQuery::basic("europe").to_params();
assert_eq!(param(¶ms, "q"), Some("europe"));
assert_eq!(param(¶ms, "wt"), Some("json"));
}
#[test]
fn field_query() {
let params = SearchQuery::field("title_t", "europe").to_params();
assert_eq!(param(¶ms, "q"), Some("title_t:europe"));
}
#[test]
fn field_terms_query() {
let params = SearchQuery::field_terms("title_t", ["economic", "policy"]).to_params();
assert_eq!(param(¶ms, "q"), Some("title_t:(economic policy)"));
}
#[test]
fn proximity_query() {
let params = SearchQuery::proximity("title_t", "economic policy", 3).to_params();
assert_eq!(param(¶ms, "q"), Some("title_t:\"economic policy\"~3"));
}
#[test]
fn pagination_and_fields() {
let params = SearchQuery::basic("europe")
.fields(["docid", "label_s"])
.page(2, 10)
.to_params();
assert_eq!(param(¶ms, "fl"), Some("docid,label_s"));
assert_eq!(param(¶ms, "rows"), Some("10"));
assert_eq!(param(¶ms, "start"), Some("20"));
}
#[test]
fn field_all_terms_uses_and() {
let params = SearchQuery::field_all_terms("title_t", ["economic", "policy"]).to_params();
assert_eq!(param(¶ms, "q"), Some("title_t:(economic AND policy)"));
}
#[test]
fn in_field_groups_words() {
let params = SearchQuery::in_field(Field::Title, "open science").to_params();
assert_eq!(param(¶ms, "q"), Some("title_t:(open science)"));
let single = SearchQuery::in_field(Field::Author, "dupont").to_params();
assert_eq!(param(&single, "q"), Some("authFullName_t:(dupont)"));
}
#[test]
fn facets_enabled() {
let params = SearchQuery::basic("europe").facet("docType_s").to_params();
assert_eq!(param(¶ms, "facet"), Some("true"));
assert_eq!(param(¶ms, "facet.field"), Some("docType_s"));
}
}