Search an app's own rows by meaning ("cheap places to eat near water" finds "budget seafood shack on the pier"), answer questions from the app's content (RAG), or rank "similar items". Two first-party pieces, no vector database, no API keys:

1. Migration: a vector column + index

-- migrations/002_docs.sql
CREATE TABLE docs (
  id         VARCHAR(20) PRIMARY KEY,
  title      TEXT NOT NULL,
  body       TEXT NOT NULL,
  embedding  vector(1536) NOT NULL
);
CREATE INDEX docs_embedding_idx ON docs USING hnsw (embedding vector_cosine_ops);

Always vector(1536) (the size embed() returns).

2. Write: embed when you insert

// functions/add-doc.js   (gipity.yaml: auth: member, tables: [docs], services: [embed])
export default async function (ctx, { db, embed, guid }) {
  const { title, body } = ctx.body;
  const v = await embed(title + '\n' + body);          // number[1536]
  await db.query('INSERT INTO docs (id, title, body, embedding) VALUES ($1, $2, $3, $4::vector)',
    [guid('doc'), title, body, JSON.stringify(v)]);      // JSON.stringify + ::vector
  return { ok: true };
}

3. Search: nearest by cosine distance

// functions/search.js   (gipity.yaml: auth: public, tables: [docs], services: [embed])
export default async function (ctx, { db, embed }) {
  const q = await embed(ctx.body.query);
  const { rows } = await db.query(
    `SELECT id, title, 1 - (embedding <=> $1::vector) AS score
       FROM docs ORDER BY embedding <=> $1::vector LIMIT 5`, [JSON.stringify(q)]);
  return { results: rows };
}

Who writes the rows

Decide this before the first deploy: a deploy flags any auth: public function that writes (fn-public-write).

RAG: answer from your rows

Search as above, then pass the top chunks to llm() (services: ['embed', 'llm'], see app-llm): put them in the prompt as numbered sources and ask it to answer only from them and cite the numbers.

Fuzzy text match (pg_trgm)

CREATE INDEX people_name_trgm ON people USING gin (name gin_trgm_ops);   -- migration

SELECT name FROM people WHERE name % $1 ORDER BY similarity(name, $1) DESC LIMIT 10 matches "Katharine Jonson" to "Katherine Johnson". Use it for names, titles, typos; use embed() for meaning.

Seed data, tests, cost