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