From 11bc55bf619ecdfe0e2fea33a1004be168cc6684 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 16 Feb 2024 01:51:27 -0500 Subject: [PATCH 1/5] Add migration to install pg_trgm extension --- migrations/2024-02-16-064035_create_pg_trgm/down.sql | 1 + migrations/2024-02-16-064035_create_pg_trgm/up.sql | 1 + 2 files changed, 2 insertions(+) create mode 100644 migrations/2024-02-16-064035_create_pg_trgm/down.sql create mode 100644 migrations/2024-02-16-064035_create_pg_trgm/up.sql diff --git a/migrations/2024-02-16-064035_create_pg_trgm/down.sql b/migrations/2024-02-16-064035_create_pg_trgm/down.sql new file mode 100644 index 0000000..47fd365 --- /dev/null +++ b/migrations/2024-02-16-064035_create_pg_trgm/down.sql @@ -0,0 +1 @@ +DROP EXTENSION pg_trgm; diff --git a/migrations/2024-02-16-064035_create_pg_trgm/up.sql b/migrations/2024-02-16-064035_create_pg_trgm/up.sql new file mode 100644 index 0000000..d497121 --- /dev/null +++ b/migrations/2024-02-16-064035_create_pg_trgm/up.sql @@ -0,0 +1 @@ +CREATE EXTENSION pg_trgm; From bd7b1ebd1a1deffc5c2c4fcb81b7556409ad114a Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 16 Feb 2024 14:18:06 -0500 Subject: [PATCH 2/5] Add migration to create artist, album, and song title indicies --- migrations/2024-02-16-191139_create_title_indicies/down.sql | 3 +++ migrations/2024-02-16-191139_create_title_indicies/up.sql | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 migrations/2024-02-16-191139_create_title_indicies/down.sql create mode 100644 migrations/2024-02-16-191139_create_title_indicies/up.sql diff --git a/migrations/2024-02-16-191139_create_title_indicies/down.sql b/migrations/2024-02-16-191139_create_title_indicies/down.sql new file mode 100644 index 0000000..1306204 --- /dev/null +++ b/migrations/2024-02-16-191139_create_title_indicies/down.sql @@ -0,0 +1,3 @@ +DROP INDEX artists_name_idx; +DROP INDEX albums_title_idx; +DROP INDEX songs_title_idx; diff --git a/migrations/2024-02-16-191139_create_title_indicies/up.sql b/migrations/2024-02-16-191139_create_title_indicies/up.sql new file mode 100644 index 0000000..61e63ab --- /dev/null +++ b/migrations/2024-02-16-191139_create_title_indicies/up.sql @@ -0,0 +1,3 @@ +CREATE INDEX artists_name_idx ON artists USING GIST (name gist_trgm_ops); +CREATE INDEX albums_title_idx ON albums USING GIST (title gist_trgm_ops); +CREATE INDEX songs_title_idx ON songs USING GIST (title gist_trgm_ops); From 69eb1e866a05eb0fb56104874fe80e9d7f8458c5 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 16 Feb 2024 14:18:29 -0500 Subject: [PATCH 3/5] Add serde feature to time crate --- Cargo.lock | 1 + Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 14117ad..3b442bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -837,6 +837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" dependencies = [ "powerfmt", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index aecc4be..93a1db9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ diesel = { version = "2.1.4", features = ["postgres", "r2d2", "time"], optional lazy_static = { version = "1.4.0", optional = true } serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } -time = "0.3.34" +time = { version = "0.3.34", features = ["serde"] } diesel_migrations = { version = "2.1.0", optional = true } actix-identity = { version = "0.7.0", optional = true } actix-session = { version = "0.9.0", features = ["redis-rs-session"], optional = true } From 201afec219643ec40174a34ffe24da96448233a2 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 16 Feb 2024 17:22:58 -0500 Subject: [PATCH 4/5] Add futures crate --- Cargo.lock | 1 + Cargo.toml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 3b442bd..78e41a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1701,6 +1701,7 @@ dependencies = [ "diesel", "diesel_migrations", "dotenv", + "futures", "http", "lazy_static", "leptos", diff --git a/Cargo.toml b/Cargo.toml index 93a1db9..6b8531e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ diesel_migrations = { version = "2.1.0", optional = true } actix-identity = { version = "0.7.0", optional = true } actix-session = { version = "0.9.0", features = ["redis-rs-session"], optional = true } pbkdf2 = { version = "0.12.2", features = ["simple"], optional = true } +futures = { version = "0.3.30", default-features = false, optional = true } [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] @@ -53,6 +54,7 @@ ssr = [ "actix-identity", "actix-session", "pbkdf2", + "futures", ] # Defines a size-optimized profile for the WASM bundle in release mode From 8066c80459c309964e1873c6af4a6dd8e2374688 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 16 Feb 2024 23:45:38 -0500 Subject: [PATCH 5/5] Add search functions --- src/lib.rs | 1 + src/models.rs | 3 ++ src/search.rs | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 src/search.rs diff --git a/src/lib.rs b/src/lib.rs index 5eaf272..1897b87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod playbar; pub mod database; pub mod models; pub mod users; +pub mod search; use cfg_if::cfg_if; cfg_if! { diff --git a/src/models.rs b/src/models.rs index 0c94672..7ad0cda 100644 --- a/src/models.rs +++ b/src/models.rs @@ -47,6 +47,7 @@ pub struct User { #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::artists))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +#[derive(Serialize, Deserialize)] pub struct Artist { /// A unique id for the artist #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] @@ -167,6 +168,7 @@ impl Artist { #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::albums))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +#[derive(Serialize, Deserialize)] pub struct Album { /// A unique id for the album #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] @@ -237,6 +239,7 @@ impl Album { #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::songs))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +#[derive(Serialize, Deserialize)] pub struct Song { /// A unique id for the song #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] diff --git a/src/search.rs b/src/search.rs new file mode 100644 index 0000000..3b24f08 --- /dev/null +++ b/src/search.rs @@ -0,0 +1,109 @@ +use leptos::*; +use crate::models::{Artist, Album, Song}; + +use cfg_if::cfg_if; + +cfg_if! { +if #[cfg(feature = "ssr")] { + use diesel::sql_types::*; + use diesel::*; + use diesel::pg::Pg; + use diesel::expression::AsExpression; + + use crate::database::get_db_conn; + + // Define pg_trgm operators + // Functions do not use indices for queries, so we need to use operators + diesel::infix_operator!(Similarity, " % ", backend: Pg); + diesel::infix_operator!(Distance, " <-> ", Float, backend: Pg); + + // Create functions to make use of the operators in queries + fn trgm_similar, U: AsExpression>(left: T, right: U) + -> Similarity { + Similarity::new(left.as_expression(), right.as_expression()) + } + + fn trgm_distance, U: AsExpression>(left: T, right: U) + -> Distance { + Distance::new(left.as_expression(), right.as_expression()) + } +} +} + +/// Search for albums by title +/// +/// # Arguments +/// `query` - The search query. This will be used to perform a fuzzy search on the album titles +/// `limit` - The maximum number of results to return +/// +/// # Returns +/// A Result containing a vector of albums if the search was successful, or an error if the search failed +#[server(endpoint = "search_albums")] +pub async fn search_albums(query: String, limit: i64) -> Result, ServerFnError> { + use crate::schema::albums::dsl::*; + + Ok(albums + .filter(trgm_similar(title, query.clone())) + .order_by(trgm_distance(title, query)) + .limit(limit) + .load(&mut get_db_conn())?) +} + +/// Search for artists by name +/// +/// # Arguments +/// `query` - The search query. This will be used to perform a fuzzy search on the artist names +/// `limit` - The maximum number of results to return +/// +/// # Returns +/// A Result containing a vector of artists if the search was successful, or an error if the search failed +#[server(endpoint = "search_artists")] +pub async fn search_artists(query: String, limit: i64) -> Result, ServerFnError> { + use crate::schema::artists::dsl::*; + + Ok(artists + .filter(trgm_similar(name, query.clone())) + .order_by(trgm_distance(name, query)) + .limit(limit) + .load(&mut get_db_conn())?) +} + +/// Search for songs by title +/// +/// # Arguments +/// `query` - The search query. This will be used to perform a fuzzy search on the song titles +/// `limit` - The maximum number of results to return +/// +/// # Returns +/// A Result containing a vector of songs if the search was successful, or an error if the search failed +#[server(endpoint = "search_songs")] +pub async fn search_songs(query: String, limit: i64) -> Result, ServerFnError> { + use crate::schema::songs::dsl::*; + + Ok(songs + .filter(trgm_similar(title, query.clone())) + .order_by(trgm_distance(title, query)) + .limit(limit) + .load(&mut get_db_conn())?) +} + +/// Search for songs, albums, and artists by title or name +/// +/// # Arguments +/// `query` - The search query. This will be used to perform a fuzzy search on the +/// song titles, album titles, and artist names +/// `limit` - The maximum number of results to return for each type +/// +/// # Returns +/// A Result containing a tuple of vectors of albums, artists, and songs if the search was successful, +#[server(endpoint = "search")] +pub async fn search(query: String, limit: i64) -> Result<(Vec, Vec, Vec), ServerFnError> { + let albums = search_albums(query.clone(), limit); + let artists = search_artists(query.clone(), limit); + let songs = search_songs(query.clone(), limit); + + use futures::join; + + let (albums, artists, songs) = join!(albums, artists, songs); + Ok((albums?, artists?, songs?)) +}