Skip to content
Blog

Building Search UIs with Typesense

How to add instant, typo-tolerant search to your app with Typesense — schema-first indexing, debounced search-as-you-type, highlighting, and faceted filters in minutes.

Published on August 10, 2026

AI Assistant

Users expect search-as-you-type. They also type “hary poter” and expect to find Harry Potter. Building this from scratch means writing your own inverted index, fuzzy matching, relevance ranking, and highlighting — a project, not a feature. Typesense is an open-source, in-memory search engine optimized for instant sub-50ms searches, and it gives you typo tolerance, faceting, geo-search, synonyms, and vector search in a single binary.

In this post, you will learn how to stand up Typesense, index your data with a schema, and build a search UI that debounces queries, renders hits with highlighting, and supports facets.

Why Typesense

Typesense is an in-memory search engine that indexes JSON documents into named collections and exposes search, filtering, and autocomplete via a simple REST API. Its headline numbers:

  • Sub-50ms responses — in-memory indexing means results arrive faster than a blink, even with millions of records.
  • Typo tolerance out of the box — no fuzzy-match code to write.
  • Easy setup — a single binary or Docker container, versus the operational weight of Elasticsearch.
  • Batteries included — faceted search, geo-search, synonyms, curation rules, and vector/semantic search in one package.

Running Typesense

The fastest path is Docker:

docker run -p 8108:8108 -v /tmp/typesense-data:/data typesense/typesense:27.1 \
  --data-dir /data --api-key=YOUR_API_KEY --enable-cors

Create a collection (the schema defines which fields are searchable and how they should be handled):

curl -X POST http://localhost:8108/collections \
  -H "X-TYPESENSE-API-KEY: YOUR_API_KEY" \
  -d '{
    "name": "books",
    "fields": [
      {"name": "title", "type": "string"},
      {"name": "authors", "type": "string[]"},
      {"name": "ratings_count", "type": "int32", "facet": true}
    ]
  }'

Then index documents:

curl -X POST http://localhost:8108/collections/books/documents/import \
  -H "X-TYPESENSE-API-KEY: YOUR_API_KEY" \
  --data-binary '{"title":"Harry Potter and the Philosopher's Stone","authors":["J.K. Rowling"],"ratings_count":23298}
{"title":"The Hobbit","authors":["J.R.R. Tolkien"],"ratings_count":4502}'

The search endpoint

Search is a single GET with a query_by parameter telling Typesense which fields to match against:

curl "http://localhost:8108/collections/books/documents/search?q=hary+poter&query_by=title,authors&highlight_full_fields=title&per_page=10"

Typo tolerance is on by default. The highlight_full_fields parameter returns _snippet and _highlight fields with <mark> tags wrapping matches — exactly what you need for the UI.

Search-as-you-type UI

Building the search UI is where the UX lives. The essentials: debounce input so you don’t fire a request per keystroke, handle empty queries gracefully, and render results with highlighted snippets.

const searchBox = document.getElementById('search-box');
const results = document.getElementById('results');
let timer;

searchBox.addEventListener('input', (e) => {
  clearTimeout(timer);
  const q = e.target.value.trim();
  if (!q) { results.innerHTML = ''; return; }
  timer = setTimeout(async () => {
    const params = new URLSearchParams({
      q,
      query_by: 'title,authors',
      highlight_full_fields: 'title,authors',
      per_page: 10,
    });
    const res = await fetch(`http://localhost:8108/collections/books/documents/search?${params}`, {
      headers: { 'X-TYPESENSE-API-KEY': 'YOUR_API_KEY' },
    });
    const data = await res.json();
    renderHits(data.hits ?? []);
  }, 200);  // debounce
});

function renderHits(hits) {
  results.innerHTML = hits.map(hit => `
    <div class="hit">
      <div class="title">${hit.highlights?.find(h => h.field === 'title')?.snippet ?? hit.document.title}</div>
      <div class="authors">${(hit.document.authors || []).join(', ')}</div>
      <div class="rating">Rating: ${hit.document.ratings_count}</div>
    </div>
  `).join('');
}

The debounce is the single most important detail — without it you’ll fire dozens of requests while the user types. Typesense’s docs also walk through the full InstantSearch.js integration, which handles debouncing, state management, and highlighting for you.

Facets and filters

Facets let users refine results by field values. You declared ratings_count as a facet in the schema; now request facet values in the search:

const params = new URLSearchParams({
  q,
  query_by: 'title,authors',
  facet_by: 'ratings_count',
  max_facet_values: 10,
});

The response includes a facet_counts array. Render facet filters as checkboxes, and when the user toggles one, add a filter:

// Faceted filter: books with ratings_count >= 1000
params.set('filter_by', 'ratings_count:>=1000');

Because the engine is in-memory, filtering is instant too — the whole “type a query, tick a facet, results narrow instantly” flow stays under 50ms.

Highlighting with InstantSearch.js

If you prefer a drop-in widget layer, Typesense publishes an InstantSearch adapter so you get a search box, results, stats, pagination, and highlighting without writing the render code:

const search = instantsearch({
  indexName: 'books',
  searchClient: typesenseInstantsearchAdapter.searchClient,
});
search.addWidgets([
  instantsearch.widgets.searchBox({ container: '#searchbox' }),
  instantsearch.widgets.hits({
    container: '#hits',
    templates: {
      item: `<div>
        <div class="hit-name">{{#helpers.highlight}}{ "attribute": "title" }{{/helpers.highlight}}</div>
        <div class="hit-authors">{{#helpers.highlight}}{ "attribute": "authors" }{{/helpers.highlight}}</div>
      </div>`,
    },
  }),
  instantsearch.widgets.pagination({ container: '#pagination' }),
]);
search.start();

Production considerations

  • Don’t expose the admin key in the browser. Typesense supports scoped, read-only API keys for search calls. Generate a search-only key for the client.
  • Keep the client-server — put the search endpoint behind a small proxy if you need to hide even the read key, or use scoped keys.
  • Synonyms — declare synonyms in the collection so “shoe” and “sneaker” match, or tie common typos to canonical terms.
  • Hybrid search — Typesense supports vector search, so you can combine keyword and semantic matching for a hybrid relevance stack.

Putting It All Together

A complete example: run Typesense in Docker, create a books collection, import a few thousand documents, and build the debounced search UI above with highlighting and a ratings facet. Type “hary poter” and watch it return Harry Potter in milliseconds. From empty repo to working instant search is under an hour.

Conclusion & Next Steps

You now understand how to stand up Typesense, model data with a schema, hit the search endpoint, debounce search-as-you-type, render highlighted hits, and add facets. Next steps: add synonyms for your domain, switch to scoped read-only keys, and explore vector search to add semantic matching on top of the keyword index.

References / Sources