Skip to content

Commit

Permalink
feat(browsing): add full text search for search3
Browse files Browse the repository at this point in the history
  • Loading branch information
vnghia committed Apr 14, 2024
1 parent 3fe5dad commit 04fb4dd
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 11 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ tracing = { version = "0.1.40" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
walkdir = { version = "2.5.0" }
xxhash-rust = { version = "0.8.10", features = ["xxh3"] }
diesel_full_text_search = "2.1.1"

[dev-dependencies]
fake = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions diesel.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
[print_schema]
file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId"]
import_types = ["diesel::sql_types::*", "diesel_full_text_search::*"]
generate_missing_sql_type_definitions = false

[migrations_directory]
dir = "migrations"
10 changes: 10 additions & 0 deletions migrations/2024-04-14-171432_add_fts/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- This file should undo anything in `up.sql`
alter table artists drop column ts;

alter table albums drop column ts;

alter table songs drop column ts;

drop text search configuration usimple;

drop extension unaccent;
18 changes: 18 additions & 0 deletions migrations/2024-04-14-171432_add_fts/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- Your SQL goes here
create extension unaccent schema public;

create text search configuration usimple (copy = simple);
alter text search configuration usimple
alter mapping for hword, hword_part, word with public.unaccent, simple;

alter table artists
add column ts tsvector not null generated always as (to_tsvector('usimple', name)) stored;
create index artists_ts_idx on artists using gin (ts);

alter table albums
add column ts tsvector not null generated always as (to_tsvector('usimple', name)) stored;
create index albums_ts_idx on albums using gin (ts);

alter table songs
add column ts tsvector not null generated always as (to_tsvector('usimple', title)) stored;
create index songs_ts_idx on songs using gin (ts);
19 changes: 19 additions & 0 deletions src/open_subsonic/common/id3/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,25 @@ impl AlbumId3Db {
}
}

impl From<BasicSongId3Db> for SongId3 {
fn from(value: BasicSongId3Db) -> Self {
Self::new(
value.id,
value.title,
value.duration as _,
value.created_at,
value.file_size as _,
value.format,
value.bitrate as _,
value.album_id,
value.year.map(|v| v as _),
value.track_number.map(|v| v as _),
value.disc_number.map(|v| v as _),
value.cover_art_id.map(|v| MediaTypedId { t: Some(MediaType::Song), id: v }),
)
}
}

impl SongId3Db {
pub async fn into(self, pool: &DatabasePool) -> Result<SongId3> {
let artists = artists::table
Expand Down
120 changes: 110 additions & 10 deletions src/open_subsonic/searching/search3.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use std::borrow::Cow;

use anyhow::Result;
use axum::extract::State;
use diesel::{ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
use diesel_full_text_search::configuration::TsConfigurationByName;
use diesel_full_text_search::*;
use futures::{stream, StreamExt, TryStreamExt};
use nghe_proc_macros::{
add_axum_response, add_common_validate, add_convert_types, add_count_offset,
Expand All @@ -14,13 +18,16 @@ use crate::open_subsonic::id3::*;
use crate::open_subsonic::permission::check_permission;
use crate::{Database, DatabasePool};

const USIMPLE_TS_CONFIGURATION: TsConfigurationByName = TsConfigurationByName("usimple");

add_common_validate!(Search3Params);
add_axum_response!(Search3Body);

#[add_convert_types(from = &Search3Params)]
#[add_convert_types(from = &'a Search3Params, refs(query))]
#[derive(Debug)]
#[cfg_attr(test, derive(Default))]
struct SearchOffsetCount {
struct SearchQueryParams<'a> {
query: Cow<'a, str>,
artist_count: Option<u32>,
artist_offset: Option<u32>,
album_count: Option<u32>,
Expand All @@ -29,18 +36,19 @@ struct SearchOffsetCount {
song_offset: Option<u32>,
}

async fn syncing(
async fn sync(
pool: &DatabasePool,
user_id: Uuid,
music_folder_ids: &Option<Vec<Uuid>>,
SearchOffsetCount {
SearchQueryParams {
artist_count,
artist_offset,
album_count,
album_offset,
song_count,
song_offset,
}: SearchOffsetCount,
..
}: SearchQueryParams<'_>,
) -> Result<Search3Result> {
let artists = #[add_permission_filter]
#[add_count_offset(artist)]
Expand Down Expand Up @@ -76,15 +84,92 @@ async fn syncing(
})
}

async fn full_text_search(
pool: &DatabasePool,
user_id: Uuid,
music_folder_ids: &Option<Vec<Uuid>>,
SearchQueryParams {
query,
artist_count,
artist_offset,
album_count,
album_offset,
song_count,
song_offset,
}: SearchQueryParams<'_>,
) -> Result<Search3Result> {
let artists = #[add_permission_filter]
#[add_count_offset(artist)]
get_basic_artist_id3_db()
.filter(
artists::ts
.matches(websearch_to_tsquery_with_search_config(USIMPLE_TS_CONFIGURATION, &query)),
)
.order(
ts_rank_cd(
artists::ts,
websearch_to_tsquery_with_search_config(USIMPLE_TS_CONFIGURATION, &query),
)
.desc(),
)
.get_results::<BasicArtistId3Db>(&mut pool.get().await?)
.await?;

let albums = #[add_permission_filter]
#[add_count_offset(album)]
get_basic_album_id3_db()
.filter(
albums::ts
.matches(websearch_to_tsquery_with_search_config(USIMPLE_TS_CONFIGURATION, &query)),
)
.order(
ts_rank_cd(
albums::ts,
websearch_to_tsquery_with_search_config(USIMPLE_TS_CONFIGURATION, &query),
)
.desc(),
)
.get_results::<BasicAlbumId3Db>(&mut pool.get().await?)
.await?;

let songs = #[add_permission_filter]
#[add_count_offset(song)]
get_basic_song_id3_db()
.filter(
songs::ts
.matches(websearch_to_tsquery_with_search_config(USIMPLE_TS_CONFIGURATION, &query)),
)
.order(
ts_rank_cd(
songs::ts,
websearch_to_tsquery_with_search_config(USIMPLE_TS_CONFIGURATION, &query),
)
.desc(),
)
.get_results::<BasicSongId3Db>(&mut pool.get().await?)
.await?;

Ok(Search3Result {
artists: artists.into_iter().map(BasicArtistId3Db::into).collect(),
albums: albums.into_iter().map(BasicAlbumId3Db::into).collect(),
songs: songs.into_iter().map(BasicSongId3Db::into).collect(),
})
}

pub async fn search3_handler(
State(database): State<Database>,
req: Search3Request,
) -> Search3JsonResponse {
check_permission(&database.pool, req.user_id, &req.params.music_folder_ids).await?;

let search_result =
syncing(&database.pool, req.user_id, &req.params.music_folder_ids, (&req.params).into())
.await?;
let search_query: SearchQueryParams = (&req.params).into();

let search_result = if search_query.query.is_empty() {
sync(&database.pool, req.user_id, &req.params.music_folder_ids, search_query).await
} else {
full_text_search(&database.pool, req.user_id, &req.params.music_folder_ids, search_query)
.await
}?;

Ok(axum::Json(Search3Body { search_result3: search_result }.into()))
}
Expand All @@ -95,10 +180,25 @@ mod tests {
use crate::utils::test::Infra;

#[tokio::test]
async fn test_syncing() {
async fn test_sync() {
let n_song = 10;
let mut infra = Infra::new().await.n_folder(1).await.add_user(None).await;
infra.add_n_song(0, n_song).scan(.., None).await;
sync(infra.pool(), infra.user_id(0), &None, Default::default()).await.unwrap();
}

#[tokio::test]
async fn test_full_text_search() {
let n_song = 10;
let mut infra = Infra::new().await.n_folder(1).await.add_user(None).await;
infra.add_n_song(0, n_song).scan(.., None).await;
syncing(infra.pool(), infra.user_id(0), &None, Default::default()).await.unwrap();
full_text_search(
infra.pool(),
infra.user_id(0),
&None,
SearchQueryParams { query: "search".into(), ..Default::default() },
)
.await
.unwrap();
}
}
Loading

0 comments on commit 04fb4dd

Please sign in to comment.