@@ -244,57 +244,94 @@ fn get_releases_by_owner(
244
244
( author_name, packages)
245
245
}
246
246
247
+ /// Get the search results for a search query
248
+ ///
249
+ /// Retrieves crates which names have a levenshtein distance of less than or equal to 3,
250
+ /// crates who fit into or otherwise are made up of the query or crates who's descriptions
251
+ /// match the search query.
252
+ ///
253
+ /// * `query`: The query string, unfiltered
254
+ /// * `page`: The page of results to show (1-indexed)
255
+ /// * `limit`: The number of results to return
256
+ ///
257
+ /// Returns `None` if no results are found and `Some` with the total number of results and the
258
+ /// currently requested results
259
+ ///
247
260
fn get_search_results (
248
261
conn : & Connection ,
249
- query : & str ,
262
+ mut query : & str ,
250
263
page : i64 ,
251
264
limit : i64 ,
252
265
) -> Option < ( i64 , Vec < Release > ) > {
266
+ query = query. trim ( ) ;
267
+ let split_query = query. replace ( ' ' , " & " ) ;
253
268
let offset = ( page - 1 ) * limit;
254
- let mut packages = Vec :: new ( ) ;
255
269
256
- let rows = match conn. query (
257
- "SELECT crates.name,
258
- releases.version,
259
- releases.description,
260
- releases.target_name,
261
- releases.release_time,
262
- releases.rustdoc_status,
263
- ts_rank_cd(crates.content, to_tsquery($1)) AS rank
264
- FROM crates
265
- INNER JOIN releases ON crates.latest_version_id = releases.id
266
- WHERE crates.name LIKE concat('%', $1, '%')
267
- OR crates.content @@ to_tsquery($1)
268
- ORDER BY crates.name = $1 DESC,
269
- crates.name LIKE concat('%', $1, '%') DESC,
270
- rank DESC
271
- LIMIT $2 OFFSET $3" ,
272
- & [ & query, & limit, & offset] ,
273
- ) {
274
- Ok ( r) => r,
275
- Err ( _) => return None ,
276
- } ;
270
+ let rows = conn
271
+ . query (
272
+ "SELECT crates.name,
273
+ MAX(releases.version) AS version,
274
+ MAX(releases.description) AS description,
275
+ MAX(releases.target_name) AS target_name,
276
+ MAX(releases.release_time) AS release_time,
277
+ -- Cast the boolean into an integer and then cast it into a boolean.
278
+ -- Posgres moves in mysterious ways, don't question it
279
+ CAST(MAX(releases.rustdoc_status::integer) AS boolean) as rustdoc_status,
280
+ crates.github_stars,
281
+ SUM(releases.downloads) AS downloads,
282
+
283
+ -- The levenshtein distance between the search query and the crate's name
284
+ levenshtein_less_equal($1, crates.name, 3) as distance,
285
+ -- The similarity of the tokens of the search vs the tokens of `crates.content`.
286
+ -- The `32` normalizes the number by using `rank / (rank + 1)`
287
+ ts_rank_cd(crates.content, to_tsquery($2), 32) as content_rank
288
+ FROM releases INNER JOIN crates on releases.crate_id = crates.id
289
+
290
+ -- Filter crates that haven't been built and crates that have been yanked
291
+ WHERE releases.rustdoc_status = true
292
+ AND releases.yanked = false
293
+ AND (
294
+ -- Crates names that match the query sandwiched between wildcards will pass
295
+ crates.name ILIKE CONCAT('%', $1, '%')
296
+ -- Crate names with which the levenshtein distance is closer or equal to 3 will pass
297
+ OR levenshtein_less_equal($1, crates.name, 3) <= 3
298
+ -- Crates where their content matches the query will pass
299
+ OR to_tsquery($2) @@ crates.content
300
+ )
301
+ GROUP BY crates.id
302
+ -- Ordering is prioritized by how closely the query matches the name, how closely the
303
+ -- query matches the description finally how many downloads the crate has
304
+ ORDER BY distance DESC,
305
+ content_rank DESC,
306
+ SUM(downloads) DESC
307
+ -- Allows pagination
308
+ LIMIT $3 OFFSET $4" ,
309
+ & [ & query, & split_query, & limit, & offset] ,
310
+ )
311
+ . ok ( ) ?;
277
312
278
- for row in & rows {
279
- let package = Release {
313
+ let packages: Vec < Release > = rows
314
+ . into_iter ( )
315
+ . map ( |row| Release {
280
316
name : row. get ( 0 ) ,
281
317
version : row. get ( 1 ) ,
282
318
description : row. get ( 2 ) ,
283
319
target_name : row. get ( 3 ) ,
284
320
release_time : row. get ( 4 ) ,
285
321
rustdoc_status : row. get ( 5 ) ,
286
- ..Release :: default ( )
287
- } ;
288
-
289
- packages. push ( package) ;
290
- }
322
+ stars : row. get ( 6 ) ,
323
+ } )
324
+ . collect ( ) ;
291
325
292
326
if !packages. is_empty ( ) {
293
- // get count of total results
327
+ // Get the total number of results that the query matches
294
328
let rows = conn
295
329
. query (
296
- "SELECT COUNT(*) FROM crates WHERE content @@ to_tsquery($1)" ,
297
- & [ & query] ,
330
+ "SELECT COUNT(*) FROM crates
331
+ WHERE crates.name ILIKE CONCAT('%', CAST($1 AS TEXT), '%')
332
+ OR levenshtein_less_equal(CAST($1 AS TEXT), crates.name, 3) <= 3
333
+ OR crates.content @@ to_tsquery(CAST($2 AS TEXT))" ,
334
+ & [ & ( query as & str ) , & ( & split_query as & str ) ] ,
298
335
)
299
336
. unwrap ( ) ;
300
337
@@ -570,17 +607,20 @@ pub fn search_handler(req: &mut Request) -> IronResult<Response> {
570
607
}
571
608
}
572
609
573
- let search_query = query. replace ( " " , " & " ) ;
574
- #[ allow( clippy:: or_fun_call) ]
575
- get_search_results ( & conn, & search_query, 1 , RELEASES_IN_RELEASES )
576
- . ok_or_else ( || IronError :: new ( Nope :: NoResults , status:: NotFound ) )
577
- . and_then ( |( _, results) | {
578
- // FIXME: There is no pagination
579
- Page :: new ( results)
580
- . set ( "search_query" , & query)
581
- . title ( & format ! ( "Search results for '{}'" , query) )
582
- . to_resp ( "releases" )
583
- } )
610
+ if let Some ( ( _, results) ) = get_search_results ( & conn, & query, 1 , RELEASES_IN_RELEASES ) {
611
+ // FIXME: There is no pagination
612
+ Page :: new ( results)
613
+ . set ( "search_query" , & query)
614
+ . title ( & format ! ( "Search results for '{}'" , query) )
615
+ . to_resp ( "releases" )
616
+ } else {
617
+ // Return an empty page with an error message and an intact query so that
618
+ // the user can edit it
619
+ Page :: new ( "" . to_string ( ) )
620
+ . set ( "search_query" , & query)
621
+ . title ( & format ! ( "No results found for '{}'" , query) )
622
+ . to_resp ( "releases" )
623
+ }
584
624
} else {
585
625
Err ( IronError :: new ( Nope :: NoResults , status:: NotFound ) )
586
626
}
0 commit comments