UNPKG

pulse-ai-utils

Version:

Utility functions and helpers for AI-powered applications

334 lines 13.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.searchContent = searchContent; exports.searchContentChunks = searchContentChunks; exports.hybridSearch = hybridSearch; exports.findSimilarContent = findSimilarContent; exports.updateContentMetadata = updateContentMetadata; const index_1 = require("./index"); const openai_helper_1 = __importDefault(require("../helpers/openai-helper")); /** * Search content using vector similarity with optional geographic filtering */ async function searchContent(options) { const { query, embedding, limit = 10, threshold = 0.6, // Increased from 0.3 to reduce false positives from location embeddings lat, lng, radius = 25, // Default 25km radius filters = {} } = options; try { // Use hybrid geohash search if geographic parameters are provided if (lat !== undefined && lng !== undefined) { console.log(`[VectorSearch] Using hybrid geohash search for query: "${query}" at ${lat}, ${lng}`); const { data, error } = await index_1.supabase.rpc('search_content_hybrid_geohash', { query_text: query, area_filter: filters.area || null, user_lat: lat, user_lng: lng, radius_km: radius, limit_count: limit }); if (error) { console.error('Error in hybrid geohash search:', error); throw error; } // Map the hybrid search results to ContentSearchResult format let results = data.map(item => ({ id: item.id, type: item.data?.type || 'unknown', title: item.title, source: item.source, source_url: item.source_url, image_url: item.image_url, data: item.data, seen: item.data?.seen || false, rank: item.rank, content_date: item.content_date, similarity: item.total_similarity || item.semantic_score || 0, // Hybrid search specific fields distance_km: item.distance_km, total_similarity: item.total_similarity, content_similarity: item.content_similarity, location_similarity: item.location_similarity, temporal_similarity: item.temporal_similarity, query_type: item.query_type, geohash_matched: item.geohash_matched, semantic_score: item.semantic_score })); // Apply additional filters that aren't in the SQL function if (filters.seen !== undefined) { results = results.filter(r => r.seen === filters.seen); } if (filters.type) { results = results.filter(r => r.type === filters.type); } if (filters.source) { results = results.filter(r => r.source === filters.source); } return results; } // Fallback to legacy vector search for non-geographic queries console.log(`[VectorSearch] Using legacy vector search for query: "${query}"`); // Use provided embedding or generate new one let searchEmbedding = embedding; if (!searchEmbedding) { const openai = new openai_helper_1.default(); searchEmbedding = await openai.generateEmbedding(query); } // Determine which embedding type to search based on query content let searchType = 'content'; // default // Detect if query has location context const locationKeywords = ['in', 'at', 'near', 'tampa', 'miami', 'orlando', 'florida']; const hasLocation = locationKeywords.some(keyword => query.toLowerCase().includes(keyword)); // Detect if query has temporal context const temporalKeywords = ['today', 'tonight', 'tomorrow', 'weekend', 'this week', 'now']; const hasTemporal = temporalKeywords.some(keyword => query.toLowerCase().includes(keyword)); if (hasLocation && hasTemporal) { searchType = 'combined'; } else if (hasLocation) { searchType = 'location'; } else if (hasTemporal) { searchType = 'temporal'; } // Call the enhanced Supabase function with multi-embedding support const { data, error } = await index_1.supabase.rpc('search_content_multi_vectors', { query_embedding: searchEmbedding, search_type: searchType, match_threshold: threshold, match_count: limit, filter_type: filters.type || null, filter_area: filters.area || null, filter_region: filters.region || null, filter_date_from: filters.dateFrom || null, filter_date_to: filters.dateTo || null }); if (error) { console.error('Error searching content:', error); throw error; } // Apply additional filters that aren't in the SQL function let results = data; if (filters.seen !== undefined) { results = results.filter(r => r.seen === filters.seen); } return results; } catch (error) { console.error('Error in searchContent:', error); throw error; } } /** * Search content chunks for longer documents */ async function searchContentChunks(query, limit = 20, threshold = 0.7) { try { const openai = new openai_helper_1.default(); const searchEmbedding = await openai.generateEmbedding(query); const { data, error } = await index_1.supabase.rpc('search_content_chunks', { query_embedding: searchEmbedding, match_threshold: threshold, match_count: limit }); if (error) { console.error('Error searching content chunks:', error); throw error; } return data; } catch (error) { console.error('Error in searchContentChunks:', error); throw error; } } /** * Hybrid search combining vector and full-text search with optional geographic filtering */ async function hybridSearch(query, options = {}) { const { vectorWeight = 0.7, textWeight = 0.3, limit = 10, lat, lng, radius = 25, filters = {} } = options; try { // Use the new hybrid geohash search if geographic parameters are provided if (lat !== undefined && lng !== undefined) { console.log(`[HybridSearch] Using hybrid geohash search for query: "${query}" at ${lat}, ${lng}`); const { data, error } = await index_1.supabase.rpc('search_content_hybrid_geohash', { query_text: query, area_filter: filters?.area || null, user_lat: lat, user_lng: lng, radius_km: radius, limit_count: limit }); if (error) { console.error('Error in hybrid geohash search:', error); throw error; } // Map the hybrid search results to ContentSearchResult format let results = data.map(item => ({ id: item.id, type: item.data?.type || 'unknown', title: item.title, source: item.source, source_url: item.source_url, image_url: item.image_url, data: item.data, seen: item.data?.seen || false, rank: item.rank, content_date: item.content_date, similarity: item.total_similarity || item.semantic_score || 0, // Hybrid search specific fields distance_km: item.distance_km, total_similarity: item.total_similarity, content_similarity: item.content_similarity, location_similarity: item.location_similarity, temporal_similarity: item.temporal_similarity, query_type: item.query_type, geohash_matched: item.geohash_matched, semantic_score: item.semantic_score })); // Apply additional filters if (filters?.seen !== undefined) { results = results.filter(r => r.seen === filters.seen); } if (filters?.type) { results = results.filter(r => r.type === filters.type); } if (filters?.source) { results = results.filter(r => r.source === filters.source); } return results; } // Fallback to legacy hybrid search for non-geographic queries console.log(`[HybridSearch] Using legacy hybrid search for query: "${query}"`); // Perform vector search const vectorResults = await searchContent({ query, limit: limit * 2, // Get more results for merging filters }); // Perform full-text search // Process query for PostgreSQL tsquery format (spaces → &) const processedQuery = query.trim().split(/\s+/).join(' & '); let textQuery = index_1.supabase .from('content') .select('*, similarity:search_text') .textSearch('search_text', processedQuery) .limit(limit * 2); // Apply filters if (filters.type) textQuery = textQuery.eq('type', filters.type); if (filters.source) textQuery = textQuery.eq('source', filters.source); if (filters.area) textQuery = textQuery.eq('data->>area', filters.area); if (filters.region) textQuery = textQuery.eq('data->>region', filters.region); if (filters.dateFrom) textQuery = textQuery.gte('content_date', filters.dateFrom); if (filters.dateTo) textQuery = textQuery.lte('content_date', filters.dateTo); if (filters.seen !== undefined) textQuery = textQuery.eq('seen', filters.seen); const { data: textResults, error } = await textQuery; if (error) { console.error('Error in text search:', error); throw error; } // Merge and score results const scoreMap = new Map(); const resultMap = new Map(); // Add vector search results vectorResults.forEach(result => { const score = result.similarity * vectorWeight; scoreMap.set(result.id, score); resultMap.set(result.id, result); }); // Add text search results textResults?.forEach((result) => { const currentScore = scoreMap.get(result.id) || 0; // Normalize text search relevance (assuming it's a count or rank) const textScore = (1 / (result.similarity + 1)) * textWeight; scoreMap.set(result.id, currentScore + textScore); if (!resultMap.has(result.id)) { resultMap.set(result.id, { ...result, similarity: textScore / textWeight // Normalize back }); } }); // Sort by combined score and return top results const sortedResults = Array.from(resultMap.entries()) .map(([id, result]) => ({ ...result, similarity: scoreMap.get(id) || 0 })) .sort((a, b) => b.similarity - a.similarity) .slice(0, limit); return sortedResults; } catch (error) { console.error('Error in hybridSearch:', error); throw error; } } /** * Get similar content based on an existing content ID */ async function findSimilarContent(contentId, limit = 5) { try { // First, get the content and its embedding const { data: content, error: fetchError } = await index_1.supabase .from('content') .select('*') .eq('id', contentId) .single(); if (fetchError || !content) { throw new Error(`Content ${contentId} not found`); } if (!content.embedding) { throw new Error(`Content ${contentId} has no embedding`); } // Search for similar content, excluding the original const { data, error } = await index_1.supabase.rpc('search_content_vectors_json', { query_embedding: content.embedding, match_threshold: 0.5, match_count: limit + 1, // Get one extra to exclude self filter_type: content.type // Only search within same type }); if (error) { console.error('Error finding similar content:', error); throw error; } // Filter out the original content const results = data .filter(r => r.id !== contentId) .slice(0, limit); return results; } catch (error) { console.error('Error in findSimilarContent:', error); throw error; } } /** * Update content metadata (seen, rank) after user interaction */ async function updateContentMetadata(contentId, updates) { try { const { error } = await index_1.supabase .from('content') .update(updates) .eq('id', contentId); if (error) { console.error('Error updating content metadata:', error); throw error; } } catch (error) { console.error('Error in updateContentMetadata:', error); throw error; } } //# sourceMappingURL=vector-search.js.map