MongoDB Geospatial Queries and Location-Based Services: Advanced Spatial Indexing and Geographic Data Management
Modern applications increasingly rely on location-aware functionality, from ride-sharing and delivery services to social media check-ins and targeted marketing. Traditional database systems struggle with complex spatial operations, often requiring specialized GIS software or complex geometric calculations that are difficult to integrate, maintain, and scale within application architectures.
MongoDB provides comprehensive native geospatial capabilities with advanced spatial indexing, sophisticated geometric operations, and high-performance location-based queries that eliminate the complexity of external GIS systems. Unlike traditional approaches that require separate spatial databases or complex geometric libraries, MongoDB's integrated geospatial features deliver superior performance through optimized spatial indexes, native coordinate system support, and seamless integration with application data models.
The Traditional Geospatial Challenge
Conventional approaches to location-based services involve significant complexity and performance limitations:
-- Traditional PostgreSQL geospatial approach - complex setup and limited optimization
-- PostGIS extension required for spatial capabilities
CREATE EXTENSION IF NOT EXISTS postgis;
-- Location-based entities with complex geometric types
CREATE TABLE locations (
location_id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
category VARCHAR(100) NOT NULL,
-- PostGIS geometry types (complex to work with)
coordinates GEOMETRY(POINT, 4326) NOT NULL, -- WGS84 coordinate system
coverage_area GEOMETRY(POLYGON, 4326),
search_radius GEOMETRY(POLYGON, 4326),
-- Additional location metadata
address TEXT,
city VARCHAR(100),
state VARCHAR(50),
country VARCHAR(50),
postal_code VARCHAR(20),
-- Business information
phone_number VARCHAR(20),
operating_hours JSONB,
rating DECIMAL(3,2),
price_range INTEGER,
-- Spatial analysis metadata
population_density INTEGER,
traffic_level VARCHAR(20),
accessibility_score DECIMAL(4,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Complex spatial indexing (manual configuration required)
CREATE INDEX idx_locations_coordinates ON locations USING GIST (coordinates);
CREATE INDEX idx_locations_coverage ON locations USING GIST (coverage_area);
CREATE INDEX idx_locations_category_coords ON locations USING GIST (coordinates, category);
-- User location tracking with spatial relationships
CREATE TABLE user_locations (
user_location_id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
location_coordinates GEOMETRY(POINT, 4326) NOT NULL,
accuracy_meters DECIMAL(8,2),
altitude_meters DECIMAL(8,2),
-- Movement tracking
speed_kmh DECIMAL(6,2),
heading_degrees DECIMAL(5,2),
-- Context information
location_method VARCHAR(50), -- GPS, WIFI, CELL, MANUAL
device_type VARCHAR(50),
battery_level INTEGER,
-- Privacy and permissions
location_sharing_level VARCHAR(20) DEFAULT 'private',
geofence_notifications BOOLEAN DEFAULT false,
-- Temporal tracking
recorded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
session_id VARCHAR(100),
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
-- Spatial indexes for user locations
CREATE INDEX idx_user_locations_coords ON user_locations USING GIST (location_coordinates);
CREATE INDEX idx_user_locations_user_time ON user_locations (user_id, recorded_at);
CREATE INDEX idx_user_locations_session ON user_locations (session_id, recorded_at);
-- Complex proximity search with performance issues
WITH nearby_locations AS (
SELECT
l.location_id,
l.name,
l.category,
l.address,
l.rating,
l.price_range,
-- Complex distance calculations
ST_Distance(
l.coordinates,
ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)::geography
) as distance_meters,
-- Geometric relationships (expensive operations)
ST_Contains(l.coverage_area, ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)) as within_coverage,
ST_Intersects(l.search_radius, ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)) as in_search_area,
-- Bearing calculation (complex trigonometry)
ST_Azimuth(
l.coordinates,
ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)
) * 180 / PI() as bearing_degrees,
-- Additional spatial analysis
l.coordinates,
l.operating_hours,
l.phone_number
FROM locations l
WHERE
-- Basic distance filter (still expensive without proper optimization)
ST_DWithin(
l.coordinates::geography,
ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)::geography,
$search_radius_meters
)
-- Category filtering
AND ($category IS NULL OR l.category = $category)
-- Rating filtering
AND ($min_rating IS NULL OR l.rating >= $min_rating)
-- Price filtering
AND ($max_price IS NULL OR l.price_range <= $max_price)
ORDER BY distance_meters
LIMIT $limit_count
),
location_analytics AS (
-- Complex spatial aggregations with performance impact
SELECT
nl.category,
COUNT(*) as location_count,
AVG(nl.rating) as avg_rating,
AVG(nl.distance_meters) as avg_distance,
MIN(nl.distance_meters) as closest_distance,
MAX(nl.distance_meters) as furthest_distance,
-- Expensive geometric calculations
ST_ConvexHull(ST_Collect(nl.coordinates)) as coverage_polygon,
ST_Centroid(ST_Collect(nl.coordinates)) as category_center,
-- Statistical analysis (resource intensive)
STDDEV_POP(nl.distance_meters) as distance_variance,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY nl.distance_meters) as median_distance
FROM nearby_locations nl
GROUP BY nl.category
),
user_movement_analysis AS (
-- Track user movement patterns (very expensive queries)
SELECT
ul.user_id,
COUNT(*) as location_updates,
-- Complex movement calculations
SUM(
ST_Distance(
ul.location_coordinates::geography,
LAG(ul.location_coordinates::geography) OVER (
PARTITION BY ul.user_id
ORDER BY ul.recorded_at
)
)
) as total_distance_traveled,
-- Speed analysis
AVG(ul.speed_kmh) as avg_speed,
MAX(ul.speed_kmh) as max_speed,
-- Time-based analysis
EXTRACT(SECONDS FROM (MAX(ul.recorded_at) - MIN(ul.recorded_at))) as session_duration_seconds,
-- Geofencing analysis (complex polygon operations)
COUNT(*) FILTER (
WHERE EXISTS (
SELECT 1 FROM locations l
WHERE ST_Contains(l.coverage_area, ul.location_coordinates)
)
) as geofence_entries,
-- Movement patterns
STRING_AGG(
DISTINCT CASE
WHEN ul.speed_kmh > 50 THEN 'highway'
WHEN ul.speed_kmh > 20 THEN 'city'
WHEN ul.speed_kmh > 5 THEN 'walking'
ELSE 'stationary'
END,
','
ORDER BY ul.recorded_at
) as movement_pattern
FROM user_locations ul
WHERE ul.recorded_at >= CURRENT_TIMESTAMP - INTERVAL '1 day'
GROUP BY ul.user_id
)
-- Final complex spatial query with multiple joins and calculations
SELECT
nl.location_id,
nl.name,
nl.category,
nl.address,
ROUND(nl.distance_meters, 2) as distance_meters,
ROUND(nl.bearing_degrees, 1) as bearing_degrees,
nl.rating,
nl.price_range,
-- Spatial relationship indicators
nl.within_coverage,
nl.in_search_area,
-- Analytics context
la.location_count as similar_nearby_count,
ROUND(la.avg_rating, 2) as category_avg_rating,
ROUND(la.avg_distance, 2) as category_avg_distance,
-- User movement context (if available)
uma.total_distance_traveled,
uma.avg_speed,
uma.movement_pattern,
-- Additional computed fields
CASE
WHEN nl.distance_meters <= 100 THEN 'immediate_vicinity'
WHEN nl.distance_meters <= 500 THEN 'very_close'
WHEN nl.distance_meters <= 1000 THEN 'walking_distance'
WHEN nl.distance_meters <= 5000 THEN 'short_drive'
ELSE 'distant'
END as proximity_category,
-- Operating status (complex JSON processing)
CASE
WHEN nl.operating_hours IS NULL THEN 'unknown'
WHEN nl.operating_hours->>(EXTRACT(DOW FROM CURRENT_TIMESTAMP)::TEXT) IS NULL THEN 'closed'
ELSE 'check_hours'
END as operating_status,
-- Recommendations based on multiple factors
CASE
WHEN nl.rating >= 4.5 AND nl.distance_meters <= 1000 THEN 'highly_recommended'
WHEN nl.rating >= 4.0 AND nl.distance_meters <= 2000 THEN 'recommended'
WHEN nl.distance_meters <= 500 THEN 'convenient'
ELSE 'standard'
END as recommendation_level
FROM nearby_locations nl
LEFT JOIN location_analytics la ON nl.category = la.category
LEFT JOIN user_movement_analysis uma ON uma.user_id = $user_id
ORDER BY
-- Complex sorting logic
CASE $sort_preference
WHEN 'distance' THEN nl.distance_meters
WHEN 'rating' THEN -nl.rating * 100 + nl.distance_meters
WHEN 'price' THEN nl.price_range * 1000 + nl.distance_meters
ELSE nl.distance_meters
END
LIMIT $result_limit;
-- Traditional geospatial approach problems:
-- 1. Requires PostGIS extension and complex geometric type management
-- 2. Expensive spatial calculations with limited built-in optimization
-- 3. Complex coordinate system transformations and projections
-- 4. Poor performance with large datasets and concurrent spatial queries
-- 5. Limited integration with application data models and business logic
-- 6. Complex indexing strategies requiring deep GIS expertise
-- 7. Difficult to maintain and scale spatial operations
-- 8. Limited support for modern location-based service patterns
-- 9. Complex query syntax requiring specialized GIS knowledge
-- 10. Poor integration with real-time and streaming location data
MongoDB provides comprehensive geospatial capabilities with native optimization and seamless integration:
// MongoDB Advanced Geospatial Operations - native spatial capabilities with optimal performance
const { MongoClient, ObjectId } = require('mongodb');
const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('location_services');
// Comprehensive MongoDB Geospatial Manager
class MongoDBGeospatialManager {
constructor(db, config = {}) {
this.db = db;
this.config = {
// Default search parameters
defaultSearchRadius: config.defaultSearchRadius || 5000, // 5km
defaultMaxResults: config.defaultMaxResults || 100,
// Performance optimization
enableSpatialIndexing: config.enableSpatialIndexing !== false,
enableQueryOptimization: config.enableQueryOptimization !== false,
enableBulkOperations: config.enableBulkOperations !== false,
// Coordinate system configuration
defaultCoordinateSystem: config.defaultCoordinateSystem || 'WGS84',
enableEarthDistance: config.enableEarthDistance !== false,
// Advanced features
enableGeofencing: config.enableGeofencing !== false,
enableLocationAnalytics: config.enableLocationAnalytics !== false,
enableRealTimeTracking: config.enableRealTimeTracking !== false,
enableSpatialAggregation: config.enableSpatialAggregation !== false,
// Performance monitoring
enablePerformanceMetrics: config.enablePerformanceMetrics !== false,
logSlowQueries: config.logSlowQueries !== false,
queryTimeoutMs: config.queryTimeoutMs || 30000,
...config
};
// Collection references
this.collections = {
locations: db.collection('locations'),
userLocations: db.collection('user_locations'),
geofences: db.collection('geofences'),
locationAnalytics: db.collection('location_analytics'),
spatialEvents: db.collection('spatial_events')
};
// Performance tracking
this.queryMetrics = {
totalQueries: 0,
averageQueryTime: 0,
spatialQueries: 0,
indexHits: 0
};
this.initializeGeospatialCollections();
}
async initializeGeospatialCollections() {
console.log('Initializing geospatial collections and spatial indexes...');
try {
// Setup locations collection with advanced spatial indexing
await this.setupLocationsCollection();
// Setup user location tracking
await this.setupUserLocationTracking();
// Setup geofencing capabilities
await this.setupGeofencingSystem();
// Setup location analytics
await this.setupLocationAnalytics();
// Setup spatial event tracking
await this.setupSpatialEventTracking();
console.log('All geospatial collections initialized successfully');
} catch (error) {
console.error('Error initializing geospatial collections:', error);
throw error;
}
}
async setupLocationsCollection() {
console.log('Setting up locations collection with spatial indexing...');
const locationsCollection = this.collections.locations;
// Create 2dsphere index for geospatial queries (primary spatial index)
await locationsCollection.createIndex(
{ coordinates: '2dsphere' },
{
background: true,
name: 'coordinates_2dsphere',
// Optimize for common query patterns
'2dsphereIndexVersion': 3
}
);
// Compound indexes for optimized spatial queries with filters
await locationsCollection.createIndex(
{ coordinates: '2dsphere', category: 1 },
{ background: true, name: 'spatial_category_index' }
);
await locationsCollection.createIndex(
{ coordinates: '2dsphere', rating: -1, priceRange: 1 },
{ background: true, name: 'spatial_rating_price_index' }
);
// Coverage area indexing for geofencing
await locationsCollection.createIndex(
{ coverageArea: '2dsphere' },
{
background: true,
sparse: true,
name: 'coverage_area_index'
}
);
// Text index for location search
await locationsCollection.createIndex(
{ name: 'text', address: 'text', category: 'text' },
{
background: true,
name: 'location_text_search',
weights: { name: 3, category: 2, address: 1 }
}
);
// Additional performance indexes
await locationsCollection.createIndex(
{ category: 1, rating: -1, createdAt: -1 },
{ background: true }
);
console.log('Locations collection spatial indexing complete');
}
async createLocation(locationData) {
console.log('Creating location with geospatial data...');
const startTime = Date.now();
try {
const locationDocument = {
locationId: locationData.locationId || new ObjectId(),
name: locationData.name,
category: locationData.category,
// GeoJSON Point for precise coordinates
coordinates: {
type: 'Point',
coordinates: [locationData.longitude, locationData.latitude] // [lng, lat] order in GeoJSON
},
// Optional coverage area as GeoJSON Polygon
coverageArea: locationData.coverageArea ? {
type: 'Polygon',
coordinates: locationData.coverageArea // Array of coordinate arrays
} : null,
// Location details
address: locationData.address,
city: locationData.city,
state: locationData.state,
country: locationData.country,
postalCode: locationData.postalCode,
// Business information
phoneNumber: locationData.phoneNumber,
website: locationData.website,
operatingHours: locationData.operatingHours || {},
rating: locationData.rating || 0,
priceRange: locationData.priceRange || 1,
// Spatial metadata
accuracy: locationData.accuracy,
altitude: locationData.altitude,
floor: locationData.floor,
// Business analytics data
popularityScore: locationData.popularityScore || 0,
trafficLevel: locationData.trafficLevel,
accessibilityFeatures: locationData.accessibilityFeatures || [],
// Temporal information
createdAt: new Date(),
updatedAt: new Date(),
// Custom attributes
customAttributes: locationData.customAttributes || {},
tags: locationData.tags || [],
// Verification status
verified: locationData.verified || false,
verificationSource: locationData.verificationSource
};
// Validate GeoJSON format
if (!this.validateGeoJSONPoint(locationDocument.coordinates)) {
throw new Error('Invalid coordinates format - must be valid GeoJSON Point');
}
const result = await this.collections.locations.insertOne(locationDocument);
const processingTime = Date.now() - startTime;
this.updateQueryMetrics('create_location', processingTime);
console.log(`Location created: ${result.insertedId} (${processingTime}ms)`);
return {
success: true,
locationId: result.insertedId,
coordinates: locationDocument.coordinates,
processingTime: processingTime
};
} catch (error) {
console.error('Error creating location:', error);
return {
success: false,
error: error.message,
processingTime: Date.now() - startTime
};
}
}
async findNearbyLocations(longitude, latitude, options = {}) {
console.log(`Finding locations near [${longitude}, ${latitude}]...`);
const startTime = Date.now();
try {
// Build aggregation pipeline for advanced spatial query
const pipeline = [
// Stage 1: Geospatial proximity matching
{
$geoNear: {
near: {
type: 'Point',
coordinates: [longitude, latitude]
},
distanceField: 'distanceMeters',
maxDistance: options.maxDistance || this.config.defaultSearchRadius,
spherical: true,
// Advanced filtering options
query: {
...(options.category && { category: options.category }),
...(options.minRating && { rating: { $gte: options.minRating } }),
...(options.maxPriceRange && { priceRange: { $lte: options.maxPriceRange } }),
...(options.verified !== undefined && { verified: options.verified }),
...(options.tags && { tags: { $in: options.tags } })
},
// Limit initial results for performance
limit: options.limit || this.config.defaultMaxResults
}
},
// Stage 2: Add computed fields and spatial analysis
{
$addFields: {
// Distance calculations
distanceKm: { $divide: ['$distanceMeters', 1000] },
// Bearing calculation (direction from search point to location)
bearing: {
$let: {
vars: {
lat1: { $degreesToRadians: latitude },
lat2: { $degreesToRadians: { $arrayElemAt: ['$coordinates.coordinates', 1] } },
lng1: { $degreesToRadians: longitude },
lng2: { $degreesToRadians: { $arrayElemAt: ['$coordinates.coordinates', 0] } }
},
in: {
$mod: [
{
$add: [
{
$radiansToDegrees: {
$atan2: [
{
$sin: { $subtract: ['$$lng2', '$$lng1'] }
},
{
$subtract: [
{
$multiply: [
{ $cos: '$$lat1' },
{ $sin: '$$lat2' }
]
},
{
$multiply: [
{ $sin: '$$lat1' },
{ $cos: '$$lat2' },
{ $cos: { $subtract: ['$$lng2', '$$lng1'] } }
]
}
]
}
]
}
},
360
]
},
360
]
}
}
},
// Proximity categorization
proximityCategory: {
$switch: {
branches: [
{ case: { $lte: ['$distanceMeters', 100] }, then: 'immediate_vicinity' },
{ case: { $lte: ['$distanceMeters', 500] }, then: 'very_close' },
{ case: { $lte: ['$distanceMeters', 1000] }, then: 'walking_distance' },
{ case: { $lte: ['$distanceMeters', 5000] }, then: 'short_drive' }
],
default: 'distant'
}
},
// Recommendation scoring
recommendationScore: {
$add: [
// Base rating score (0-5 scale)
{ $multiply: ['$rating', 2] },
// Distance penalty (closer is better)
{
$subtract: [
10,
{ $divide: ['$distanceMeters', 500] }
]
},
// Popularity bonus
{ $multiply: ['$popularityScore', 0.5] },
// Verification bonus
{ $cond: [{ $eq: ['$verified', true] }, 2, 0] }
]
}
}
},
// Stage 3: Operating hours analysis (if requested)
...(options.checkOperatingHours ? [{
$addFields: {
currentlyOpen: {
$let: {
vars: {
now: new Date(),
dayOfWeek: { $dayOfWeek: new Date() }, // 1 = Sunday, 7 = Saturday
currentTime: {
$dateToString: {
format: '%H:%M',
date: new Date()
}
}
},
in: {
// Simplified operating hours check
$cond: [
{ $ne: ['$operatingHours', null] },
true, // Would implement complex time checking logic
null
]
}
}
}
}
}] : []),
// Stage 4: Coverage area intersection (if requested)
...(options.checkCoverageArea ? [{
$addFields: {
withinCoverageArea: {
$cond: [
{ $ne: ['$coverageArea', null] },
{
$function: {
body: `function(coverageArea, searchPoint) {
// Simplified point-in-polygon check
// In production, use MongoDB's native $geoIntersects
return true; // Placeholder for complex geometric calculation
}`,
args: ['$coverageArea', { type: 'Point', coordinates: [longitude, latitude] }],
lang: 'js'
}
},
null
]
}
}
}] : []),
// Stage 5: Final sorting and formatting
{
$sort: {
// Default sort by recommendation score, fallback to distance
recommendationScore: options.sortBy === 'recommendation' ? -1 : 1,
distanceMeters: options.sortBy === 'distance' ? 1 : -1,
rating: -1
}
},
// Stage 6: Limit results
{ $limit: options.limit || this.config.defaultMaxResults },
// Stage 7: Project final result structure
{
$project: {
locationId: 1,
name: 1,
category: 1,
coordinates: 1,
address: 1,
city: 1,
state: 1,
country: 1,
phoneNumber: 1,
website: 1,
rating: 1,
priceRange: 1,
// Spatial analysis results
distanceMeters: { $round: ['$distanceMeters', 2] },
distanceKm: { $round: ['$distanceKm', 3] },
bearing: { $round: ['$bearing', 1] },
proximityCategory: 1,
recommendationScore: { $round: ['$recommendationScore', 2] },
// Conditional fields
...(options.checkOperatingHours && { currentlyOpen: 1 }),
...(options.checkCoverageArea && { withinCoverageArea: 1 }),
// Metadata
verified: 1,
tags: 1,
createdAt: 1,
// Custom attributes if requested
...(options.includeCustomAttributes && { customAttributes: 1 })
}
}
];
// Execute aggregation pipeline
const locations = await this.collections.locations.aggregate(
pipeline,
{
allowDiskUse: true,
maxTimeMS: this.config.queryTimeoutMs,
hint: 'coordinates_2dsphere' // Use spatial index
}
).toArray();
const processingTime = Date.now() - startTime;
this.updateQueryMetrics('nearby_search', processingTime);
console.log(`Found ${locations.length} nearby locations (${processingTime}ms)`);
return {
success: true,
locations: locations,
searchParams: {
coordinates: [longitude, latitude],
maxDistance: options.maxDistance || this.config.defaultSearchRadius,
filters: options
},
resultsCount: locations.length,
processingTime: processingTime
};
} catch (error) {
console.error('Error finding nearby locations:', error);
return {
success: false,
error: error.message,
processingTime: Date.now() - startTime
};
}
}
async setupUserLocationTracking() {
console.log('Setting up user location tracking...');
const userLocationsCollection = this.collections.userLocations;
// Spatial index for user locations
await userLocationsCollection.createIndex(
{ coordinates: '2dsphere' },
{ background: true, name: 'user_coordinates_spatial' }
);
// Compound indexes for user tracking queries
await userLocationsCollection.createIndex(
{ userId: 1, recordedAt: -1 },
{ background: true, name: 'user_timeline' }
);
await userLocationsCollection.createIndex(
{ sessionId: 1, recordedAt: 1 },
{ background: true, name: 'session_tracking' }
);
// Geofencing compound index
await userLocationsCollection.createIndex(
{ coordinates: '2dsphere', userId: 1, recordedAt: -1 },
{ background: true, name: 'spatial_user_timeline' }
);
console.log('User location tracking setup complete');
}
async trackUserLocation(userId, longitude, latitude, metadata = {}) {
console.log(`Tracking location for user ${userId}: [${longitude}, ${latitude}]`);
const startTime = Date.now();
try {
const locationDocument = {
userId: userId,
coordinates: {
type: 'Point',
coordinates: [longitude, latitude]
},
// Accuracy and technical metadata
accuracy: metadata.accuracy,
altitude: metadata.altitude,
speed: metadata.speed,
heading: metadata.heading,
// Device and method information
locationMethod: metadata.locationMethod || 'GPS',
deviceType: metadata.deviceType,
batteryLevel: metadata.batteryLevel,
// Session context
sessionId: metadata.sessionId,
applicationContext: metadata.applicationContext,
// Privacy and sharing
locationSharingLevel: metadata.locationSharingLevel || 'private',
allowGeofenceNotifications: metadata.allowGeofenceNotifications || false,
// Temporal information
recordedAt: metadata.recordedAt || new Date(),
serverProcessedAt: new Date(),
// Movement analysis
isStationary: metadata.isStationary || false,
movementType: metadata.movementType, // walking, driving, cycling, stationary
// Custom context
customData: metadata.customData || {}
};
// Validate coordinates
if (!this.validateGeoJSONPoint(locationDocument.coordinates)) {
throw new Error('Invalid coordinates for user location tracking');
}
const result = await this.collections.userLocations.insertOne(locationDocument);
// Check for geofence triggers (if enabled)
if (this.config.enableGeofencing) {
await this.checkGeofenceEvents(userId, longitude, latitude);
}
const processingTime = Date.now() - startTime;
this.updateQueryMetrics('track_user_location', processingTime);
return {
success: true,
locationId: result.insertedId,
coordinates: locationDocument.coordinates,
processingTime: processingTime,
geofenceChecked: this.config.enableGeofencing
};
} catch (error) {
console.error('Error tracking user location:', error);
return {
success: false,
error: error.message,
processingTime: Date.now() - startTime
};
}
}
async getUserLocationHistory(userId, options = {}) {
console.log(`Retrieving location history for user ${userId}...`);
const startTime = Date.now();
try {
const pipeline = [
// Stage 1: Filter by user and time range
{
$match: {
userId: userId,
recordedAt: {
$gte: options.startDate || new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)), // 7 days default
$lte: options.endDate || new Date()
},
...(options.sessionId && { sessionId: options.sessionId }),
...(options.locationSharingLevel && { locationSharingLevel: options.locationSharingLevel })
}
},
// Stage 2: Sort chronologically
{ $sort: { recordedAt: 1 } },
// Stage 3: Add movement analysis
{
$addFields: {
// Calculate time since last location update
timeSincePrevious: {
$subtract: [
'$recordedAt',
{ $ifNull: [{ $lag: '$recordedAt', offset: 1 }, '$recordedAt'] }
]
}
}
},
// Stage 4: Movement calculations using $setWindowFields
{
$setWindowFields: {
partitionBy: '$userId',
sortBy: { recordedAt: 1 },
output: {
// Distance from previous location
distanceFromPrevious: {
$function: {
body: `function(currentCoords, previousCoords) {
if (!previousCoords) return 0;
// Haversine formula for distance calculation
const R = 6371000; // Earth's radius in meters
const lat1 = currentCoords.coordinates[1] * Math.PI / 180;
const lat2 = previousCoords.coordinates[1] * Math.PI / 180;
const deltaLat = (previousCoords.coordinates[1] - currentCoords.coordinates[1]) * Math.PI / 180;
const deltaLng = (previousCoords.coordinates[0] - currentCoords.coordinates[0]) * Math.PI / 180;
const a = Math.sin(deltaLat/2) * Math.sin(deltaLat/2) +
Math.cos(lat1) * Math.cos(lat2) *
Math.sin(deltaLng/2) * Math.sin(deltaLng/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}`,
args: ['$coordinates', { $lag: ['$coordinates', 1] }],
lang: 'js'
}
},
// Running total distance
totalDistanceTraveled: {
$sum: '$distanceFromPrevious',
window: { documents: ['unbounded', 'current'] }
}
}
}
},
// Stage 5: Limit results
{ $limit: options.limit || 1000 },
// Stage 6: Project final format
{
$project: {
coordinates: 1,
accuracy: 1,
altitude: 1,
speed: 1,
heading: 1,
locationMethod: 1,
recordedAt: 1,
sessionId: 1,
movementType: 1,
// Calculated fields
distanceFromPrevious: { $round: ['$distanceFromPrevious', 2] },
totalDistanceTraveled: { $round: ['$totalDistanceTraveled', 2] },
timeSincePrevious: { $divide: ['$timeSincePrevious', 1000] }, // Convert to seconds
// Privacy filtered custom data
...(options.includeCustomData && { customData: 1 })
}
}
];
const locationHistory = await this.collections.userLocations.aggregate(
pipeline,
{ allowDiskUse: true, maxTimeMS: this.config.queryTimeoutMs }
).toArray();
// Calculate summary statistics
const totalDistance = locationHistory.reduce((sum, loc) => sum + (loc.distanceFromPrevious || 0), 0);
const timespan = locationHistory.length > 0 ?
new Date(locationHistory[locationHistory.length - 1].recordedAt) - new Date(locationHistory[0].recordedAt) : 0;
const processingTime = Date.now() - startTime;
this.updateQueryMetrics('user_location_history', processingTime);
return {
success: true,
locationHistory: locationHistory,
summary: {
totalPoints: locationHistory.length,
totalDistanceMeters: Math.round(totalDistance),
totalDistanceKm: Math.round(totalDistance / 1000 * 100) / 100,
timespanHours: Math.round(timespan / (1000 * 60 * 60) * 100) / 100,
averageSpeed: timespan > 0 ? Math.round((totalDistance / (timespan / 1000)) * 3.6 * 100) / 100 : 0 // km/h
},
processingTime: processingTime
};
} catch (error) {
console.error('Error retrieving user location history:', error);
return {
success: false,
error: error.message,
processingTime: Date.now() - startTime
};
}
}
async setupGeofencingSystem() {
console.log('Setting up geofencing system...');
const geofencesCollection = this.collections.geofences;
// Spatial index for geofence areas
await geofencesCollection.createIndex(
{ area: '2dsphere' },
{ background: true, name: 'geofence_spatial' }
);
// Compound indexes for geofence queries
await geofencesCollection.createIndex(
{ ownerId: 1, isActive: 1 },
{ background: true }
);
await geofencesCollection.createIndex(
{ category: 1, isActive: 1 },
{ background: true }
);
console.log('Geofencing system setup complete');
}
async createGeofence(ownerId, geofenceData) {
console.log(`Creating geofence for owner ${ownerId}...`);
const startTime = Date.now();
try {
const geofenceDocument = {
geofenceId: new ObjectId(),
ownerId: ownerId,
name: geofenceData.name,
description: geofenceData.description,
category: geofenceData.category || 'custom',
// GeoJSON area (Polygon or Circle)
area: geofenceData.area,
// Geofence behavior
triggerOnEntry: geofenceData.triggerOnEntry !== false,
triggerOnExit: geofenceData.triggerOnExit !== false,
triggerOnDwell: geofenceData.triggerOnDwell || false,
dwellTimeSeconds: geofenceData.dwellTimeSeconds || 300, // 5 minutes
// Notification settings
notificationSettings: {
enabled: geofenceData.notifications?.enabled !== false,
methods: geofenceData.notifications?.methods || ['push'],
message: geofenceData.notifications?.message
},
// Targeting
targetUsers: geofenceData.targetUsers || [], // Specific user IDs
targetUserGroups: geofenceData.targetUserGroups || [],
// Scheduling
schedule: geofenceData.schedule || {
enabled: true,
startTime: null,
endTime: null,
daysOfWeek: [1, 2, 3, 4, 5, 6, 7] // All days
},
// State management
isActive: geofenceData.isActive !== false,
createdAt: new Date(),
updatedAt: new Date(),
// Analytics
entryCount: 0,
exitCount: 0,
dwellCount: 0,
lastTriggered: null,
// Custom data
customData: geofenceData.customData || {}
};
// Validate GeoJSON area
if (!this.validateGeoJSONGeometry(geofenceDocument.area)) {
throw new Error('Invalid geofence area geometry');
}
const result = await geofencesCollection.insertOne(geofenceDocument);
const processingTime = Date.now() - startTime;
return {
success: true,
geofenceId: result.insertedId,
area: geofenceDocument.area,
processingTime: processingTime
};
} catch (error) {
console.error('Error creating geofence:', error);
return {
success: false,
error: error.message,
processingTime: Date.now() - startTime
};
}
}
async checkGeofenceEvents(userId, longitude, latitude) {
console.log(`Checking geofence events for user ${userId} at [${longitude}, ${latitude}]...`);
try {
const userPoint = {
type: 'Point',
coordinates: [longitude, latitude]
};
// Find all active geofences that intersect with user location
const intersectingGeofences = await this.collections.geofences.find({
isActive: true,
// Spatial intersection query
area: {
$geoIntersects: {
$geometry: userPoint
}
},
// Check if user is targeted (empty array means all users)
$or: [
{ targetUsers: { $size: 0 } },
{ targetUsers: userId }
]
}).toArray();
// Process each intersecting geofence
const geofenceEvents = [];
for (const geofence of intersectingGeofences) {
// Check if this is a new entry or existing presence
const recentUserLocation = await this.collections.userLocations.findOne({
userId: userId,
recordedAt: { $gte: new Date(Date.now() - (5 * 60 * 1000)) } // Last 5 minutes
}, { sort: { recordedAt: -1 } });
let eventType = 'dwelling';
if (!recentUserLocation) {
eventType = 'entry';
}
// Create geofence event
const geofenceEvent = {
eventId: new ObjectId(),
userId: userId,
geofenceId: geofence.geofenceId,
geofenceName: geofence.name,
eventType: eventType,
coordinates: userPoint,
eventTime: new Date(),
// Context information
geofenceCategory: geofence.category,
dwellTimeSeconds: eventType === 'dwelling' ?
(recentUserLocation ? (Date.now() - recentUserLocation.recordedAt.getTime()) / 1000 : 0) : 0,
// Notification triggered
notificationTriggered: geofence.notificationSettings.enabled &&
((eventType === 'entry' && geofence.triggerOnEntry) ||
(eventType === 'dwelling' && geofence.triggerOnDwell)),
customData: geofence.customData
};
// Store the event
await this.collections.spatialEvents.insertOne(geofenceEvent);
// Update geofence statistics
const updateFields = {};
updateFields[`${eventType}Count`] = 1;
updateFields.lastTriggered = new Date();
await this.collections.geofences.updateOne(
{ geofenceId: geofence.geofenceId },
{
$inc: updateFields,
$set: { updatedAt: new Date() }
}
);
geofenceEvents.push(geofenceEvent);
// Trigger notifications if configured
if (geofenceEvent.notificationTriggered) {
await this.triggerGeofenceNotification(userId, geofenceEvent);
}
}
return {
success: true,
eventsTriggered: geofenceEvents.length,
events: geofenceEvents
};
} catch (error) {
console.error('Error checking geofence events:', error);
return {
success: false,
error: error.message
};
}
}
async triggerGeofenceNotification(userId, geofenceEvent) {
// Placeholder for notification system integration
console.log(`Geofence notification triggered for user ${userId}:`, {
geofence: geofenceEvent.geofenceName,
eventType: geofenceEvent.eventType,
location: geofenceEvent.coordinates
});
// In a real implementation, this would integrate with:
// - Push notification services
// - SMS/Email services
// - Webhook endpoints
// - Real-time messaging systems
}
validateGeoJSONPoint(coordinates) {
return coordinates &&
coordinates.type === 'Point' &&
Array.isArray(coordinates.coordinates) &&
coordinates.coordinates.length === 2 &&
typeof coordinates.coordinates[0] === 'number' &&
typeof coordinates.coordinates[1] === 'number' &&
coordinates.coordinates[0] >= -180 && coordinates.coordinates[0] <= 180 &&
coordinates.coordinates[1] >= -90 && coordinates.coordinates[1] <= 90;
}
validateGeoJSONGeometry(geometry) {
if (!geometry || !geometry.type) return false;
switch (geometry.type) {
case 'Point':
return this.validateGeoJSONPoint(geometry);
case 'Polygon':
return geometry.coordinates &&
Array.isArray(geometry.coordinates) &&
geometry.coordinates.length > 0 &&
Array.isArray(geometry.coordinates[0]) &&
geometry.coordinates[0].length >= 4; // Minimum for polygon
case 'Circle':
// MongoDB extension for circular geofences
return geometry.coordinates &&
Array.isArray(geometry.coordinates) &&
geometry.coordinates.length === 2 &&
typeof geometry.radius === 'number' &&
geometry.radius > 0;
default:
return false;
}
}
updateQueryMetrics(queryType, duration) {
this.queryMetrics.totalQueries++;
this.queryMetrics.averageQueryTime =
(this.queryMetrics.averageQueryTime + duration) / 2;
if (queryType.includes('spatial') || queryType.includes('nearby') || queryType.includes('geofence')) {
this.queryMetrics.spatialQueries++;
}
if (this.config.logSlowQueries && duration > 1000) {
console.log(`Slow query detected: ${queryType} took ${duration}ms`);
}
}
async getPerformanceMetrics() {
return {
queryMetrics: this.queryMetrics,
indexMetrics: await this.analyzeIndexPerformance(),
collectionStats: await this.getCollectionStatistics()
};
}
async analyzeIndexPerformance() {
const metrics = {};
for (const [collectionName, collection] of Object.entries(this.collections)) {
try {
const indexStats = await collection.aggregate([{ $indexStats: {} }]).toArray();
metrics[collectionName] = indexStats;
} catch (error) {
console.error(`Error analyzing indexes for ${collectionName}:`, error);
}
}
return metrics;
}
async getCollectionStatistics() {
const stats = {};
for (const [collectionName, collection] of Object.entries(this.collections)) {
try {
stats[collectionName] = await collection.stats();
} catch (error) {
console.error(`Error getting stats for ${collectionName}:`, error);
}
}
return stats;
}
async shutdown() {
console.log('Shutting down geospatial manager...');
// Log final performance metrics
if (this.config.enablePerformanceMetrics) {
const metrics = await this.getPerformanceMetrics();
console.log('Final Performance Metrics:', metrics.queryMetrics);
}
console.log('Geospatial manager shutdown complete');
}
}
// Benefits of MongoDB Geospatial Operations:
// - Native 2dsphere indexing with optimized spatial queries
// - Comprehensive GeoJSON support for points, polygons, and complex geometries
// - High-performance proximity searches with built-in distance calculations
// - Advanced geofencing capabilities with real-time event triggering
// - Seamless integration with application data without external GIS systems
// - Sophisticated spatial aggregation and analytics capabilities
// - Built-in coordinate system support and projection handling
// - Optimized query performance with spatial index utilization
// - SQL-compatible geospatial operations through QueryLeaf integration
// - Scalable location-based services with MongoDB's distributed architecture
module.exports = {
MongoDBGeospatialManager
};
Understanding MongoDB Geospatial Architecture
Advanced Spatial Indexing and Query Optimization Patterns
Implement sophisticated geospatial strategies for production MongoDB deployments:
// Production-ready MongoDB geospatial operations with advanced optimization and analytics
class ProductionGeospatialProcessor extends MongoDBGeospatialManager {
constructor(db, productionConfig) {
super(db, productionConfig);
this.productionConfig = {
...productionConfig,
enableAdvancedAnalytics: true,
enableSpatialCaching: true,
enableLocationIntelligence: true,
enablePredictiveGeofencing: true,
enableSpatialDataMining: true,
enableRealtimeLocationStreams: true
};
this.setupProductionOptimizations();
this.initializeAdvancedGeospatial();
this.setupLocationIntelligence();
}
async implementAdvancedSpatialAnalytics() {
console.log('Implementing advanced spatial analytics capabilities...');
const analyticsStrategy = {
// Location intelligence
locationIntelligence: {
enableHeatmapGeneration: true,
enableClusterAnalysis: true,
enablePatternDetection: true,
enablePredictiveModeling: true
},
// Spatial data mining
spatialDataMining: {
enableLocationCorrelation: true,
enableMovementPatternAnalysis: true,
enableSpatialAnomalyDetection: true,
enableLocationRecommendations: true
},
// Real-time processing
realtimeProcessing: {
enableStreamingGeoprocessing: true,
enableDynamicGeofencing: true,
enableLocationEventCorrelation: true,
enableSpatialAlertSystems: true
}
};
return await this.deployAdvancedSpatialAnalytics(analyticsStrategy);
}
async setupSpatialCachingSystem() {
console.log('Setting up advanced spatial caching system...');
const cachingConfig = {
// Spatial query caching
spatialQueryCache: {
enableProximityCache: true,
cacheRadius: 1000, // Cache results within 1km
cacheExpiration: 300, // 5 minutes
maxCacheEntries: 10000
},
// Geofence optimization
geofenceOptimization: {
enableGeofenceIndex: true,
spatialPartitioning: true,
dynamicGeofenceLoading: true,
geofenceHierarchy: true
},
// Location intelligence cache
locationIntelligenceCache: {
enableHeatmapCache: true,
enablePatternCache: true,
enablePredictionCache: true
}
};
return await this.deploySpatalCaching(cachingConfig);
}
async implementPredictiveGeofencing() {
console.log('Implementing predictive geofencing capabilities...');
const predictiveConfig = {
// Movement prediction
movementPrediction: {
enableTrajectoryPrediction: true,
predictionAccuracy: 0.85,
predictionTimeHorizon: 1800, // 30 minutes
learningModelUpdates: true
},
// Dynamic geofence creation
dynamicGeofencing: {
enablePredictiveGeofences: true,
contextAwareGeofences: true,
temporalGeofences: true,
adaptiveGeofenceSizes: true
},
// Behavioral analysis
behavioralAnalysis: {
enableLocationPatterns: true,
enableRoutePrediction: true,
enableDestinationPrediction: true,
enableActivityRecognition: true
}
};
return await this.deployPredictiveGeofencing(predictiveConfig);
}
}
SQL-Style Geospatial Operations with QueryLeaf
QueryLeaf provides familiar SQL syntax for MongoDB geospatial operations and location-based services:
-- QueryLeaf geospatial operations with SQL-familiar syntax for MongoDB
-- Create location-enabled table with spatial indexing
CREATE TABLE locations (
location_id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
category VARCHAR(100) NOT NULL,
-- Geospatial coordinates (automatically creates 2dsphere index)
coordinates POINT NOT NULL,
coverage_area POLYGON,
-- Location details
address TEXT,
city VARCHAR(100),
state VARCHAR(50),
country VARCHAR(50),
postal_code VARCHAR(20),
-- Business information
phone_number VARCHAR(20),
website VARCHAR(255),
operating_hours DOCUMENT,
rating DECIMAL(3,2) DEFAULT 0,
price_range INTEGER DEFAULT 1,
-- Analytics and metadata
popularity_score DECIMAL(6,2) DEFAULT 0,
verified BOOLEAN DEFAULT false,
tags TEXT[],
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
WITH SPATIAL_INDEXING (
coordinates USING '2dsphere',
coverage_area USING '2dsphere',
-- Compound spatial indexes for optimized queries
COMPOUND INDEX (coordinates, category),
COMPOUND INDEX (coordinates, rating DESC, price_range ASC)
);
-- User location tracking table
CREATE TABLE user_locations (
user_location_id UUID PRIMARY KEY,
user_id VARCHAR(50) NOT NULL,
coordinates POINT NOT NULL,
-- Accuracy and technical details
accuracy_meters DECIMAL(8,2),
altitude_meters DECIMAL(8,2),
speed_kmh DECIMAL(6,2),
heading_degrees DECIMAL(5,2),
-- Context and metadata
location_method VARCHAR(50) DEFAULT 'GPS',
device_type VARCHAR(50),
session_id VARCHAR(100),
-- Temporal tracking
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Privacy settings
location_sharing_level VARCHAR(20) DEFAULT 'private'
)
WITH SPATIAL_INDEXING (
coordinates USING '2dsphere',
COMPOUND INDEX (user_id, recorded_at DESC),
COMPOUND INDEX (coordinates, user_id, recorded_at DESC)
);
-- Insert locations with spatial data
INSERT INTO locations (
name, category, coordinates, address, city, state, country,
phone_number, rating, price_range, tags
) VALUES
('Central Park Cafe', 'restaurant', POINT(-73.965355, 40.782865),
'123 Central Park West', 'New York', 'NY', 'USA',
'+1-212-555-0123', 4.5, 2, ARRAY['cafe', 'outdoor_seating', 'wifi']),
('Brooklyn Bridge Pizza', 'restaurant', POINT(-73.997638, 40.706877),
'456 Brooklyn Bridge Blvd', 'New York', 'NY', 'USA',
'+1-718-555-0456', 4.2, 1, ARRAY['pizza', 'takeout', 'delivery']),
('Times Square Hotel', 'hotel', POINT(-73.985130, 40.758896),
'789 Times Square', 'New York', 'NY', 'USA',
'+1-212-555-0789', 4.0, 3, ARRAY['hotel', 'tourist_area', 'business_center']);
-- Advanced proximity search with spatial functions
WITH nearby_search AS (
SELECT
location_id,
name,
category,
coordinates,
address,
rating,
price_range,
tags,
-- Distance calculation using spatial functions
ST_DISTANCE(coordinates, POINT(-73.985130, 40.758896)) as distance_meters,
-- Bearing (direction) from search point to location
ST_AZIMUTH(POINT(-73.985130, 40.758896), coordinates) as bearing_radians,
ST_AZIMUTH(POINT(-73.985130, 40.758896), coordinates) * 180 / PI() as bearing_degrees,
-- Proximity categorization
CASE
WHEN ST_DISTANCE(coordinates, POINT(-73.985130, 40.758896)) <= 100 THEN 'immediate_vicinity'
WHEN ST_DISTANCE(coordinates, POINT(-73.985130, 40.758896)) <= 500 THEN 'very_close'
WHEN ST_DISTANCE(coordinates, POINT(-73.985130, 40.758896)) <= 1000 THEN 'walking_distance'
WHEN ST_DISTANCE(coordinates, POINT(-73.985130, 40.758896)) <= 5000 THEN 'short_drive'
ELSE 'distant'
END as proximity_category
FROM locations
WHERE
-- Spatial proximity filter (uses spatial index automatically)
ST_DWITHIN(coordinates, POINT(-73.985130, 40.758896), 2000) -- Within 2km
-- Additional filters
AND category = 'restaurant'
AND rating >= 4.0
AND price_range <= 2
ORDER BY distance_meters ASC
LIMIT 20
),
enhanced_results AS (
SELECT
ns.*,
-- Enhanced distance information
ROUND(distance_meters, 2) as distance_meters_rounded,
ROUND(distance_meters / 1000, 3) as distance_km,
-- Cardinal direction
CASE
WHEN bearing_degrees >= 337.5 OR bearing_degrees < 22.5 THEN 'North'
WHEN bearing_degrees >= 22.5 AND bearing_degrees < 67.5 THEN 'Northeast'
WHEN bearing_degrees >= 67.5 AND bearing_degrees < 112.5 THEN 'East'
WHEN bearing_degrees >= 112.5 AND bearing_degrees < 157.5 THEN 'Southeast'
WHEN bearing_degrees >= 157.5 AND bearing_degrees < 202.5 THEN 'South'
WHEN bearing_degrees >= 202.5 AND bearing_degrees < 247.5 THEN 'Southwest'
WHEN bearing_degrees >= 247.5 AND bearing_degrees < 292.5 THEN 'West'
WHEN bearing_degrees >= 292.5 AND bearing_degrees < 337.5 THEN 'Northwest'
END as direction,
-- Recommendation scoring
(
rating * 2 + -- Rating component
CASE proximity_category
WHEN 'immediate_vicinity' THEN 10
WHEN 'very_close' THEN 8
WHEN 'walking_distance' THEN 6
WHEN 'short_drive' THEN 4
ELSE 2
END +
(3 - price_range) * 1.5 -- Price component (lower price = higher score)
) as recommendation_score,
-- Walking time estimation (average 5 km/h walking speed)
ROUND(distance_meters / 1000 / 5 * 60, 0) as estimated_walking_minutes
FROM nearby_search ns
)
SELECT
location_id,
name,
category,
address,
rating,
price_range,
tags,
-- Distance and direction
distance_meters_rounded as distance_meters,
distance_km,
direction,
proximity_category,
-- Practical information
estimated_walking_minutes,
recommendation_score,
-- Helpful descriptions
CONCAT(
name, ' is ', distance_meters_rounded, 'm ', direction,
' (', estimated_walking_minutes, ' min walk)'
) as location_description
FROM enhanced_results
ORDER BY recommendation_score DESC, distance_meters ASC;
-- Geofencing operations with spatial containment
CREATE TABLE geofences (
geofence_id UUID PRIMARY KEY,
owner_id VARCHAR(50) NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
category VARCHAR(100) DEFAULT 'custom',
-- Geofence area (polygon or circle)
area POLYGON NOT NULL,
-- Behavior configuration
trigger_on_entry BOOLEAN DEFAULT true,
trigger_on_exit BOOLEAN DEFAULT true,
trigger_on_dwell BOOLEAN DEFAULT false,
dwell_time_seconds INTEGER DEFAULT 300,
-- Targeting
target_users VARCHAR(50)[],
target_user_groups VARCHAR(50)[],
-- Status and analytics
is_active BOOLEAN DEFAULT true,
entry_count INTEGER DEFAULT 0,
exit_count INTEGER DEFAULT 0,
dwell_count INTEGER DEFAULT 0,
last_triggered TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
WITH SPATIAL_INDEXING (
area USING '2dsphere',
COMPOUND INDEX (owner_id, is_active),
COMPOUND INDEX (category, is_active)
);
-- Create geofences with various geometric shapes
INSERT INTO geofences (
owner_id, name, description, category, area, target_users
) VALUES
-- Circular geofence around Central Park
('business_123', 'Central Park Zone', 'Marketing zone around Central Park', 'marketing',
ST_BUFFER(POINT(-73.965355, 40.782865), 500), -- 500m radius circle
ARRAY[]); -- Empty array means all users
-- Polygon geofence for Times Square area
INSERT INTO geofences (
owner_id, name, description, category, area, trigger_on_entry, trigger_on_exit
) VALUES
('business_456', 'Times Square District', 'High-traffic commercial zone', 'commercial',
POLYGON((
(-73.987140, 40.755751), -- Southwest corner
(-73.982915, 40.755751), -- Southeast corner
(-73.982915, 40.762077), -- Northeast corner
(-73.987140, 40.762077), -- Northwest corner
(-73.987140, 40.755751) -- Close the polygon
)),
true, true);
-- Advanced geofence event detection query
WITH user_location_check AS (
SELECT
ul.user_id,
ul.coordinates,
ul.recorded_at,
-- Find intersecting geofences
g.geofence_id,
g.name as geofence_name,
g.category,
g.trigger_on_entry,
g.trigger_on_exit,
g.trigger_on_dwell,
g.dwell_time_seconds,
-- Check spatial containment
ST_CONTAINS(g.area, ul.coordinates) as is_inside_geofence,
-- Previous location analysis for entry/exit detection
LAG(ul.coordinates) OVER (
PARTITION BY ul.user_id
ORDER BY ul.recorded_at
) as previous_coordinates,
LAG(ul.recorded_at) OVER (
PARTITION BY ul.user_id
ORDER BY ul.recorded_at
) as previous_timestamp
FROM user_locations ul
CROSS JOIN geofences g
WHERE
ul.recorded_at >= CURRENT_TIMESTAMP - INTERVAL '5 minutes'
AND g.is_active = true
AND (
ARRAY_LENGTH(g.target_users, 1) IS NULL OR -- No specific targeting
ul.user_id = ANY(g.target_users) -- User is specifically targeted
)
AND ST_DWITHIN(ul.coordinates, g.area, 100) -- Pre-filter for performance
),
geofence_events AS (
SELECT
ulc.*,
-- Event type detection
CASE
WHEN is_inside_geofence AND previous_coordinates IS NULL THEN 'entry'
WHEN is_inside_geofence AND NOT ST_CONTAINS(
(SELECT area FROM geofences WHERE geofence_id = ulc.geofence_id),
previous_coordinates
) THEN 'entry'
WHEN NOT is_inside_geofence AND ST_CONTAINS(
(SELECT area FROM geofences WHERE geofence_id = ulc.geofence_id),
previous_coordinates
) THEN 'exit'
WHEN is_inside_geofence AND ST_CONTAINS(
(SELECT area FROM geofences WHERE geofence_id = ulc.geofence_id),
previous_coordinates
) THEN 'dwelling'
ELSE 'none'
END as event_type,
-- Dwell time calculation
CASE
WHEN previous_timestamp IS NOT NULL THEN
EXTRACT(EPOCH FROM (recorded_at - previous_timestamp))
ELSE 0
END as dwell_time_seconds_calculated
FROM user_location_check ulc
WHERE is_inside_geofence = true OR previous_coordinates IS NOT NULL
),
actionable_events AS (
SELECT
ge.*,
-- Determine if event should trigger notifications
CASE
WHEN event_type = 'entry' AND trigger_on_entry THEN true
WHEN event_type = 'exit' AND trigger_on_exit THEN true
WHEN event_type = 'dwelling' AND trigger_on_dwell AND
dwell_time_seconds_calculated >= dwell_time_seconds THEN true
ELSE false
END as should_trigger_notification,
-- Event metadata
CURRENT_TIMESTAMP as event_processed_at,
GENERATE_UUID() as event_id
FROM geofence_events ge
WHERE event_type != 'none'
)
SELECT
event_id,
user_id,
geofence_id,
geofence_name,
category,
event_type,
coordinates,
recorded_at,
should_trigger_notification,
dwell_time_seconds_calculated,
-- Event context
CASE event_type
WHEN 'entry' THEN CONCAT('User entered ', geofence_name)
WHEN 'exit' THEN CONCAT('User exited ', geofence_name)
WHEN 'dwelling' THEN CONCAT('User dwelling in ', geofence_name, ' for ',
ROUND(dwell_time_seconds_calculated), ' seconds')
END as event_description,
-- Notification priority
CASE
WHEN category = 'security' THEN 'high'
WHEN category = 'marketing' AND event_type = 'entry' THEN 'medium'
WHEN event_type = 'dwelling' THEN 'low'
ELSE 'normal'
END as notification_priority
FROM actionable_events
WHERE should_trigger_notification = true
ORDER BY recorded_at DESC, notification_priority DESC;
-- Location analytics and heatmap generation
WITH location_density_analysis AS (
SELECT
-- Create spatial grid cells (approximately 100m x 100m)
FLOOR(ST_X(coordinates) * 1000) / 1000 as grid_lng,
FLOOR(ST_Y(coordinates) * 1000) / 1000 as grid_lat,
-- Calculate grid center point
ST_POINT(
(FLOOR(ST_X(coordinates) * 1000) + 0.5) / 1000,
(FLOOR(ST_Y(coordinates) * 1000) + 0.5) / 1000
) as grid_center,
COUNT(*) as location_count,
COUNT(DISTINCT user_id) as unique_users,
-- Temporal analysis
DATE_TRUNC('hour', recorded_at) as hour_bucket,
-- Movement analysis
AVG(speed_kmh) as avg_speed,
AVG(accuracy_meters) as avg_accuracy,
-- Activity classification
COUNT(*) FILTER (WHERE speed_kmh < 5) as stationary_count,
COUNT(*) FILTER (WHERE speed_kmh >= 5 AND speed_kmh < 25) as walking_count,
COUNT(*) FILTER (WHERE speed_kmh >= 25 AND speed_kmh < 60) as driving_count,
COUNT(*) FILTER (WHERE speed_kmh >= 60) as highway_count
FROM user_locations
WHERE recorded_at >= CURRENT_TIMESTAMP - INTERVAL '24 hours'
GROUP BY grid_lng, grid_lat, hour_bucket
),
heatmap_data AS (
SELECT
grid_center,
grid_lng,
grid_lat,
-- Density metrics
SUM(location_count) as total_locations,
COUNT(DISTINCT hour_bucket) as active_hours,
AVG(location_count) as avg_locations_per_hour,
MAX(location_count) as peak_hour_locations,
-- User engagement
SUM(unique_users) as total_unique_users,
AVG(unique_users) as avg_unique_users,
-- Activity distribution
SUM(stationary_count) as total_stationary,
SUM(walking_count) as total_walking,
SUM(driving_count) as total_driving,
SUM(highway_count) as total_highway,
-- Movement characteristics
AVG(avg_speed) as overall_avg_speed,
AVG(avg_accuracy) as overall_avg_accuracy,
-- Heat intensity calculation
LN(SUM(location_count) + 1) * LOG(SUM(unique_users) + 1) as heat_intensity
FROM location_density_analysis
GROUP BY grid_center, grid_lng, grid_lat
),
hotspot_analysis AS (
SELECT
hd.*,
-- Percentile rankings for intensity
PERCENT_RANK() OVER (ORDER BY heat_intensity) as intensity_percentile,
PERCENT_RANK() OVER (ORDER BY total_unique_users) as user_percentile,
PERCENT_RANK() OVER (ORDER BY total_locations) as activity_percentile,
-- Classification
CASE
WHEN heat_intensity > (SELECT PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY heat_intensity) FROM heatmap_data) THEN 'extreme_hotspot'
WHEN heat_intensity > (SELECT PERCENTILE_CONT(0.85) WITHIN GROUP (ORDER BY heat_intensity) FROM heatmap_data) THEN 'major_hotspot'
WHEN heat_intensity > (SELECT PERCENTILE_CONT(0.70) WITHIN GROUP (ORDER BY heat_intensity) FROM heatmap_data) THEN 'moderate_hotspot'
WHEN heat_intensity > (SELECT PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY heat_intensity) FROM heatmap_data) THEN 'minor_activity'
ELSE 'low_activity'
END as hotspot_classification,
-- Activity type classification
CASE
WHEN total_stationary > (total_walking + total_driving + total_highway) * 0.7 THEN 'destination_area'
WHEN total_walking > total_locations * 0.6 THEN 'pedestrian_area'
WHEN total_driving > total_locations * 0.6 THEN 'transit_area'
WHEN total_highway > total_locations * 0.4 THEN 'highway_corridor'
ELSE 'mixed_use_area'
END as area_type
FROM heatmap_data hd
WHERE total_locations >= 10 -- Filter out low-activity areas
)
SELECT
grid_center,
ST_X(grid_center) as longitude,
ST_Y(grid_center) as latitude,
-- Density and activity metrics
total_locations,
total_unique_users,
active_hours,
avg_locations_per_hour,
peak_hour_locations,
-- Classification results
hotspot_classification,
area_type,
-- Intensity and ranking
ROUND(heat_intensity, 3) as heat_intensity,
ROUND(intensity_percentile * 100, 1) as intensity_percentile_rank,
-- Activity breakdown
ROUND((total_stationary::NUMERIC / total_locations) * 100, 1) as stationary_pct,
ROUND((total_walking::NUMERIC / total_locations) * 100, 1) as walking_pct,
ROUND((total_driving::NUMERIC / total_locations) * 100, 1) as driving_pct,
-- Movement characteristics
ROUND(overall_avg_speed, 2) as avg_speed_kmh,
ROUND(overall_avg_accuracy, 1) as avg_accuracy_meters,
-- Insights and recommendations
CASE hotspot_classification
WHEN 'extreme_hotspot' THEN 'High-priority area for business development'
WHEN 'major_hotspot' THEN 'Significant commercial opportunity'
WHEN 'moderate_hotspot' THEN 'Growing activity area with potential'
ELSE 'Monitor for emerging trends'
END as business_recommendation
FROM hotspot_analysis
ORDER BY heat_intensity DESC, total_unique_users DESC
LIMIT 100;
-- Advanced user movement pattern analysis
WITH user_journeys AS (
SELECT
user_id,
coordinates,
recorded_at,
speed_kmh,
-- Movement analysis using window functions
LAG(coordinates) OVER (
PARTITION BY user_id
ORDER BY recorded_at
) as prev_coordinates,
LAG(recorded_at) OVER (
PARTITION BY user_id
ORDER BY recorded_at
) as prev_timestamp,
LEAD(coordinates) OVER (
PARTITION BY user_id
ORDER BY recorded_at
) as next_coordinates,
-- Session detection (gap > 30 minutes = new session)
SUM(CASE
WHEN recorded_at - LAG(recorded_at) OVER (
PARTITION BY user_id ORDER BY recorded_at
) > INTERVAL '30 minutes' THEN 1
ELSE 0
END) OVER (
PARTITION BY user_id
ORDER BY recorded_at
ROWS UNBOUNDED PRECEDING
) as session_number
FROM user_locations
WHERE recorded_at >= CURRENT_TIMESTAMP - INTERVAL '7 days'
),
journey_segments AS (
SELECT
uj.*,
-- Distance calculations
CASE
WHEN prev_coordinates IS NOT NULL THEN
ST_DISTANCE(coordinates, prev_coordinates)
ELSE 0
END as distance_from_previous,
-- Time calculations
CASE
WHEN prev_timestamp IS NOT NULL THEN
EXTRACT(EPOCH FROM (recorded_at - prev_timestamp))
ELSE 0
END as time_since_previous,
-- Direction calculations
CASE
WHEN prev_coordinates IS NOT NULL THEN
ST_AZIMUTH(prev_coordinates, coordinates) * 180 / PI()
ELSE NULL
END as bearing_from_previous,
-- Stop detection
CASE
WHEN speed_kmh < 2 AND
LAG(speed_kmh) OVER (PARTITION BY user_id ORDER BY recorded_at) < 2
THEN true
ELSE false
END as is_stopped
FROM user_journeys uj
),
movement_patterns AS (
SELECT
user_id,
session_number,
-- Session boundaries
MIN(recorded_at) as session_start,
MAX(recorded_at) as session_end,
EXTRACT(SECONDS FROM (MAX(recorded_at) - MIN(recorded_at))) as session_duration_seconds,
-- Movement statistics
COUNT(*) as total_location_points,
SUM(distance_from_previous) as total_distance_meters,
AVG(speed_kmh) as avg_speed_kmh,
MAX(speed_kmh) as max_speed_kmh,
-- Stop analysis
COUNT(*) FILTER (WHERE is_stopped) as stop_count,
AVG(time_since_previous) FILTER (WHERE is_stopped) as avg_stop_duration,
-- Geographic analysis
ST_EXTENT(coordinates) as bounding_box,
ST_CENTROID(ST_COLLECT(coordinates)) as activity_center,
-- Movement characteristics
CASE
WHEN AVG(speed_kmh) < 5 THEN 'pedestrian'
WHEN AVG(speed_kmh) < 25 THEN 'urban_transit'
WHEN AVG(speed_kmh) < 80 THEN 'highway_driving'
ELSE 'high_speed_transit'
END as primary_movement_mode,
-- Journey classification
CASE
WHEN SUM(distance_from_previous) < 500 THEN 'local_area'
WHEN SUM(distance_from_previous) < 5000 THEN 'neighborhood'
WHEN SUM(distance_from_previous) < 50000 THEN 'city_wide'
ELSE 'long_distance'
END as journey_scope
FROM journey_segments
WHERE distance_from_previous IS NOT NULL
GROUP BY user_id, session_number
)
SELECT
user_id,
session_number,
session_start,
session_end,
-- Duration and distance
ROUND(session_duration_seconds / 60, 1) as duration_minutes,
ROUND(total_distance_meters, 2) as distance_meters,
ROUND(total_distance_meters / 1000, 3) as distance_km,
-- Movement characteristics
primary_movement_mode,
journey_scope,
ROUND(avg_speed_kmh, 2) as avg_speed_kmh,
ROUND(max_speed_kmh, 2) as max_speed_kmh,
-- Activity analysis
total_location_points,
stop_count,
ROUND(avg_stop_duration / 60, 1) as avg_stop_duration_minutes,
-- Geographic insights
ST_X(activity_center) as center_longitude,
ST_Y(activity_center) as center_latitude,
-- Journey insights
CASE
WHEN stop_count > total_location_points * 0.3 THEN 'multi_destination_trip'
WHEN stop_count > 0 THEN 'trip_with_stops'
ELSE 'direct_trip'
END as trip_pattern,
-- Efficiency metrics
CASE
WHEN session_duration_seconds > 0 THEN
ROUND((total_distance_meters / session_duration_seconds) * 3.6, 2) -- km/h
ELSE 0
END as overall_journey_speed,
-- Movement efficiency (straight line vs actual distance)
CASE
WHEN bounding_box IS NOT NULL THEN
ROUND(
(ST_DISTANCE(
ST_POINT(ST_XMIN(bounding_box), ST_YMIN(bounding_box)),
ST_POINT(ST_XMAX(bounding_box), ST_YMAX(bounding_box))
) / NULLIF(total_distance_meters, 0)) * 100,
2
)
ELSE NULL
END as route_efficiency_pct
FROM movement_patterns
WHERE session_duration_seconds > 60 -- Filter very short sessions
ORDER BY user_id, session_start DESC;
-- QueryLeaf provides comprehensive geospatial capabilities:
-- 1. SQL-familiar spatial data types and indexing (POINT, POLYGON, etc.)
-- 2. Advanced spatial functions (ST_DISTANCE, ST_CONTAINS, ST_BUFFER, etc.)
-- 3. Optimized proximity searches with automatic spatial index utilization
-- 4. Sophisticated geofencing with entry/exit/dwell event detection
-- 5. Location analytics and heatmap generation with spatial aggregation
-- 6. Movement pattern analysis with trajectory and behavioral insights
-- 7. Real-time spatial event processing and notification triggers
-- 8. Integration with MongoDB's native 2dsphere indexing optimization
-- 9. Complex spatial queries with business logic and filtering
-- 10. Production-ready geospatial operations with familiar SQL syntax
Best Practices for Geospatial Implementation
Spatial Index Strategy and Performance Optimization
Essential principles for effective MongoDB geospatial deployment:
- Index Design: Create compound spatial indexes that combine location data with frequently queried attributes
- Query Optimization: Structure queries to leverage spatial indexes effectively and minimize computational overhead
- Coordinate System: Standardize on WGS84 (EPSG:4326) for consistency and optimal MongoDB performance
- Data Validation: Implement comprehensive GeoJSON validation to prevent spatial query errors
- Scaling Strategy: Design geospatial collections for horizontal scaling with appropriate shard key selection
- Caching Strategy: Implement spatial query result caching for frequently accessed location data
Production Deployment and Location Intelligence
Optimize geospatial operations for enterprise-scale location-based services:
- Real-Time Processing: Leverage change streams and geofencing for responsive location-aware applications
- Analytics Integration: Combine spatial data with business intelligence for location-driven insights
- Privacy Compliance: Implement location data privacy controls and user consent management
- Performance Monitoring: Track spatial query performance and optimize based on usage patterns
- Fault Tolerance: Design location services with redundancy and failover capabilities
- Mobile Optimization: Optimize for mobile device constraints including battery usage and network efficiency
Conclusion
MongoDB geospatial capabilities provide comprehensive native location-based services that eliminate the complexity of external GIS systems through advanced spatial indexing, sophisticated geometric operations, and seamless integration with application data models. The combination of high-performance spatial queries with real-time geofencing and location analytics makes MongoDB ideal for modern location-aware applications.
Key MongoDB Geospatial benefits include:
- Native Spatial Indexing: Advanced 2dsphere indexes with optimized geometric operations and coordinate system support
- Comprehensive GeoJSON Support: Full support for points, polygons, lines, and complex geometries with native validation
- High-Performance Proximity: Optimized distance calculations and bearing analysis for location-based queries
- Real-Time Geofencing: Advanced geofence event detection with entry, exit, and dwell time triggers
- Location Analytics: Sophisticated spatial aggregation for heatmaps, movement patterns, and location intelligence
- SQL Accessibility: Familiar SQL-style spatial operations through QueryLeaf for accessible geospatial development
Whether you're building ride-sharing platforms, delivery services, social media applications, or location-based marketing systems, MongoDB geospatial capabilities with QueryLeaf's familiar SQL interface provide the foundation for scalable, high-performance location services.
QueryLeaf Integration: QueryLeaf seamlessly manages MongoDB geospatial operations while providing SQL-familiar spatial data types, indexing strategies, and location-based query capabilities. Advanced geospatial patterns including proximity searches, geofencing, movement analysis, and location analytics are elegantly handled through familiar SQL constructs, making sophisticated location-based services both powerful and accessible to SQL-oriented development teams.
The combination of MongoDB's robust geospatial capabilities with SQL-style location operations makes it an ideal platform for applications requiring both advanced spatial functionality and familiar database interaction patterns, ensuring your location services can scale efficiently while delivering precise, real-time geographic experiences.