@@ -14,6 +14,14 @@ pub struct SearchQuery {
1414 pub q : String ,
1515}
1616
17+ #[ derive( Debug , Clone , Serialize , Deserialize , sqlx:: FromRow ) ]
18+ pub struct NftTokenResult {
19+ pub contract_address : String ,
20+ pub token_id : String ,
21+ pub name : Option < String > ,
22+ pub image_url : Option < String > ,
23+ }
24+
1725#[ derive( Serialize ) ]
1826#[ serde( tag = "type" ) ]
1927pub enum SearchResult {
@@ -25,6 +33,8 @@ pub enum SearchResult {
2533 Address ( Address ) ,
2634 #[ serde( rename = "nft_collection" ) ]
2735 NftCollection ( NftContract ) ,
36+ #[ serde( rename = "nft" ) ]
37+ Nft ( NftTokenResult ) ,
2838 #[ serde( rename = "erc20_token" ) ]
2939 Erc20Token ( Erc20Contract ) ,
3040}
@@ -91,17 +101,22 @@ pub async fn search(
91101 }
92102 }
93103
94- // Text search for tokens/collections if no hex/number results and query is meaningful
95- if results. is_empty ( ) && query. len ( ) >= 2 {
96- // Run NFT and ERC-20 searches in parallel
97- let ( nft_results, erc20_results) = tokio:: join!(
104+ // Text search for tokens/collections if no hex/number results and query is long
105+ // enough to benefit from the pg_trgm GIN indexes (trigrams require >= 3 chars).
106+ if results. is_empty ( ) && query. len ( ) >= 3 {
107+ // Run NFT collection, NFT token, and ERC-20 searches in parallel
108+ let ( nft_collection_results, nft_token_results, erc20_results) = tokio:: join!(
98109 search_nft_collections( & state, query) ,
110+ search_nft_tokens( & state, query) ,
99111 search_erc20_tokens( & state, query)
100112 ) ;
101113
102- for nft in nft_results ? {
114+ for nft in nft_collection_results ? {
103115 results. push ( SearchResult :: NftCollection ( nft) ) ;
104116 }
117+ for token in nft_token_results? {
118+ results. push ( SearchResult :: Nft ( token) ) ;
119+ }
105120 for token in erc20_results? {
106121 results. push ( SearchResult :: Erc20Token ( token) ) ;
107122 }
@@ -175,11 +190,18 @@ async fn search_block_by_number(
175190 . map_err ( Into :: into)
176191}
177192
193+ fn like_escape ( input : & str ) -> String {
194+ input
195+ . replace ( '\\' , "\\ \\ " )
196+ . replace ( '%' , "\\ %" )
197+ . replace ( '_' , "\\ _" )
198+ }
199+
178200async fn search_nft_collections (
179201 state : & AppState ,
180202 query : & str ,
181203) -> Result < Vec < NftContract > , atlas_common:: AtlasError > {
182- let pattern = format ! ( "%{}%" , query) ;
204+ let pattern = format ! ( "%{}%" , like_escape ( query) ) ;
183205 sqlx:: query_as (
184206 "SELECT address, name, symbol, total_supply, first_seen_block
185207 FROM nft_contracts
@@ -193,11 +215,29 @@ async fn search_nft_collections(
193215 . map_err ( Into :: into)
194216}
195217
218+ async fn search_nft_tokens (
219+ state : & AppState ,
220+ query : & str ,
221+ ) -> Result < Vec < NftTokenResult > , atlas_common:: AtlasError > {
222+ let pattern = format ! ( "%{}%" , like_escape( query) ) ;
223+ sqlx:: query_as (
224+ "SELECT contract_address, token_id::text AS token_id, name, image_url
225+ FROM nft_tokens
226+ WHERE name ILIKE $1
227+ ORDER BY last_transfer_block DESC NULLS LAST
228+ LIMIT 5" ,
229+ )
230+ . bind ( & pattern)
231+ . fetch_all ( & state. pool )
232+ . await
233+ . map_err ( Into :: into)
234+ }
235+
196236async fn search_erc20_tokens (
197237 state : & AppState ,
198238 query : & str ,
199239) -> Result < Vec < Erc20Contract > , atlas_common:: AtlasError > {
200- let pattern = format ! ( "%{}%" , query) ;
240+ let pattern = format ! ( "%{}%" , like_escape ( query) ) ;
201241 sqlx:: query_as (
202242 "SELECT address, name, symbol, decimals, total_supply, first_seen_block
203243 FROM erc20_contracts
0 commit comments