pulse-ai-utils
Version:
Utility functions and helpers for AI-powered applications
299 lines • 12.2 kB
JavaScript
;
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;
exports.detectSearchType = detectSearchType;
const index_1 = require("./index");
const openai_helper_1 = __importDefault(require("../helpers/openai-helper"));
/**
* Improved search type detection logic
*/
function detectSearchType(query) {
const lowerQuery = query.toLowerCase();
// Content-focused keywords that should override location detection
const contentKeywords = [
'family', 'friendly', 'kids', 'children', 'activities', 'things to do',
'events', 'restaurants', 'deals', 'shows', 'entertainment', 'attractions',
'museums', 'parks', 'beaches', 'shopping', 'dining', 'nightlife',
'tours', 'experiences', 'adventures', 'fun', 'visit', 'explore',
'arts', 'art', 'culture', 'theater', 'theatre', 'gallery', 'galleries',
'music', 'concert', 'festival', 'performance', 'exhibition', 'creative'
];
// Strong temporal indicators
const strongTemporalKeywords = ['today', 'tonight', 'tomorrow', 'this weekend'];
// Check for strong content focus
const hasContentFocus = contentKeywords.some(keyword => lowerQuery.includes(keyword));
// Check for temporal context
const hasStrongTemporal = strongTemporalKeywords.some(keyword => lowerQuery.includes(keyword));
const hasWeakTemporal = ['weekend', 'week', 'month', 'now', 'soon'].some(keyword => lowerQuery.includes(keyword));
// Location indicators (but weaker than content)
const locationPrepositions = ['in', 'at', 'near', 'around'];
const hasLocationPrep = locationPrepositions.some(prep => {
// Check if it's actually used as a location indicator
const regex = new RegExp(`\\b${prep}\\s+\\w+`, 'i');
return regex.test(query);
});
// Decision logic with content priority
if (hasContentFocus) {
// If there's clear content focus, prioritize content search
if (hasStrongTemporal) {
return 'combined'; // Content + temporal
}
return 'content'; // Pure content search
}
// If no content focus, check other signals
if (hasStrongTemporal && hasLocationPrep) {
return 'combined';
}
else if (hasStrongTemporal) {
return 'temporal';
}
else if (hasLocationPrep && !hasWeakTemporal) {
return 'location';
}
// Default to content search for better general results
return 'content';
}
/**
* Search content using vector similarity with improved type detection
*/
async function searchContent(options) {
const { query, embedding, limit = 10, threshold = 0.3, filters = {} } = options;
try {
// Use provided embedding or generate new one
let searchEmbedding = embedding;
if (!searchEmbedding) {
const openai = new openai_helper_1.default();
searchEmbedding = await openai.generateEmbedding(query);
}
// Use improved search type detection
const searchType = detectSearchType(query);
console.log(`[VectorSearch] Query: "${query}"`);
console.log(`[VectorSearch] Detected search type: ${searchType}`);
if (options.lat && options.lng) {
console.log(`[VectorSearch] Geographic search: lat=${options.lat}, lng=${options.lng}, radius=${options.radius || 'default'}km`);
}
// Use geohash-enhanced search if geographic parameters are provided
const useGeohash = options.lat !== undefined && options.lng !== undefined;
// Build parameters based on which function we're calling
const rpcParams = {
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
};
// Only add geographic parameters for geohash function
if (useGeohash) {
rpcParams.center_lat = options.lat;
rpcParams.center_lng = options.lng;
rpcParams.radius = options.radius || null;
}
// Call the appropriate Supabase function
const { data, error } = await index_1.supabase.rpc(useGeohash ? 'search_content_with_geohash' : 'search_content_multi_vectors', rpcParams);
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);
}
// Filter out low-quality matches for content searches
if (searchType === 'content' || searchType === 'combined') {
results = results.filter(r => r.similarity > 0.35);
}
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
*/
async function hybridSearch(query, options = {}) {
const { vectorWeight = 0.7, textWeight = 0.3, limit = 10, filters = {} } = options;
try {
// Perform vector search with improved detection and geographic parameters
const vectorResults = await searchContent({
query,
limit: limit * 2, // Get more results for merging
filters,
lat: options.lat,
lng: options.lng,
radius: options.radius
});
// 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 geographic filter if provided
if (options.lat !== undefined && options.lng !== undefined) {
// Use the geographic search function for text search too
const { data: geoFilteredIds } = await index_1.supabase.rpc('search_content_by_geohash', {
center_lat: options.lat,
center_lng: options.lng,
radius: options.radius || 10,
result_limit: 1000 // Get more IDs for filtering
});
if (geoFilteredIds && geoFilteredIds.length > 0) {
const ids = geoFilteredIds.map((item) => item.id);
textQuery = textQuery.in('id', ids);
}
}
// 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-improved.js.map