implement glue code; Server is now startable
Server can now be used. All features except preview and gc should work.
This commit is contained in:
165
server/texture-sync-server/src/search/mod.rs
Normal file
165
server/texture-sync-server/src/search/mod.rs
Normal file
@ -0,0 +1,165 @@
|
||||
use crate::model::*;
|
||||
|
||||
mod query_filter;
|
||||
pub use self::query_filter::QueryFilterSyntaxError;
|
||||
use self::query_filter::*;
|
||||
|
||||
pub struct Query {
|
||||
filters: Vec<QueryFilter>,
|
||||
}
|
||||
|
||||
impl Query {
|
||||
pub fn search(&self, input: &mut Iterator<Item = &Texture>) -> Vec<Texture> {
|
||||
let mut results: Vec<(i64, &Texture)> = Vec::new();
|
||||
|
||||
// We use pseudo decimal fixed point numbers here.
|
||||
// 1.000_000 = 1_000_000;
|
||||
|
||||
// This is done, since algorithms like quicksort can fail on floats.
|
||||
let required_score = self.required_score() * 1_000_000i64;
|
||||
|
||||
'texture_loop: for texture in input {
|
||||
let mut score = 0i64;
|
||||
|
||||
for (pos, filter) in self.filters.iter().enumerate() {
|
||||
match filter.score(texture) {
|
||||
Score::RequiredMatch(true) => (),
|
||||
Score::RequiredMatch(false) => {
|
||||
// skip this texture
|
||||
continue 'texture_loop;
|
||||
}
|
||||
Score::Match(true) => {
|
||||
score += 1_000_000i64 + ((100_000.0 / f64::sqrt(pos as f64 + 1.0)) as i64);
|
||||
}
|
||||
Score::Match(false) => (),
|
||||
}
|
||||
}
|
||||
|
||||
if score >= required_score {
|
||||
results.push((score, texture))
|
||||
}
|
||||
}
|
||||
|
||||
results.sort_by_key(|(score, _)| score * -1);
|
||||
|
||||
results
|
||||
.iter()
|
||||
.map(|(_, texture)| Texture::clone(texture))
|
||||
.collect::<Vec<Texture>>()
|
||||
}
|
||||
|
||||
fn required_score(&self) -> i64 {
|
||||
let non_special = self.filters.iter().filter(|f| !f.is_special()).count() as i64;
|
||||
|
||||
// ceil(non_special / 2)
|
||||
(non_special + 1) / 2
|
||||
}
|
||||
|
||||
pub fn parse(input: &[String]) -> Result<Query, QueryFilterSyntaxError> {
|
||||
let mut result = Query { filters: vec![] };
|
||||
|
||||
for fltr in input {
|
||||
let qryfltr = fltr.parse::<QueryFilter>()?;
|
||||
result.filters.push(qryfltr);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// These tests check against the example section in the search design document.
|
||||
mod test {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// just a shorthand
|
||||
fn tex(id: i32, name: &str, tags: &str, added_on: &str, resolution: u64) -> Texture {
|
||||
Texture {
|
||||
id: format!("{}", id), // Id should actaly be a uuid, but for testing this is fine.
|
||||
name: name.to_string(),
|
||||
tags: tags.split(",").map(|s| s.trim().to_string()).collect(),
|
||||
added_on: Date::from_str(added_on).unwrap(),
|
||||
resolution: (resolution, resolution),
|
||||
format: TextureFormat::JPEG,
|
||||
texture_hash: Sha256::from_data(b"Some Hash"),
|
||||
}
|
||||
}
|
||||
|
||||
// data of example section of search document.
|
||||
fn test_data() -> Vec<Texture> {
|
||||
vec![
|
||||
tex(
|
||||
1,
|
||||
"wood_185841",
|
||||
"Holz, Dunkel, Rot, Edel",
|
||||
"2019-05-15",
|
||||
4 * 1024,
|
||||
),
|
||||
tex(2, "wood_84846", "Holz, Hell", "2019-05-13", 2 * 1024),
|
||||
tex(3, "silk_large", "Stoff, Rot, Edel", "2018-01-01", 8 * 1024),
|
||||
tex(4, "cotton_xxx", "Stoff, Rot, Rau", "2018-02-01", 2048),
|
||||
tex(5, "green_frabric", "Grün, Stoff", "2018-03-01", 1024),
|
||||
tex(6, "tin54_45", "Metall, Hell", "2018-03-01", 4 * 1024),
|
||||
tex(7, "copper4_1k", "Rot, Metall", "2016-03-01", 1024),
|
||||
tex(
|
||||
8,
|
||||
"rusty_metall",
|
||||
"Rot, Metall, Rost, Dunkel",
|
||||
"2015-03-01",
|
||||
8 * 1024,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn assert_query(query: &str, expected: Vec<u8>) {
|
||||
let q = Query::parse(
|
||||
&query
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let data = test_data();
|
||||
|
||||
let result = q.search(&mut data.iter());
|
||||
|
||||
let only_ids = result
|
||||
.into_iter()
|
||||
.map(|tex| tex.id.parse::<u8>().unwrap())
|
||||
.collect::<Vec<u8>>();
|
||||
|
||||
assert_eq!(only_ids, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_examples_from_doc_1() {
|
||||
assert_query("Holz Dunkel", vec![1, 2, 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_examples_from_doc_2() {
|
||||
assert_query("n:wood_", vec![1, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_examples_from_doc_3() {
|
||||
assert_query("before:2019-05-31 after:2019-04-30", vec![1, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_examples_from_doc_4() {
|
||||
assert_query("Stoff Rot Edel", vec![3, 4, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_examples_from_doc_5() {
|
||||
assert_query("Stoff Rot !Edel", vec![4, 3, 5, 7, 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_examples_from_doc_6() {
|
||||
assert_query("Metall Dunkel res:4k", vec![8, 6, 1]);
|
||||
}
|
||||
}
|
207
server/texture-sync-server/src/search/query_filter.rs
Normal file
207
server/texture-sync-server/src/search/query_filter.rs
Normal file
@ -0,0 +1,207 @@
|
||||
use crate::model::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum QueryFilter {
|
||||
Not(Box<QueryFilter>),
|
||||
Tag(String),
|
||||
SpecialInName(String),
|
||||
SpecialBeforeDate(Date),
|
||||
SpecialAfterDate(Date),
|
||||
SpecialMinResolution(u64),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Score {
|
||||
RequiredMatch(bool),
|
||||
Match(bool),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum QueryFilterSyntaxError {
|
||||
UnknownSpecialFilter,
|
||||
ResolutionArgumentInvalid,
|
||||
DateArgumentInvalid,
|
||||
NameArgumentInvalid,
|
||||
ArgumentInvalid,
|
||||
}
|
||||
|
||||
use std::ops::Not;
|
||||
impl Not for Score {
|
||||
type Output = Score;
|
||||
|
||||
fn not(self) -> Score {
|
||||
match self {
|
||||
Score::RequiredMatch(b) => Score::RequiredMatch(!b),
|
||||
Score::Match(b) => Score::Match(!b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryFilter {
|
||||
pub fn is_special(&self) -> bool {
|
||||
use QueryFilter::*;
|
||||
match self {
|
||||
Not(inner) => inner.is_special(),
|
||||
Tag(_) => false,
|
||||
SpecialInName(_) => true,
|
||||
SpecialBeforeDate(_) => true,
|
||||
SpecialAfterDate(_) => true,
|
||||
SpecialMinResolution(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn score(&self, texture: &Texture) -> Score {
|
||||
use QueryFilter::*;
|
||||
match self {
|
||||
Not(inner) => inner.score(texture).not(),
|
||||
|
||||
Tag(tag) => Score::Match(
|
||||
texture
|
||||
.tags
|
||||
.iter()
|
||||
.find(|tt| tt.to_lowercase() == tag.to_lowercase())
|
||||
.is_some(),
|
||||
),
|
||||
|
||||
SpecialInName(name) => Score::RequiredMatch(
|
||||
//
|
||||
texture.name.contains(name),
|
||||
),
|
||||
|
||||
SpecialBeforeDate(date) => Score::RequiredMatch(texture.added_on <= *date),
|
||||
|
||||
SpecialAfterDate(date) => Score::RequiredMatch(texture.added_on > *date),
|
||||
|
||||
SpecialMinResolution(required) => {
|
||||
let smaller_resolution = u64::min(texture.resolution.0, texture.resolution.1);
|
||||
|
||||
Score::RequiredMatch(smaller_resolution >= *required)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for QueryFilter {
|
||||
type Err = QueryFilterSyntaxError;
|
||||
|
||||
fn from_str(input: &str) -> Result<QueryFilter, QueryFilterSyntaxError> {
|
||||
const NEGATION_CHAR: char = '!';
|
||||
const SPLIT_CHAR: char = ':';
|
||||
|
||||
if input.is_empty() {
|
||||
return Err(QueryFilterSyntaxError::ArgumentInvalid);
|
||||
} else if input.starts_with(NEGATION_CHAR) {
|
||||
// A Not.
|
||||
let inner = Self::from_str(&input[1..])?;
|
||||
return Ok(QueryFilter::Not(Box::new(inner)));
|
||||
} else if input.contains(SPLIT_CHAR) {
|
||||
// A Special Filter
|
||||
let mut parts = input.splitn(2, SPLIT_CHAR);
|
||||
let filter_name = parts.next().unwrap().to_lowercase();
|
||||
let filter_arg = parts.next().unwrap();
|
||||
|
||||
match filter_name.as_str() {
|
||||
"n" | "name" => {
|
||||
if filter_arg.is_empty() {
|
||||
return Err(QueryFilterSyntaxError::NameArgumentInvalid);
|
||||
}
|
||||
Ok(QueryFilter::SpecialInName(filter_arg.to_string()))
|
||||
}
|
||||
"r" | "res" | "resolution" => {
|
||||
let num: u64;
|
||||
|
||||
if filter_arg.ends_with("k") {
|
||||
num = *&filter_arg[0..(filter_arg.len() - 1)]
|
||||
.parse::<u32>()
|
||||
.map(|n| (n as u64) * 1024)
|
||||
.map_err(|_e| QueryFilterSyntaxError::ResolutionArgumentInvalid)?;
|
||||
} else {
|
||||
num = filter_arg
|
||||
.parse()
|
||||
.map_err(|_e| QueryFilterSyntaxError::ResolutionArgumentInvalid)?;
|
||||
}
|
||||
|
||||
Ok(QueryFilter::SpecialMinResolution(num))
|
||||
}
|
||||
"a" | "after" => {
|
||||
let date = Date::from_str(filter_arg)
|
||||
.map_err(|_| QueryFilterSyntaxError::DateArgumentInvalid)?;
|
||||
|
||||
Ok(QueryFilter::SpecialAfterDate(date))
|
||||
}
|
||||
"b" | "bef" | "before" => {
|
||||
let date = Date::from_str(filter_arg)
|
||||
.map_err(|_| QueryFilterSyntaxError::DateArgumentInvalid)?;
|
||||
|
||||
Ok(QueryFilter::SpecialBeforeDate(date))
|
||||
}
|
||||
|
||||
_ => Err(QueryFilterSyntaxError::UnknownSpecialFilter),
|
||||
}
|
||||
} else {
|
||||
// A Tag
|
||||
Ok(QueryFilter::Tag(input.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parsing() {
|
||||
assert!(QueryFilter::from_str("cats:meep").is_err());
|
||||
assert!(QueryFilter::from_str("name:").is_err());
|
||||
assert!(QueryFilter::from_str("res:-400k").is_err());
|
||||
assert!(QueryFilter::from_str("res:4647846846846864864846868446864846844684784k").is_err());
|
||||
assert!(QueryFilter::from_str("!!!!a:80-50-50").is_err());
|
||||
|
||||
assert_eq!(
|
||||
QueryFilter::from_str("n:hello"),
|
||||
Ok(QueryFilter::SpecialInName("hello".to_string()))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
QueryFilter::from_str("NaMe:hello"),
|
||||
Ok(QueryFilter::SpecialInName("hello".to_string()))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
QueryFilter::from_str("res:4k"),
|
||||
Ok(QueryFilter::SpecialMinResolution(4096))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
QueryFilter::from_str("res:4096"),
|
||||
Ok(QueryFilter::SpecialMinResolution(4096))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
QueryFilter::from_str("a:2019-10-10"),
|
||||
Ok(QueryFilter::SpecialAfterDate(Date::new(2019, 10, 10)))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
QueryFilter::from_str("!before:2019-10-10"),
|
||||
Ok(QueryFilter::Not(
|
||||
QueryFilter::SpecialBeforeDate(Date::new(2019, 10, 10)).into()
|
||||
))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
QueryFilter::from_str("!before:2019-10-10"),
|
||||
Ok(QueryFilter::Not(
|
||||
QueryFilter::SpecialBeforeDate(Date::new(2019, 10, 10)).into()
|
||||
))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
QueryFilter::from_str("!Wood"),
|
||||
Ok(QueryFilter::Not(
|
||||
QueryFilter::Tag("Wood".to_string()).into()
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user