GET /search
Autocomplete symbol search with prefix-boost ranking
/search powers the Charting Library's symbol-picker autocomplete. As the user types, the library calls this endpoint with the current input and shows up to limit results.
- Method:
GET - Path:
/api/nepse/tradingview/search - Content-Type:
application/json(bare array — see below)
Query parameters
| Param | Required | Type | Default | Notes |
|---|---|---|---|---|
query | ✗ | string | "" | Substring to match against company symbol/name, index tv_symbol/index_name, or sub-index tv_symbol/name. Empty → browse mode: return first N. Typing ^ alone surfaces every index + sub-index. |
limit | ✗ | int | 30 | Clamped to [1, 100]. Invalid, zero, negative, or over-cap values reset to 30. |
type | ✗ | string | — | Accepted for UDF compatibility. The backend does not filter on it — stock and index results are always merged. |
exchange | ✗ | string | — | Accepted for UDF compatibility but ignored (NEPSE is the only exchange). |
Response
The response is a bare JSON array at the root — NOT an object wrapping it. If you return {"results": [...]}, the TradingView search box silently shows "no matches" forever. This is the most common UDF implementation mistake.
[
{
"symbol": "^NEPSE",
"full_name": "NEPSE:^NEPSE",
"description": "NEPSE Index",
"exchange": "NEPSE",
"ticker": "^NEPSE",
"type": "index"
},
{
"symbol": "NABBC",
"full_name": "NEPSE:NABBC",
"description": "Narayani Development Bank Limited",
"exchange": "NEPSE",
"ticker": "NABBC",
"type": "stock"
},
{
"symbol": "NABIL",
"full_name": "NEPSE:NABIL",
"description": "Nabil Bank Limited",
"exchange": "NEPSE",
"ticker": "NABIL",
"type": "stock"
}
][]A bare empty array — never null, never {}. Enforced in the handler:
if results == nil {
results = []SearchResult{}
}
writeJSON(w, http.StatusOK, results)type SearchResult struct {
Symbol string `json:"symbol"` // ticker (includes `^` for indices)
FullName string `json:"full_name"` // "NEPSE:<symbol>"
Description string `json:"description"` // company/index/sub-index name
Exchange string `json:"exchange"` // "NEPSE"
Ticker string `json:"ticker"` // same as Symbol
Type string `json:"type"` // "stock" | "index"
}For stocks the ticker comes from nepse_companies.symbol. For indices and sub-indices the SQL prepends the ^ ('^' || tv_symbol) so the returned symbol can be round-tripped into /symbols or /history unchanged. full_name is always "NEPSE:" + symbol.
Unified search (stocks + indices + sub-indices)
A single SearchForTV query UNIONs three branches and ranks with the same prefix-boost shape. Postgres forbids expression-based ORDER BY on a bare UNION, so the branches are wrapped in a subquery so the outer CASE can run against the merged column set:
SELECT tv_symbol, description, kind
FROM (
(
SELECT symbol AS tv_symbol, name AS description, 'stock'::text AS kind
FROM nepse_companies
WHERE status = 'A'
AND (symbol ILIKE '%' || @search || '%' OR name ILIKE '%' || @search || '%')
) UNION ALL (
SELECT '^' || tv_symbol AS tv_symbol, index_name AS description, 'index'::text AS kind
FROM nepse_indices
WHERE tv_symbol ILIKE '%' || @search || '%'
OR index_name ILIKE '%' || @search || '%'
OR ('^' || tv_symbol) ILIKE '%' || @search || '%'
) UNION ALL (
SELECT '^' || tv_symbol AS tv_symbol, name AS description, 'index'::text AS kind
FROM nepse_sub_indices
WHERE tv_symbol ILIKE '%' || @search || '%'
OR name ILIKE '%' || @search || '%'
OR ('^' || tv_symbol) ILIKE '%' || @search || '%'
)
) AS merged
ORDER BY
CASE WHEN tv_symbol ILIKE @search || '%' THEN 0 ELSE 1 END,
tv_symbol
LIMIT @lim;Each index branch matches against both the bare tv_symbol (so typing NEP still finds ^NEPSE) and the ^-prefixed form (so typing ^NEP also works). The Go handler maps the kind column into the JSON type field (stock → stock, index → index).
Ranking examples
For query=NAB (stocks only — no indices match):
| Rank | Symbol | Why |
|---|---|---|
| 1 | NABBC | symbol starts with NAB |
| 2 | NABIL | symbol starts with NAB |
| 3 | NABILD2089 | symbol starts with NAB |
| 4 | NABILD87 | symbol starts with NAB |
| 5 | KDBY | matches because name = "Kumari Dhanabriddhi Yojana" contains nab |
For query=BANK:
| Rank | Symbol | Kind | Why |
|---|---|---|---|
| 1 | ^BANKING | index | prefix match on tv_symbol |
| 2 | BCBL | stock | symbol prefix; name contains BANK |
| 3 | KBBL | stock | name contains "Bank" |
| … | ^DEVBANK | index | substring match via ^-prefixed form |
For query=^ (browse all indices):
All 4 top-level indices + 13 sub-indices appear; no stocks match because companies don't contain ^.
Graceful degradation
/search never returns 5xx or 504 to the client. A DB error or timeout degrades silently to []. Rationale: a 500 response would blank the library's search UI for the rest of the session; an empty array just means "no results right now."
results, err := h.svc.SearchSymbols(ctx, query, limit)
if err != nil {
slog.Warn("tradingview: search degraded", "error", err, "query", query)
writeJSON(w, http.StatusOK, []SearchResult{})
return
}This applies to all error classes including timeouts — the server-side error is logged, the browser sees [].
Limit clamping
Values are normalized in the handler before hitting the service:
| Raw input | Effective |
|---|---|
| omitted | 30 |
0 or negative | 30 |
1–100 | unchanged |
> 100 | 30 |
non-numeric (abc) | 30 |
This matches UDF's expected behavior: a sane default for browse-mode and no way for a misbehaving frontend to fetch unbounded result sets.
Caching
/search is not Redis-cached — queries are highly variable (every keystroke). Nginx is configured with a 1-minute proxy_cache_valid 200 which is usually enough to absorb a burst of identical requests from a single session.
Verification
# Bare array, prefix-boost visible
curl -s "$BASE/search?query=NAB&limit=5" | jq .
# Empty query returns first N alphabetically
curl -s "$BASE/search?limit=3" | jq '[.[].ticker]'
# ["ACLBSL", "ADBL", "ADBLD83"]
# No matches → empty array, not null
curl -s "$BASE/search?query=ZZZZZZZ"
# []
# limit=9999 clamps to default 30
curl -s "$BASE/search?query=A&limit=9999" | jq length
# 30Implementation
- Handler:
internal/modules/nepse/tradingview/handler.go→(*Handler).Search - Service:
internal/modules/nepse/tradingview/service.go→(*Service).SearchSymbols - Type:
internal/modules/nepse/tradingview/types.go→SearchResult - sqlc:
SearchForTVininternal/platform/database/queries/charts.sql(companies + indices + sub-indices UNION)