UNPKG

pulse-ai-utils

Version:

Utility functions and helpers for AI-powered applications

223 lines 9.71 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GeoSearchService = void 0; const location_service_1 = require("./location-service"); const geo_search_1 = require("./geo-search"); class GeoSearchService { constructor(options = {}) { this.progressiveRadii = [5, 10, 20, 40, 50]; // miles this.firestoreDb = options.firestoreDb || null; this.supabaseClient = options.supabaseClient || null; this.locationService = options.locationService || new location_service_1.LocationService(); this.useSupabase = options.useSupabase !== false; this.minResults = options.minResults || 20; } /** * Search for content near a geographic location */ async searchNearby(userLat, userLng, options = {}) { const { type, initialRadius = 5, maxRadius = 50, minResults = this.minResults, limit = 50, includeEmbeddings = false } = options; // Try Supabase first if enabled if (this.useSupabase && this.supabaseClient) { try { const { data, error } = await this.supabaseClient.rpc('search_content_geographic', { user_lat: userLat, user_lng: userLng, initial_radius: initialRadius, max_radius: maxRadius, min_results: minResults, filter_type: type || null }); if (error) throw error; // Extract actual search radius from results let searchRadius = initialRadius; if (data && data.length > 0) { const maxDistance = Math.max(...data.map((r) => r.distance_miles || 0)); // Find which radius was actually used for (const radius of this.progressiveRadii) { if (maxDistance <= radius) { searchRadius = radius; break; } } } return { results: data.slice(0, limit), searchRadius, totalFound: data.length, searchLocation: { lat: userLat, lng: userLng } }; } catch (error) { console.error('Supabase geographic search failed:', error); if (!this.firestoreDb) throw error; // Fall back to Firestore } } // Firestore implementation with progressive radius expansion if (this.firestoreDb) { for (const radius of this.progressiveRadii) { if (radius > maxRadius) break; const results = await this.searchFirestoreByRadius(userLat, userLng, radius, type, limit); if (results.length >= minResults) { return { results, searchRadius: radius, totalFound: results.length, searchLocation: { lat: userLat, lng: userLng } }; } } // Return whatever we found at max radius const finalResults = await this.searchFirestoreByRadius(userLat, userLng, maxRadius, type, limit); return { results: finalResults, searchRadius: maxRadius, totalFound: finalResults.length, searchLocation: { lat: userLat, lng: userLng } }; } throw new Error('No database configured for geographic search'); } /** * Search by area name */ async searchByArea(areaId, options = {}) { const location = await this.locationService.getLocationFromArea(areaId); if (!location) { throw new Error(`Unknown area: ${areaId}`); } return this.searchNearby(location.lat, location.lng, options); } /** * Search near multiple locations */ async searchMultiLocation(locations, options = {}) { const allResults = new Map(); // Search near each location for (const location of locations) { const searchResult = await this.searchNearby(location.lat, location.lng, { ...options, minResults: Math.floor((options.minResults || 20) / locations.length) }); // Merge results, keeping highest relevance score for (const result of searchResult.results) { const existing = allResults.get(result.id); const locationRelevance = location.relevance || 1.0; const adjustedScore = result.relevance_score * locationRelevance; if (!existing || adjustedScore > existing.relevance_score) { allResults.set(result.id, { ...result, relevance_score: adjustedScore }); } } } // Sort by relevance and return const results = Array.from(allResults.values()) .sort((a, b) => b.relevance_score - a.relevance_score) .slice(0, options.limit || 50); return { results, totalFound: allResults.size, searchRadius: options.maxRadius || 50, searchLocation: locations[0] // Primary location }; } /** * Firestore search implementation */ async searchFirestoreByRadius(userLat, userLng, radiusMiles, type, limit = 50) { if (!this.firestoreDb) return []; // Generate geohash bounds for the search radius const bounds = (0, geo_search_1.getGeohashBounds)(userLat, userLng, radiusMiles); const results = []; // Query each geohash range for (const bound of bounds) { let query = this.firestoreDb.collection('content') .where('geohash', '>=', bound.lower) .where('geohash', '<=', bound.upper); if (type) { // Can't add another where clause due to Firestore limitations // Will filter in memory } const snapshot = await query.limit(limit * 2).get(); // Get extra for filtering snapshot.forEach(doc => { const data = doc.data(); // Type filter if needed if (type && data.type !== type) return; // Calculate actual distance if (data.primary_location) { const distance = (0, geo_search_1.calculateDistance)(userLat, userLng, data.primary_location.lat || data.primary_location._latitude, data.primary_location.lng || data.primary_location._longitude); // Only include if within actual radius if (distance <= radiusMiles) { results.push({ id: doc.id, ...data, distance_miles: distance, relevance_score: 1 / (1 + distance * 0.1) }); } } }); } // Sort by distance and limit return results .sort((a, b) => a.distance_miles - b.distance_miles) .slice(0, limit); } /** * Get content by IDs with location enrichment */ async getContentByIds(ids, userLat, userLng) { if (this.useSupabase && this.supabaseClient) { const { data, error } = await this.supabaseClient .from('content') .select('*') .in('id', ids); if (error) throw error; return data.map(item => { let distance = 0; let relevance = 1; if (userLat && userLng && item.primary_location) { distance = (0, geo_search_1.calculateDistance)(userLat, userLng, item.primary_location.coordinates[1], // PostGIS: [lng, lat] item.primary_location.coordinates[0]); relevance = 1 / (1 + distance * 0.1); } return { ...item, distance_miles: distance, relevance_score: relevance }; }); } if (this.firestoreDb) { const results = []; for (const id of ids) { const doc = await this.firestoreDb.collection('content').doc(id).get(); if (doc.exists) { const data = doc.data(); let distance = 0; let relevance = 1; if (userLat && userLng && data.primary_location) { distance = (0, geo_search_1.calculateDistance)(userLat, userLng, data.primary_location.lat || data.primary_location._latitude, data.primary_location.lng || data.primary_location._longitude); relevance = 1 / (1 + distance * 0.1); } results.push({ id: doc.id, ...data, distance_miles: distance, relevance_score: relevance }); } } return results.sort((a, b) => a.distance_miles - b.distance_miles); } throw new Error('No database configured'); } } exports.GeoSearchService = GeoSearchService; //# sourceMappingURL=geo-search-service.js.map