use crate::models::error::ApiError; use crate::models::mods::SearchRequest; use actix_web::http::StatusCode; use actix_web::web::HttpResponse; use meilisearch_sdk::client::Client; use meilisearch_sdk::document::Document; use meilisearch_sdk::search::Query; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use thiserror::Error; pub mod indexing; #[derive(Error, Debug)] pub enum SearchError { #[error("Error while connecting to the MeiliSearch database")] IndexDBError(#[from] meilisearch_sdk::errors::Error), #[error("Error while serializing or deserializing JSON: {0}")] SerDeError(#[from] serde_json::Error), #[error("Error while parsing an integer: {0}")] IntParsingError(#[from] std::num::ParseIntError), #[error("Environment Error")] EnvError(#[from] dotenv::Error), } impl actix_web::ResponseError for SearchError { fn status_code(&self) -> StatusCode { match self { SearchError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR, SearchError::IndexDBError(..) => StatusCode::INTERNAL_SERVER_ERROR, SearchError::SerDeError(..) => StatusCode::BAD_REQUEST, SearchError::IntParsingError(..) => StatusCode::BAD_REQUEST, } } fn error_response(&self) -> HttpResponse { HttpResponse::build(self.status_code()).json(ApiError { error: match self { SearchError::EnvError(..) => "environment_error", SearchError::IndexDBError(..) => "indexdb_error", SearchError::SerDeError(..) => "invalid_input", SearchError::IntParsingError(..) => "invalid_input", }, description: &self.to_string(), }) } } /// A mod document used for uploading mods to meilisearch's indices. /// This contains some extra data that is not returned by search results. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct UploadSearchMod { pub mod_id: String, pub author: String, pub title: String, pub description: String, pub categories: Vec, pub versions: Vec, pub downloads: i32, pub page_url: String, pub icon_url: String, pub author_url: String, pub latest_version: String, /// RFC 3339 formatted creation date of the mod pub date_created: String, /// Unix timestamp of the creation date of the mod pub created_timestamp: i64, /// RFC 3339 formatted date/time of last major modification (update) pub date_modified: String, /// Unix timestamp of the last major modification pub modified_timestamp: i64, /// Must be "{}{}{}", a hack until meilisearch supports searches /// with empty queries (https://github.com/meilisearch/MeiliSearch/issues/729) // This is a Cow to prevent unnecessary allocations for a static // string pub empty: Cow<'static, str>, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ResultSearchMod { pub mod_id: String, pub author: String, pub title: String, pub description: String, pub categories: Vec, // TODO: more efficient format for listing versions, without many repetitions pub versions: Vec, pub downloads: i32, pub page_url: String, pub icon_url: String, pub author_url: String, /// RFC 3339 formatted creation date of the mod pub date_created: String, /// RFC 3339 formatted modification date of the mod pub date_modified: String, pub latest_version: String, } impl Document for UploadSearchMod { type UIDType = String; fn get_uid(&self) -> &Self::UIDType { &self.mod_id } } impl Document for ResultSearchMod { type UIDType = String; fn get_uid(&self) -> &Self::UIDType { &self.mod_id } } pub async fn search_for_mod(info: &SearchRequest) -> Result, SearchError> { let address = &*dotenv::var("MEILISEARCH_ADDR")?; let client = Client::new(address, ""); let filters: Cow<_> = match (info.filters.as_deref(), info.version.as_deref()) { (Some(f), Some(v)) => format!("({}) AND ({})", f, v).into(), (Some(f), None) => f.into(), (None, Some(v)) => v.into(), (None, None) => "".into(), }; let offset = info.offset.as_deref().unwrap_or("0").parse()?; let index = info.index.as_deref().unwrap_or("relevance"); let search_query: &str = info .query .as_deref() .filter(|s| !s.is_empty()) .unwrap_or("{}{}{}"); let mut query = Query::new(search_query).with_limit(10).with_offset(offset); if !filters.is_empty() { query = query.with_filters(&filters); } if let Some(facets) = &info.facets { let facets = serde_json::from_str::>>(facets)?; query = query.with_facet_filters(facets); } Ok(client .get_index(format!("{}_mods", index).as_ref()) .await? .search::(&query) .await? .hits) }