Search API
Government global search across four entity types
Search is an authenticated government convenience endpoint. It searches organizations, submissions, engagements, and Calls for Technology using a hybrid backend: PostgreSQL full-text search runs first — with boolean operators (AND implicit between words, OR, -word or !word negation), "quoted phrase" matching, and prefix* wildcards, plus an implicit prefix match on the last word of an operator-free query so type-ahead keeps matching a partially typed word — and when it finds nothing the search degrades to typo-tolerant pg_trgm trigram fuzzy matching (word_similarity() / %> over GIN trigram indexes), so misspelled queries like autonmy still match "autonomy" records. Queries using operator syntax never fall through to the fuzzy stage (it is operator-blind and would match excluded terms), so an operator query with no full-text matches returns an empty result. The fuzzy threshold defaults to 0.2 and is tunable via SEARCH_SIMILARITY_THRESHOLD; either stage is selectable on its own via SEARCH_BACKEND. All backends preserve this response contract.
GET /api/search
Auth: Any accepted government role. Success: 200.
| Query field | Type | Required? | Constraints |
|---|---|---|---|
q | string | Yes | 1–255 characters. |
limit | integer | No | Coerced integer, 1–50; default backend limit 10. |
types | CSV enum list | No | organization, submission, engagement, call; invalid values reject. |
curl 'http://localhost:3000/api/search?q=Pelagic&limit=10&types=organization,submission' \
-H 'cookie: next-auth.session-token=GOV_SESSION'Example:
{"data":{"query":"Pelagic","total":2,"groups":[{"type":"organization","label":"Organizations","items":[{"type":"organization","id":"clx-pelagic","title":"Pelagic Dynamics","snippet":"Small business building low-cost autonomous undersea vehicles...","url":"/gov/organizations/clx-pelagic","meta":"COMPANY · ACTIVE · uuv,autonomy,acoustics"}]}]}}The backend matches names/descriptions/tags for organizations, titles/summaries/tags for submissions, titles/summaries/location for engagements, and titles/descriptions/tags for calls. The full-text stage ranks by ts_rank relevance; the fuzzy stage scores the best-matching extent within a field, so a term buried inside a long description still matches. Each group is capped by the per-type limit, snippets are at most 160 characters, and results are grouped with a total.
Errors: 401 unauthenticated, 400 missing/too-long query or invalid type, 500 unexpected search/database failure. Search does not bypass destination-page authorization: opening a URL still applies the page's role and command scope.
Result item fields
| Field | Meaning |
|---|---|
type | One of organization, submission, engagement, call. |
id | Prisma record ID. |
title | Display title/name. |
snippet | Up to 160 characters, centered on a literal query occurrence when present (fuzzy-only matches fall back to the leading text). |
url | In-app destination. |
meta | Optional compact status, organization, command, date, or tags. |
The result contract deliberately avoids returning full record bodies. The destination page performs the deeper read and its own authorization check.