Nativ ui
Documentation

Search

Best practices for implementing search functionality in web applications.

Global Recommendation

The best approach is to implement a backend-side search with a dedicated endpoint and a persistent index (DB or search engine), keeping the frontend as a pure paginated consumer. Recursive search or client-side indexing won't scale or maintain consistency once you exceed a few hundred files.

  • Create a /files/search endpoint on your API that queries a server-side index (SQL table or dedicated engine)
  • Start with database + full-text index (Postgres/MySQL/SQLite) to keep infrastructure simple
  • Move to ElasticSearch/Meilisearch only if you significantly exceed ~100k records or need advanced scoring/aggregations
  • On the UI side: server-side search, paginated, with debounce (300–400ms) and incremental results, no complete recursive scan on client

Why Avoid Client-Side Options

  • Requires traversing the entire tree on each search, so O(N) files per keystroke
  • Not scalable beyond 1000+ files, very poor on mobile / slow networks, requires keeping a large state in memory
  • Fragile UX: depends on user's navigation state (local cache vs reality)

Client-Side Index (Service Worker)

  • Consistency problem: multi-device, multi-session, server updates not reflected, complicated invalidation
  • No value for SEO or other clients (mobile app, desktop app, etc.), search logic duplicated everywhere
  • Lots of complexity for limited gain when a server index is simpler to centralize

These options can remain as micro-optimizations (pre-filtering on server query results), but should not be the source of truth.

Minimal Data Model

Store one record per file in your DB (Postgres recommended) with:

FieldDescription
idUnique identifier
owner_idUser / workspace
pathFull path /projects/discord/discord_logo.svg
nameFile name
extensionFile extension
sizeFile size
created_atCreation timestamp
updated_atLast update timestamp
tagsOptional tags / favorites

On Postgres, you can:

  • Index (owner_id, name) for fast ILIKE queries on small volumes
  • Set up full-text search to search in name + tags + optionally indexed content

Endpoint Design

Typical pattern:

GET /files/search

Query Parameters:

ParamTypeDescription
qstringSearch query
pathstringOptional, restrict to subfolder
favoritesboolOptional filter
pageintPage number
pageSizeintItems per page
sortBystringField to sort by
orderstringasc or desc

Response:

{
  "data": {
    "items": [
      {
        "id": "file_123",
        "name": "discord_logo.svg",
        "path": "/assets/discord",
        "isFavorite": true,
        "size": 12345,
        "updatedAt": "2026-01-23T15:00:00Z"
      }
    ],
    "total": 42,
    "page": 1,
    "pageSize": 20,
    "totalPages": 3
  }
}

This "envelope > data > items" structure is recommended for a consistent and extensible API.

Search Behavior

  • Always filter by owner_id / workspace server-side
  • Search options:
    • Simple: WHERE name ILIKE '%discord%' with index on LOWER(name) or trigram index for better performance
    • Full-text: build a tsvector column (name + tags) and use to_tsquery / plainto_tsquery
  • Add a deterministic ORDER BY (e.g., ORDER BY is_favorite DESC, updated_at DESC)

Frontend Integration (React + TanStack Query)

// Hook: useFileSearch(query, options)
const useFileSearch = (query: string, options?: SearchOptions) => {
  return useQuery({
    queryKey: ['files', 'search', query, options],
    queryFn: () => searchFiles(query, options),
    enabled: query.length > 0,
  })
}

UX Guidelines:

  • Debounce of 300–400ms on the query
  • Call /files/search when query.length > 0
  • Pagination: page, pageSize in TanStack Query key
  • Loader skeleton during fetch
  • "No results" if items.length === 0 and query !== ''
  • Show "recent files" or "favorites" when query === ''

Option 2: ElasticSearch / Meilisearch (At Scale)

When It Becomes Relevant

  • Volumes of hundreds of thousands to millions of files
  • Need for advanced scoring, suggestions, typo-tolerance, facets (by type, size, dates)
  • Need for aggregations (file statistics) and ultra-low latency for complex queries

Postgres/SQL full-text is generally sufficient up to ~100k documents, with acceptable latencies (under 200ms) if properly indexed.

Basic Architecture

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   Database  │────▶│  Sync Worker │────▶│   Search    │
│   (Source)  │     │              │     │   Engine    │
└─────────────┘     └──────────────┘     └─────────────┘
                                                │
                                                ▼
                                         ┌─────────────┐
                                         │  /search    │
                                         │  endpoint   │
                                         └─────────────┘
  • DB remains the source of truth
  • A job or worker syncs files to the search engine
  • The /files/search API queries exclusively the search engine and returns unified format

Trade-off: You add a service to maintain, indexing pipelines, and a slight lag between DB and index.

Key Implementation Points

PrincipleDescription
Always server-sideSearch must happen backend-side, never via complete client scan
Dedicated indexTable or full-text column with index, not just LIKE '%...%' without index
Clear APISingle /files/search endpoint, GET with well-defined filtering/pagination params
Mandatory paginationLimit results per page, always return total and page for smooth UX
Debounce and cacheFrontend debounce + TanStack Query to avoid spamming backend while keeping UX reactive
EvolvableDesign API to switch search layer later (SQL → Meilisearch/ES) without changing frontend contract

Quick Reference

// Debounced search input
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
 
// Search hook with pagination
const { data, isLoading } = useFileSearch(debouncedQuery, {
  page: 1,
  pageSize: 20,
  sortBy: 'updatedAt',
  order: 'desc'
})