/ astro-integrations / How to Integrate Typesense with Astro: Complete Guide
astro-integrations 6 min read

How to Integrate Typesense with Astro: Complete Guide

Step-by-step guide to integrating Typesense with your Astro website.

Typesense is an open-source, typo-tolerant search engine designed for instant search experiences. It is fast (single-digit millisecond latency), easy to set up, and works well for content-heavy sites. What sets Typesense apart from other search engines is its strict schema enforcement and built-in API key scoping, which gives you more control over what users can search and access.

For Astro blogs and documentation sites, Typesense provides a solid middle ground between basic client-side search and expensive hosted services. You can self-host it for free or use Typesense Cloud for a managed experience.

Prerequisites

  • Node.js 18+
  • An Astro project (npm create astro@latest)
  • Typesense server running locally or on Typesense Cloud
  • Docker (recommended for local development)

Running Typesense

Start Typesense locally with Docker:

docker run -d \
  -p 8108:8108 \
  -v $(pwd)/typesense-data:/data \
  typesense/typesense:27.1 \
  --data-dir /data \
  --api-key=your_api_key_here \
  --enable-cors

Typesense is now running at http://localhost:8108.

Installation

Install the Typesense client and the InstantSearch adapter:

npm install typesense typesense-instantsearch-adapter

The adapter lets you use Algolia's InstantSearch UI components with Typesense as the backend.

Configuration

Add your Typesense credentials to .env:

TYPESENSE_HOST=localhost
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=http
TYPESENSE_API_KEY=your_api_key_here
PUBLIC_TYPESENSE_HOST=localhost
PUBLIC_TYPESENSE_PORT=8108
PUBLIC_TYPESENSE_PROTOCOL=http
PUBLIC_TYPESENSE_SEARCH_KEY=your_search_only_key

Create the Typesense client:

// src/lib/typesense.ts
import Typesense from "typesense";

// Admin client (server-side, for indexing)
export const adminClient = new Typesense.Client({
  nodes: [
    {
      host: import.meta.env.TYPESENSE_HOST || "localhost",
      port: parseInt(import.meta.env.TYPESENSE_PORT || "8108"),
      protocol: import.meta.env.TYPESENSE_PROTOCOL || "http",
    },
  ],
  apiKey: import.meta.env.TYPESENSE_API_KEY,
  connectionTimeoutSeconds: 5,
});

Creating the Collection Schema

Typesense requires you to define a schema before indexing. This is different from Meilisearch or Elasticsearch, which can infer schemas:

// scripts/create-typesense-schema.ts
import Typesense from "typesense";

const client = new Typesense.Client({
  nodes: [{ host: "localhost", port: 8108, protocol: "http" }],
  apiKey: process.env.TYPESENSE_API_KEY || "your_api_key_here",
});

const schema = {
  name: "posts",
  fields: [
    { name: "title", type: "string" as const },
    { name: "description", type: "string" as const },
    { name: "content", type: "string" as const },
    { name: "slug", type: "string" as const, facet: false },
    { name: "category", type: "string" as const, facet: true },
    { name: "tags", type: "string[]" as const, facet: true },
    { name: "author", type: "string" as const, facet: true },
    { name: "publishDate", type: "int64" as const, sort: true },
  ],
  default_sorting_field: "publishDate",
};

try {
  await client.collections("posts").delete();
  console.log("Deleted existing collection");
} catch (e) {
  // Collection does not exist yet
}

await client.collections().create(schema);
console.log("Created posts collection");

Indexing Content

// scripts/index-to-typesense.ts
import Typesense from "typesense";
import fs from "fs";
import path from "path";
import matter from "gray-matter";

const client = new Typesense.Client({
  nodes: [{ host: "localhost", port: 8108, protocol: "http" }],
  apiKey: process.env.TYPESENSE_API_KEY || "your_api_key_here",
});

const postsDir = path.join(process.cwd(), "public/data/posts");
const files = fs.readdirSync(postsDir).filter((f) => f.endsWith(".mdx"));

const documents = files.map((file) => {
  const raw = fs.readFileSync(path.join(postsDir, file), "utf-8");
  const { data, content } = matter(raw);

  return {
    id: file.replace(".mdx", ""),
    title: data.title || "",
    description: data.description || "",
    content: content.slice(0, 10000),
    slug: file.replace(".mdx", ""),
    category: data.category || "Uncategorized",
    tags: data.tags || [],
    author: data.author || "unknown",
    publishDate: Math.floor(new Date(data.publishDate).getTime() / 1000),
  };
});

const results = await client
  .collections("posts")
  .documents()
  .import(documents, { action: "upsert" });

const success = results.filter((r) => r.success).length;
const failed = results.filter((r) => !r.success).length;
console.log("Indexed: " + success + " success, " + failed + " failed");

Building the Search UI

Create a search component using vanilla JavaScript with safe DOM methods:

---
// src/components/TypesenseSearch.astro
---

<div id="search-wrapper">
  <input type="text" id="ts-search-input" placeholder="Search articles..." autocomplete="off" />
  <ul id="ts-search-results"></ul>
</div>

<script>
  import Typesense from "typesense";

  const client = new Typesense.Client({
    nodes: [
      {
        host: import.meta.env.PUBLIC_TYPESENSE_HOST,
        port: parseInt(import.meta.env.PUBLIC_TYPESENSE_PORT),
        protocol: import.meta.env.PUBLIC_TYPESENSE_PROTOCOL,
      },
    ],
    apiKey: import.meta.env.PUBLIC_TYPESENSE_SEARCH_KEY,
    connectionTimeoutSeconds: 5,
  });

  const input = document.getElementById("ts-search-input") as HTMLInputElement;
  const resultsList = document.getElementById("ts-search-results") as HTMLUListElement;
  let timer: ReturnType<typeof setTimeout>;

  input.addEventListener("input", () => {
    clearTimeout(timer);
    timer = setTimeout(async () => {
      const query = input.value.trim();
      if (!query) {
        resultsList.replaceChildren();
        return;
      }

      const res = await client.collections("posts").documents().search({
        q: query,
        query_by: "title,description,content,tags",
        query_by_weights: "4,3,1,2",
        per_page: 10,
      });

      resultsList.replaceChildren();
      (res.hits || []).forEach((hit: any) => {
        const doc = hit.document;
        const li = document.createElement("li");

        const link = document.createElement("a");
        link.href = "/blog/" + doc.slug;

        const title = document.createElement("h3");
        title.textContent = doc.title;

        const desc = document.createElement("p");
        desc.textContent = doc.description;

        link.appendChild(title);
        link.appendChild(desc);
        li.appendChild(link);
        resultsList.appendChild(li);
      });
    }, 200);
  });
</script>

The query_by_weights parameter controls relevance. In this example, title matches rank highest (4), followed by description (3), tags (2), and content (1).

Scoped API Keys

Typesense lets you create scoped API keys that restrict what a user can search. This is useful for multi-tenant applications:

const scopedKey = adminClient.keys().generateScopedSearchKey(
  "your_search_only_key",
  {
    filter_by: "category:=Tutorials",
    expires_at: Math.floor(Date.now() / 1000) + 3600, // 1 hour
  }
);

Production Tips

  1. Use JSONL import for large datasets. Typesense supports bulk imports via JSONL format. For thousands of documents, this is significantly faster than individual inserts.

  2. Set up synonyms. Configure synonyms so "JS" matches "JavaScript" and "k8s" matches "Kubernetes". This improves search quality without requiring users to know exact terms.

  3. Enable analytics. Typesense Cloud provides search analytics. For self-hosted, log queries and click-through rates yourself to identify gaps in your content.

  4. Use geo-search if applicable. Typesense supports geographic search. If your content is location-based, add a geopoint field to sort results by proximity.

  5. Scale with replicas. For high-traffic sites, run Typesense in a cluster with 3 or 5 nodes for high availability. The raft consensus protocol handles leader election automatically.

Alternatives to Consider

  • Meilisearch if you prefer a more forgiving setup without strict schemas and want schemaless indexing.
  • Algolia if you want a fully managed service and your search volume fits within the free tier.
  • Pagefind if your site is fully static and you want zero-dependency, build-time search.

Wrapping Up

Typesense brings fast, typo-tolerant search to Astro sites with more control than most alternatives. The strict schema enforcement catches data issues early, the scoped API keys provide fine-grained access control, and the InstantSearch adapter means you get a polished UI with minimal effort. Whether you self-host or use Typesense Cloud, the search experience your visitors get will be fast, relevant, and forgiving of typos.