pulse-ai-utils
Version:
Utility functions and helpers for AI-powered applications
314 lines • 13.1 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 adjacent area support
*/
async function searchContent(options) {
const { query, embedding, limit = 10, threshold = 0.3, filters = {}, includeAdjacent = true } = 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}`);
console.log(`[VectorSearch] Include adjacent areas: ${includeAdjacent}`);
// Call the enhanced Supabase function with adjacent area support
const { data, error } = await index_1.supabase.rpc('search_content_multi_vectors_adjacent', {
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,
include_adjacent: includeAdjacent
});
if (error) {
// Fallback to original function if new one doesn't exist
if (error.message?.includes('function search_content_multi_vectors_adjacent')) {
console.warn('[VectorSearch] Adjacent area function not found, falling back to original');
const { data: fallbackData, error: fallbackError } = 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 (fallbackError) {
console.error('Error searching content (fallback):', fallbackError);
throw fallbackError;
}
return fallbackData;
}
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);
}
// Log area relevance info for debugging
if (includeAdjacent && filters.area && results.length > 0) {
console.log(`[VectorSearch] Results with area relevance for ${filters.area}:`);
results.slice(0, 3).forEach((r, i) => {
console.log(` ${i + 1}. ${r.title} (${r.primary_area}) - Relevance: ${r.area_relevance?.toFixed(2)}`);
});
}
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 adjacent area support
*/
async function hybridSearch(query, options = {}) {
const { vectorWeight = 0.7, textWeight = 0.3, limit = 10, filters = {}, includeAdjacent = true } = options;
try {
// Perform vector search with adjacent area support
const vectorResults = await searchContent({
query,
limit: limit * 2, // Get more results for merging
filters,
includeAdjacent
});
// 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 - with adjacent area support
if (filters.type)
textQuery = textQuery.eq('type', filters.type);
if (filters.source)
textQuery = textQuery.eq('source', filters.source);
// For area filter, use applicable_areas array if includeAdjacent is true
if (filters.area) {
if (includeAdjacent) {
textQuery = textQuery.contains('applicable_areas', [filters.area]);
}
else {
textQuery = textQuery.eq('primary_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 => {
// Include area relevance in the score calculation
const areaMultiplier = result.area_relevance || 1.0;
const score = result.similarity * vectorWeight * areaMultiplier;
scoreMap.set(result.id, score);
resultMap.set(result.id, result);
});
// Add text search results
textResults?.forEach((result) => {
const currentScore = scoreMap.get(result.id) || 0;
// Calculate area relevance for text results
let areaMultiplier = 1.0;
if (filters.area && result.area_relevance_matrix) {
areaMultiplier = result.area_relevance_matrix[filters.area] || 0.3;
}
// Normalize text search relevance (assuming it's a count or rank)
const textScore = (1 / (result.similarity + 1)) * textWeight * areaMultiplier;
scoreMap.set(result.id, currentScore + textScore);
if (!resultMap.has(result.id)) {
resultMap.set(result.id, {
...result,
similarity: textScore / textWeight, // Normalize back
area_relevance: areaMultiplier
});
}
});
// 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-adjacent.js.map