Quickstart
This is the precise, no-narration version of Build a search experience. Keyword-only search needs no LLM.
Prerequisites
Section titled “Prerequisites”- Bun 1.3+ (Node also works)
- Postgres 15+ with the required extensions
createdb samesake_devpsql samesake_dev -c "CREATE EXTENSION vector; CREATE EXTENSION pg_trgm; CREATE EXTENSION unaccent; CREATE EXTENSION fuzzystrmatch;"Install
Section titled “Install”bun add @samesake/core @samesake/servernpm install @samesake/core @samesake/serverpnpm add @samesake/core @samesake/serverDATABASE_URL=postgres://localhost:5432/samesake_devAPI_KEY=dev-key-please-changeBuild it
Section titled “Build it”-
Declare the catalog.
searchablefields feed keyword search;filterablefields become hard filters.catalog.ts import { collection, f, Channels } from "@samesake/core";export const products = collection("products", {fields: {title: f.text({ searchable: true }),brand: f.text({ filterable: true }),price: f.number({ filterable: true, budget: true }),color: f.text({ filterable: true }),available: f.boolean({ filterable: true }),},embeddings: {doc: { source: "$title $brand $color", model: "your-model", dim: 1536 },},search: {channels: [Channels.fts({ fields: ["title", "brand", "color"], weight: 1 }),Channels.cosine({ embedding: "doc", weight: 1 }),],combiner: "rrf",},}); -
Create the matcher. Supply your embedding function. For a keyword-only smoke test, a stub embedding is fine.
search.ts import { createMatcher } from "@samesake/server";import { products } from "./catalog.ts";const matcher = createMatcher({databaseUrl: process.env.DATABASE_URL!,apiKey: process.env.API_KEY!,embed: async ({ text, dim }) => stubEmbed(text, dim), // swap for a real model later}); -
Apply, push, index.
search.ts await matcher.apply("shop", { entities: [], collections: [products] });await matcher.pushDocuments("shop", "products", [{ id: "1", data: { title: "ivory linen slip dress", brand: "atelier", price: 12900, color: "ivory", available: true } },{ id: "2", data: { title: "black sequin party dress", brand: "luxe", price: 28000, color: "black", available: true } },]);await matcher.index("shop", "products"); -
Search.
search.ts const hits = await matcher.search("shop", "products", {q: "linen dress under 15000",filters: { available: true },limit: 10,});console.log(hits.map((h) => h.id));
Serve it over HTTP
Section titled “Serve it over HTTP”The same matcher exposes a web-standard fetch handler and a Hono app:
// Bun.serve / Workers / Vercel / Denoexport default { fetch: (req: Request) => matcher.fetch(req) };Where to go next
Section titled “Where to go next”- Build a search experience — the same thing, explained from scratch.
- Connect a real store: MedusaJS · headless Shopify · headless WooCommerce.
- Eval from search snapshots — measure keyword vs hybrid on a real corpus.