Improve search performance

This commit is contained in:
2022-01-05 11:58:08 +01:00
parent 37a7cbba6e
commit dc5460af0d
2 changed files with 46 additions and 25 deletions

View File

@ -1,4 +1,5 @@
use crate::css::C; use crate::css::C;
use crate::fuzzy::FuzzyScore;
use crate::query::ParsedQuery; use crate::query::ParsedQuery;
use crate::song::Song; use crate::song::Song;
use anyhow::anyhow; use anyhow::anyhow;
@ -12,12 +13,21 @@ use std::cmp::Reverse;
use web_sys::Element; use web_sys::Element;
pub struct Model { pub struct Model {
songs: Vec<Song>, songs: Vec<(Reverse<FuzzyScore>, Song)>,
/// The search string
query: String, query: String,
filtered_songs: Vec<usize>,
hidden_songs: usize, /// The number of songs currently in view. Goes up when the user scrolls down.
shown_songs: usize, shown_songs: usize,
/// The number of songs that didn't match the search critera
hidden_songs: usize,
/// Whether we're filtering by video
filter_video: bool, filter_video: bool,
/// Whether we're filtering by duets
filter_duets: bool, filter_duets: bool,
} }
@ -25,23 +35,33 @@ const SCROLL_THRESHOLD: usize = 50;
const INITIAL_ELEM_COUNT: usize = 100; const INITIAL_ELEM_COUNT: usize = 100;
pub enum Msg { pub enum Msg {
/// The user entered something into the search field
Search(String), Search(String),
/// The user pressed the Toggle Video button
ToggleVideo, ToggleVideo,
/// The user pressed the Toggle Duets button
ToggleDuets, ToggleDuets,
/// The user pressed the Shuffle button
Shuffle, Shuffle,
/// The user scrolled the song list
Scroll, Scroll,
} }
pub fn init(_url: Url, _orders: &mut impl Orders<Msg>) -> Model { pub fn init(_url: Url, _orders: &mut impl Orders<Msg>) -> Model {
let songs: Vec<Song> = let mut songs: Vec<Song> =
serde_json::from_str(include_str!("../static/songs.json")).expect("parse songs"); serde_json::from_str(include_str!("../static/songs.json")).expect("parse songs");
let mut filtered_songs: Vec<usize> = (0..songs.len()).collect(); songs.shuffle(&mut thread_rng());
filtered_songs.shuffle(&mut thread_rng());
Model { Model {
songs, songs: songs
.into_iter()
.map(|song| (Default::default(), song))
.collect(),
query: String::new(), query: String::new(),
filtered_songs,
hidden_songs: 0, hidden_songs: 0,
shown_songs: INITIAL_ELEM_COUNT, shown_songs: INITIAL_ELEM_COUNT,
filter_video: false, filter_video: false,
@ -64,19 +84,19 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
update(Msg::Shuffle, model, orders) update(Msg::Shuffle, model, orders)
} else { } else {
let query = ParsedQuery::parse(&model.query); let query = ParsedQuery::parse(&model.query);
model.filtered_songs.sort_by_cached_key(|&i| { model.filter_duets = query.duet == Some(true);
let song = &model.songs[i]; model.filter_video = query.video == Some(true);
let score = song.fuzzy_compare(&query);
if score < Default::default() { // calculate search scores & sort list
for (score, song) in model.songs.iter_mut() {
let new_score = song.fuzzy_compare(&query);
if new_score < Default::default() {
model.hidden_songs += 1; model.hidden_songs += 1;
} }
let top_score = Reverse(score); *score = Reverse(new_score);
}
(top_score, &song.title, &song.artist, &song.song_hash) model.songs.sort_unstable();
});
model.filter_duets = query.duet == Some(true);
model.filter_video = query.video == Some(true);
} }
} }
Msg::ToggleVideo => { Msg::ToggleVideo => {
@ -100,7 +120,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
model.shown_songs = INITIAL_ELEM_COUNT; model.shown_songs = INITIAL_ELEM_COUNT;
scroll_to_top(); scroll_to_top();
model.query.clear(); model.query.clear();
model.filtered_songs.shuffle(&mut thread_rng()); model.songs.shuffle(&mut thread_rng());
} }
Msg::Scroll => { Msg::Scroll => {
let (scroll, max_scroll) = match get_scroll() { let (scroll, max_scroll) = match get_scroll() {
@ -171,6 +191,7 @@ pub fn view(model: &Model) -> Vec<Node<Msg>> {
"Duet", "Duet",
div![ div![
C![C.marquee], C![C.marquee],
// add duplicates to get the repeating marquee effect
p![" 🗲 ", p1, " 🗲 ", p2, " 🗲 ", p1, " 🗲 ", p2] p![" 🗲 ", p1, " 🗲 ", p2, " 🗲 ", p1, " 🗲 ", p2]
], ],
], ],
@ -219,11 +240,11 @@ pub fn view(model: &Model) -> Vec<Node<Msg>> {
attrs! {At::Id => SONG_LIST_ID}, attrs! {At::Id => SONG_LIST_ID},
ev(Ev::Scroll, |_| Msg::Scroll), ev(Ev::Scroll, |_| Msg::Scroll),
model model
.filtered_songs .songs
.iter() .iter()
.map(|&i| &model.songs[i]) .map(|(_, song)| song)
.map(song_card) .map(song_card)
.take(model.filtered_songs.len() - model.hidden_songs) .take(model.songs.len() - model.hidden_songs)
.take(model.shown_songs), .take(model.shown_songs),
], ],
] ]
@ -234,7 +255,7 @@ const SONG_LIST_ID: &str = "song_list";
fn get_song_list_element() -> anyhow::Result<Element> { fn get_song_list_element() -> anyhow::Result<Element> {
document() document()
.get_element_by_id(SONG_LIST_ID) .get_element_by_id(SONG_LIST_ID)
.ok_or(anyhow!("Failed to access song list element")) .ok_or_else(|| anyhow!("Failed to access song list element"))
} }
fn scroll_to_top() { fn scroll_to_top() {

View File

@ -3,12 +3,12 @@ use crate::query::ParsedQuery;
use serde::Deserialize; use serde::Deserialize;
use std::cmp::max; use std::cmp::max;
#[derive(Deserialize, Debug, Clone, Default)] #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct Song { pub struct Song {
pub title: String, pub title: String,
pub artist: String, pub artist: String,
pub cover: Option<String>,
pub song_hash: String, pub song_hash: String,
pub cover: Option<String>,
pub language: Option<String>, pub language: Option<String>,
pub video: Option<String>, pub video: Option<String>,
pub year: Option<String>, pub year: Option<String>,