pulse-ai-utils
Version:
Utility functions and helpers for AI-powered applications
106 lines • 4.49 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GeohashFirestoreQuery = void 0;
const geohash_service_1 = require("./geohash-service");
class GeohashFirestoreQuery {
constructor() {
this.geohashService = new geohash_service_1.GeohashService();
}
/**
* Perform efficient geohash-based query on Firestore
*/
async queryByRadius(db, collection, centerLat, centerLng, radius, filters = {}) {
// Get optimized query ranges
const ranges = this.geohashService.getSearchRanges(centerLat, centerLng, radius);
console.log(`[GeohashQuery] Searching with ${ranges.length} ranges for ${radius}km radius`);
// Execute queries in parallel
const queryPromises = ranges.map(([start, end]) => this.executeRangeQuery(db, collection, start, end, filters));
const results = await Promise.all(queryPromises);
const allDocs = results.flat();
// Remove duplicates and filter by exact radius
const uniqueDocs = this.deduplicateAndFilter(allDocs, centerLat, centerLng, radius);
// Sort by distance
return uniqueDocs.sort((a, b) => a.distance - b.distance);
}
/**
* Execute a single range query
*/
async executeRangeQuery(db, collection, startHash, endHash, filters) {
let query = db.collection(collection)
.where('geohash', '>=', startHash)
.where('geohash', '<', endHash);
// Apply additional filters
const now = new Date().toISOString();
query = query.where('expire_at', '>', now);
// Apply other filters (but be careful with Firestore's composite index requirements)
if (filters.type) {
query = query.where('type', '==', filters.type);
}
// Note: Can't use additional inequality filters due to Firestore limitations
// Other filters will need to be applied client-side
const snapshot = await query.get();
return snapshot.docs.map(doc => ({
id: doc.id,
data: doc.data()
}));
}
/**
* Deduplicate results and filter by exact radius
*/
deduplicateAndFilter(docs, centerLat, centerLng, radius) {
const seen = new Set();
const results = [];
for (const doc of docs) {
// Skip if already seen
if (seen.has(doc.id))
continue;
seen.add(doc.id);
// Skip if no coordinates
if (!doc.data.lat || !doc.data.lng)
continue;
// Calculate exact distance
const distance = this.haversineDistance(centerLat, centerLng, doc.data.lat, doc.data.lng);
// Filter by exact radius
if (distance <= radius) {
results.push({
id: doc.id,
distance,
data: doc.data
});
}
}
return results;
}
/**
* Calculate distance between two points
*/
haversineDistance(lat1, lng1, lat2, lng2) {
const R = 6371; // km
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLng / 2) * Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
/**
* Progressive radius search - start small and expand if needed
*/
async progressiveRadiusSearch(db, collection, centerLat, centerLng, options = {}) {
const { initialRadius = 5, maxRadius = 50, minResults = 20, filters = {} } = options;
let currentRadius = initialRadius;
let results = [];
while (currentRadius <= maxRadius && results.length < minResults) {
console.log(`[GeohashQuery] Searching with radius ${currentRadius}km`);
results = await this.queryByRadius(db, collection, centerLat, centerLng, currentRadius, filters);
if (results.length >= minResults)
break;
// Expand radius
currentRadius = Math.min(currentRadius * 2, maxRadius);
}
return results.slice(0, minResults * 2); // Return extra for client filtering
}
}
exports.GeohashFirestoreQuery = GeohashFirestoreQuery;
//# sourceMappingURL=geohash-firestore-query.js.map