A geopoint field holds a latitude and a longitude, and a geo test on it is an ordinary filter, so it combines with a search term, with other filters, and with facets. You build one file here, depots.ts, for a courier with depots in Ghana, Nigeria, Fiji, and Tonga, in which you keep the depots inside a radius and inside a polygon, flip the test, and filter across the antimeridian. Each code block below holds the whole file, with green lines marking what each step adds and struck lines marking what it takes away.
Store a location with each document
A geopoint takes an object with lat and lon in degrees. The engine refuses a latitude outside -90 to 90, a longitude outside -180 to 180, a coordinate that is not a number, and an array in place of the object, each with SCHEMA_INVALID_GEOPOINT. A radius filter takes a centre, a distance, and a unit of km, mi, or m, and under the query below the engine keeps the depots within 50 km of central Accra.
import { createNarsil } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('depots', {
schema: { name: 'string', city: 'string', location: 'geopoint' },
})
await narsil.insertBatch('depots', [
{ id: 'accra-central', name: 'Accra central depot', city: 'Accra', location: { lat: 5.5502, lon: -0.2174 } },
{ id: 'tema-harbour', name: 'Tema harbour depot', city: 'Tema', location: { lat: 5.6698, lon: -0.0166 } },
{ id: 'kumasi-north', name: 'Kumasi north depot', city: 'Kumasi', location: { lat: 6.6885, lon: -1.6244 } },
{ id: 'lagos-island', name: 'Lagos island depot', city: 'Lagos', location: { lat: 6.4541, lon: 3.3947 } },
{ id: 'suva-walu', name: 'Suva Walu Bay depot', city: 'Suva', location: { lat: -18.1416, lon: 178.4419 } },
{ id: 'nukualofa-mau', name: "Nuku'alofa depot", city: "Nuku'alofa", location: { lat: -21.1393, lon: -175.2049 } },
])
const results = await narsil.query('depots', {
term: 'depot',
filters: { fields: { location: { radius: { lat: 5.5502, lon: -0.2174, distance: 50, unit: 'km' } } } },
document: false,
})
console.log(JSON.stringify({ count: results.count, ids: results.hits.map((hit) => hit.id) }, null, 2))
await narsil.shutdown(){
"count": 2,
"ids": [
"accra-central",
"tema-harbour"
]
}The Tema depot lies close enough to fall inside the circle with the Accra depot, while the Kumasi and Lagos depots lie outside it. The engine measures a radius on a sphere by default, while with highPrecision: true it measures on the WGS-84 ellipsoid, which moves the distance from central Accra to the Kumasi depot from 200.55 km to 200.25 km and the distance to the Lagos depot from 411.89 km to 412.21 km. A unit outside km, mi, and m fails with SEARCH_INVALID_FILTER.
Keep the documents inside a polygon
A polygon filter takes its corners in order, and the engine closes the ring for you. List the corners counter-clockwise around the area, the order that GeoJSON uses, although for a small area such as this box around southern Ghana the engine matches the same depots in either order. A ring of fewer than three corners fails with SEARCH_INVALID_FILTER.
import { createNarsil } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('depots', {
schema: { name: 'string', city: 'string', location: 'geopoint' },
})
await narsil.insertBatch('depots', [
{ id: 'accra-central', name: 'Accra central depot', city: 'Accra', location: { lat: 5.5502, lon: -0.2174 } },
{ id: 'tema-harbour', name: 'Tema harbour depot', city: 'Tema', location: { lat: 5.6698, lon: -0.0166 } },
{ id: 'kumasi-north', name: 'Kumasi north depot', city: 'Kumasi', location: { lat: 6.6885, lon: -1.6244 } },
{ id: 'lagos-island', name: 'Lagos island depot', city: 'Lagos', location: { lat: 6.4541, lon: 3.3947 } },
{ id: 'suva-walu', name: 'Suva Walu Bay depot', city: 'Suva', location: { lat: -18.1416, lon: 178.4419 } },
{ id: 'nukualofa-mau', name: "Nuku'alofa depot", city: "Nuku'alofa", location: { lat: -21.1393, lon: -175.2049 } },
])
const results = await narsil.query('depots', {
term: 'depot',
filters: { fields: { location: { radius: { lat: 5.5502, lon: -0.2174, distance: 50, unit: 'km' } } } },
filters: {
fields: {
location: {
polygon: {
points: [
{ lat: 4.5, lon: -3 },
{ lat: 4.5, lon: 1 },
{ lat: 8, lon: 1 },
{ lat: 8, lon: -3 },
],
},
},
},
},
document: false,
})
console.log(JSON.stringify({ count: results.count, ids: results.hits.map((hit) => hit.id) }, null, 2))
await narsil.shutdown(){
"count": 3,
"ids": [
"accra-central",
"kumasi-north",
"tema-harbour"
]
}The box encloses Kumasi as well as Accra and Tema, while Lagos lies east of it. A radius and a polygon on the same field both apply, so a document has to pass both tests.
Keep the documents outside an area
With inside: false on either test, the engine keeps the documents outside the area.
import { createNarsil } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('depots', {
schema: { name: 'string', city: 'string', location: 'geopoint' },
})
await narsil.insertBatch('depots', [
{ id: 'accra-central', name: 'Accra central depot', city: 'Accra', location: { lat: 5.5502, lon: -0.2174 } },
{ id: 'tema-harbour', name: 'Tema harbour depot', city: 'Tema', location: { lat: 5.6698, lon: -0.0166 } },
{ id: 'kumasi-north', name: 'Kumasi north depot', city: 'Kumasi', location: { lat: 6.6885, lon: -1.6244 } },
{ id: 'lagos-island', name: 'Lagos island depot', city: 'Lagos', location: { lat: 6.4541, lon: 3.3947 } },
{ id: 'suva-walu', name: 'Suva Walu Bay depot', city: 'Suva', location: { lat: -18.1416, lon: 178.4419 } },
{ id: 'nukualofa-mau', name: "Nuku'alofa depot", city: "Nuku'alofa", location: { lat: -21.1393, lon: -175.2049 } },
])
const results = await narsil.query('depots', {
term: 'depot',
filters: {
fields: {
location: {
polygon: {
points: [
{ lat: 4.5, lon: -3 },
{ lat: 4.5, lon: 1 },
{ lat: 8, lon: 1 },
{ lat: 8, lon: -3 },
],
inside: false,
},
},
},
},
document: false,
})
console.log(JSON.stringify({ count: results.count, ids: results.hits.map((hit) => hit.id) }, null, 2))
await narsil.shutdown(){
"count": 3,
"ids": [
"nukualofa-mau",
"lagos-island",
"suva-walu"
]
}The three depots outside southern Ghana come back. The engine returns only the documents that hold a location outside the area, so a document with no location appears under neither form of the test.
Filter across the antimeridian without a search term
A query with no term matches nothing, even under a geo filter, so listDocuments is the call that filters an index by location alone. The box in this step crosses the antimeridian, the line where longitude passes from 180 to -180, so its corners go from longitude 170 eastwards to -170.
import { createNarsil } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('depots', {
schema: { name: 'string', city: 'string', location: 'geopoint' },
})
await narsil.insertBatch('depots', [
{ id: 'accra-central', name: 'Accra central depot', city: 'Accra', location: { lat: 5.5502, lon: -0.2174 } },
{ id: 'tema-harbour', name: 'Tema harbour depot', city: 'Tema', location: { lat: 5.6698, lon: -0.0166 } },
{ id: 'kumasi-north', name: 'Kumasi north depot', city: 'Kumasi', location: { lat: 6.6885, lon: -1.6244 } },
{ id: 'lagos-island', name: 'Lagos island depot', city: 'Lagos', location: { lat: 6.4541, lon: 3.3947 } },
{ id: 'suva-walu', name: 'Suva Walu Bay depot', city: 'Suva', location: { lat: -18.1416, lon: 178.4419 } },
{ id: 'nukualofa-mau', name: "Nuku'alofa depot", city: "Nuku'alofa", location: { lat: -21.1393, lon: -175.2049 } },
])
const results = await narsil.query('depots', {
term: 'depot',
const listing = await narsil.listDocuments('depots', {
filters: {
fields: {
location: {
polygon: {
points: [
{ lat: 4.5, lon: -3 },
{ lat: 4.5, lon: 1 },
{ lat: 8, lon: 1 },
{ lat: 8, lon: -3 },
{ lat: -25, lon: 170 },
{ lat: -25, lon: -170 },
{ lat: -15, lon: -170 },
{ lat: -15, lon: 170 },
],
inside: false,
},
},
},
},
document: false,
})
console.log(JSON.stringify({ count: results.count, ids: results.hits.map((hit) => hit.id) }, null, 2))
console.log(JSON.stringify({ total: listing.total, ids: listing.documents.map((entry) => entry.id) }, null, 2))
await narsil.shutdown(){
"total": 2,
"ids": [
"nukualofa-mau",
"suva-walu"
]
}Suva and Nuku'alofa come back, because the engine matches the area on the left of the ring's path wherever the corners span 180 degrees of longitude or more, and these corners go round that area counter-clockwise. Listed clockwise, the same corners describe the band of latitude that wraps the other way round the globe, which holds none of the depots. A radius needs no such care, because the engine measures its distance across the line directly, so an 800 km circle around Suva encloses Nuku'alofa.
The engine returns no distance with a hit, and a sort on a geopoint field fails with SEARCH_INVALID_FIELD, so compute the distance in your own code wherever you need to order hits by it.
Where to go next
- Filters and facets covers the other filters that combine with a location test.
- Indexes and documents covers the listing that pages through an index without a search term.