Merge pull request #43 from meilisearch/facet-filters

enable faceted searches
This commit is contained in:
marin 2021-02-17 14:11:10 +01:00 committed by GitHub
commit 2d7b2e651d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 105 additions and 37 deletions

1
Cargo.lock generated
View File

@ -1633,6 +1633,7 @@ dependencies = [
"chrono", "chrono",
"crossbeam-channel", "crossbeam-channel",
"dashmap", "dashmap",
"either",
"env_logger 0.8.2", "env_logger 0.8.2",
"flate2", "flate2",
"fst", "fst",

View File

@ -57,6 +57,7 @@ tokio = { version = "0.2", features = ["full"] }
dashmap = "4.0.2" dashmap = "4.0.2"
uuid = "0.8.2" uuid = "0.8.2"
itertools = "0.10.0" itertools = "0.10.0"
either = "1.6.1"
[dependencies.sentry] [dependencies.sentry]
default-features = false default-features = false

View File

@ -3,17 +3,21 @@ use std::mem;
use std::time::Instant; use std::time::Instant;
use anyhow::{bail, Context}; use anyhow::{bail, Context};
use either::Either;
use heed::RoTxn;
use meilisearch_tokenizer::{Analyzer, AnalyzerConfig}; use meilisearch_tokenizer::{Analyzer, AnalyzerConfig};
use milli::{Index, obkv_to_json, FacetCondition}; use milli::{obkv_to_json, FacetCondition, Index};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{Value, Map}; use serde_json::{Map, Value};
use crate::index_controller::IndexController;
use super::Data; use super::Data;
use crate::index_controller::IndexController;
pub const DEFAULT_SEARCH_LIMIT: usize = 20; pub const DEFAULT_SEARCH_LIMIT: usize = 20;
const fn default_search_limit() -> usize { DEFAULT_SEARCH_LIMIT } const fn default_search_limit() -> usize {
DEFAULT_SEARCH_LIMIT
}
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)]
@ -31,11 +35,10 @@ pub struct SearchQuery {
pub matches: Option<bool>, pub matches: Option<bool>,
pub facet_filters: Option<Value>, pub facet_filters: Option<Value>,
pub facets_distribution: Option<Vec<String>>, pub facets_distribution: Option<Vec<String>>,
pub facet_condition: Option<String>,
} }
impl SearchQuery { impl SearchQuery {
pub fn perform(&self, index: impl AsRef<Index>) -> anyhow::Result<SearchResult>{ pub fn perform(&self, index: impl AsRef<Index>) -> anyhow::Result<SearchResult> {
let index = index.as_ref(); let index = index.as_ref();
let before_search = Instant::now(); let before_search = Instant::now();
let rtxn = index.read_txn()?; let rtxn = index.read_txn()?;
@ -49,14 +52,17 @@ impl SearchQuery {
search.limit(self.limit); search.limit(self.limit);
search.offset(self.offset.unwrap_or_default()); search.offset(self.offset.unwrap_or_default());
if let Some(ref condition) = self.facet_condition { if let Some(ref facets) = self.facet_filters {
if !condition.trim().is_empty() { if let Some(facets) = parse_facets(facets, index, &rtxn)? {
let condition = FacetCondition::from_str(&rtxn, &index, &condition)?; search.facet_condition(facets);
search.facet_condition(condition);
} }
} }
let milli::SearchResult { documents_ids, found_words, candidates } = search.execute()?; let milli::SearchResult {
documents_ids,
found_words,
candidates,
} = search.execute()?;
let mut documents = Vec::new(); let mut documents = Vec::new();
let fields_ids_map = index.fields_ids_map(&rtxn)?; let fields_ids_map = index.fields_ids_map(&rtxn)?;
@ -133,25 +139,31 @@ impl<'a, A: AsRef<[u8]>> Highlighter<'a, A> {
for (word, token) in analyzed.reconstruct() { for (word, token) in analyzed.reconstruct() {
if token.is_word() { if token.is_word() {
let to_highlight = words_to_highlight.contains(token.text()); let to_highlight = words_to_highlight.contains(token.text());
if to_highlight { string.push_str("<mark>") } if to_highlight {
string.push_str("<mark>")
}
string.push_str(word); string.push_str(word);
if to_highlight { string.push_str("</mark>") } if to_highlight {
string.push_str("</mark>")
}
} else { } else {
string.push_str(word); string.push_str(word);
} }
} }
Value::String(string) Value::String(string)
}, }
Value::Array(values) => { Value::Array(values) => Value::Array(
Value::Array(values.into_iter() values
.into_iter()
.map(|v| self.highlight_value(v, words_to_highlight)) .map(|v| self.highlight_value(v, words_to_highlight))
.collect()) .collect(),
}, ),
Value::Object(object) => { Value::Object(object) => Value::Object(
Value::Object(object.into_iter() object
.into_iter()
.map(|(k, v)| (k, self.highlight_value(v, words_to_highlight))) .map(|(k, v)| (k, self.highlight_value(v, words_to_highlight)))
.collect()) .collect(),
}, ),
} }
} }
@ -172,7 +184,11 @@ impl<'a, A: AsRef<[u8]>> Highlighter<'a, A> {
} }
impl Data { impl Data {
pub fn search<S: AsRef<str>>(&self, index: S, search_query: SearchQuery) -> anyhow::Result<SearchResult> { pub fn search<S: AsRef<str>>(
&self,
index: S,
search_query: SearchQuery,
) -> anyhow::Result<SearchResult> {
match self.index_controller.index(&index)? { match self.index_controller.index(&index)? {
Some(index) => Ok(search_query.perform(index)?), Some(index) => Ok(search_query.perform(index)?),
None => bail!("index {:?} doesn't exists", index.as_ref()), None => bail!("index {:?} doesn't exists", index.as_ref()),
@ -187,7 +203,7 @@ impl Data {
attributes_to_retrieve: Option<Vec<S>>, attributes_to_retrieve: Option<Vec<S>>,
) -> anyhow::Result<Vec<Map<String, Value>>> ) -> anyhow::Result<Vec<Map<String, Value>>>
where where
S: AsRef<str> + Send + Sync + 'static S: AsRef<str> + Send + Sync + 'static,
{ {
let index_controller = self.index_controller.clone(); let index_controller = self.index_controller.clone();
let documents: anyhow::Result<_> = tokio::task::spawn_blocking(move || { let documents: anyhow::Result<_> = tokio::task::spawn_blocking(move || {
@ -207,9 +223,7 @@ impl Data {
None => fields_ids_map.iter().map(|(id, _)| id).collect(), None => fields_ids_map.iter().map(|(id, _)| id).collect(),
}; };
let iter = index.documents.range(&txn, &(..))? let iter = index.documents.range(&txn, &(..))?.skip(offset).take(limit);
.skip(offset)
.take(limit);
let mut documents = Vec::new(); let mut documents = Vec::new();
@ -220,7 +234,8 @@ impl Data {
} }
Ok(documents) Ok(documents)
}).await?; })
.await?;
documents documents
} }
@ -255,16 +270,68 @@ impl Data {
.get(document_id.as_ref().as_bytes()) .get(document_id.as_ref().as_bytes())
.with_context(|| format!("Document with id {} not found", document_id.as_ref()))?; .with_context(|| format!("Document with id {} not found", document_id.as_ref()))?;
let document = index.documents(&txn, std::iter::once(internal_id))? let document = index
.documents(&txn, std::iter::once(internal_id))?
.into_iter() .into_iter()
.next() .next()
.map(|(_, d)| d); .map(|(_, d)| d);
match document { match document {
Some(document) => Ok(obkv_to_json(&attributes_to_retrieve_ids, &fields_ids_map, document)?), Some(document) => Ok(obkv_to_json(
&attributes_to_retrieve_ids,
&fields_ids_map,
document,
)?),
None => bail!("Document with id {} not found", document_id.as_ref()), None => bail!("Document with id {} not found", document_id.as_ref()),
} }
}).await?; })
.await?;
document document
} }
} }
fn parse_facets_array(
txn: &RoTxn,
index: &Index,
arr: &Vec<Value>,
) -> anyhow::Result<Option<FacetCondition>> {
let mut ands = Vec::new();
for value in arr {
match value {
Value::String(s) => ands.push(Either::Right(s.clone())),
Value::Array(arr) => {
let mut ors = Vec::new();
for value in arr {
match value {
Value::String(s) => ors.push(s.clone()),
v => bail!("Invalid facet expression, expected String, found: {:?}", v),
}
}
ands.push(Either::Left(ors));
}
v => bail!(
"Invalid facet expression, expected String or [String], found: {:?}",
v
),
}
}
FacetCondition::from_array(txn, index, ands)
}
fn parse_facets(
facets: &Value,
index: &Index,
txn: &RoTxn,
) -> anyhow::Result<Option<FacetCondition>> {
match facets {
// Disabled for now
//Value::String(expr) => Ok(Some(FacetCondition::from_str(txn, index, expr)?)),
Value::Array(arr) => parse_facets_array(txn, index, arr),
v => bail!(
"Invalid facet expression, expected Array, found: {:?}",
v
),
}
}

View File

@ -67,7 +67,6 @@ impl TryFrom<SearchQueryGet> for SearchQuery {
matches: other.matches, matches: other.matches,
facet_filters, facet_filters,
facets_distribution, facets_distribution,
facet_condition: None,
}) })
} }
} }