pulse-ai-utils
Version:
Utility functions and helpers for AI-powered applications
223 lines • 9.71 kB
JavaScript
;
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