2023-12-03 06:27:12 -08:00
use crate ::database ::models ::project_item ::{ GalleryItem , LinkUrl } ;
2020-07-03 12:44:39 -05:00
use crate ::models ::error ::ApiError ;
2023-12-03 06:27:12 -08:00
use crate ::models ::projects ::{ MonetizationStatus , ProjectStatus , 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 ,
2023-12-03 06:27:12 -08:00
pub summary : 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 ,
2023-12-03 06:27:12 -08:00
pub icon_url : Option < 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
2023-12-03 06:27:12 -08:00
// Hidden fields to get the Project model out of the search results.
pub license_url : Option < String > ,
pub monetization_status : Option < MonetizationStatus > ,
pub team_id : String ,
pub thread_id : String ,
pub versions : Vec < String > ,
pub date_published : DateTime < Utc > ,
pub date_queued : Option < DateTime < Utc > > ,
pub status : ProjectStatus ,
pub requested_status : Option < ProjectStatus > ,
pub loaders : Vec < String > , // Search uses loaders as categories- this is purely for the Project model.
pub links : Vec < LinkUrl > ,
pub gallery_items : Vec < GalleryItem > , // Gallery *only* urls are stored in gallery, but the gallery items are stored here- required for the Project model.
pub games : Vec < String > , // Todo: in future, could be a searchable field.
pub organization_id : Option < String > , // Todo: in future, could be a searchable field.
2023-11-11 16:40:10 -08:00
#[ serde(flatten) ]
2023-12-03 06:27:12 -08:00
pub loader_fields : HashMap < String , Vec < serde_json ::Value > > ,
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 ,
2023-12-03 06:27:12 -08:00
pub summary : String ,
2020-07-27 18:54:10 -05:00
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 ,
2023-12-03 06:27:12 -08:00
pub icon_url : Option < 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
2023-12-03 06:27:12 -08:00
// Hidden fields to get the Project model out of the search results.
pub license_url : Option < String > ,
pub monetization_status : Option < String > ,
pub team_id : String ,
pub thread_id : String ,
pub versions : Vec < String > ,
pub date_published : String ,
pub date_queued : Option < String > ,
pub status : String ,
pub requested_status : Option < String > ,
pub loaders : Vec < String > , // Search uses loaders as categories- this is purely for the Project model.
pub links : Vec < LinkUrl > ,
pub gallery_items : Vec < GalleryItem > , // Gallery *only* urls are stored in gallery, but the gallery items are stored here- required for the Project model.
2023-12-11 20:01:15 -08:00
pub games : Vec < String > , // Todo: in future, could be a searchable field.
2023-12-03 06:27:12 -08:00
pub organization_id : Option < String > , // Todo: in future, could be a searchable field.
2023-11-11 16:40:10 -08:00
#[ serde(flatten) ]
2023-12-03 06:27:12 -08:00
pub loader_fields : HashMap < String , Vec < serde_json ::Value > > ,
2023-11-11 16:40:10 -08:00
}
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 {
2023-12-11 20:01:15 -08:00
vec! [ serde_json ::from_value ::< String > ( facet )
2023-11-28 10:36:59 -08:00
. 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
}