mirror of
https://github.com/meilisearch/MeiliSearch
synced 2025-07-03 20:07:09 +02:00
Rename MeiliDB into MeiliSearch
This commit is contained in:
parent
58eaf78dc4
commit
7cc096e0a2
94 changed files with 126 additions and 126 deletions
570
meilisearch-http/src/helpers/meilisearch.rs
Normal file
570
meilisearch-http/src/helpers/meilisearch.rs
Normal file
|
@ -0,0 +1,570 @@
|
|||
use crate::routes::setting::{RankingOrdering, SettingBody};
|
||||
use indexmap::IndexMap;
|
||||
use log::error;
|
||||
use meilisearch_core::criterion::*;
|
||||
use meilisearch_core::Highlight;
|
||||
use meilisearch_core::{Index, RankedMap};
|
||||
use meilisearch_schema::{Schema, SchemaAttr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::From;
|
||||
use std::error;
|
||||
use std::fmt;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
SearchDocuments(String),
|
||||
RetrieveDocument(u64, String),
|
||||
DocumentNotFound(u64),
|
||||
CropFieldWrongType(String),
|
||||
AttributeNotFoundOnDocument(String),
|
||||
AttributeNotFoundOnSchema(String),
|
||||
MissingFilterValue,
|
||||
UnknownFilteredAttribute,
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
impl error::Error for Error {}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use Error::*;
|
||||
|
||||
match self {
|
||||
SearchDocuments(err) => write!(f, "impossible to search documents; {}", err),
|
||||
RetrieveDocument(id, err) => write!(
|
||||
f,
|
||||
"impossible to retrieve the document with id: {}; {}",
|
||||
id, err
|
||||
),
|
||||
DocumentNotFound(id) => write!(f, "document {} not found", id),
|
||||
CropFieldWrongType(field) => {
|
||||
write!(f, "the field {} cannot be cropped it's not a string", field)
|
||||
}
|
||||
AttributeNotFoundOnDocument(field) => {
|
||||
write!(f, "field {} is not found on document", field)
|
||||
}
|
||||
AttributeNotFoundOnSchema(field) => write!(f, "field {} is not found on schema", field),
|
||||
MissingFilterValue => f.write_str("a filter doesn't have a value to compare it with"),
|
||||
UnknownFilteredAttribute => {
|
||||
f.write_str("a filter is specifying an unknown schema attribute")
|
||||
}
|
||||
Internal(err) => write!(f, "internal error; {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<meilisearch_core::Error> for Error {
|
||||
fn from(error: meilisearch_core::Error) -> Self {
|
||||
Error::Internal(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IndexSearchExt {
|
||||
fn new_search(&self, query: String) -> SearchBuilder;
|
||||
}
|
||||
|
||||
impl IndexSearchExt for Index {
|
||||
fn new_search(&self, query: String) -> SearchBuilder {
|
||||
SearchBuilder {
|
||||
index: self,
|
||||
query,
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
attributes_to_crop: None,
|
||||
attributes_to_retrieve: None,
|
||||
attributes_to_search_in: None,
|
||||
attributes_to_highlight: None,
|
||||
filters: None,
|
||||
timeout: Duration::from_millis(30),
|
||||
matches: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SearchBuilder<'a> {
|
||||
index: &'a Index,
|
||||
query: String,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
attributes_to_crop: Option<HashMap<String, usize>>,
|
||||
attributes_to_retrieve: Option<HashSet<String>>,
|
||||
attributes_to_search_in: Option<HashSet<String>>,
|
||||
attributes_to_highlight: Option<HashSet<String>>,
|
||||
filters: Option<String>,
|
||||
timeout: Duration,
|
||||
matches: bool,
|
||||
}
|
||||
|
||||
impl<'a> SearchBuilder<'a> {
|
||||
pub fn offset(&mut self, value: usize) -> &SearchBuilder {
|
||||
self.offset = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn limit(&mut self, value: usize) -> &SearchBuilder {
|
||||
self.limit = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn attributes_to_crop(&mut self, value: HashMap<String, usize>) -> &SearchBuilder {
|
||||
self.attributes_to_crop = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn attributes_to_retrieve(&mut self, value: HashSet<String>) -> &SearchBuilder {
|
||||
self.attributes_to_retrieve = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_retrievable_field(&mut self, value: String) -> &SearchBuilder {
|
||||
let attributes_to_retrieve = self.attributes_to_retrieve.get_or_insert(HashSet::new());
|
||||
attributes_to_retrieve.insert(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn attributes_to_search_in(&mut self, value: HashSet<String>) -> &SearchBuilder {
|
||||
self.attributes_to_search_in = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_attribute_to_search_in(&mut self, value: String) -> &SearchBuilder {
|
||||
let attributes_to_search_in = self.attributes_to_search_in.get_or_insert(HashSet::new());
|
||||
attributes_to_search_in.insert(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn attributes_to_highlight(&mut self, value: HashSet<String>) -> &SearchBuilder {
|
||||
self.attributes_to_highlight = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn filters(&mut self, value: String) -> &SearchBuilder {
|
||||
self.filters = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn timeout(&mut self, value: Duration) -> &SearchBuilder {
|
||||
self.timeout = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_matches(&mut self) -> &SearchBuilder {
|
||||
self.matches = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn search(&self, reader: &heed::RoTxn) -> Result<SearchResult, Error> {
|
||||
let schema = self.index.main.schema(reader);
|
||||
let schema = schema.map_err(|e| Error::Internal(e.to_string()))?;
|
||||
let schema = match schema {
|
||||
Some(schema) => schema,
|
||||
None => return Err(Error::Internal(String::from("missing schema"))),
|
||||
};
|
||||
|
||||
let ranked_map = self.index.main.ranked_map(reader);
|
||||
let ranked_map = ranked_map.map_err(|e| Error::Internal(e.to_string()))?;
|
||||
let ranked_map = ranked_map.unwrap_or_default();
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
// Change criteria
|
||||
let mut query_builder = match self.get_criteria(reader, &ranked_map, &schema)? {
|
||||
Some(criteria) => self.index.query_builder_with_criteria(criteria),
|
||||
None => self.index.query_builder(),
|
||||
};
|
||||
|
||||
// Filter searchable fields
|
||||
if let Some(fields) = &self.attributes_to_search_in {
|
||||
for attribute in fields.iter().filter_map(|f| schema.attribute(f)) {
|
||||
query_builder.add_searchable_attribute(attribute.0);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(filters) = &self.filters {
|
||||
let mut split = filters.split(':');
|
||||
match (split.next(), split.next()) {
|
||||
(Some(_), None) | (Some(_), Some("")) => return Err(Error::MissingFilterValue),
|
||||
(Some(attr), Some(value)) => {
|
||||
let ref_reader = reader;
|
||||
let ref_index = &self.index;
|
||||
let value = value.trim().to_lowercase();
|
||||
|
||||
let attr = match schema.attribute(attr) {
|
||||
Some(attr) => attr,
|
||||
None => return Err(Error::UnknownFilteredAttribute),
|
||||
};
|
||||
|
||||
query_builder.with_filter(move |id| {
|
||||
let attr = attr;
|
||||
let index = ref_index;
|
||||
let reader = ref_reader;
|
||||
|
||||
match index.document_attribute::<Value>(reader, id, attr) {
|
||||
Ok(Some(Value::String(s))) => s.to_lowercase() == value,
|
||||
Ok(Some(Value::Bool(b))) => {
|
||||
(value == "true" && b) || (value == "false" && !b)
|
||||
}
|
||||
Ok(Some(Value::Array(a))) => {
|
||||
a.into_iter().any(|s| s.as_str() == Some(&value))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
}
|
||||
(_, _) => (),
|
||||
}
|
||||
}
|
||||
|
||||
query_builder.with_fetch_timeout(self.timeout);
|
||||
|
||||
let docs =
|
||||
query_builder.query(reader, &self.query, self.offset..(self.offset + self.limit));
|
||||
|
||||
let mut hits = Vec::with_capacity(self.limit);
|
||||
for doc in docs.map_err(|e| Error::SearchDocuments(e.to_string()))? {
|
||||
// retrieve the content of document in kv store
|
||||
let mut fields: Option<HashSet<&str>> = None;
|
||||
if let Some(attributes_to_retrieve) = &self.attributes_to_retrieve {
|
||||
let mut set = HashSet::new();
|
||||
for field in attributes_to_retrieve {
|
||||
set.insert(field.as_str());
|
||||
}
|
||||
fields = Some(set);
|
||||
}
|
||||
|
||||
let document: IndexMap<String, Value> = self
|
||||
.index
|
||||
.document(reader, fields.as_ref(), doc.id)
|
||||
.map_err(|e| Error::RetrieveDocument(doc.id.0, e.to_string()))?
|
||||
.ok_or(Error::DocumentNotFound(doc.id.0))?;
|
||||
|
||||
let mut formatted = document.clone();
|
||||
let mut matches = doc.highlights.clone();
|
||||
|
||||
// Crops fields if needed
|
||||
if let Some(fields) = &self.attributes_to_crop {
|
||||
crop_document(&mut formatted, &mut matches, &schema, fields);
|
||||
}
|
||||
|
||||
// Transform to readable matches
|
||||
let matches = calculate_matches(matches, self.attributes_to_retrieve.clone(), &schema);
|
||||
|
||||
if !self.matches {
|
||||
if let Some(attributes_to_highlight) = &self.attributes_to_highlight {
|
||||
formatted = calculate_highlights(&formatted, &matches, attributes_to_highlight);
|
||||
}
|
||||
}
|
||||
|
||||
let matches_info = if self.matches { Some(matches) } else { None };
|
||||
|
||||
let hit = SearchHit {
|
||||
document,
|
||||
formatted,
|
||||
matches_info,
|
||||
};
|
||||
|
||||
hits.push(hit);
|
||||
}
|
||||
|
||||
let time_ms = start.elapsed().as_millis() as usize;
|
||||
|
||||
let results = SearchResult {
|
||||
hits,
|
||||
offset: self.offset,
|
||||
limit: self.limit,
|
||||
processing_time_ms: time_ms,
|
||||
query: self.query.to_string(),
|
||||
};
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub fn get_criteria(
|
||||
&self,
|
||||
reader: &heed::RoTxn,
|
||||
ranked_map: &'a RankedMap,
|
||||
schema: &Schema,
|
||||
) -> Result<Option<Criteria<'a>>, Error> {
|
||||
let current_settings = match self.index.main.customs(reader).unwrap() {
|
||||
Some(bytes) => bincode::deserialize(bytes).unwrap(),
|
||||
None => SettingBody::default(),
|
||||
};
|
||||
|
||||
let ranking_rules = ¤t_settings.ranking_rules;
|
||||
let ranking_order = ¤t_settings.ranking_order;
|
||||
|
||||
if let Some(ranking_rules) = ranking_rules {
|
||||
let mut builder = CriteriaBuilder::with_capacity(7 + ranking_rules.len());
|
||||
if let Some(ranking_rules_order) = ranking_order {
|
||||
for rule in ranking_rules_order {
|
||||
match rule.as_str() {
|
||||
"_sum_of_typos" => builder.push(SumOfTypos),
|
||||
"_number_of_words" => builder.push(NumberOfWords),
|
||||
"_word_proximity" => builder.push(WordsProximity),
|
||||
"_sum_of_words_attribute" => builder.push(SumOfWordsAttribute),
|
||||
"_sum_of_words_position" => builder.push(SumOfWordsPosition),
|
||||
"_exact" => builder.push(Exact),
|
||||
_ => {
|
||||
let order = match ranking_rules.get(rule.as_str()) {
|
||||
Some(o) => o,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let custom_ranking = match order {
|
||||
RankingOrdering::Asc => {
|
||||
SortByAttr::lower_is_better(&ranked_map, &schema, &rule)
|
||||
.unwrap()
|
||||
}
|
||||
RankingOrdering::Dsc => {
|
||||
SortByAttr::higher_is_better(&ranked_map, &schema, &rule)
|
||||
.unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
builder.push(custom_ranking);
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.push(DocumentId);
|
||||
return Ok(Some(builder.build()));
|
||||
} else {
|
||||
builder.push(SumOfTypos);
|
||||
builder.push(NumberOfWords);
|
||||
builder.push(WordsProximity);
|
||||
builder.push(SumOfWordsAttribute);
|
||||
builder.push(SumOfWordsPosition);
|
||||
builder.push(Exact);
|
||||
for (rule, order) in ranking_rules.iter() {
|
||||
let custom_ranking = match order {
|
||||
RankingOrdering::Asc => {
|
||||
SortByAttr::lower_is_better(&ranked_map, &schema, &rule).unwrap()
|
||||
}
|
||||
RankingOrdering::Dsc => {
|
||||
SortByAttr::higher_is_better(&ranked_map, &schema, &rule).unwrap()
|
||||
}
|
||||
};
|
||||
builder.push(custom_ranking);
|
||||
}
|
||||
builder.push(DocumentId);
|
||||
return Ok(Some(builder.build()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||
pub struct MatchPosition {
|
||||
pub start: usize,
|
||||
pub length: usize,
|
||||
}
|
||||
|
||||
impl Ord for MatchPosition {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
match self.start.cmp(&other.start) {
|
||||
Ordering::Equal => self.length.cmp(&other.length),
|
||||
_ => self.start.cmp(&other.start),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type HighlightInfos = HashMap<String, Value>;
|
||||
pub type MatchesInfos = HashMap<String, Vec<MatchPosition>>;
|
||||
// pub type RankingInfos = HashMap<String, u64>;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SearchHit {
|
||||
#[serde(flatten)]
|
||||
pub document: IndexMap<String, Value>,
|
||||
#[serde(rename = "_formatted", skip_serializing_if = "IndexMap::is_empty")]
|
||||
pub formatted: IndexMap<String, Value>,
|
||||
#[serde(rename = "_matchesInfo", skip_serializing_if = "Option::is_none")]
|
||||
pub matches_info: Option<MatchesInfos>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SearchResult {
|
||||
pub hits: Vec<SearchHit>,
|
||||
pub offset: usize,
|
||||
pub limit: usize,
|
||||
pub processing_time_ms: usize,
|
||||
pub query: String,
|
||||
// pub parsed_query: String,
|
||||
// pub params: Option<String>,
|
||||
}
|
||||
|
||||
fn crop_text(
|
||||
text: &str,
|
||||
matches: impl IntoIterator<Item = Highlight>,
|
||||
context: usize,
|
||||
) -> (String, Vec<Highlight>) {
|
||||
let mut matches = matches.into_iter().peekable();
|
||||
|
||||
let char_index = matches.peek().map(|m| m.char_index as usize).unwrap_or(0);
|
||||
let start = char_index.saturating_sub(context);
|
||||
let text = text.chars().skip(start).take(context * 2).collect();
|
||||
|
||||
let matches = matches
|
||||
.take_while(|m| (m.char_index as usize) + (m.char_length as usize) <= start + (context * 2))
|
||||
.map(|match_| Highlight {
|
||||
char_index: match_.char_index - start as u16,
|
||||
..match_
|
||||
})
|
||||
.collect();
|
||||
|
||||
(text, matches)
|
||||
}
|
||||
|
||||
fn crop_document(
|
||||
document: &mut IndexMap<String, Value>,
|
||||
matches: &mut Vec<Highlight>,
|
||||
schema: &Schema,
|
||||
fields: &HashMap<String, usize>,
|
||||
) {
|
||||
matches.sort_unstable_by_key(|m| (m.char_index, m.char_length));
|
||||
|
||||
for (field, length) in fields {
|
||||
let attribute = match schema.attribute(field) {
|
||||
Some(attribute) => attribute,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let selected_matches = matches
|
||||
.iter()
|
||||
.filter(|m| SchemaAttr::new(m.attribute) == attribute)
|
||||
.cloned();
|
||||
|
||||
if let Some(Value::String(ref mut original_text)) = document.get_mut(field) {
|
||||
let (cropped_text, cropped_matches) =
|
||||
crop_text(original_text, selected_matches, *length);
|
||||
|
||||
*original_text = cropped_text;
|
||||
|
||||
matches.retain(|m| SchemaAttr::new(m.attribute) != attribute);
|
||||
matches.extend_from_slice(&cropped_matches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_matches(
|
||||
matches: Vec<Highlight>,
|
||||
attributes_to_retrieve: Option<HashSet<String>>,
|
||||
schema: &Schema,
|
||||
) -> MatchesInfos {
|
||||
let mut matches_result: HashMap<String, Vec<MatchPosition>> = HashMap::new();
|
||||
for m in matches.iter() {
|
||||
let attribute = schema
|
||||
.attribute_name(SchemaAttr::new(m.attribute))
|
||||
.to_string();
|
||||
if let Some(attributes_to_retrieve) = attributes_to_retrieve.clone() {
|
||||
if !attributes_to_retrieve.contains(attribute.as_str()) {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(pos) = matches_result.get_mut(&attribute) {
|
||||
pos.push(MatchPosition {
|
||||
start: m.char_index as usize,
|
||||
length: m.char_length as usize,
|
||||
});
|
||||
} else {
|
||||
let mut positions = Vec::new();
|
||||
positions.push(MatchPosition {
|
||||
start: m.char_index as usize,
|
||||
length: m.char_length as usize,
|
||||
});
|
||||
matches_result.insert(attribute, positions);
|
||||
}
|
||||
}
|
||||
for (_, val) in matches_result.iter_mut() {
|
||||
val.sort_unstable();
|
||||
val.dedup();
|
||||
}
|
||||
matches_result
|
||||
}
|
||||
|
||||
fn calculate_highlights(
|
||||
document: &IndexMap<String, Value>,
|
||||
matches: &MatchesInfos,
|
||||
attributes_to_highlight: &HashSet<String>,
|
||||
) -> IndexMap<String, Value> {
|
||||
let mut highlight_result = IndexMap::new();
|
||||
|
||||
for (attribute, matches) in matches.iter() {
|
||||
if attributes_to_highlight.contains(attribute) {
|
||||
if let Some(Value::String(value)) = document.get(attribute) {
|
||||
let value: Vec<_> = value.chars().collect();
|
||||
let mut highlighted_value = String::new();
|
||||
let mut index = 0;
|
||||
for m in matches {
|
||||
if m.start >= index {
|
||||
let before = value.get(index..m.start);
|
||||
let highlighted = value.get(m.start..(m.start + m.length));
|
||||
if let (Some(before), Some(highlighted)) = (before, highlighted) {
|
||||
highlighted_value.extend(before);
|
||||
highlighted_value.push_str("<em>");
|
||||
highlighted_value.extend(highlighted);
|
||||
highlighted_value.push_str("</em>");
|
||||
index = m.start + m.length;
|
||||
} else {
|
||||
error!("value: {:?}; index: {:?}, match: {:?}", value, index, m);
|
||||
}
|
||||
}
|
||||
}
|
||||
highlighted_value.extend(value[index..].iter());
|
||||
highlight_result.insert(attribute.to_string(), Value::String(highlighted_value));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
highlight_result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn calculate_highlights() {
|
||||
let data = r#"{
|
||||
"title": "Fondation (Isaac ASIMOV)",
|
||||
"description": "En ce début de trentième millénaire, l'Empire n'a jamais été aussi puissant, aussi étendu à travers toute la galaxie. C'est dans sa capitale, Trantor, que l'éminent savant Hari Seldon invente la psychohistoire, une science toute nouvelle, à base de psychologie et de mathématiques, qui lui permet de prédire l'avenir... C'est-à-dire l'effondrement de l'Empire d'ici cinq siècles et au-delà, trente mille années de chaos et de ténèbres. Pour empêcher cette catastrophe et sauver la civilisation, Seldon crée la Fondation."
|
||||
}"#;
|
||||
|
||||
let document: IndexMap<String, Value> = serde_json::from_str(data).unwrap();
|
||||
let mut attributes_to_highlight = HashSet::new();
|
||||
attributes_to_highlight.insert("title".to_string());
|
||||
attributes_to_highlight.insert("description".to_string());
|
||||
|
||||
let mut matches = HashMap::new();
|
||||
|
||||
let mut m = Vec::new();
|
||||
m.push(MatchPosition {
|
||||
start: 0,
|
||||
length: 9,
|
||||
});
|
||||
matches.insert("title".to_string(), m);
|
||||
|
||||
let mut m = Vec::new();
|
||||
m.push(MatchPosition {
|
||||
start: 510,
|
||||
length: 9,
|
||||
});
|
||||
matches.insert("description".to_string(), m);
|
||||
let result = super::calculate_highlights(&document, &matches, &attributes_to_highlight);
|
||||
|
||||
let mut result_expected = IndexMap::new();
|
||||
result_expected.insert(
|
||||
"title".to_string(),
|
||||
Value::String("<em>Fondation</em> (Isaac ASIMOV)".to_string()),
|
||||
);
|
||||
result_expected.insert("description".to_string(), Value::String("En ce début de trentième millénaire, l'Empire n'a jamais été aussi puissant, aussi étendu à travers toute la galaxie. C'est dans sa capitale, Trantor, que l'éminent savant Hari Seldon invente la psychohistoire, une science toute nouvelle, à base de psychologie et de mathématiques, qui lui permet de prédire l'avenir... C'est-à-dire l'effondrement de l'Empire d'ici cinq siècles et au-delà, trente mille années de chaos et de ténèbres. Pour empêcher cette catastrophe et sauver la civilisation, Seldon crée la <em>Fondation</em>.".to_string()));
|
||||
|
||||
assert_eq!(result, result_expected);
|
||||
}
|
||||
}
|
2
meilisearch-http/src/helpers/mod.rs
Normal file
2
meilisearch-http/src/helpers/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod meilisearch;
|
||||
pub mod tide;
|
118
meilisearch-http/src/helpers/tide.rs
Normal file
118
meilisearch-http/src/helpers/tide.rs
Normal file
|
@ -0,0 +1,118 @@
|
|||
use crate::error::{ResponseError, SResult};
|
||||
use crate::models::token::*;
|
||||
use crate::Data;
|
||||
use chrono::Utc;
|
||||
use heed::types::{SerdeBincode, Str};
|
||||
use meilisearch_core::Index;
|
||||
use serde_json::Value;
|
||||
use tide::Context;
|
||||
|
||||
pub trait ContextExt {
|
||||
fn is_allowed(&self, acl: ACL) -> SResult<()>;
|
||||
fn header(&self, name: &str) -> Result<String, ResponseError>;
|
||||
fn url_param(&self, name: &str) -> Result<String, ResponseError>;
|
||||
fn index(&self) -> Result<Index, ResponseError>;
|
||||
fn identifier(&self) -> Result<String, ResponseError>;
|
||||
}
|
||||
|
||||
impl ContextExt for Context<Data> {
|
||||
fn is_allowed(&self, acl: ACL) -> SResult<()> {
|
||||
let api_key = match &self.state().api_key {
|
||||
Some(api_key) => api_key,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let user_api_key = self.header("X-Meili-API-Key")?;
|
||||
if user_api_key == *api_key {
|
||||
return Ok(());
|
||||
}
|
||||
let request_index: Option<String> = None; //self.param::<String>("index").ok();
|
||||
|
||||
let db = &self.state().db;
|
||||
let env = &db.env;
|
||||
let reader = env.read_txn().map_err(ResponseError::internal)?;
|
||||
|
||||
let token_key = format!("{}{}", TOKEN_PREFIX_KEY, user_api_key);
|
||||
|
||||
let token_config = db
|
||||
.common_store()
|
||||
.get::<Str, SerdeBincode<Token>>(&reader, &token_key)
|
||||
.map_err(ResponseError::internal)?
|
||||
.ok_or(ResponseError::invalid_token(format!(
|
||||
"Api key does not exist: {}",
|
||||
user_api_key
|
||||
)))?;
|
||||
|
||||
if token_config.revoked {
|
||||
return Err(ResponseError::invalid_token("token revoked"));
|
||||
}
|
||||
|
||||
if let Some(index) = request_index {
|
||||
if !token_config
|
||||
.indexes
|
||||
.iter()
|
||||
.any(|r| match_wildcard(&r, &index))
|
||||
{
|
||||
return Err(ResponseError::invalid_token(
|
||||
"token is not allowed to access to this index",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if token_config.expires_at < Utc::now() {
|
||||
return Err(ResponseError::invalid_token("token expired"));
|
||||
}
|
||||
|
||||
if token_config.acl.contains(&ACL::All) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !token_config.acl.contains(&acl) {
|
||||
return Err(ResponseError::invalid_token("token do not have this ACL"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn header(&self, name: &str) -> Result<String, ResponseError> {
|
||||
let header = self
|
||||
.headers()
|
||||
.get(name)
|
||||
.ok_or(ResponseError::missing_header(name))?
|
||||
.to_str()
|
||||
.map_err(|_| ResponseError::missing_header("X-Meili-API-Key"))?
|
||||
.to_string();
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
fn url_param(&self, name: &str) -> Result<String, ResponseError> {
|
||||
let param = self
|
||||
.param::<String>(name)
|
||||
.map_err(|e| ResponseError::bad_parameter(name, e))?;
|
||||
Ok(param)
|
||||
}
|
||||
|
||||
fn index(&self) -> Result<Index, ResponseError> {
|
||||
let index_uid = self.url_param("index")?;
|
||||
let index = self
|
||||
.state()
|
||||
.db
|
||||
.open_index(&index_uid)
|
||||
.ok_or(ResponseError::index_not_found(index_uid))?;
|
||||
Ok(index)
|
||||
}
|
||||
|
||||
fn identifier(&self) -> Result<String, ResponseError> {
|
||||
let name = self
|
||||
.param::<Value>("identifier")
|
||||
.as_ref()
|
||||
.map(meilisearch_core::serde::value_to_string)
|
||||
.map_err(|e| ResponseError::bad_parameter("identifier", e))?
|
||||
.ok_or(ResponseError::bad_parameter(
|
||||
"identifier",
|
||||
"missing parameter",
|
||||
))?;
|
||||
|
||||
Ok(name)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue