Skip to content

Commit 6d5de9b

Browse files
authored
feat: add NFT token name search to global search bar (#76)
* feat: add NFT token name search to global search bar (#71) * fix: raise min search length to 3, escape LIKE wildcards in user input * fix: rustfmt like_escape method chain
1 parent b4f7fab commit 6d5de9b

2 files changed

Lines changed: 49 additions & 7 deletions

File tree

backend/crates/atlas-server/src/api/handlers/search.rs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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")]
1927
pub 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+
178200
async 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+
196236
async 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
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
2+
CREATE INDEX IF NOT EXISTS idx_nft_tokens_name_trgm ON nft_tokens USING GIN (name gin_trgm_ops) WHERE name IS NOT NULL;

0 commit comments

Comments
 (0)