diff --git a/src/controllers/krate/search.rs b/src/controllers/krate/search.rs index 318e8239e2a..2d897481194 100644 --- a/src/controllers/krate/search.rs +++ b/src/controllers/krate/search.rs @@ -58,10 +58,11 @@ pub fn search(req: &mut dyn Request) -> CargoResult { has_filter = true; if !q_string.is_empty() { let sort = params.get("sort").map(|s| &**s).unwrap_or("relevance"); + let q = plainto_tsquery(q_string); query = query.filter( q.matches(crates::textsearchable_index_col) - .or(Crate::with_name(q_string)), + .or(Crate::like_name(&q_string)), ); query = query.select(( diff --git a/src/models/krate.rs b/src/models/krate.rs index 8a546c292b1..376dcdb5a7d 100644 --- a/src/models/krate.rs +++ b/src/models/krate.rs @@ -86,6 +86,8 @@ pub const MAX_NAME_LENGTH: usize = 64; type CanonCrateName = self::canon_crate_name::HelperType; type All = diesel::dsl::Select; type WithName<'a> = diesel::dsl::Eq, CanonCrateName<&'a str>>; +/// The result of a loose search +type LikeName = diesel::dsl::Like, CanonCrateName>; type ByName<'a> = diesel::dsl::Filter>; type ByExactName<'a> = diesel::dsl::Filter>; @@ -234,6 +236,13 @@ impl<'a> NewCrate<'a> { } impl Crate { + /// SQL filter with the `like` binary operator. Adds wildcards to the beginning and end to get + /// substring matches. + pub fn like_name(name: &str) -> LikeName { + let wildcard_name = format!("%{}%", name); + canon_crate_name(crates::name).like(canon_crate_name(wildcard_name)) + } + /// SQL filter with the = binary operator pub fn with_name(name: &str) -> WithName<'_> { canon_crate_name(crates::name).eq(canon_crate_name(name)) } diff --git a/src/tests/krate.rs b/src/tests/krate.rs index 4e2b803ce51..a14621fe554 100644 --- a/src/tests/krate.rs +++ b/src/tests/krate.rs @@ -414,6 +414,46 @@ fn exact_match_on_queries_with_sort() { assert_eq!(json.crates[3].name, "other_sort"); } +#[test] +fn loose_search_order() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + let ordered = app.db(|conn| { + // exact match should be first + let one = CrateBuilder::new("temp", user.id) + .readme("readme") + .description("description") + .keyword("kw1") + .expect_build(conn); + // temp_udp should match second because of _ + let two = CrateBuilder::new("temp_utp", user.id) + .readme("readme") + .description("description") + .keyword("kw1") + .expect_build(conn); + // evalrs should match 3rd because of readme + let three = CrateBuilder::new("evalrs", user.id) + .readme("evalrs_temp evalrs_temp evalrs_temp") + .description("description") + .keyword("kw1") + .expect_build(conn); + // tempfile should appear 4th + let four = CrateBuilder::new("tempfile", user.id) + .readme("readme") + .description("description") + .keyword("kw1") + .expect_build(conn); + vec![one, two, three, four] + }); + let search_temp = anon.search("q=temp"); + assert_eq!(search_temp.meta.total, 4); + assert_eq!(search_temp.crates.len(), 4); + for (lhs, rhs) in search_temp.crates.iter().zip(ordered) { + assert_eq!(lhs.name, rhs.name); + } +} + #[test] fn show() { let (app, anon, user) = TestApp::init().with_user();