Merge branch '16-implement-song-artist-and-album-search' into 'main'

Implement song, artist, and album search

Closes #16

See merge request libretunes/libretunes!9
This commit is contained in:
Ethan Girouard 2024-02-29 20:28:05 -05:00
commit d2ee1fdf9e
9 changed files with 126 additions and 1 deletions

2
Cargo.lock generated
View File

@ -837,6 +837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc"
dependencies = [ dependencies = [
"powerfmt", "powerfmt",
"serde",
] ]
[[package]] [[package]]
@ -1700,6 +1701,7 @@ dependencies = [
"diesel", "diesel",
"diesel_migrations", "diesel_migrations",
"dotenv", "dotenv",
"futures",
"http", "http",
"lazy_static", "lazy_static",
"leptos", "leptos",

View File

@ -29,11 +29,12 @@ diesel = { version = "2.1.4", features = ["postgres", "r2d2", "time"], optional
lazy_static = { version = "1.4.0", optional = true } lazy_static = { version = "1.4.0", optional = true }
serde = { versions = "1.0.195", features = ["derive"] } serde = { versions = "1.0.195", features = ["derive"] }
openssl = { version = "0.10.63", optional = true } 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 } diesel_migrations = { version = "2.1.0", optional = true }
actix-identity = { version = "0.7.0", optional = true } actix-identity = { version = "0.7.0", optional = true }
actix-session = { version = "0.9.0", features = ["redis-rs-session"], optional = true } actix-session = { version = "0.9.0", features = ["redis-rs-session"], optional = true }
pbkdf2 = { version = "0.12.2", features = ["simple"], optional = true } pbkdf2 = { version = "0.12.2", features = ["simple"], optional = true }
futures = { version = "0.3.30", default-features = false, optional = true }
[features] [features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
@ -53,6 +54,7 @@ ssr = [
"actix-identity", "actix-identity",
"actix-session", "actix-session",
"pbkdf2", "pbkdf2",
"futures",
] ]
# Defines a size-optimized profile for the WASM bundle in release mode # Defines a size-optimized profile for the WASM bundle in release mode

View File

@ -0,0 +1 @@
DROP EXTENSION pg_trgm;

View File

@ -0,0 +1 @@
CREATE EXTENSION pg_trgm;

View File

@ -0,0 +1,3 @@
DROP INDEX artists_name_idx;
DROP INDEX albums_title_idx;
DROP INDEX songs_title_idx;

View File

@ -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);

View File

@ -6,6 +6,7 @@ pub mod playbar;
pub mod database; pub mod database;
pub mod models; pub mod models;
pub mod users; pub mod users;
pub mod search;
use cfg_if::cfg_if; use cfg_if::cfg_if;
cfg_if! { cfg_if! {

View File

@ -47,6 +47,7 @@ pub struct User {
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))]
#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::artists))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::artists))]
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
#[derive(Serialize, Deserialize)]
pub struct Artist { pub struct Artist {
/// A unique id for the artist /// A unique id for the artist
#[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] #[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", derive(Queryable, Selectable, Insertable))]
#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::albums))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::albums))]
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
#[derive(Serialize, Deserialize)]
pub struct Album { pub struct Album {
/// A unique id for the album /// A unique id for the album
#[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] #[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", derive(Queryable, Selectable, Insertable))]
#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::songs))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::songs))]
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
#[derive(Serialize, Deserialize)]
pub struct Song { pub struct Song {
/// A unique id for the song /// A unique id for the song
#[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))]

109
src/search.rs Normal file
View File

@ -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<T: AsExpression<Text>, U: AsExpression<Text>>(left: T, right: U)
-> Similarity<T::Expression, U::Expression> {
Similarity::new(left.as_expression(), right.as_expression())
}
fn trgm_distance<T: AsExpression<Text>, U: AsExpression<Text>>(left: T, right: U)
-> Distance<T::Expression, U::Expression> {
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<Vec<Album>, 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<Vec<Artist>, 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<Vec<Song>, 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<Album>, Vec<Artist>, Vec<Song>), 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?))
}