You've already forked AstralRinth
forked from didirus/AstralRinth
* feat(indexing): Reindex curseforge & local database at an interval * fix(indexing): Use strings for meilisearch primary key Fixes #17 by prefixing curseforge ids with "curse-" and local ids with "local-". * feat(indexing): Add newly created mods to the index more quickly * feat(indexing): Implement faceted search, update to meilisearch master Fixes #9, but only uses faceted search for categories. It should be reasonably simple to add support for versions, but it may not be as useful due to the large number of versions and the large number of supported versions for each mod. * feat(indexing): Allow skipping initial indexing Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
153 lines
4.9 KiB
Rust
153 lines
4.9 KiB
Rust
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<String>,
|
|
pub versions: Vec<String>,
|
|
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<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,
|
|
}
|
|
|
|
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<Vec<ResultSearchMod>, 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::<Vec<Vec<&str>>>(facets)?;
|
|
query = query.with_facet_filters(facets);
|
|
}
|
|
|
|
Ok(client
|
|
.get_index(format!("{}_mods", index).as_ref())
|
|
.await?
|
|
.search::<ResultSearchMod>(&query)
|
|
.await?
|
|
.hits)
|
|
}
|