Claude Code for Typesense: Open-Source Instant Search — Claude Skills 360 Blog
Blog / AI / Claude Code for Typesense: Open-Source Instant Search
AI

Claude Code for Typesense: Open-Source Instant Search

Published: July 15, 2027
Read time: 5 min read
By: Claude Skills 360

Typesense delivers sub-millisecond typo-tolerant search — new Typesense.Client({ nodes: [{ host, port, protocol }], apiKey }) initializes the client. Schema creation: client.collections().create({ name, fields: [{ name, type, facet?, sort? }], default_sorting_field }). Import: client.collections("products").documents().import(docs, { action: "upsert" }). Search: client.collections("products").documents().search({ q: query, query_by: "title,description", filter_by: "price:<100 && inStock:true", sort_by: "price:asc", facet_by: "category,brand", per_page: 20 }). Response: result.hits with [{ document, highlights, text_match }], result.facet_counts for aggregations. Multi-search: client.multiSearch.perform({ searches: [params1, params2] }) runs parallel queries. Geo search: add { name: "_geo", type: "geopoint" } field, filter filter_by: "_geoRadius(lat, lng, radiusMeters)", sort sort_by: "_geoPoint(lat,lng):asc". Curations: client.collections("products").overrides().upsert(id, { rule, includes, excludes }) pins or buries results. typesenseInstantsearchAdapter bridges to react-instantsearch. Claude Code generates Typesense instant search hooks, faceted navigation, and multi-collection federated search.

CLAUDE.md for Typesense

## Typesense Stack
- Version: typesense >= 3.x
- Init: const client = new Typesense.Client({ nodes: [{ host: process.env.TYPESENSE_HOST!, port: 443, protocol: "https" }], apiKey: process.env.TYPESENSE_API_KEY!, connectionTimeoutSeconds: 2 })
- Search: const results = await client.collections("products").documents().search({ q, query_by: "title,description", filter_by: "inStock:true", per_page: 20, facet_by: "category" })
- Hits: results.hits — [{ document: Record<string, unknown>, highlights: [{ field, snippet }], text_match: number }]
- Import docs: await client.collections("products").documents().import(docs, { action: "upsert" })
- Create collection: await client.collections().create(schema) — schema has name, fields[], default_sorting_field

Typesense Client

// lib/typesense/client.ts — Typesense SDK helpers with TypeScript
import Typesense from "typesense"
import type { SearchParams, SearchResponse } from "typesense/lib/Typesense/Documents"
import type { CollectionCreateSchema } from "typesense/lib/Typesense/Collections"

export const tsClient = new Typesense.Client({
  nodes: [
    {
      host: process.env.TYPESENSE_HOST!,
      port: 443,
      protocol: "https",
    },
  ],
  apiKey: process.env.TYPESENSE_API_KEY!,
  connectionTimeoutSeconds: 2,
})

// ── Collection management ──────────────────────────────────────────────────

export const PRODUCT_SCHEMA: CollectionCreateSchema = {
  name: "products",
  fields: [
    { name: "id",           type: "string" },
    { name: "title",        type: "string"  },
    { name: "description",  type: "string"  },
    { name: "brand",        type: "string",  facet: true },
    { name: "category",     type: "string",  facet: true },
    { name: "price",        type: "float",   facet: true, sort: true },
    { name: "rating",       type: "float",               sort: true },
    { name: "inStock",      type: "bool",    facet: true },
    { name: "tags",         type: "string[]", facet: true },
    { name: "_geo",         type: "geopoint", optional: true },
    { name: "createdAt",    type: "int64",               sort: true },
  ],
  default_sorting_field: "rating",
}

export async function ensureCollection(schema: CollectionCreateSchema): Promise<void> {
  try {
    await tsClient.collections(schema.name).retrieve()
  } catch {
    await tsClient.collections().create(schema)
  }
}

export async function dropCollection(name: string): Promise<void> {
  await tsClient.collections(name).delete()
}

// ── Document operations ────────────────────────────────────────────────────

export async function upsertDocuments<T extends { id: string }>(
  collection: string,
  documents: T[],
): Promise<void> {
  const BATCH = 1000
  for (let i = 0; i < documents.length; i += BATCH) {
    const batch = documents.slice(i, i + BATCH)
    await tsClient.collections(collection).documents().import(batch, { action: "upsert" })
  }
}

export async function deleteDocument(collection: string, id: string): Promise<void> {
  await tsClient.collections(collection).documents(id).delete()
}

// ── Search ─────────────────────────────────────────────────────────────────

export type SearchOptions = {
  query: string
  queryBy: string | string[]
  filterBy?: string
  sortBy?: string
  facetBy?: string | string[]
  page?: number
  perPage?: number
  highlightFields?: string | string[]
  numTypos?: number
  prefix?: boolean
  geoPoint?: { lat: number; lng: number; radiusMeters: number }
}

export type SearchResult<T> = {
  hits: Array<{ document: T; highlights: Array<{ field: string; snippet: string; value?: string }>; textMatch: number }>
  found: number
  page: number
  searchTimeMs: number
  facetCounts?: Array<{ fieldName: string; counts: Array<{ value: string; count: number }> }>
}

export async function search<T extends Record<string, unknown>>(
  collection: string,
  options: SearchOptions,
): Promise<SearchResult<T>> {
  const {
    query,
    queryBy,
    filterBy,
    sortBy,
    facetBy,
    page = 1,
    perPage = 20,
    highlightFields,
    numTypos,
    prefix,
    geoPoint,
  } = options

  const qb = Array.isArray(queryBy) ? queryBy.join(",") : queryBy
  const fb = Array.isArray(facetBy) ? facetBy.join(",") : facetBy
  const hf = Array.isArray(highlightFields) ? highlightFields.join(",") : highlightFields

  let filter = filterBy ?? ""
  if (geoPoint) {
    const gf = `_geoRadius(${geoPoint.lat}, ${geoPoint.lng}, ${geoPoint.radiusMeters})`
    filter = filter ? `${filter} && ${gf}` : gf
  }

  const params: SearchParams = {
    q: query,
    query_by: qb,
    page,
    per_page: perPage,
    ...(filter           ? { filter_by: filter }                                        : {}),
    ...(sortBy           ? { sort_by: sortBy }                                          : {}),
    ...(fb               ? { facet_by: fb }                                             : {}),
    ...(hf               ? { highlight_fields: hf }                                     : {}),
    ...(numTypos !== undefined ? { num_typos: numTypos }                                : {}),
    ...(prefix !== undefined  ? { prefix: String(prefix) as "true" | "false" }         : {}),
    ...(geoPoint         ? { sort_by: sortBy ?? `_geoPoint(${geoPoint.lat},${geoPoint.lng}):asc` } : {}),
  }

  const raw = (await tsClient
    .collections(collection)
    .documents()
    .search(params)) as SearchResponse<T>

  return {
    hits: (raw.hits ?? []).map((h) => ({
      document: h.document,
      highlights: h.highlights ?? [],
      textMatch: h.text_match ?? 0,
    })),
    found: raw.found,
    page: raw.page,
    searchTimeMs: raw.search_time_ms,
    facetCounts: raw.facet_counts?.map((fc) => ({
      fieldName: fc.field_name,
      counts: fc.counts.map((c) => ({ value: c.value, count: c.count })),
    })),
  }
}

/** Multi-search: execute multiple searches in a single request */
export async function multiSearch<T extends Record<string, unknown>>(
  searches: Array<{ collection: string; options: SearchOptions }>,
): Promise<SearchResult<T>[]> {
  const queries = searches.map(({ collection, options }) => ({
    collection,
    q: options.query,
    query_by: Array.isArray(options.queryBy) ? options.queryBy.join(",") : options.queryBy,
    per_page: options.perPage ?? 5,
    ...(options.filterBy ? { filter_by: options.filterBy } : {}),
  }))

  const results = await tsClient.multiSearch.perform({ searches: queries }, {})
  return (results.results as SearchResponse<T>[]).map((raw) => ({
    hits: (raw.hits ?? []).map((h) => ({ document: h.document, highlights: h.highlights ?? [], textMatch: h.text_match ?? 0 })),
    found: raw.found,
    page: raw.page,
    searchTimeMs: raw.search_time_ms,
  }))
}

React InstantSearch Adapter

// components/search/TypesenseSearch.tsx — react-instantsearch with Typesense adapter
"use client"
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter"
import {
  InstantSearch,
  SearchBox,
  Hits,
  RefinementList,
  CurrentRefinements,
  useInstantSearch,
} from "react-instantsearch"
import type { Hit } from "instantsearch.js"

const adapter = new TypesenseInstantSearchAdapter({
  server: {
    apiKey: process.env.NEXT_PUBLIC_TYPESENSE_SEARCH_KEY!,
    nodes: [{ host: process.env.NEXT_PUBLIC_TYPESENSE_HOST!, port: 443, protocol: "https" }],
  },
  additionalSearchParameters: {
    query_by: "title,description",
    highlight_full_fields: "title",
    num_typos: 2,
  },
})

type ProductDoc = { id: string; title: string; description: string; brand: string; price: number }

function ProductHit({ hit }: { hit: Hit<ProductDoc> }) {
  return (
    <div className="rounded-lg border p-4">
      <h3 className="font-semibold" dangerouslySetInnerHTML={{ __html: hit._highlightResult?.title?.value ?? hit.title }} />
      <p className="text-sm text-gray-500 mt-1 line-clamp-2">{hit.description}</p>
      <div className="flex justify-between items-center mt-2">
        <span className="text-xs text-gray-400">{hit.brand}</span>
        <span className="font-bold text-indigo-600">${hit.price}</span>
      </div>
    </div>
  )
}

function Stats() {
  const { results } = useInstantSearch()
  return results?.nbHits != null ? (
    <p className="text-sm text-gray-400">{results.nbHits} results in {results.processingTimeMS}ms</p>
  ) : null
}

export default function TypesenseSearch() {
  return (
    <InstantSearch searchClient={adapter.searchClient} indexName="products">
      <SearchBox classNames={{ input: "w-full rounded-xl border px-4 py-3 mb-4 text-lg focus:ring-2 focus:ring-indigo-400" }} placeholder="Search…" />
      <CurrentRefinements />
      <div className="flex gap-6">
        <aside className="w-44 shrink-0">
          <p className="text-xs uppercase tracking-wide text-gray-400 font-semibold mb-1">Category</p>
          <RefinementList attribute="category" />
          <p className="text-xs uppercase tracking-wide text-gray-400 font-semibold mt-4 mb-1">Brand</p>
          <RefinementList attribute="brand" showMore />
        </aside>
        <main className="flex-1">
          <Stats />
          <Hits hitComponent={ProductHit} classNames={{ list: "grid grid-cols-2 lg:grid-cols-3 gap-4 mt-2" }} />
        </main>
      </div>
    </InstantSearch>
  )
}

For the Meilisearch alternative when self-hosting open-source search under the SSPL license is acceptable, or preferring Meilisearch’s slightly simpler index settings API and built-in UI dashboard — Meilisearch and Typesense are functionally near-identical with Typesense offering a more permissive GPL-3 license, both use the react-instantsearch adapter, see the Meilisearch guide. For the Algolia alternative when needing a fully-managed cloud search service with AI-powered ranking, A/B testing, and Algolia’s enterprise SLAs with no infrastructure to operate — Algolia is the leading managed service while Typesense is its open-source self-hosted alternative with compatible react-instantsearch components, see the Algolia guide. The Claude Skills 360 bundle includes Typesense skill sets covering instant search, facets, and multi-search. Start with the free tier to try Typesense search generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free