@@ -469,8 +469,8 @@ impl Database {
469469 let limit = query. limit . unwrap_or ( 25 ) . clamp ( 1 , 100 ) ;
470470 let offset = query. offset . unwrap_or ( 0 ) . max ( 0 ) ;
471471 let q = query. q . clone ( ) . unwrap_or_default ( ) ;
472- let q_pattern = format ! ( "%{}%" , q) ;
473472
473+ // Use full-text search when a query term is provided, fall back to no filter
474474 let rows = sqlx:: query_as :: < _ , TradeSearchResult > (
475475 r#"
476476 WITH latest_trade_events AS (
@@ -484,10 +484,11 @@ impl Database {
484484 trade_base AS (
485485 SELECT
486486 (e.data->>'trade_id')::BIGINT AS trade_id,
487- e.data->>'seller' AS seller,
488- e.data->>'buyer' AS buyer,
489- (e.data->>'amount')::BIGINT AS amount,
490- e.timestamp AS created_at
487+ e.data->>'seller' AS seller,
488+ e.data->>'buyer' AS buyer,
489+ (e.data->>'amount')::BIGINT AS amount,
490+ e.timestamp AS created_at,
491+ e.search_vec
491492 FROM events e
492493 WHERE e.event_type = 'trade_created'
493494 )
@@ -501,18 +502,19 @@ impl Database {
501502 FROM trade_base tb
502503 JOIN latest_trade_events lte ON lte.trade_id = tb.trade_id
503504 WHERE
504- ($1 = '' OR tb.trade_id::TEXT ILIKE $2 OR tb.seller ILIKE $2 OR tb.buyer ILIKE $2)
505- AND ($3::TEXT IS NULL OR lte.event_type = $3)
506- AND ($4::TEXT IS NULL OR tb.seller = $4)
507- AND ($5::TEXT IS NULL OR tb.buyer = $5)
508- AND ($6::BIGINT IS NULL OR tb.amount >= $6)
509- AND ($7::BIGINT IS NULL OR tb.amount <= $7)
510- ORDER BY tb.created_at DESC
511- LIMIT $8 OFFSET $9
505+ ($1 = '' OR tb.search_vec @@ plainto_tsquery('english', $1))
506+ AND ($2::TEXT IS NULL OR lte.event_type = $2)
507+ AND ($3::TEXT IS NULL OR tb.seller = $3)
508+ AND ($4::TEXT IS NULL OR tb.buyer = $4)
509+ AND ($5::BIGINT IS NULL OR tb.amount >= $5)
510+ AND ($6::BIGINT IS NULL OR tb.amount <= $6)
511+ ORDER BY
512+ CASE WHEN $1 = '' THEN 0 ELSE ts_rank(tb.search_vec, plainto_tsquery('english', $1)) END DESC,
513+ tb.created_at DESC
514+ LIMIT $7 OFFSET $8
512515 "# ,
513516 )
514517 . bind ( q. as_str ( ) )
515- . bind ( q_pattern. as_str ( ) )
516518 . bind ( query. status . as_deref ( ) )
517519 . bind ( query. seller . as_deref ( ) )
518520 . bind ( query. buyer . as_deref ( ) )
@@ -532,20 +534,19 @@ impl Database {
532534 ) -> Result < Vec < DiscoveryResult > , AppError > {
533535 let limit = query. limit . unwrap_or ( 25 ) . clamp ( 1 , 100 ) ;
534536 let q = query. q . clone ( ) . unwrap_or_default ( ) ;
535- let q_pattern = format ! ( "%{}%" , q) ;
536537
537538 let rows = sqlx:: query_as :: < _ , DiscoveryResult > (
538539 r#"
539540 WITH entities AS (
540- SELECT data->>'seller' AS address, 'user' AS role, timestamp
541+ SELECT data->>'seller' AS address, 'user' AS role, timestamp, search_vec
541542 FROM events
542543 WHERE event_type = 'trade_created' AND data->>'seller' IS NOT NULL
543544 UNION ALL
544- SELECT data->>'buyer' AS address, 'user' AS role, timestamp
545+ SELECT data->>'buyer' AS address, 'user' AS role, timestamp, search_vec
545546 FROM events
546547 WHERE event_type = 'trade_created' AND data->>'buyer' IS NOT NULL
547548 UNION ALL
548- SELECT data->>'arbitrator' AS address, 'arbitrator' AS role, timestamp
549+ SELECT data->>'arbitrator' AS address, 'arbitrator' AS role, timestamp, search_vec
549550 FROM events
550551 WHERE event_type = 'arb_reg' AND data->>'arbitrator' IS NOT NULL
551552 )
@@ -556,15 +557,14 @@ impl Database {
556557 MAX(timestamp) AS last_seen
557558 FROM entities
558559 WHERE
559- ($1 = '' OR address ILIKE $2 )
560- AND ($3 ::TEXT IS NULL OR role = $3 )
560+ ($1 = '' OR search_vec @@ plainto_tsquery('english', $1) )
561+ AND ($2 ::TEXT IS NULL OR role = $2 )
561562 GROUP BY address, role
562563 ORDER BY seen_count DESC, last_seen DESC
563- LIMIT $4
564+ LIMIT $3
564565 "# ,
565566 )
566567 . bind ( q. as_str ( ) )
567- . bind ( q_pattern. as_str ( ) )
568568 . bind ( query. role . as_deref ( ) )
569569 . bind ( limit)
570570 . fetch_all ( & self . pool )
@@ -573,6 +573,34 @@ impl Database {
573573 Ok ( rows)
574574 }
575575
576+ /// Full-text search over registered user profiles.
577+ pub async fn search_users (
578+ & self ,
579+ q : & str ,
580+ limit : i64 ,
581+ offset : i64 ,
582+ ) -> Result < Vec < crate :: models:: UserProfile > , AppError > {
583+ let rows = sqlx:: query_as :: < _ , crate :: models:: UserProfile > (
584+ r#"
585+ SELECT *
586+ FROM user_profiles
587+ WHERE $1 = '' OR search_vec @@ plainto_tsquery('english', $1)
588+ ORDER BY
589+ CASE WHEN $1 = '' THEN 0
590+ ELSE ts_rank(search_vec, plainto_tsquery('english', $1))
591+ END DESC,
592+ registered_at DESC
593+ LIMIT $2 OFFSET $3
594+ "# ,
595+ )
596+ . bind ( q)
597+ . bind ( limit. clamp ( 1 , 100 ) )
598+ . bind ( offset. max ( 0 ) )
599+ . fetch_all ( & self . pool )
600+ . await ?;
601+ Ok ( rows)
602+ }
603+
576604 pub async fn get_search_suggestions (
577605 & self ,
578606 prefix : & str ,
0 commit comments