2020-07-03 12:44:39 -05:00
|
|
|
use crate::models::error::ApiError;
|
2021-05-30 15:02:07 -07:00
|
|
|
use crate::models::projects::SearchRequest;
|
2020-07-03 12:44:39 -05:00
|
|
|
use actix_web::http::StatusCode;
|
2022-02-26 21:16:11 -07:00
|
|
|
use actix_web::HttpResponse;
|
2022-07-31 13:29:20 -07:00
|
|
|
use chrono::{DateTime, Utc};
|
2023-11-28 10:36:59 -08:00
|
|
|
use itertools::Itertools;
|
2020-06-30 19:23:52 +02:00
|
|
|
use meilisearch_sdk::client::Client;
|
2020-09-26 22:49:16 -07:00
|
|
|
use serde::{Deserialize, Serialize};
|
2023-11-28 10:36:59 -08:00
|
|
|
use serde_json::Value;
|
2020-07-27 18:54:10 -05:00
|
|
|
use std::borrow::Cow;
|
2020-09-07 11:44:21 -07:00
|
|
|
use std::cmp::min;
|
2023-11-11 16:40:10 -08:00
|
|
|
use std::collections::HashMap;
|
2022-08-16 17:42:04 -07:00
|
|
|
use std::fmt::Write;
|
2020-06-30 19:23:52 +02:00
|
|
|
use thiserror::Error;
|
|
|
|
|
|
|
|
|
|
pub mod indexing;
|
|
|
|
|
|
|
|
|
|
#[derive(Error, Debug)]
|
|
|
|
|
pub enum SearchError {
|
2021-02-13 12:11:13 -07:00
|
|
|
#[error("MeiliSearch Error: {0}")]
|
2022-03-27 19:12:42 -07:00
|
|
|
MeiliSearch(#[from] meilisearch_sdk::errors::Error),
|
2020-07-03 12:44:39 -05:00
|
|
|
#[error("Error while serializing or deserializing JSON: {0}")]
|
2022-03-27 19:12:42 -07:00
|
|
|
Serde(#[from] serde_json::Error),
|
2020-07-03 12:44:39 -05:00
|
|
|
#[error("Error while parsing an integer: {0}")]
|
2022-03-27 19:12:42 -07:00
|
|
|
IntParsing(#[from] std::num::ParseIntError),
|
2022-08-16 17:42:04 -07:00
|
|
|
#[error("Error while formatting strings: {0}")]
|
|
|
|
|
FormatError(#[from] std::fmt::Error),
|
2020-06-30 19:23:52 +02:00
|
|
|
#[error("Environment Error")]
|
2022-10-22 21:23:31 -07:00
|
|
|
Env(#[from] dotenvy::Error),
|
2020-10-16 12:04:38 -05:00
|
|
|
#[error("Invalid index to sort by: {0}")]
|
|
|
|
|
InvalidIndex(String),
|
2020-06-30 19:23:52 +02:00
|
|
|
}
|
|
|
|
|
|
2020-07-03 12:44:39 -05:00
|
|
|
impl actix_web::ResponseError for SearchError {
|
|
|
|
|
fn status_code(&self) -> StatusCode {
|
|
|
|
|
match self {
|
2022-03-27 19:12:42 -07:00
|
|
|
SearchError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
SearchError::MeiliSearch(..) => StatusCode::BAD_REQUEST,
|
|
|
|
|
SearchError::Serde(..) => StatusCode::BAD_REQUEST,
|
|
|
|
|
SearchError::IntParsing(..) => StatusCode::BAD_REQUEST,
|
2020-10-16 12:04:38 -05:00
|
|
|
SearchError::InvalidIndex(..) => StatusCode::BAD_REQUEST,
|
2022-08-16 17:42:04 -07:00
|
|
|
SearchError::FormatError(..) => StatusCode::BAD_REQUEST,
|
2020-07-03 12:44:39 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn error_response(&self) -> HttpResponse {
|
|
|
|
|
HttpResponse::build(self.status_code()).json(ApiError {
|
|
|
|
|
error: match self {
|
2022-03-27 19:12:42 -07:00
|
|
|
SearchError::Env(..) => "environment_error",
|
|
|
|
|
SearchError::MeiliSearch(..) => "meilisearch_error",
|
|
|
|
|
SearchError::Serde(..) => "invalid_input",
|
|
|
|
|
SearchError::IntParsing(..) => "invalid_input",
|
2020-10-16 12:04:38 -05:00
|
|
|
SearchError::InvalidIndex(..) => "invalid_input",
|
2022-08-16 17:42:04 -07:00
|
|
|
SearchError::FormatError(..) => "invalid_input",
|
2020-07-03 12:44:39 -05:00
|
|
|
},
|
|
|
|
|
description: &self.to_string(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-19 14:23:05 -07:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct SearchConfig {
|
|
|
|
|
pub address: String,
|
|
|
|
|
pub key: String,
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-12 11:26:59 +08:00
|
|
|
impl SearchConfig {
|
|
|
|
|
pub fn make_client(&self) -> Client {
|
|
|
|
|
Client::new(self.address.as_str(), self.key.as_str())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A project document used for uploading projects to MeiliSearch's indices.
|
2020-07-27 18:54:10 -05:00
|
|
|
/// This contains some extra data that is not returned by search results.
|
2020-06-30 19:23:52 +02:00
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
2021-05-30 15:02:07 -07:00
|
|
|
pub struct UploadSearchProject {
|
2023-11-11 16:40:10 -08:00
|
|
|
pub version_id: String,
|
2021-05-30 15:02:07 -07:00
|
|
|
pub project_id: String,
|
2023-11-16 10:36:03 -08:00
|
|
|
pub project_types: Vec<String>,
|
2020-12-13 18:10:58 -07:00
|
|
|
pub slug: Option<String>,
|
2020-06-30 19:23:52 +02:00
|
|
|
pub author: String,
|
2023-12-01 19:15:00 -08:00
|
|
|
pub name: String,
|
2020-06-30 19:23:52 +02:00
|
|
|
pub description: String,
|
2021-06-02 18:33:11 -07:00
|
|
|
pub categories: Vec<String>,
|
2022-07-31 13:29:20 -07:00
|
|
|
pub display_categories: Vec<String>,
|
2021-03-04 20:35:23 -07:00
|
|
|
pub follows: i32,
|
2020-06-30 19:23:52 +02:00
|
|
|
pub downloads: i32,
|
|
|
|
|
pub icon_url: String,
|
2021-03-07 18:44:24 -07:00
|
|
|
pub license: String,
|
2021-07-27 16:50:07 -07:00
|
|
|
pub gallery: Vec<String>,
|
2022-12-30 13:56:41 -07:00
|
|
|
pub featured_gallery: Option<String>,
|
2021-05-30 15:02:07 -07:00
|
|
|
/// RFC 3339 formatted creation date of the project
|
2022-07-31 13:29:20 -07:00
|
|
|
pub date_created: DateTime<Utc>,
|
2021-05-30 15:02:07 -07:00
|
|
|
/// Unix timestamp of the creation date of the project
|
2020-07-27 18:54:10 -05:00
|
|
|
pub created_timestamp: i64,
|
|
|
|
|
/// RFC 3339 formatted date/time of last major modification (update)
|
2022-07-31 13:29:20 -07:00
|
|
|
pub date_modified: DateTime<Utc>,
|
2020-07-27 18:54:10 -05:00
|
|
|
/// Unix timestamp of the last major modification
|
|
|
|
|
pub modified_timestamp: i64,
|
2022-11-29 23:53:24 -05:00
|
|
|
pub open_source: bool,
|
2022-12-29 17:20:50 -07:00
|
|
|
pub color: Option<u32>,
|
2023-11-11 16:40:10 -08:00
|
|
|
|
|
|
|
|
#[serde(flatten)]
|
|
|
|
|
pub loader_fields: HashMap<String, Vec<String>>,
|
2020-07-27 18:54:10 -05:00
|
|
|
}
|
|
|
|
|
|
2020-09-06 08:19:53 -07:00
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
|
|
|
pub struct SearchResults {
|
2021-05-30 15:02:07 -07:00
|
|
|
pub hits: Vec<ResultSearchProject>,
|
2020-09-06 08:19:53 -07:00
|
|
|
pub offset: usize,
|
|
|
|
|
pub limit: usize,
|
2020-09-07 11:44:21 -07:00
|
|
|
pub total_hits: usize,
|
2020-09-06 08:19:53 -07:00
|
|
|
}
|
|
|
|
|
|
2020-07-27 18:54:10 -05:00
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
2021-05-30 15:02:07 -07:00
|
|
|
pub struct ResultSearchProject {
|
2023-11-11 16:40:10 -08:00
|
|
|
pub version_id: String,
|
2021-05-30 15:02:07 -07:00
|
|
|
pub project_id: String,
|
2023-11-16 10:36:03 -08:00
|
|
|
pub project_types: Vec<String>,
|
2020-12-13 18:10:58 -07:00
|
|
|
pub slug: Option<String>,
|
2020-07-27 18:54:10 -05:00
|
|
|
pub author: String,
|
2023-12-01 19:15:00 -08:00
|
|
|
pub name: String,
|
2020-07-27 18:54:10 -05:00
|
|
|
pub description: String,
|
|
|
|
|
pub categories: Vec<String>,
|
2022-07-31 13:29:20 -07:00
|
|
|
pub display_categories: Vec<String>,
|
2020-07-27 18:54:10 -05:00
|
|
|
pub downloads: i32,
|
2021-03-07 18:44:24 -07:00
|
|
|
pub follows: i32,
|
2020-07-27 18:54:10 -05:00
|
|
|
pub icon_url: String,
|
2021-05-30 15:02:07 -07:00
|
|
|
/// RFC 3339 formatted creation date of the project
|
2020-07-27 18:54:10 -05:00
|
|
|
pub date_created: String,
|
2021-05-30 15:02:07 -07:00
|
|
|
/// RFC 3339 formatted modification date of the project
|
2020-06-30 19:23:52 +02:00
|
|
|
pub date_modified: String,
|
2021-03-07 18:44:24 -07:00
|
|
|
pub license: String,
|
2021-07-27 16:50:07 -07:00
|
|
|
pub gallery: Vec<String>,
|
2022-12-30 13:56:41 -07:00
|
|
|
pub featured_gallery: Option<String>,
|
2022-12-29 17:20:50 -07:00
|
|
|
pub color: Option<u32>,
|
2023-11-11 16:40:10 -08:00
|
|
|
|
|
|
|
|
#[serde(flatten)]
|
|
|
|
|
pub loader_fields: HashMap<String, Vec<String>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_sort_index(index: &str) -> Result<(&str, [&str; 1]), SearchError> {
|
|
|
|
|
Ok(match index {
|
|
|
|
|
"relevance" => ("projects", ["downloads:desc"]),
|
|
|
|
|
"downloads" => ("projects_filtered", ["downloads:desc"]),
|
|
|
|
|
"follows" => ("projects", ["follows:desc"]),
|
|
|
|
|
"updated" => ("projects", ["date_modified:desc"]),
|
|
|
|
|
"newest" => ("projects", ["date_created:desc"]),
|
|
|
|
|
i => return Err(SearchError::InvalidIndex(i.to_string())),
|
|
|
|
|
})
|
2020-06-30 19:23:52 +02:00
|
|
|
}
|
|
|
|
|
|
2021-05-30 15:02:07 -07:00
|
|
|
pub async fn search_for_project(
|
2020-10-19 14:23:05 -07:00
|
|
|
info: &SearchRequest,
|
|
|
|
|
config: &SearchConfig,
|
|
|
|
|
) -> Result<SearchResults, SearchError> {
|
2020-10-19 19:46:33 -07:00
|
|
|
let client = Client::new(&*config.address, &*config.key);
|
2020-06-30 19:23:52 +02:00
|
|
|
|
2020-07-03 12:44:39 -05:00
|
|
|
let offset = info.offset.as_deref().unwrap_or("0").parse()?;
|
|
|
|
|
let index = info.index.as_deref().unwrap_or("relevance");
|
2020-10-01 00:07:52 -05:00
|
|
|
let limit = info.limit.as_deref().unwrap_or("10").parse()?;
|
2020-06-30 19:23:52 +02:00
|
|
|
|
2023-11-11 16:40:10 -08:00
|
|
|
let sort = get_sort_index(index)?;
|
2020-10-16 12:04:38 -05:00
|
|
|
|
2022-03-27 19:12:42 -07:00
|
|
|
let meilisearch_index = client.get_index(sort.0).await?;
|
|
|
|
|
|
|
|
|
|
let mut filter_string = String::new();
|
|
|
|
|
|
|
|
|
|
let results = {
|
|
|
|
|
let mut query = meilisearch_index.search();
|
|
|
|
|
|
|
|
|
|
query
|
|
|
|
|
.with_limit(min(100, limit))
|
|
|
|
|
.with_offset(offset)
|
|
|
|
|
.with_query(info.query.as_deref().unwrap_or_default())
|
|
|
|
|
.with_sort(&sort.1);
|
|
|
|
|
|
|
|
|
|
if let Some(new_filters) = info.new_filters.as_deref() {
|
|
|
|
|
query.with_filter(new_filters);
|
|
|
|
|
} else {
|
|
|
|
|
let facets = if let Some(facets) = &info.facets {
|
2023-11-28 10:36:59 -08:00
|
|
|
Some(serde_json::from_str::<Vec<Vec<Value>>>(facets)?)
|
2022-03-27 19:12:42 -07:00
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
2023-04-20 16:38:30 -07:00
|
|
|
let filters: Cow<_> = match (info.filters.as_deref(), info.version.as_deref()) {
|
|
|
|
|
(Some(f), Some(v)) => format!("({f}) AND ({v})").into(),
|
|
|
|
|
(Some(f), None) => f.into(),
|
|
|
|
|
(None, Some(v)) => v.into(),
|
|
|
|
|
(None, None) => "".into(),
|
|
|
|
|
};
|
2022-03-27 19:12:42 -07:00
|
|
|
|
|
|
|
|
if let Some(facets) = facets {
|
2023-11-28 10:36:59 -08:00
|
|
|
// Search can now *optionally* have a third inner array: So Vec(AND)<Vec(OR)<Vec(AND)< _ >>>
|
|
|
|
|
// For every inner facet, we will check if it can be deserialized into a Vec<&str>, and do so.
|
|
|
|
|
// If not, we will assume it is a single facet and wrap it in a Vec.
|
|
|
|
|
let facets: Vec<Vec<Vec<String>>> = facets
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|facets| {
|
|
|
|
|
facets
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|facet| {
|
|
|
|
|
if facet.is_array() {
|
|
|
|
|
serde_json::from_value::<Vec<String>>(facet).unwrap_or_default()
|
|
|
|
|
} else {
|
|
|
|
|
vec![serde_json::from_value::<String>(facet.clone())
|
|
|
|
|
.unwrap_or_default()]
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect_vec()
|
|
|
|
|
})
|
|
|
|
|
.collect_vec();
|
|
|
|
|
|
2022-03-27 19:12:42 -07:00
|
|
|
filter_string.push('(');
|
2023-11-28 10:36:59 -08:00
|
|
|
for (index, facet_outer_list) in facets.iter().enumerate() {
|
2022-03-27 19:12:42 -07:00
|
|
|
filter_string.push('(');
|
|
|
|
|
|
2023-11-28 10:36:59 -08:00
|
|
|
for (facet_outer_index, facet_inner_list) in facet_outer_list.iter().enumerate()
|
|
|
|
|
{
|
|
|
|
|
filter_string.push('(');
|
|
|
|
|
for (facet_inner_index, facet) in facet_inner_list.iter().enumerate() {
|
|
|
|
|
filter_string.push_str(&facet.replace(':', " = "));
|
|
|
|
|
if facet_inner_index != (facet_inner_list.len() - 1) {
|
|
|
|
|
filter_string.push_str(" AND ")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
filter_string.push(')');
|
|
|
|
|
|
|
|
|
|
if facet_outer_index != (facet_outer_list.len() - 1) {
|
2022-03-27 19:12:42 -07:00
|
|
|
filter_string.push_str(" OR ")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filter_string.push(')');
|
|
|
|
|
|
|
|
|
|
if index != (facets.len() - 1) {
|
|
|
|
|
filter_string.push_str(" AND ")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
filter_string.push(')');
|
|
|
|
|
|
|
|
|
|
if !filters.is_empty() {
|
2023-02-01 10:08:02 -07:00
|
|
|
write!(filter_string, " AND ({filters})")?;
|
2022-03-27 19:12:42 -07:00
|
|
|
}
|
|
|
|
|
} else {
|
2022-11-17 17:09:53 +00:00
|
|
|
filter_string.push_str(&filters);
|
2022-03-27 19:12:42 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !filter_string.is_empty() {
|
|
|
|
|
query.with_filter(&filter_string);
|
|
|
|
|
}
|
2020-11-05 09:38:03 -06:00
|
|
|
}
|
|
|
|
|
|
2022-03-27 19:12:42 -07:00
|
|
|
query.execute::<ResultSearchProject>().await?
|
|
|
|
|
};
|
2020-09-06 08:19:53 -07:00
|
|
|
|
|
|
|
|
Ok(SearchResults {
|
2020-11-05 09:38:03 -06:00
|
|
|
hits: results.hits.into_iter().map(|r| r.result).collect(),
|
2023-02-22 16:11:14 -07:00
|
|
|
offset: results.offset.unwrap_or_default(),
|
|
|
|
|
limit: results.limit.unwrap_or_default(),
|
|
|
|
|
total_hits: results.estimated_total_hits.unwrap_or_default(),
|
2020-09-06 08:19:53 -07:00
|
|
|
})
|
2020-06-30 19:23:52 +02:00
|
|
|
}
|