Search modes

Geosearch

Index coordinates as geopoints and filter by radius or polygon alongside a search term and every other query feature.

Table of Contents

A geopoint field holds a { lat, lon } pair, and geo conditions are ordinary filters, so they compose with a term, other filters, facets, and every other query feature. A filter refines a search rather than running on its own, so each query below pairs the term market with a location filter. This page builds one file, stores.ts, step by step. You index a handful of markets around Toronto, find the ones within a radius, then switch that radius for a polygon.

Index locations

Start stores.ts with an engine, a stores index that declares a geopoint field, and a few markets with real coordinates around Toronto.

stores.ts
import { createNarsil } from '@delali/narsil'
 
const narsil = await createNarsil()
 
await narsil.createIndex('stores', {
  schema: {
    name: 'string',
    location: 'geopoint',
  },
})
 
await narsil.insertBatch('stores', [
  { id: 'st-lawrence', name: 'St. Lawrence Market', location: { lat: 43.6487, lon: -79.3716 } },
  { id: 'kensington', name: 'Kensington Market', location: { lat: 43.6547, lon: -79.4005 } },
  { id: 'markham', name: 'Markham Farmers Market', location: { lat: 43.8561, lon: -79.337 } },
  { id: 'mississauga', name: 'Mississauga Chinatown Market', location: { lat: 43.589, lon: -79.6441 } },
])

Search by radius

Add a query that searches for market and keeps every result within five kilometres of a point in downtown Toronto. The radius filter takes a centre, a distance, and a unit. The print rounds the scores to two places but leaves lat and lon untouched, so the coordinates stay exact.

stores.ts
import { createNarsil } from '@delali/narsil'
 
const narsil = await createNarsil()
 
await narsil.createIndex('stores', {
  schema: {
    name: 'string',
    location: 'geopoint',
  },
})
 
await narsil.insertBatch('stores', [
  { id: 'st-lawrence', name: 'St. Lawrence Market', location: { lat: 43.6487, lon: -79.3716 } },
  { id: 'kensington', name: 'Kensington Market', location: { lat: 43.6547, lon: -79.4005 } },
  { id: 'markham', name: 'Markham Farmers Market', location: { lat: 43.8561, lon: -79.337 } },
  { id: 'mississauga', name: 'Mississauga Chinatown Market', location: { lat: 43.589, lon: -79.6441 } },
])
 
const nearby = await narsil.query('stores', { 
  term: 'market',
  filters: {
    fields: {
      location: {
        radius: { lat: 43.652, lon: -79.385, distance: 5, unit: 'km' },
      },
    },
  },
})
 
console.log(JSON.stringify(nearby, (k, v) => (typeof v === 'number' && k !== 'lat' && k !== 'lon' ? Number(v.toFixed(2)) : v), 2))

The two downtown markets fall inside the circle, and both score on the term market in their names. Markham and Mississauga are well beyond five kilometres, so they drop out. Radius filters measure Haversine distance by default and accept unit: 'km' | 'mi' | 'm'. Set highPrecision: true to switch to Vincenty's iterative formula for long-distance accuracy.

Search by polygon

A radius covers a circle, but a delivery zone or a district boundary is rarely a neat circle. Swap the radius filter for a polygon filter that lists the corners of the area you care about. The rest of the query stays the same.

stores.ts
import { createNarsil } from '@delali/narsil'
 
const narsil = await createNarsil()
 
await narsil.createIndex('stores', {
  schema: {
    name: 'string',
    location: 'geopoint',
  },
})
 
await narsil.insertBatch('stores', [
  { id: 'st-lawrence', name: 'St. Lawrence Market', location: { lat: 43.6487, lon: -79.3716 } },
  { id: 'kensington', name: 'Kensington Market', location: { lat: 43.6547, lon: -79.4005 } },
  { id: 'markham', name: 'Markham Farmers Market', location: { lat: 43.8561, lon: -79.337 } },
  { id: 'mississauga', name: 'Mississauga Chinatown Market', location: { lat: 43.589, lon: -79.6441 } },
])
 
const nearby = await narsil.query('stores', {
  term: 'market',
  filters: {
    fields: {
      location: {
        radius: { lat: 43.652, lon: -79.385, distance: 5, unit: 'km' }, 
        polygon: { 
          points: [
            { lat: 43.63, lon: -79.42 },
            { lat: 43.63, lon: -79.36 },
            { lat: 43.68, lon: -79.36 },
            { lat: 43.68, lon: -79.42 },
          ],
        },
      },
    },
  },
})
 
console.log(JSON.stringify(nearby, (k, v) => (typeof v === 'number' && k !== 'lat' && k !== 'lon' ? Number(v.toFixed(2)) : v), 2))

Polygon filters test containment with ray casting, so the points describe the boundary in order and the filter returns every market inside it. The downtown box holds the same two markets the radius did.

Invert the match

Both filter shapes accept inside: false to flip the result and return the documents outside the area instead. This variation stands on its own; keep the same stores index and centre point when you try it.

const outside = await narsil.query('stores', {
  term: 'market',
  filters: {
    fields: {
      location: {
        radius: { lat: 43.652, lon: -79.385, distance: 5, unit: 'km', inside: false },
      },
    },
  },
})
 
console.log(JSON.stringify(outside, (k, v) => (typeof v === 'number' && k !== 'lat' && k !== 'lon' ? Number(v.toFixed(2)) : v), 2))

Flipping the radius returns the two markets outside the circle, Markham to the north and Mississauga to the west, in place of the downtown pair.