@meetoza28/adonis-geo-search
v1.0.1
Published
High-performance AdonisJS geospatial search — radius filtering, nearest-to sorting, and distance calculation using MySQL ST_Distance_Sphere with bounding-box pre-indexing
Downloads
188
Maintainers
Readme
@meetoza28/adonis-geo-search
🔥 Production-ready geo search for scalable apps
A high-performance AdonisJS v6 geospatial search package — adds radius-based filtering, nearest-to sorting, and distance calculation to any Lucid ORM model using MySQL
ST_Distance_Spherewith optimized bounding-box pre-indexing.
✨ Why use this?
- 🚀 200x faster than naive geo queries
- 📦 Plug-and-play with AdonisJS Lucid ORM
- 🎯 Accurate radius filtering using MySQL native functions
- ⚡ Optimized with bounding-box pre-filtering
📦 Installation
npm install @meetoza28/adonis-geo-searchRequirements:
- AdonisJS v6 with
@adonisjs/lucid ^21.0.0- MySQL 8+ or MariaDB 10.5+
latitudeandlongitudenumeric columns on your model's table
🚀 Quick Start
Step 1 — Add mixin to your model
import { BaseModel, column } from '@adonisjs/lucid/orm'
import { GeoSearchMixin } from '@meetoza28/adonis-geo-search'
export default class Property extends GeoSearchMixin(BaseModel) {
@column({ isPrimary: true })
declare id: string
@column()
declare latitude: number | null
@column()
declare longitude: number | null
}Step 2 — Add database index
// In your migration file:
table.index(['latitude', 'longitude'])Step 3 — Use geo scopes
const properties = await Property.query()
.where('status', 'published')
.apply(s => s.withinRadius({ lat: 21.17, lng: 72.83, radiusKm: 5 }))
.apply(s => s.nearestTo({ lat: 21.17, lng: 72.83 }))
.apply(s => s.distanceFrom(21.17, 72.83))
.limit(20)
properties[0].$extras.distance_km // → '0.843'📚 API Reference
GeoSearchMixin
Extends any Lucid BaseModel with three geospatial query scopes.
.withinRadius(query, options)
Filter rows within a radius. Uses bounding box pre-filter + exact ST_Distance_Sphere for performance.
Property.query()
.apply(s => s.withinRadius({
lat: 21.17, // required
lng: 72.83, // required
radiusKm: 5, // required — must be > 0
latCol: 'latitude', // optional — default: 'latitude'
lngCol: 'longitude', // optional — default: 'longitude'
})).nearestTo(query, point)
Order by ascending distance. Pair with .withinRadius() for best performance.
Property.query()
.apply(s => s.withinRadius({ lat: 21.17, lng: 72.83, radiusKm: 10 }))
.apply(s => s.nearestTo({ lat: 21.17, lng: 72.83 }))
.limit(5).distanceFrom(query, lat, lng, latCol?, lngCol?)
Add computed distance_km column to results. Access via row.$extras.distance_km.
const rows = await Property.query()
.apply(s => s.withinRadius({ lat: 21.17, lng: 72.83, radiusKm: 3 }))
.apply(s => s.distanceFrom(21.17, 72.83))
rows.forEach(row => {
console.log(`${row.name} is ${row.$extras.distance_km} km away`)
})PropertySearch.search(filters, options?)
⚠️ Throws
PropertySearchValidationErrorfor invalid inputs (e.g., missing coordinates, invalid radius)
Unified search API with filtering, pagination, and distance in one call.
import { PropertySearch, PropertySearchValidationError } from '@meetoza28/adonis-geo-search'
const { rows, total, page, limit } = await PropertySearch.search(
{ lat: 21.17, lng: 72.83, radiusKm: 5, minRent: 50000, sortBy: 'distance' },
{ page: 1, limit: 20, includeDistance: true }
)🧪 Use Cases
- 🏠 Real estate apps (property search)
- 🍔 Food delivery apps (Swiggy/Zomato-style nearby search)
- 🚗 Ride-sharing apps
- 📍 Location-based services
⚡ Performance
Two-step strategy for large datasets:
- Bounding box — O(log n) index scan, rejects ~95% of rows instantly
- ST_Distance_Sphere — exact circle filter on small remaining set
On 1M rows: ~5,000 calculations instead of 1,000,000 → 200x faster
📄 License
ISC © Meet Oza
