Number fields search
This commit is contained in:
parent
0fe3555560
commit
be97bccab1
2 changed files with 112 additions and 13 deletions
|
@ -37,7 +37,6 @@ pub enum NumberField {
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum NumberOp {
|
pub enum NumberOp {
|
||||||
Eq,
|
Eq,
|
||||||
NotEq,
|
|
||||||
Greater,
|
Greater,
|
||||||
GreaterOrEq,
|
GreaterOrEq,
|
||||||
Less,
|
Less,
|
||||||
|
@ -110,7 +109,6 @@ pub fn make_parser() -> impl Parser<char, Expr, Error = Simple<char>> {
|
||||||
|
|
||||||
let number_op = choice((
|
let number_op = choice((
|
||||||
just("=").to(NumberOp::Eq),
|
just("=").to(NumberOp::Eq),
|
||||||
just("!=").to(NumberOp::NotEq),
|
|
||||||
just(">=").to(NumberOp::GreaterOrEq),
|
just(">=").to(NumberOp::GreaterOrEq),
|
||||||
just(">").to(NumberOp::Greater),
|
just(">").to(NumberOp::Greater),
|
||||||
just("<=").to(NumberOp::LessOrEq),
|
just("<=").to(NumberOp::LessOrEq),
|
||||||
|
@ -271,10 +269,6 @@ fn can_parse_number_operators() {
|
||||||
parser.parse(r#"discnumber = 6"#).unwrap(),
|
parser.parse(r#"discnumber = 6"#).unwrap(),
|
||||||
Expr::NumberCmp(NumberField::DiscNumber, NumberOp::Eq, 6),
|
Expr::NumberCmp(NumberField::DiscNumber, NumberOp::Eq, 6),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
parser.parse(r#"discnumber != 6"#).unwrap(),
|
|
||||||
Expr::NumberCmp(NumberField::DiscNumber, NumberOp::NotEq, 6),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parser.parse(r#"discnumber > 6"#).unwrap(),
|
parser.parse(r#"discnumber > 6"#).unwrap(),
|
||||||
Expr::NumberCmp(NumberField::DiscNumber, NumberOp::Greater, 6),
|
Expr::NumberCmp(NumberField::DiscNumber, NumberOp::Greater, 6),
|
||||||
|
|
|
@ -3,7 +3,7 @@ use lasso2::{RodeoReader, Spur};
|
||||||
use nohash_hasher::{IntMap, IntSet};
|
use nohash_hasher::{IntMap, IntSet};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{BTreeMap, HashMap},
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
@ -104,7 +104,7 @@ impl Search {
|
||||||
Literal::Number(n) => {
|
Literal::Number(n) => {
|
||||||
let mut songs = IntSet::default();
|
let mut songs = IntSet::default();
|
||||||
for field in self.number_fields.values() {
|
for field in self.number_fields.values() {
|
||||||
songs.extend(field.find_equal(*n));
|
songs.extend(field.find(*n as i64, NumberOp::Eq));
|
||||||
}
|
}
|
||||||
songs
|
songs
|
||||||
.union(&self.eval_fuzzy(strings, &Literal::Text(n.to_string())))
|
.union(&self.eval_fuzzy(strings, &Literal::Text(n.to_string())))
|
||||||
|
@ -138,7 +138,10 @@ impl Search {
|
||||||
operator: NumberOp,
|
operator: NumberOp,
|
||||||
value: i32,
|
value: i32,
|
||||||
) -> IntSet<SongKey> {
|
) -> IntSet<SongKey> {
|
||||||
todo!()
|
let Some(field_index) = self.number_fields.get(&field) else {
|
||||||
|
return IntSet::default();
|
||||||
|
};
|
||||||
|
field_index.find(value as i64, operator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,14 +213,28 @@ impl TextFieldIndex {
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Serialize)]
|
#[derive(Default, Deserialize, Serialize)]
|
||||||
struct NumberFieldIndex {
|
struct NumberFieldIndex {
|
||||||
values: HashMap<i32, HashSet<SongKey>>,
|
values: BTreeMap<i64, IntSet<SongKey>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NumberFieldIndex {
|
impl NumberFieldIndex {
|
||||||
pub fn insert(&mut self, raw_value: &str, value: Spur, key: SongKey) {}
|
pub fn insert(&mut self, value: i64, key: SongKey) {
|
||||||
|
self.values.entry(value).or_default().insert(key);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find_equal(&self, value: i32) -> HashSet<SongKey> {
|
pub fn find(&self, value: i64, operator: NumberOp) -> IntSet<SongKey> {
|
||||||
todo!()
|
let range = match operator {
|
||||||
|
NumberOp::Eq => self.values.range(value..=value),
|
||||||
|
NumberOp::Greater => self.values.range((value + 1)..),
|
||||||
|
NumberOp::GreaterOrEq => self.values.range(value..),
|
||||||
|
NumberOp::Less => self.values.range(..value),
|
||||||
|
NumberOp::LessOrEq => self.values.range(..=value),
|
||||||
|
};
|
||||||
|
let candidates = range.map(|(_n, songs)| songs).collect::<Vec<_>>();
|
||||||
|
let mut results = Vec::with_capacity(candidates.iter().map(|c| c.len()).sum());
|
||||||
|
candidates
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|songs| results.extend(songs.iter()));
|
||||||
|
IntSet::from_iter(results)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,6 +286,13 @@ impl Builder {
|
||||||
.insert(str, *spur, song_key);
|
.insert(str, *spur, song_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(disc_number) = &scanner_song.disc_number {
|
||||||
|
self.number_fields
|
||||||
|
.entry(NumberField::DiscNumber)
|
||||||
|
.or_default()
|
||||||
|
.insert(*disc_number, song_key);
|
||||||
|
}
|
||||||
|
|
||||||
for (str, spur) in scanner_song.genres.iter().zip(storage_song.genres.iter()) {
|
for (str, spur) in scanner_song.genres.iter().zip(storage_song.genres.iter()) {
|
||||||
self.text_fields
|
self.text_fields
|
||||||
.entry(TextField::Genre)
|
.entry(TextField::Genre)
|
||||||
|
@ -306,6 +330,20 @@ impl Builder {
|
||||||
.or_default()
|
.or_default()
|
||||||
.insert(str, spur, song_key);
|
.insert(str, spur, song_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(track_number) = &scanner_song.track_number {
|
||||||
|
self.number_fields
|
||||||
|
.entry(NumberField::TrackNumber)
|
||||||
|
.or_default()
|
||||||
|
.insert(*track_number, song_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(year) = &scanner_song.year {
|
||||||
|
self.number_fields
|
||||||
|
.entry(NumberField::Year)
|
||||||
|
.or_default()
|
||||||
|
.insert(*year, song_key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(self) -> Search {
|
pub fn build(self) -> Search {
|
||||||
|
@ -443,6 +481,73 @@ mod test {
|
||||||
assert!(songs.contains(&PathBuf::from("seasons.mp3")));
|
assert!(songs.contains(&PathBuf::from("seasons.mp3")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_query_number_fields() {
|
||||||
|
let (search, strings, canon) = setup_test(vec![
|
||||||
|
scanner::Song {
|
||||||
|
virtual_path: PathBuf::from("1999.mp3"),
|
||||||
|
year: Some(1999),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
scanner::Song {
|
||||||
|
virtual_path: PathBuf::from("2000.mp3"),
|
||||||
|
year: Some(2000),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
scanner::Song {
|
||||||
|
virtual_path: PathBuf::from("2001.mp3"),
|
||||||
|
year: Some(2001),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
let songs = search.find_songs(&strings, &canon, "year=2000").unwrap();
|
||||||
|
assert_eq!(songs.len(), 1);
|
||||||
|
assert!(songs.contains(&PathBuf::from("2000.mp3")));
|
||||||
|
|
||||||
|
let songs = search.find_songs(&strings, &canon, "year>2000").unwrap();
|
||||||
|
assert_eq!(songs.len(), 1);
|
||||||
|
assert!(songs.contains(&PathBuf::from("2001.mp3")));
|
||||||
|
|
||||||
|
let songs = search.find_songs(&strings, &canon, "year<2000").unwrap();
|
||||||
|
assert_eq!(songs.len(), 1);
|
||||||
|
assert!(songs.contains(&PathBuf::from("1999.mp3")));
|
||||||
|
|
||||||
|
let songs = search.find_songs(&strings, &canon, "year>=2000").unwrap();
|
||||||
|
assert_eq!(songs.len(), 2);
|
||||||
|
assert!(songs.contains(&PathBuf::from("2000.mp3")));
|
||||||
|
assert!(songs.contains(&PathBuf::from("2001.mp3")));
|
||||||
|
|
||||||
|
let songs = search.find_songs(&strings, &canon, "year<=2000").unwrap();
|
||||||
|
assert_eq!(songs.len(), 2);
|
||||||
|
assert!(songs.contains(&PathBuf::from("1999.mp3")));
|
||||||
|
assert!(songs.contains(&PathBuf::from("2000.mp3")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fuzzy_numbers_query_all_fields() {
|
||||||
|
let (search, strings, canon) = setup_test(vec![
|
||||||
|
scanner::Song {
|
||||||
|
virtual_path: PathBuf::from("music.mp3"),
|
||||||
|
year: Some(2000),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
scanner::Song {
|
||||||
|
virtual_path: PathBuf::from("fireworks 2000.mp3"),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
scanner::Song {
|
||||||
|
virtual_path: PathBuf::from("calcium.mp3"),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
let songs = search.find_songs(&strings, &canon, "2000").unwrap();
|
||||||
|
assert_eq!(songs.len(), 2);
|
||||||
|
assert!(songs.contains(&PathBuf::from("music.mp3")));
|
||||||
|
assert!(songs.contains(&PathBuf::from("fireworks 2000.mp3")));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_use_and_operator() {
|
fn can_use_and_operator() {
|
||||||
let (search, strings, canon) = setup_test(vec![
|
let (search, strings, canon) = setup_test(vec![
|
||||||
|
|
Loading…
Add table
Reference in a new issue