Skip to main content
Search is one of the most-used features on any content-heavy site. When it works well, visitors find exactly what they need in seconds. When it doesn’t, they bounce. This guide covers the two main approaches — built-in database search and hosted search (Algolia) — plus the UX details that separate a good search box from a frustrating one.

Three Paths

Describe what you want:
Add a search box to my site header that searches blog 
posts and product pages. Show results as a dropdown 
as the user types.
The AI scaffolds the schema, search endpoint, and UI component.

Database Full-Text Search (PostgreSQL)

1

Add a search column

ALTER TABLE blog_posts ADD COLUMN search_vector tsvector;

UPDATE blog_posts SET search_vector = 
  setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
  setweight(to_tsvector('english', coalesce(body, '')), 'B');

CREATE INDEX ON blog_posts USING gin(search_vector);
The setweight call gives title matches more weight than body matches in ranking.
2

Keep it updated on insert/update

CREATE TRIGGER blog_posts_search_trigger
BEFORE INSERT OR UPDATE ON blog_posts
FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(
  search_vector, 'pg_catalog.english', title, body
);
3

Write the search query

SELECT id, title, ts_rank(search_vector, query) as rank
FROM blog_posts, plainto_tsquery('english', 'your search term') query
WHERE search_vector @@ query
ORDER BY rank DESC LIMIT 10;
4

Expose via API route

export default async function handler(req: Request) {
  const url = new URL(req.url);
  const q = url.searchParams.get('q') ?? '';
  
  const results = await db.query(
    `SELECT id, title, slug, ts_rank(search_vector, query) as rank
     FROM blog_posts, plainto_tsquery('english', $1) query
     WHERE search_vector @@ query
     ORDER BY rank DESC LIMIT 10`,
    [q]
  );
  
  return Response.json({ results });
}
5

Build the frontend component

A search input that calls the API as the user types (debounced 300ms), renders results in a dropdown below the input.

Algolia Pattern

For larger sites or when you need typo-tolerance:
1

Sign up for Algolia

Go to algolia.com, create an app. Free tier covers up to 10k searches/month.
2

Copy your keys

  • Application ID — public identifier
  • Search-only API key — safe to expose in the browser
  • Admin API key — keep server-side only (used for indexing)
3

Install the SDK

npm install algoliasearch react-instantsearch
4

Index your content

import algoliasearch from 'algoliasearch';

const client = algoliasearch(APP_ID, ADMIN_KEY);
const index = client.initIndex('blog_posts');

const posts = await db.query('SELECT id, title, body, slug FROM blog_posts');
await index.saveObjects(
  posts.map(p => ({ objectID: p.id, ...p }))
);
Run this on a schedule (or on content change) to keep Algolia in sync.
5

Add the search UI

import { InstantSearch, SearchBox, Hits } from 'react-instantsearch';
import algoliasearch from 'algoliasearch/lite';

const searchClient = algoliasearch(APP_ID, SEARCH_KEY);

export default function Search() {
  return (
    <InstantSearch searchClient={searchClient} indexName="blog_posts">
      <SearchBox />
      <Hits hitComponent={Hit} />
    </InstantSearch>
  );
}

Search UX Best Practices

The plumbing is easy. The UX is where most search implementations fall down.
  • Results as user types. Debounce 300ms so you’re not hammering the API on every keystroke.
  • Highlight matched terms. Bold the portions of each result that match the query. Makes relevance obvious.
  • Keyboard navigation. Arrow keys to move through results, Enter to open, Escape to close.
  • Zero-results state. Don’t just say “No results.” Suggest popular queries, link to browse pages, show recent posts.
  • Recent searches. If the user has searched before, show recent queries when the box is empty. Saves typing.
  • Query tracking. Log queries to analytics so you know what users look for. See Track Analytics.

Filter by Content Type

On sites with multiple content types (blog, products, docs), let users narrow:
<SearchFilters>
  <Filter value="all">All</Filter>
  <Filter value="blog">Blog</Filter>
  <Filter value="products">Products</Filter>
  <Filter value="docs">Docs</Filter>
</SearchFilters>
For catalogs with structured metadata, add facet filters:
  • Category
  • Tags
  • Date range
  • Price range
  • Author
Both Algolia and PostgreSQL full-text search support this — Algolia handles it natively with InstantSearch UI components; in PostgreSQL you add WHERE clauses.

For Docs Sites Specifically

If you’re building a documentation site, Algolia DocSearch is free for open-source docs and purpose-built for the use case (good hierarchical ranking, keyboard nav, autocomplete). Apply and Algolia handles indexing for you.
Search queries are one of your best sources of content and product ideas. Users are literally telling you what they want. Review top queries weekly and look for gaps — queries with zero results are a roadmap of missing content.

Verify It Worked

1

Open your site

Find the search box (usually in the header).
2

Type a query

Use a term you know exists in your content. Results should appear within 300-500ms.
3

Click a result

Confirm it takes you to the right page.
4

Test a typo

If using Algolia, “webistes” should still return “websites” content. If using plain PostgreSQL, typos won’t match — consider adding pg_trgm for similarity matching.
5

Test the empty state

Clear the box. Confirm your zero-query state (popular posts, suggestions) renders.

Troubleshooting

The index is empty. For PostgreSQL: the search_vector column is null — run the UPDATE from Step 1 to backfill. For Algolia: you haven’t pushed content yet — run the indexing script.
For PostgreSQL: missing GIN index on search_vector. Check with \d+ blog_posts and confirm an index on that column. Also look for N+1 queries — you might be fetching full rows per result when IDs would do.
PostgreSQL full-text search doesn’t handle typos out of the box. Add the pg_trgm extension and use similarity() or % operator for fuzzy matching. Or switch to Algolia, which handles typos automatically.
Add ranking signals beyond raw match. Good options: recency (newer posts rank higher), popularity (view count, engagement), author authority, content type priority. Combine them into a composite score that you ORDER BY.
Make sure your content-update workflow pushes to Algolia. Add an action after any blog/product insert or update that calls index.saveObject(...). For deletes, call index.deleteObject(objectID).

What’s Next?

Track Analytics

Log search queries so you can learn what users want

Structured Data

Help search engines index your content too