Skip to content

Quickstart

This is the precise, no-narration version of Build a search experience. Keyword-only search needs no LLM.

  • Bun 1.3+ (Node also works)
  • Postgres 15+ with the required extensions
Terminal window
createdb samesake_dev
psql samesake_dev -c "CREATE EXTENSION vector; CREATE EXTENSION pg_trgm; CREATE EXTENSION unaccent; CREATE EXTENSION fuzzystrmatch;"
Terminal window
bun add @samesake/core @samesake/postgres
.env
SAMESAKE_DATABASE_URL=postgres://localhost:5432/samesake_dev
  1. Declare the catalog. searchable fields feed keyword search; filterable fields become hard filters.

    catalog.ts
    import { collection, f, Channels, gates } 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 }),
    },
    // Required before indexing. Empty here — this data is already clean, so the
    // surfaces below read `data` directly (a messier catalog adds parse stages).
    enrich: { stages: [] },
    indexing: {
    surfaces: {
    embed_doc: { kind: "dense", embedding: "doc", build: ({ data }) => `${data.title} ${data.brand} ${data.color}`.trim() },
    fts_doc: { kind: "fts", build: ({ data }) => `${data.title} ${data.brand} ${data.color}`.trim() },
    },
    gate: gates.always,
    },
    embeddings: { doc: { model: "gemini-embedding-2", dim: 1536 } },
    search: {
    channels: [
    Channels.fts({ fields: ["title", "brand", "color"], weight: 1 }),
    Channels.cosine({ embedding: "doc", weight: 1 }),
    ],
    combiner: "rrf",
    nlq: { instructions: "Shopping queries for a fashion catalog. Extract price budgets and attributes." },
    },
    });
  2. Create the bundle. Supply your embedding function. For a keyword-only smoke test, a stub embedding is fine.

    search.ts
    import { samesake } from "@samesake/postgres";
    import { products } from "./catalog.ts";
    const app = samesake({
    url: process.env.SAMESAKE_DATABASE_URL!,
    collection: products,
    models: {
    embed: async ({ text, dim }) => stubEmbed(text, dim), // swap for a real model later
    },
    });
  3. Migrate, push, enrich.

    search.ts
    await app.migrate();
    await app.enrich.upsert([
    { 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 app.enrich.enrich();
  4. Search.

    search.ts
    const { hits } = await app.search("linen dress under 15000", {
    filters: { available: true },
    limit: 10,
    });
    console.log(hits.map((h) => h.id));

samesake is not a hosted service — there is no bundled fetch handler. Wrap app.search in whatever web-standard handler your runtime uses:

// Bun.serve / Workers / Vercel / Deno
export default {
fetch: async (req: Request) => {
const { searchParams } = new URL(req.url);
const result = await app.search(searchParams.get("q") ?? "", { limit: 10 });
return Response.json(result);
},
};