Files
AstralRinth/src/search/mod.rs
Aeledfyr d477874535 Optimize and fix some bugs in indexing (#98)
* Improve curseforge and local indexing

This should make curseforge indexing more efficient, and reuses
some of the normal local indexing for the queued indexing of
recently created mods.

* Unify impls for single and multiple routes for mods and versions

This uses the same backend for the single and multiple query
routes so that they no longer return inconsistent information.

* Cache valid curseforge mod ids to reduce request load

This caches the ids of minecraft mods and reuses them on indexing
to reduce the amount of unused addons that are returned.
2020-11-03 17:55:50 -07:00

197 lines
6.0 KiB
Rust

use crate::models::error::ApiError;
use crate::models::mods::SearchRequest;
use actix_web::http::StatusCode;
use actix_web::web::HttpResponse;
use chrono::{DateTime, Utc};
use meilisearch_sdk::client::Client;
use meilisearch_sdk::document::Document;
use meilisearch_sdk::search::Query;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::cmp::min;
use thiserror::Error;
pub mod indexing;
#[derive(Error, Debug)]
pub enum SearchError {
#[error("Error while connecting to the MeiliSearch database: {0}")]
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),
#[error("Invalid index to sort by: {0}")]
InvalidIndex(String),
}
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,
SearchError::InvalidIndex(..) => 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",
SearchError::InvalidIndex(..) => "invalid_input",
},
description: &self.to_string(),
})
}
}
#[derive(Clone)]
pub struct SearchConfig {
pub address: String,
pub key: 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<Cow<'static, str>>,
pub versions: Vec<String>,
pub downloads: i32,
pub page_url: String,
pub icon_url: String,
pub author_url: String,
pub latest_version: Cow<'static, str>,
/// RFC 3339 formatted creation date of the mod
pub date_created: DateTime<Utc>,
/// 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: DateTime<Utc>,
/// Unix timestamp of the last major modification
pub modified_timestamp: i64,
pub host: Cow<'static, str>,
/// 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)]
pub struct SearchResults {
pub hits: Vec<ResultSearchMod>,
pub offset: usize,
pub limit: usize,
pub total_hits: usize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ResultSearchMod {
pub mod_id: String,
pub author: String,
pub title: String,
pub description: String,
pub categories: Vec<String>,
// TODO: more efficient format for listing versions, without many repetitions
pub versions: Vec<String>,
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,
/// The host of the mod: Either `modrinth` or `curseforge`
pub host: 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,
config: &SearchConfig,
) -> Result<SearchResults, SearchError> {
let client = Client::new(&*config.address, &*config.key);
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 limit = info.limit.as_deref().unwrap_or("10").parse()?;
let search_query: &str = info
.query
.as_deref()
.filter(|s| !s.is_empty())
.unwrap_or("{}{}{}");
let mut query = Query::new(search_query)
.with_limit(min(100, limit))
.with_offset(offset);
if !filters.is_empty() {
query = query.with_filters(&filters);
}
if let Some(facets) = &info.facets {
let facets = serde_json::from_str::<Vec<Vec<&str>>>(facets)?;
query = query.with_facet_filters(facets);
}
let index = match index {
"relevance" => "relevance_mods",
"downloads" => "downloads_mods",
"updated" => "updated_mods",
"newest" => "newest_mods",
i => return Err(SearchError::InvalidIndex(i.to_string())),
};
let results = client
.get_index(index)
.await?
.search::<ResultSearchMod>(&query)
.await?;
Ok(SearchResults {
hits: results.hits,
offset: results.offset,
limit: results.limit,
total_hits: results.nb_hits,
})
}