Search & Filter tweaks
This commit is contained in:
@@ -258,7 +258,16 @@ impl Catalog {
|
|||||||
sql.push_str(" AND COALESCE(lc.quantity_owned, 0) = 0");
|
sql.push_str(" AND COALESCE(lc.quantity_owned, 0) = 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.push_str(" ORDER BY oc.edhrec_rank IS NULL, oc.edhrec_rank, oc.name COLLATE NOCASE LIMIT ?");
|
sql.push_str(" ORDER BY ");
|
||||||
|
// Same reasoning as `NameIndex::search`: a card whose name *is* the query
|
||||||
|
// outranks more popular cards that merely contain it, and must not be cut
|
||||||
|
// off by the LIMIT. Bound after the WHERE values and before the LIMIT, so
|
||||||
|
// the positional order still lines up.
|
||||||
|
if let Some(text) = filter.text.as_ref().map(|t| t.trim()).filter(|t| !t.is_empty()) {
|
||||||
|
sql.push_str("oc.name_lower <> ?, ");
|
||||||
|
vals.push(Value::Text(text.to_lowercase()));
|
||||||
|
}
|
||||||
|
sql.push_str("oc.edhrec_rank IS NULL, oc.edhrec_rank, oc.name COLLATE NOCASE LIMIT ?");
|
||||||
vals.push(Value::Integer(limit as i64));
|
vals.push(Value::Integer(limit as i64));
|
||||||
|
|
||||||
let mut stmt = self.conn.prepare(&sql)?;
|
let mut stmt = self.conn.prepare(&sql)?;
|
||||||
@@ -324,6 +333,33 @@ mod tests {
|
|||||||
assert!(all.contains(&"Serra Angel".to_string()));
|
assert!(all.contains(&"Serra Angel".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_name_search_puts_the_exactly_matching_card_first() {
|
||||||
|
let fx = owned_fixture();
|
||||||
|
// Every one of these contains "explore"; the exact match is deliberately
|
||||||
|
// given the *worst* popularity rank, since that ordering is what buried
|
||||||
|
// it behind the others.
|
||||||
|
for (i, name) in ["Explorer's Scope", "Wildwood Explorer", "Explore"].iter().enumerate() {
|
||||||
|
fx.catalog
|
||||||
|
.conn
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO ref.oracle_cards (oracle_id, name, name_lower, edhrec_rank)
|
||||||
|
VALUES (?1, ?2, ?3, ?4)",
|
||||||
|
rusqlite::params![format!("oid-{i}"), name, name.to_lowercase(), i as i64],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let f = LibraryFilter { text: Some("explore".into()), ..Default::default() };
|
||||||
|
assert_eq!(names(&fx.catalog.search_reference(&f, 10).unwrap())[0], "Explore");
|
||||||
|
// And it survives a limit tight enough to have dropped it entirely.
|
||||||
|
assert_eq!(names(&fx.catalog.search_reference(&f, 1).unwrap()), vec!["Explore"]);
|
||||||
|
|
||||||
|
// Without a text filter the popularity ordering is untouched.
|
||||||
|
let all = names(&fx.catalog.search_reference(&LibraryFilter::default(), 10).unwrap());
|
||||||
|
assert_eq!(all[0], "Explorer's Scope");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vanilla_only_keeps_flagged_creatures_and_is_off_by_default() {
|
fn vanilla_only_keeps_flagged_creatures_and_is_off_by_default() {
|
||||||
let fx = owned_fixture();
|
let fx = owned_fixture();
|
||||||
|
|||||||
@@ -92,9 +92,19 @@ impl NameIndex {
|
|||||||
}
|
}
|
||||||
let mut matcher = Matcher::new(Config::DEFAULT);
|
let mut matcher = Matcher::new(Config::DEFAULT);
|
||||||
let pattern = Pattern::parse(query, CaseMatching::Ignore, Normalization::Smart);
|
let pattern = Pattern::parse(query, CaseMatching::Ignore, Normalization::Smart);
|
||||||
let mut matches = pattern.match_list(self.entries.iter(), &mut matcher);
|
let matches = pattern.match_list(self.entries.iter(), &mut matcher);
|
||||||
matches.truncate(limit);
|
// Typing a card's full name means *that* card, whatever the fuzzy score
|
||||||
matches.into_iter().map(|(e, _score)| e.card.clone()).collect()
|
// says. "Explore" otherwise sits below "Explorer's Scope", "Explore the
|
||||||
|
// Vastlands" and a dozen other names that merely contain the word — and
|
||||||
|
// with a small `limit` it can fall off the end entirely, making the card
|
||||||
|
// unreachable. Promote before truncating, for that reason.
|
||||||
|
let needle = query.trim().to_lowercase();
|
||||||
|
let (mut ranked, rest): (Vec<_>, Vec<_>) = matches
|
||||||
|
.into_iter()
|
||||||
|
.partition(|(e, _)| e.card.name.to_lowercase() == needle);
|
||||||
|
ranked.extend(rest);
|
||||||
|
ranked.truncate(limit);
|
||||||
|
ranked.into_iter().map(|(e, _score)| e.card.clone()).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +124,56 @@ mod tests {
|
|||||||
assert_eq!(hits[0].name, "Llanowar Elves");
|
assert_eq!(hits[0].name, "Llanowar Elves");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Seed the reference db with names that all contain "Explore", the real
|
||||||
|
/// case that made the card itself unreachable.
|
||||||
|
fn explore_fixture() -> crate::test_support::Fixture {
|
||||||
|
let fx = crate::test_support::make();
|
||||||
|
for (i, name) in [
|
||||||
|
"Explorer's Scope",
|
||||||
|
"Explore",
|
||||||
|
"Explorer of the Vastlands",
|
||||||
|
"Wildwood Explorer",
|
||||||
|
"Explosive Vegetation",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
fx.catalog
|
||||||
|
.conn
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO ref.oracle_cards (oracle_id, name, name_lower, edhrec_rank)
|
||||||
|
VALUES (?1, ?2, ?3, ?4)",
|
||||||
|
rusqlite::params![format!("oid-{i}"), name, name.to_lowercase(), i as i64],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
fx
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_exact_name_match_outranks_longer_names_containing_it() {
|
||||||
|
let fx = explore_fixture();
|
||||||
|
let idx = fx.catalog.build_name_index().unwrap();
|
||||||
|
|
||||||
|
let hits = idx.search("Explore", 10);
|
||||||
|
assert_eq!(hits[0].name, "Explore");
|
||||||
|
// Case shouldn't matter — the entry field is whatever the user typed.
|
||||||
|
assert_eq!(idx.search("explore", 10)[0].name, "Explore");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_exact_match_survives_a_limit_that_would_cut_it_off() {
|
||||||
|
// The reason promotion happens *before* truncating: rapid entry asks for
|
||||||
|
// a handful of results, and a buried exact match would be dropped
|
||||||
|
// outright rather than merely ranked low.
|
||||||
|
let fx = explore_fixture();
|
||||||
|
let idx = fx.catalog.build_name_index().unwrap();
|
||||||
|
|
||||||
|
let hits = idx.search("Explore", 1);
|
||||||
|
assert_eq!(hits.len(), 1);
|
||||||
|
assert_eq!(hits[0].name, "Explore");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn prefix_and_limit() {
|
fn prefix_and_limit() {
|
||||||
let fx = test_support::make();
|
let fx = test_support::make();
|
||||||
|
|||||||
Reference in New Issue
Block a user