Skip to content

Commit c56e87b

Browse files
author
Robert Masen
committed
loosen search
1 parent 4eedb3a commit c56e87b

File tree

2 files changed

+203
-1
lines changed

2 files changed

+203
-1
lines changed

src/controllers/krate/search.rs

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ pub fn search(req: &mut dyn Request) -> CargoResult<Response> {
3737
let conn = req.db_conn()?;
3838
let (offset, limit) = req.pagination(10, 100)?;
3939
let params = req.query();
40+
//extract the search param for loose searching
41+
let search_q = if let Some(q) = params.get("q") {
42+
format!("%{}%", q)
43+
} else {
44+
String::new()
45+
};
4046
let sort = params
4147
.get("sort")
4248
.map(|s| &**s)
@@ -56,7 +62,7 @@ pub fn search(req: &mut dyn Request) -> CargoResult<Response> {
5662
let q = plainto_tsquery(q_string);
5763
query = query.filter(
5864
q.matches(crates::textsearchable_index_col)
59-
.or(Crate::with_name(q_string)),
65+
.or(Crate::like_name(&search_q)),
6066
);
6167

6268
query = query.select((
@@ -206,3 +212,182 @@ pub fn search(req: &mut dyn Request) -> CargoResult<Response> {
206212
meta: Meta { total },
207213
}))
208214
}
215+
pub fn loose_search(req: &mut dyn Request) -> CargoResult<Response> {
216+
use diesel::sql_types::Bool;
217+
218+
let conn = req.db_conn()?;
219+
let (offset, limit) = req.pagination(10, 100)?;
220+
let params = req.query();
221+
let sort = params
222+
.get("sort")
223+
.map(|s| &**s)
224+
.unwrap_or("recent-downloads");
225+
226+
let loose_q = if let Some(q_string) = params.get("q") {
227+
format!("%{}%", q_string)
228+
} else {
229+
String::new()
230+
};
231+
let mut query = crates::table
232+
.left_join(recent_crate_downloads::table)
233+
.select((
234+
ALL_COLUMNS,
235+
false.into_sql::<Bool>(),
236+
recent_crate_downloads::downloads.nullable(),
237+
)).into_boxed();
238+
if let Some(q_string) = params.get("q") {
239+
if !q_string.is_empty() {
240+
let sort = params.get("sort").map(|s| &**s).unwrap_or("relevance");
241+
let q = plainto_tsquery(q_string);
242+
query = query.filter(
243+
q.matches(crates::textsearchable_index_col)
244+
.or(Crate::like_name(&loose_q)),
245+
);
246+
247+
query = query.select((
248+
ALL_COLUMNS,
249+
Crate::with_name(q_string),
250+
recent_crate_downloads::downloads.nullable(),
251+
));
252+
query = query.order(Crate::with_name(q_string).desc());
253+
254+
if sort == "relevance" {
255+
let rank = ts_rank_cd(crates::textsearchable_index_col, q);
256+
query = query.then_order_by(rank.desc())
257+
}
258+
}
259+
}
260+
261+
if let Some(cat) = params.get("category") {
262+
query = query.filter(
263+
crates::id.eq_any(
264+
crates_categories::table
265+
.select(crates_categories::crate_id)
266+
.inner_join(categories::table)
267+
.filter(
268+
categories::slug
269+
.eq(cat)
270+
.or(categories::slug.like(format!("{}::%", cat))),
271+
),
272+
),
273+
);
274+
}
275+
276+
if let Some(kw) = params.get("keyword") {
277+
query = query.filter(
278+
crates::id.eq_any(
279+
crates_keywords::table
280+
.select(crates_keywords::crate_id)
281+
.inner_join(keywords::table)
282+
.filter(::lower(keywords::keyword).eq(::lower(kw))),
283+
),
284+
);
285+
} else if let Some(letter) = params.get("letter") {
286+
let pattern = format!(
287+
"{}%",
288+
letter
289+
.chars()
290+
.next()
291+
.unwrap()
292+
.to_lowercase()
293+
.collect::<String>()
294+
);
295+
query = query.filter(canon_crate_name(crates::name).like(pattern));
296+
} else if let Some(user_id) = params.get("user_id").and_then(|s| s.parse::<i32>().ok()) {
297+
query = query.filter(
298+
crates::id.eq_any(
299+
crate_owners::table
300+
.select(crate_owners::crate_id)
301+
.filter(crate_owners::owner_id.eq(user_id))
302+
.filter(crate_owners::deleted.eq(false))
303+
.filter(crate_owners::owner_kind.eq(OwnerKind::User as i32)),
304+
),
305+
);
306+
} else if let Some(team_id) = params.get("team_id").and_then(|s| s.parse::<i32>().ok()) {
307+
query = query.filter(
308+
crates::id.eq_any(
309+
crate_owners::table
310+
.select(crate_owners::crate_id)
311+
.filter(crate_owners::owner_id.eq(team_id))
312+
.filter(crate_owners::deleted.eq(false))
313+
.filter(crate_owners::owner_kind.eq(OwnerKind::Team as i32)),
314+
),
315+
);
316+
} else if params.get("following").is_some() {
317+
query = query.filter(
318+
crates::id.eq_any(
319+
follows::table
320+
.select(follows::crate_id)
321+
.filter(follows::user_id.eq(req.user()?.id)),
322+
),
323+
);
324+
}
325+
326+
if sort == "downloads" {
327+
query = query.then_order_by(crates::downloads.desc())
328+
} else if sort == "recent-downloads" {
329+
query = query.then_order_by(recent_crate_downloads::downloads.desc().nulls_last())
330+
} else if sort == "recent-updates" {
331+
query = query.order(crates::updated_at.desc());
332+
} else {
333+
query = query.then_order_by(crates::name.asc())
334+
}
335+
println!("{:?}", diesel::debug_query(&query));
336+
// The database query returns a tuple within a tuple, with the root
337+
// tuple containing 3 items.
338+
let data = query
339+
.paginate(limit, offset)
340+
.load::<((Crate, bool, Option<i64>), i64)>(&*conn)?;
341+
let total = data.first().map(|&(_, t)| t).unwrap_or(0);
342+
let perfect_matches = data.iter().map(|&((_, b, _), _)| b).collect::<Vec<_>>();
343+
let recent_downloads = data
344+
.iter()
345+
.map(|&((_, _, s), _)| s.unwrap_or(0))
346+
.collect::<Vec<_>>();
347+
let crates = data.into_iter().map(|((c, _, _), _)| c).collect::<Vec<_>>();
348+
349+
let versions = crates
350+
.versions()
351+
.load::<Version>(&*conn)?
352+
.grouped_by(&crates)
353+
.into_iter()
354+
.map(|versions| Version::max(versions.into_iter().map(|v| v.num)));
355+
356+
let badges = CrateBadge::belonging_to(&crates)
357+
.select((badges::crate_id, badges::all_columns))
358+
.load::<CrateBadge>(&conn)?
359+
.grouped_by(&crates)
360+
.into_iter()
361+
.map(|badges| badges.into_iter().map(|cb| cb.badge).collect());
362+
363+
let crates = versions
364+
.zip(crates)
365+
.zip(perfect_matches)
366+
.zip(recent_downloads)
367+
.zip(badges)
368+
.map(
369+
|((((max_version, krate), perfect_match), recent_downloads), badges)| {
370+
krate.minimal_encodable(
371+
&max_version,
372+
Some(badges),
373+
perfect_match,
374+
Some(recent_downloads),
375+
)
376+
},
377+
).collect();
378+
379+
#[derive(Serialize)]
380+
struct R {
381+
crates: Vec<EncodableCrate>,
382+
meta: Meta,
383+
}
384+
#[derive(Serialize)]
385+
struct Meta {
386+
total: i64,
387+
}
388+
389+
Ok(req.json(&R {
390+
crates,
391+
meta: Meta { total },
392+
}))
393+
}

src/models/krate.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ pub const MAX_NAME_LENGTH: usize = 64;
9090
type CanonCrateName<T> = self::canon_crate_name::HelperType<T>;
9191
type All = diesel::dsl::Select<crates::table, AllColumns>;
9292
type WithName<'a> = diesel::dsl::Eq<CanonCrateName<crates::name>, CanonCrateName<&'a str>>;
93+
/// The result of a loose search
94+
type LikeName<'a> = diesel::dsl::Like<CanonCrateName<crates::name>, CanonCrateName<&'a str>>;
9395
type ByName<'a> = diesel::dsl::Filter<All, WithName<'a>>;
9496
type ByExactName<'a> = diesel::dsl::Filter<All, diesel::dsl::Eq<crates::name, &'a str>>;
9597

@@ -237,6 +239,21 @@ impl<'a> NewCrate<'a> {
237239
}
238240

239241
impl Crate {
242+
/// SQL filter with the `like` binary operator
243+
/// ```sql
244+
/// SELECT *
245+
/// FROM crates
246+
/// WHERE name like $1
247+
/// ```
248+
pub fn like_name(name: &str) -> LikeName<'_> {
249+
canon_crate_name(crates::name).like(canon_crate_name(name))
250+
}
251+
/// SQL filter with the = binary operator
252+
/// ```sql
253+
/// SELECT *
254+
/// FROM crates
255+
/// WHERE name = $1
256+
/// ```
240257
pub fn with_name(name: &str) -> WithName<'_> {
241258
canon_crate_name(crates::name).eq(canon_crate_name(name))
242259
}

0 commit comments

Comments
 (0)