@@ -18,7 +18,7 @@ const RELEASES_IN_RELEASES: i64 = 30;
18
18
/// Releases in recent releases feed
19
19
const RELEASES_IN_FEED : i64 = 150 ;
20
20
21
- #[ derive( Debug , Clone ) ]
21
+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
22
22
pub struct Release {
23
23
name : String ,
24
24
version : String ,
@@ -64,13 +64,20 @@ impl ToJson for Release {
64
64
}
65
65
}
66
66
67
+ #[ derive( Debug , Copy , Clone , PartialEq , Eq ) ]
67
68
enum Order {
68
69
ReleaseTime , // this is default order
69
70
GithubStars ,
70
71
RecentFailures ,
71
72
FailuresByGithubStars ,
72
73
}
73
74
75
+ impl Default for Order {
76
+ fn default ( ) -> Self {
77
+ Self :: ReleaseTime
78
+ }
79
+ }
80
+
74
81
fn get_releases ( conn : & Connection , page : i64 , limit : i64 , order : Order ) -> Vec < Release > {
75
82
let offset = ( page - 1 ) * limit;
76
83
@@ -259,57 +266,85 @@ fn get_releases_by_owner(
259
266
///
260
267
fn get_search_results (
261
268
conn : & Connection ,
262
- mut query : & str ,
269
+ query : & str ,
263
270
page : i64 ,
264
271
limit : i64 ,
265
- ) -> Option < ( i64 , Vec < Release > ) > {
266
- query = query. trim ( ) ;
267
- let split_query = query. replace ( ' ' , " & " ) ;
272
+ ) -> ( i64 , Vec < Release > ) {
273
+ let query = query. trim ( ) . to_lowercase ( ) ;
268
274
let offset = ( page - 1 ) * limit;
269
275
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,
276
+ let statement = "SELECT
277
+ crates.name,
278
+ latest_release.version AS version,
279
+ latest_release.description AS description,
280
+ latest_release.target_name AS target_name,
281
+ latest_release.release_time AS release_time,
282
+ latest_release.rustdoc_status AS rustdoc_status,
280
283
crates.github_stars,
281
284
SUM(releases.downloads) AS downloads,
285
+ -- Get the total number of results, disregarding the limit
286
+ COUNT(*) OVER() as total,
282
287
283
288
-- The levenshtein distance between the search query and the crate's name
284
- levenshtein_less_equal($1, crates.name, 3) as distance,
289
+ levenshtein_less_equal(CAST($1 AS TEXT), CAST( crates.name AS TEXT) , 3) as distance,
285
290
-- The similarity of the tokens of the search vs the tokens of `crates.content`.
286
291
-- 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
292
+ ts_rank_cd(crates.content, plainto_tsquery($1), 32) as content_rank
293
+ FROM
294
+ crates
295
+ INNER JOIN releases ON releases.crate_id = crates.id
296
+ INNER JOIN (
297
+ SELECT DISTINCT ON (crate_id)
298
+ crate_id,
299
+ version,
300
+ description,
301
+ target_name,
302
+ release_time,
303
+ rustdoc_status,
304
+ yanked
305
+ FROM
306
+ releases
307
+ ORDER BY
308
+ crate_id,
309
+ release_time DESC
310
+ ) AS latest_release ON latest_release.crate_id = crates.id
311
+
312
+ -- Filter crates that haven't been built and crates that have been yanked and
313
+ -- crates that don't match the query closely enough
314
+ WHERE
315
+ latest_release.rustdoc_status
316
+ AND NOT latest_release.yanked
293
317
AND (
294
318
-- Crates names that match the query sandwiched between wildcards will pass
295
319
crates.name ILIKE CONCAT('%', $1, '%')
296
320
-- Crate names with which the levenshtein distance is closer or equal to 3 will pass
297
321
OR levenshtein_less_equal($1, crates.name, 3) <= 3
298
322
-- Crates where their content matches the query will pass
299
- OR to_tsquery($2 ) @@ crates.content
323
+ OR plainto_tsquery($1 ) @@ crates.content
300
324
)
301
- GROUP BY crates.id
325
+
326
+ GROUP BY crates.id, releases.id
327
+
302
328
-- Ordering is prioritized by how closely the query matches the name, how closely the
303
329
-- query matches the description finally how many downloads the crate has
304
- ORDER BY distance DESC,
330
+ ORDER BY
331
+ distance ASC,
305
332
content_rank DESC,
306
- SUM(downloads) DESC
333
+ downloads_total DESC
334
+
307
335
-- Allows pagination
308
- LIMIT $3 OFFSET $4" ,
309
- & [ & query, & split_query, & limit, & offset] ,
310
- )
311
- . ok ( ) ?;
336
+ LIMIT $2 OFFSET $3" ;
312
337
338
+ let rows = if let Ok ( rows) = conn
339
+ . query ( statement, & [ & query, & limit, & offset] )
340
+ . map_err ( |err| dbg ! ( err) )
341
+ {
342
+ rows
343
+ } else {
344
+ return ( 0 , Vec :: new ( ) ) ;
345
+ } ;
346
+
347
+ let total_results: i64 = rows. iter ( ) . next ( ) . map ( |row| row. get ( 8 ) ) . unwrap_or_default ( ) ;
313
348
let packages: Vec < Release > = rows
314
349
. into_iter ( )
315
350
. map ( |row| Release {
@@ -323,22 +358,7 @@ fn get_search_results(
323
358
} )
324
359
. collect ( ) ;
325
360
326
- if !packages. is_empty ( ) {
327
- // Get the total number of results that the query matches
328
- let rows = conn
329
- . 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 ) ] ,
335
- )
336
- . unwrap ( ) ;
337
-
338
- Some ( ( rows. get ( 0 ) . get ( 0 ) , packages) )
339
- } else {
340
- None
341
- }
361
+ ( total_results, packages)
342
362
}
343
363
344
364
pub fn home_page ( req : & mut Request ) -> IronResult < Response > {
@@ -607,20 +627,18 @@ pub fn search_handler(req: &mut Request) -> IronResult<Response> {
607
627
}
608
628
}
609
629
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" )
630
+ let ( _, results) = get_search_results ( & conn, & query, 1 , RELEASES_IN_RELEASES ) ;
631
+ let title = if results. is_empty ( ) {
632
+ format ! ( "No results found for '{}'" , query)
616
633
} 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
- }
634
+ format ! ( "Search results for '{}'" , query)
635
+ } ;
636
+
637
+ // FIXME: There is no pagination
638
+ Page :: new ( results)
639
+ . set ( "search_query" , & query)
640
+ . title ( & title)
641
+ . to_resp ( "releases" )
624
642
} else {
625
643
Err ( IronError :: new ( Nope :: NoResults , status:: NotFound ) )
626
644
}
@@ -674,3 +692,96 @@ pub fn build_queue_handler(req: &mut Request) -> IronResult<Response> {
674
692
. set_true ( "releases_queue_tab" )
675
693
. to_resp ( "releases_queue" )
676
694
}
695
+
696
+ #[ cfg( test) ]
697
+ mod tests {
698
+ use super :: * ;
699
+ use crate :: test:: { wrapper, TestDatabase } ;
700
+
701
+ #[ test]
702
+ fn database_search ( ) {
703
+ wrapper ( |env| {
704
+ let db = env. db ( ) ;
705
+
706
+ db. fake_release ( ) . name ( "foo" ) . version ( "0.0.0" ) . create ( ) ?;
707
+ db. fake_release ( )
708
+ . name ( "bar-foo" )
709
+ . version ( "0.0.0" )
710
+ . create ( ) ?;
711
+ db. fake_release ( )
712
+ . name ( "foo-bar" )
713
+ . version ( "0.0.1" )
714
+ . create ( ) ?;
715
+ db. fake_release ( ) . name ( "fo0" ) . version ( "0.0.0" ) . create ( ) ?;
716
+ db. fake_release ( )
717
+ . name ( "fool" )
718
+ . version ( "0.0.0" )
719
+ . build_result_successful ( false )
720
+ . create ( ) ?;
721
+ db. fake_release ( )
722
+ . name ( "freakin" )
723
+ . version ( "0.0.0" )
724
+ . create ( ) ?;
725
+ db. fake_release ( )
726
+ . name ( "something unreleated" )
727
+ . version ( "0.0.0" )
728
+ . create ( ) ?;
729
+
730
+ let ( num_results, results) = get_search_results ( & db. conn ( ) , "foo" , 1 , 100 ) ;
731
+ let mut results = results. into_iter ( ) ;
732
+
733
+ assert_eq ! ( num_results, 4 ) ;
734
+
735
+ let expected = [ "foo" , "fo0" , "bar-foo" , "foo-bar" ] ;
736
+ for expected in expected. iter ( ) {
737
+ assert_eq ! ( expected, & results. next( ) . unwrap( ) . name) ;
738
+ }
739
+ assert ! ( results. collect:: <Vec <_>>( ) . is_empty( ) ) ;
740
+
741
+ Ok ( ( ) )
742
+ } )
743
+ }
744
+
745
+ fn non_exact ( db : & TestDatabase ) -> Result < ( ) , crate :: error:: Error > {
746
+ db. fake_release ( ) . name ( "reg3x" ) . version ( "0.0.0" ) . create ( ) ?;
747
+ db. fake_release ( ) . name ( "regex-" ) . version ( "0.0.0" ) . create ( ) ?;
748
+ db. fake_release ( )
749
+ . name ( "regex-syntax" )
750
+ . version ( "0.0.0" )
751
+ . create ( ) ?;
752
+
753
+ Ok ( ( ) )
754
+ }
755
+
756
+ fn rest_non_exact ( mut rest : Vec < Release > ) {
757
+ for name in [ "reg3x" , "regex-" , "regex-syntax" ] . iter ( ) {
758
+ assert_eq ! ( rest. remove( 0 ) . name, * name) ;
759
+ }
760
+
761
+ assert ! ( rest. is_empty( ) ) ;
762
+ }
763
+
764
+ #[ test]
765
+ fn exacts_dont_care ( ) {
766
+ let near_matches = [ "Regex" , "rEgex" , "reGex" , "regEx" , "regeX" ] ;
767
+
768
+ for name in near_matches. iter ( ) {
769
+ wrapper ( |env| {
770
+ let db = env. db ( ) ;
771
+ db. fake_release ( ) . name ( name) . version ( "0.0.0" ) . create ( ) ?;
772
+
773
+ non_exact ( & db) ?;
774
+
775
+ let ( num_results, results) = get_search_results ( & db. conn ( ) , "foo" , 1 , 100 ) ;
776
+ let mut results = results. into_iter ( ) ;
777
+
778
+ assert_eq ! ( num_results, 4 ) ;
779
+
780
+ assert_eq ! ( & results. next( ) . unwrap( ) . name, "regex" ) ;
781
+ rest_non_exact ( results. collect ( ) ) ;
782
+
783
+ Ok ( ( ) )
784
+ } )
785
+ }
786
+ }
787
+ }
0 commit comments