Typesense
"Typesense: open-source search, typo tolerance, faceting, curation, geo search, InstantSearch adapter"
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 linesTypesense
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
importfor bulk indexing instead of individual document creates. It is orders of magnitude faster and handles batching internally. - Define
default_sorting_fieldon every collection. Typesense requires it for relevance tie-breaking when no explicit sort is provided. - Mark facetable fields with
facet: trueat 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_weightsto 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
importfor bulk operations. - Filtering on non-faceted fields. Fields must be declared with
facet: trueorindex: trueto 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
importmethod 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
Related Skills
Algolia
"Algolia: instant search, faceted search, InstantSearch.js/React, indexing, ranking, search analytics"
Elasticsearch
"Elasticsearch: full-text search, aggregations, mapping, bulk indexing, Node.js client, relevance tuning"
Fuse Js
Fuse.js is a lightweight, powerful fuzzy-search library for JavaScript that runs entirely client-side. It's ideal for quickly adding flexible, typo-tolerant search capabilities to web applications without server-side infrastructure.
Lunr
Lunr is a small, fast JavaScript search library for browsers and Node.js. It allows you to build a search index directly within your application, providing full-text search capabilities without a backend API or external service. It's ideal for static sites, documentation, or client-side applications requiring offline-capable search.
Manticore Search
"Manticore Search: open-source full-text search, SQL-based queries, real-time indexes, columnar storage, Elasticsearch-compatible API"
Meilisearch
"Meilisearch: self-hosted search engine, typo tolerance, faceting, filtering, sorting, REST API, JavaScript SDK"