Search, vector, and geospatial
Full-text, vector, hybrid, and geographic queries.
Client definitions declare searchIndexes; you plan and apply them explicitly, then query through db.search(...). Results are SDK Search hits and metadata — not hydrated model documents. For numeric lat/lon GSI reads, use db.geo(...) with strategy: 'gsi'.
Declare a Search index
import { createCouchsetClient, defineModel } from 'couchset/next';
type Place = {
id: string;
name: string;
location: { lat: number; lon: number };
boundary?: unknown;
embedding: number[];
};
const places = defineModel<Place>({
name: 'Place',
searchIndexes: [
{
name: 'places_search',
scoped: true,
fields: {
name: { type: 'text', store: true },
location: { type: 'geopoint' },
boundary: { type: 'geoshape' },
embedding: { type: 'vector', dims: 384 },
},
},
],
});
const db = createCouchsetClient({ bucketName: 'app', models: [places] });
db.model(places);
const plan = await db.planSearchIndexes(); // read-only
await db.applySearchIndexPlan(plan);Field mappings support text, number, boolean, datetime, geopoint, geoshape, and vector. Vector fields require dims; similarity defaults to dot_product. Indexes cover the declared collection. Model Search queries add a keyword _type constraint.
Planning and apply
- Planning is read-only. Apply checks catalog UUIDs and refuses stale plans.
- Missing indexes are created explicitly.
- Drift replacement needs
allowReplace: true, may rebuild the index, and never auto-drops. - The planner compares managed mappings and partition count (normalizing catalog defaults). Server storage-tuning settings are not managed.
- Readiness means a Search query can execute, not that every mutation is indexed. Use consistency tokens or your own ingestion checks when freshness matters.
- Vector and scoped indexes need a compatible Couchbase version and Search service.
Query helpers
const search = db.search(places, 'places_search');
const textResult = await search.match('name', 'coffee');
const vectorResult = await search.vector('embedding', vector, {
filter,
searchQuery,
numCandidates: 20,
});
await search.withinRadius({
field: 'location',
center: { lat: 43.65, lon: -79.44 },
radius: '5km',
strategy: 'search',
});Every method returns the SDK SearchResult (rows / hits plus metadata). Stored fields remain untyped. query(raw, sdkOptions) exposes facets, highlighting, sort, limit / skip, and consistency.
| Helper | Notes |
|---|---|
match | Requires a declared text mapping |
vector | Finite values matching declared dims; numCandidates defaults to 10 (positive integer); filter defaults to match-all; optional searchQuery for hybrid text/Search |
| Geo helpers | See table below; each takes SDK SearchQueryOptions as the second argument |
Geo Search methods
| Method | Required arguments |
|---|---|
withinRadius | { field, center, radius, strategy: 'search' } on geopoint; radius like 5km, 3mi, 500m |
withinBox | { field, bounds: { north, south, east, west }, strategy: 'search' } on geopoint |
withinPolygon | { field, points: [at least 3 GeoPoint], strategy: 'search' } on geopoint |
shape | { field, geometry, relation, strategy: 'search' } on geoshape; relation is intersects, within, or contains |
Points use { lat, lon }. GeoJSON coordinate arrays use [lon, lat]. Geometry validation is ultimately enforced by Couchbase. SDK sorting can order by geo_distance. Search limit does not bound all candidate work — inspect SDK metadata for timing and hit counts.
Soft delete and defaultWhere
Search rejects definitions with SQL++ defaultWhere or softDelete. Those predicates cannot be translated safely. Use a separate Search-friendly definition and supply explicit Search filters. Do not treat model filters as authorization.
GSI geospatial (db.geo)
const geo = db.geo(places);
const box = await geo.withinBox({
field: 'location',
bounds: { north: 44, south: 43, east: -79, west: -80 },
strategy: 'gsi',
where: { status: 'open' },
limit: 10,
});
const nearby = await geo.withinRadius({
field: 'location',
center: { lat: 43.65, lon: -79.44 },
radius: '5km',
strategy: 'gsi',
});
// nearby: { document, distanceKm }[]| Path | Behavior |
|---|---|
withinBox + gsi | Model read over numeric field.lat / field.lon; returns parsed documents; default limit 10 |
withinRadius + gsi | Bounding prefilter + great-circle SQL++; default limit 20 / offset 0; ordered by distance then document id; returns { document, distanceKm }[] |
GSI geo retains the definition's defaultWhere / soft-delete precedence and parses codecs. Named scope views are separate — pass extra predicates with where. Provision numeric GSI indexes explicitly. Helpers handle antimeridian and polar bounds; distances assume a spherical Earth (6371 km).
Helpers: geoPoint, geoBounds, geoRadiusKm, radiusBounds validate or derive inputs.
Pitfalls
- Expecting Search hits to be hydrated models or fully typed stored fields.
- Declaring
softDelete/defaultWhereon a Search model — rejected at query time. - Mixing up point shapes:
{ lat, lon }vs GeoJSON[lon, lat]. - Treating Search readiness as “all recent writes are indexed.”
- Forgetting to provision GSI indexes for
strategy: 'gsi'geo reads.
See also
- Models and definitions —
searchIndexeson definitions - Provisioning and transactions — plan/apply patterns
- CRUD and queries — SQL++ filters vs Search
- API reference — Search and geo entrypoints
- Search notes (repo) — deeper unpublished notes