Skip to main content
Technology & EngineeringSearch Services279 lines

Typesense

"Typesense: open-source search, typo tolerance, faceting, curation, geo search, InstantSearch adapter"

Quick Summary18 lines
Typesense is an open-source, typo-tolerant search engine optimized for instant search experiences. Its design principles are:

## Key Points

- **Speed over flexibility** — an in-memory architecture delivers single-digit millisecond latencies. The data model is intentionally simpler than Elasticsearch to stay fast.
- **Explicit schema** — every collection has a defined schema with typed fields. This catches data issues at index time rather than at query time.
- **Batteries included** — typo tolerance, faceting, filtering, sorting, geo search, grouping, and curation ship out of the box with zero plugins.
- **Easy operations** — a single binary with no external dependencies (no JVM, no separate coordinator). Clustering is built in via Raft consensus.
- **InstantSearch compatible** — the official adapter lets you use Algolia's InstantSearch.js/React UI components with a Typesense backend.
- **Use `import` for bulk indexing** instead of individual document creates. It is orders of magnitude faster and handles batching internally.
- **Define `default_sorting_field`** on every collection. Typesense requires it for relevance tie-breaking when no explicit sort is provided.
- **Mark facetable fields with `facet: true`** at schema creation time. Adding facets later requires re-creating the collection.
- **Use collection aliases** for zero-downtime reindexing. Swap the alias atomically after the new collection is fully populated.
- **Generate scoped search keys** for multi-tenant apps. The embedded filter cannot be overridden by the client.
- **Set `query_by_weights`** to control which fields contribute most to relevance. Higher weights on title fields improve perceived result quality.
- **Use the InstantSearch adapter** to get a polished search UI with minimal frontend code.
skilldb get search-services-skills/TypesenseFull skill: 279 lines
Paste into your CLAUDE.md or agent config

Typesense

Core Philosophy

Typesense is an open-source, typo-tolerant search engine optimized for instant search experiences. Its design principles are:

  • Speed over flexibility — an in-memory architecture delivers single-digit millisecond latencies. The data model is intentionally simpler than Elasticsearch to stay fast.
  • Explicit schema — every collection has a defined schema with typed fields. This catches data issues at index time rather than at query time.
  • Batteries included — typo tolerance, faceting, filtering, sorting, geo search, grouping, and curation ship out of the box with zero plugins.
  • Easy operations — a single binary with no external dependencies (no JVM, no separate coordinator). Clustering is built in via Raft consensus.
  • InstantSearch compatible — the official adapter lets you use Algolia's InstantSearch.js/React UI components with a Typesense backend.

Use Typesense when you want Algolia-class search UX without vendor lock-in or per-search pricing.

Setup

Define a collection schema and index documents:

// npm install typesense

import Typesense from "typesense";

const client = new Typesense.Client({
  nodes: [{ host: "localhost", port: 8108, protocol: "http" }],
  apiKey: "YOUR_API_KEY",
  connectionTimeoutSeconds: 5,
});

// Create a collection with an explicit schema
async function createCollection() {
  await client.collections().create({
    name: "products",
    fields: [
      { name: "name", type: "string" },
      { name: "description", type: "string" },
      { name: "price", type: "float" },
      { name: "brand", type: "string", facet: true },
      { name: "categories", type: "string[]", facet: true },
      { name: "rating", type: "float" },
      { name: "in_stock", type: "bool" },
      { name: "location", type: "geopoint" },       // [lat, lng]
      { name: "popularity", type: "int32" },
    ],
    default_sorting_field: "popularity",
  });
}

// Index documents
async function indexProducts(products: Product[]) {
  // Use import for bulk indexing — far faster than individual creates
  const results = await client
    .collections("products")
    .documents()
    .import(products, { action: "upsert" });

  const failures = results.filter((r) => !r.success);
  if (failures.length > 0) {
    console.error(`${failures.length} documents failed to index`, failures);
  }
}

Key Techniques

Search with Filtering and Faceting

async function searchProducts(query: string, options: SearchOptions = {}) {
  const results = await client
    .collections("products")
    .documents()
    .search({
      q: query,
      query_by: "name,description",
      query_by_weights: "3,1",              // name matches rank higher
      filter_by: options.filterBy ?? "",     // e.g. "brand:=Nike && price:<100"
      sort_by: options.sortBy ?? "popularity:desc",
      facet_by: "categories,brand",
      max_facet_values: 20,
      per_page: 20,
      page: options.page ?? 1,
      highlight_full_fields: "name",
      highlight_start_tag: "<mark>",
      highlight_end_tag: "</mark>",
    });

  return {
    hits: results.hits?.map((h) => h.document) ?? [],
    found: results.found,
    facets: results.facet_counts,
    processingTimeMs: results.search_time_ms,
  };
}

interface SearchOptions {
  filterBy?: string;
  sortBy?: string;
  page?: number;
}

Geo Search

async function searchNearby(lat: number, lng: number, radiusKm: number) {
  return client
    .collections("products")
    .documents()
    .search({
      q: "*",
      query_by: "name",
      filter_by: `location:(${lat}, ${lng}, ${radiusKm} km)`,
      sort_by: `location(${lat}, ${lng}):asc`,
      per_page: 20,
    });
}

Curation (Pinning and Hiding Results)

async function createCuration() {
  // Override results for specific queries
  await client.collections("products").overrides().upsert("summer-sale", {
    rule: {
      query: "summer",
      match: "contains",
    },
    // Pin these documents at the top
    includes: [
      { id: "prod_101", position: 1 },
      { id: "prod_205", position: 2 },
    ],
    // Hide these documents from results
    excludes: [{ id: "prod_999" }],
  });
}

Synonyms

async function manageSynonyms() {
  // Multi-way synonym
  await client.collections("products").synonyms().upsert("shoe-synonyms", {
    synonyms: ["shoe", "sneaker", "trainer", "footwear"],
  });

  // One-way synonym
  await client.collections("products").synonyms().upsert("blazer-synonym", {
    root: "blazer",
    synonyms: ["jacket", "sport coat"],
  });
}

InstantSearch Adapter (React)

// npm install typesense-instantsearch-adapter react-instantsearch

import TypesenseInstantsearchAdapter from "typesense-instantsearch-adapter";
import {
  InstantSearch,
  SearchBox,
  Hits,
  RefinementList,
  Pagination,
  Highlight,
} from "react-instantsearch";
import React from "react";

const adapter = new TypesenseInstantsearchAdapter({
  server: {
    apiKey: "YOUR_SEARCH_ONLY_KEY",
    nodes: [{ host: "localhost", port: 8108, protocol: "http" }],
  },
  additionalSearchParameters: {
    query_by: "name,description",
    query_by_weights: "3,1",
  },
});

const searchClient = adapter.searchClient;

function ProductHit({ hit }: { hit: any }) {
  return (
    <div>
      <h3><Highlight attribute="name" hit={hit} /></h3>
      <p>${hit.price}</p>
    </div>
  );
}

function SearchPage() {
  return (
    <InstantSearch searchClient={searchClient} indexName="products">
      <aside>
        <RefinementList attribute="brand" />
        <RefinementList attribute="categories" />
      </aside>
      <main>
        <SearchBox />
        <Hits hitComponent={ProductHit} />
        <Pagination />
      </main>
    </InstantSearch>
  );
}

Collection Aliases for Zero-Downtime Reindexing

async function reindexWithAlias(newProducts: Product[]) {
  const timestamped = `products_${Date.now()}`;

  // Create a new collection with the same schema
  const schema = await client.collections("products").retrieve();
  await client.collections().create({
    name: timestamped,
    fields: schema.fields,
    default_sorting_field: schema.default_sorting_field,
  });

  // Index all data into the new collection
  await client.collections(timestamped).documents().import(newProducts, { action: "create" });

  // Atomically swap the alias
  await client.aliases().upsert("products", {
    collection_name: timestamped,
  });

  // Delete the old collection
  const oldName = schema.name;
  if (oldName !== timestamped) {
    await client.collections(oldName).delete();
  }
}

Scoped API Keys

function createScopedSearchKey(tenantId: string): string {
  // Generates a key that embeds a filter — the client cannot remove it
  return client.keys().generateScopedSearchKey("YOUR_SEARCH_ONLY_KEY", {
    filter_by: `tenant_id:=${tenantId}`,
    expires_at: Math.floor(Date.now() / 1000) + 3600,
  });
}

Best Practices

  • Use import for bulk indexing instead of individual document creates. It is orders of magnitude faster and handles batching internally.
  • Define default_sorting_field on every collection. Typesense requires it for relevance tie-breaking when no explicit sort is provided.
  • Mark facetable fields with facet: true at schema creation time. Adding facets later requires re-creating the collection.
  • Use collection aliases for zero-downtime reindexing. Swap the alias atomically after the new collection is fully populated.
  • Generate scoped search keys for multi-tenant apps. The embedded filter cannot be overridden by the client.
  • Set query_by_weights to control which fields contribute most to relevance. Higher weights on title fields improve perceived result quality.
  • Use the InstantSearch adapter to get a polished search UI with minimal frontend code.

Anti-Patterns

  • Not defining a schema. Unlike Meilisearch, Typesense requires explicit field definitions. Sending documents without a matching schema causes errors.
  • Using single-document create in a loop. Always use import for bulk operations.
  • Filtering on non-faceted fields. Fields must be declared with facet: true or index: true to be filterable. Queries against un-indexed fields fail.
  • Exposing the admin API key to the frontend. Use a search-only key or a scoped key. The admin key can delete collections.
  • Ignoring import errors. The import method returns per-document results. Always check for failures.
  • Storing large text blobs in indexed fields. Typesense is in-memory. Keep documents lean to control RAM usage.
  • Skipping highlight_full_fields. Without it, highlights may be truncated, causing confusing UI behavior.

Install this skill directly: skilldb add search-services-skills

Get CLI access →