@@ -559,8 +559,8 @@ impl Database {
559559 let limit = query. limit . unwrap_or ( 25 ) . clamp ( 1 , 100 ) ;
560560 let offset = query. offset . unwrap_or ( 0 ) . max ( 0 ) ;
561561 let q = query. q . clone ( ) . unwrap_or_default ( ) ;
562- let q_pattern = format ! ( "%{}%" , q) ;
563562
563+ // Use full-text search when a query term is provided, fall back to no filter
564564 let rows = sqlx:: query_as :: < _ , TradeSearchResult > (
565565 r#"
566566 WITH latest_trade_events AS (
@@ -574,10 +574,11 @@ impl Database {
574574 trade_base AS (
575575 SELECT
576576 (e.data->>'trade_id')::BIGINT AS trade_id,
577- e.data->>'seller' AS seller,
578- e.data->>'buyer' AS buyer,
579- (e.data->>'amount')::BIGINT AS amount,
580- e.timestamp AS created_at
577+ e.data->>'seller' AS seller,
578+ e.data->>'buyer' AS buyer,
579+ (e.data->>'amount')::BIGINT AS amount,
580+ e.timestamp AS created_at,
581+ e.search_vec
581582 FROM events e
582583 WHERE e.event_type = 'trade_created'
583584 )
@@ -591,18 +592,19 @@ impl Database {
591592 FROM trade_base tb
592593 JOIN latest_trade_events lte ON lte.trade_id = tb.trade_id
593594 WHERE
594- ($1 = '' OR tb.trade_id::TEXT ILIKE $2 OR tb.seller ILIKE $2 OR tb.buyer ILIKE $2)
595- AND ($3::TEXT IS NULL OR lte.event_type = $3)
596- AND ($4::TEXT IS NULL OR tb.seller = $4)
597- AND ($5::TEXT IS NULL OR tb.buyer = $5)
598- AND ($6::BIGINT IS NULL OR tb.amount >= $6)
599- AND ($7::BIGINT IS NULL OR tb.amount <= $7)
600- ORDER BY tb.created_at DESC
601- LIMIT $8 OFFSET $9
595+ ($1 = '' OR tb.search_vec @@ plainto_tsquery('english', $1))
596+ AND ($2::TEXT IS NULL OR lte.event_type = $2)
597+ AND ($3::TEXT IS NULL OR tb.seller = $3)
598+ AND ($4::TEXT IS NULL OR tb.buyer = $4)
599+ AND ($5::BIGINT IS NULL OR tb.amount >= $5)
600+ AND ($6::BIGINT IS NULL OR tb.amount <= $6)
601+ ORDER BY
602+ CASE WHEN $1 = '' THEN 0 ELSE ts_rank(tb.search_vec, plainto_tsquery('english', $1)) END DESC,
603+ tb.created_at DESC
604+ LIMIT $7 OFFSET $8
602605 "# ,
603606 )
604607 . bind ( q. as_str ( ) )
605- . bind ( q_pattern. as_str ( ) )
606608 . bind ( query. status . as_deref ( ) )
607609 . bind ( query. seller . as_deref ( ) )
608610 . bind ( query. buyer . as_deref ( ) )
@@ -622,20 +624,19 @@ impl Database {
622624 ) -> Result < Vec < DiscoveryResult > , AppError > {
623625 let limit = query. limit . unwrap_or ( 25 ) . clamp ( 1 , 100 ) ;
624626 let q = query. q . clone ( ) . unwrap_or_default ( ) ;
625- let q_pattern = format ! ( "%{}%" , q) ;
626627
627628 let rows = sqlx:: query_as :: < _ , DiscoveryResult > (
628629 r#"
629630 WITH entities AS (
630- SELECT data->>'seller' AS address, 'user' AS role, timestamp
631+ SELECT data->>'seller' AS address, 'user' AS role, timestamp, search_vec
631632 FROM events
632633 WHERE event_type = 'trade_created' AND data->>'seller' IS NOT NULL
633634 UNION ALL
634- SELECT data->>'buyer' AS address, 'user' AS role, timestamp
635+ SELECT data->>'buyer' AS address, 'user' AS role, timestamp, search_vec
635636 FROM events
636637 WHERE event_type = 'trade_created' AND data->>'buyer' IS NOT NULL
637638 UNION ALL
638- SELECT data->>'arbitrator' AS address, 'arbitrator' AS role, timestamp
639+ SELECT data->>'arbitrator' AS address, 'arbitrator' AS role, timestamp, search_vec
639640 FROM events
640641 WHERE event_type = 'arb_reg' AND data->>'arbitrator' IS NOT NULL
641642 )
@@ -646,15 +647,14 @@ impl Database {
646647 MAX(timestamp) AS last_seen
647648 FROM entities
648649 WHERE
649- ($1 = '' OR address ILIKE $2 )
650- AND ($3 ::TEXT IS NULL OR role = $3 )
650+ ($1 = '' OR search_vec @@ plainto_tsquery('english', $1) )
651+ AND ($2 ::TEXT IS NULL OR role = $2 )
651652 GROUP BY address, role
652653 ORDER BY seen_count DESC, last_seen DESC
653- LIMIT $4
654+ LIMIT $3
654655 "# ,
655656 )
656657 . bind ( q. as_str ( ) )
657- . bind ( q_pattern. as_str ( ) )
658658 . bind ( query. role . as_deref ( ) )
659659 . bind ( limit)
660660 . fetch_all ( & self . pool )
@@ -663,6 +663,34 @@ impl Database {
663663 Ok ( rows)
664664 }
665665
666+ /// Full-text search over registered user profiles.
667+ pub async fn search_users (
668+ & self ,
669+ q : & str ,
670+ limit : i64 ,
671+ offset : i64 ,
672+ ) -> Result < Vec < crate :: models:: UserProfile > , AppError > {
673+ let rows = sqlx:: query_as :: < _ , crate :: models:: UserProfile > (
674+ r#"
675+ SELECT *
676+ FROM user_profiles
677+ WHERE $1 = '' OR search_vec @@ plainto_tsquery('english', $1)
678+ ORDER BY
679+ CASE WHEN $1 = '' THEN 0
680+ ELSE ts_rank(search_vec, plainto_tsquery('english', $1))
681+ END DESC,
682+ registered_at DESC
683+ LIMIT $2 OFFSET $3
684+ "# ,
685+ )
686+ . bind ( q)
687+ . bind ( limit. clamp ( 1 , 100 ) )
688+ . bind ( offset. max ( 0 ) )
689+ . fetch_all ( & self . pool )
690+ . await ?;
691+ Ok ( rows)
692+ }
693+
666694 pub async fn get_search_suggestions (
667695 & self ,
668696 prefix : & str ,
0 commit comments