Site search on Postgres full-text: per-language stemming, prefix matching, safe snippets and typo fallback
Install
genpm add @core/searchWhat you get
- Source in src/lib/search/, 8 files. (13.2 kB)
- AI rules in src/lib/search/AGENTS.md, plus IDE rule files.
- Resolves @core/contracts, @core/db, @core/jobs for you.
README
This package has no README.
This is exactly what your AI reads when it works in src/lib/search. Nothing else is added to its context.
@core/search — rules for AI agents
Purpose
Site search on Postgres full-text search: one search_documents table fed by any SearchSource (@core/contracts),
weighted title/body vectors with the stemmer of each document's language, prefix matching for search-as-you-type,
snippets returned as plain parts (never HTML) and, if the pg_trgm extension exists, a typo-tolerant fallback on
titles. No external engine, no semantic search.
Map
index.ts— public API:search,registerSearchSource,reindex,indexDocument,removeDocument,reindexJob.search.ts— indexing and querying.schema.ts— table and GIN index.adapters/hono.ts—searchRoutes().adapters/next.ts—searchRoute(GET).
Integration
- Generate and apply migrations (see
src/lib/db/AGENTS.md). Optional typo tolerance: runcreate extension if not exists pg_trgm;once (available on Neon, Supabase, RDS). - Register sources in the project file
src/genpm/search.ts, imported at startup:registerSearchSource(contentSearchSource('pages', { path: ({ slug }) =>/${slug}})). - Build the index once and then daily:
await reindexJob.enqueue({})andschedule('search.reindex', '0 3 * * *')(@core/jobs). - Query:
const { hits } = await search(q, { locale }), or mountsearchRoutes()/searchRouteat/api/search. - Render snippets by mapping parts:
hit.snippet.map(p => p.match ? <mark>{p.text}</mark> : p.text). - Verify: index a document and find it by the first letters of a word in its title.
Conventions
- Sources yield plain text bodies (strip Markdown/HTML first, e.g. with
toPlainTextof @core/rich-text). - Pass
localewhen you know it: the query uses that language's stemmer and the GIN index. - Only index published, public content; private data never goes into
search_documents.
Don't
- Don't build SQL from the query string;
search()already sanitizes it to words. - Don't render snippets with
dangerouslySetInnerHTML. - Don't call
reindex()inside a request: use the job.
# @core/search — rules for AI agents
## Purpose
Site search on Postgres full-text search: one `search_documents` table fed by any `SearchSource` (@core/contracts),
weighted title/body vectors with the stemmer of each document's language, prefix matching for search-as-you-type,
snippets returned as plain parts (never HTML) and, if the `pg_trgm` extension exists, a typo-tolerant fallback on
titles. No external engine, no semantic search.
## Map
- `index.ts` — public API: `search`, `registerSearchSource`, `reindex`, `indexDocument`, `removeDocument`, `reindexJob`.
- `search.ts` — indexing and querying. `schema.ts` — table and GIN index.
- `adapters/hono.ts` — `searchRoutes()`. `adapters/next.ts` — `searchRoute` (GET).
## Integration
1. Generate and apply migrations (see `src/lib/db/AGENTS.md`). Optional typo tolerance: run
`create extension if not exists pg_trgm;` once (available on Neon, Supabase, RDS).
2. Register sources in the project file `src/genpm/search.ts`, imported at startup:
`registerSearchSource(contentSearchSource('pages', { path: ({ slug }) => `/${slug}` }))`.
3. Build the index once and then daily: `await reindexJob.enqueue({})` and `schedule('search.reindex', '0 3 * * *')` (@core/jobs).
4. Query: `const { hits } = await search(q, { locale })`, or mount `searchRoutes()` / `searchRoute` at `/api/search`.
5. Render snippets by mapping parts: `hit.snippet.map(p => p.match ? <mark>{p.text}</mark> : p.text)`.
6. Verify: index a document and find it by the first letters of a word in its title.
## Conventions
- Sources yield plain text bodies (strip Markdown/HTML first, e.g. with `toPlainText` of @core/rich-text).
- Pass `locale` when you know it: the query uses that language's stemmer and the GIN index.
- Only index published, public content; private data never goes into `search_documents`.
## Don't
- Don't build SQL from the query string; `search()` already sanitizes it to words.
- Don't render snippets with `dangerouslySetInnerHTML`.
- Don't call `reindex()` inside a request: use the job.
The exact tree that will be injected, after .genpmignore. Pinned to
This package declares no MCP servers.
| Version | Commit | Published | Scan |
|---|---|---|---|
| 1.0.0 | ae66128 | 1 hour ago | scan passed |
- npm
- zod ^4.0.0
- proposed
- GenPM proposes the npm command and runs it only if you say yes.
- Used by (1)
- @core/kit-cms ^1.0.0
- scan
- scan passed · 0 findings
- commit
- v1.0.0 → ae6612826aed11b87040adc5f891b5bf6e858c58 · verified after fetch
- scripts
- None. GenPM never runs package code.
- license
- MIT
- Quality
- 100/100
- Recognized licensepassed
- AGENTS.md explains its purposepassed
- AGENTS.md has integration stepspassed
- AGENTS.md lists conventions or don'tspassed
- Includes testspassed
- Security scan passedpassed
- Released in the last 6 monthspassed
- Verified publisherpassed
- Summary and keywordspassed
- report
- See something wrong?