UNPKG

semem

Version:

Semantic Memory for Intelligent Agents

638 lines (541 loc) 18 kB
/** * Ragno: Enhanced Search API - Production Search Endpoints * * This module provides comprehensive search endpoints that build upon the existing * SearchAPI to provide production-ready search functionality with advanced filtering, * faceting, and result processing capabilities. */ import express from 'express' import { DualSearch, VectorIndex } from '../search/index.js' import { PersonalizedPageRank } from '../algorithms/index.js' import GraphCache from '../cache/GraphCache.js' import GraphMetrics from '../monitoring/GraphMetrics.js' import SPARQLHelpers from '../../utils/SPARQLHelpers.js' import { logger } from '../../Utils.js' /** * Enhanced Search API with advanced search capabilities */ export class SearchAPIEnhanced { constructor(options = {}) { this.options = { // Core dependencies sparqlEndpoint: options.sparqlEndpoint, llmHandler: options.llmHandler, embeddingHandler: options.embeddingHandler, vectorIndex: options.vectorIndex, // Search configuration defaultLimit: options.defaultLimit || 10, maxLimit: options.maxLimit || 100, searchTimeout: options.searchTimeout || 30000, // Caching and metrics enableCaching: options.enableCaching !== false, enableMetrics: options.enableMetrics !== false, cacheTimeouts: { entitySearch: 300, semanticSearch: 600, facets: 900, stats: 300 }, // Search types and weights searchTypes: options.searchTypes || { 'ragno:Entity': { weight: 1.0, exactMatchBoost: 2.0 }, 'ragno:Unit': { weight: 0.8, vectorSearchBoost: 1.5 }, 'ragno:Attribute': { weight: 0.9, contextBoost: 1.2 }, 'ragno:CommunityElement': { weight: 0.7, summaryBoost: 1.3 } }, ...options } // Initialize components this.cache = this.options.enableCaching ? new GraphCache(options.cacheOptions) : null this.metrics = this.options.enableMetrics ? new GraphMetrics(options.metricsOptions) : null this.dualSearch = new DualSearch(this.options) // Create Express router this.router = express.Router() this._setupRoutes() this._setupMiddleware() } /** * Setup search API routes */ _setupRoutes() { // Core Search Operations this.router.post('/unified', this._handleUnifiedSearch.bind(this)) this.router.post('/semantic', this._handleSemanticSearch.bind(this)) this.router.post('/entities', this._handleEntitySearch.bind(this)) this.router.post('/similarity', this._handleSimilaritySearch.bind(this)) this.router.post('/graph-traversal', this._handleGraphTraversal.bind(this)) // Advanced Search Operations this.router.post('/faceted', this._handleFacetedSearch.bind(this)) this.router.post('/contextual', this._handleContextualSearch.bind(this)) this.router.post('/temporal', this._handleTemporalSearch.bind(this)) this.router.post('/batch', this._handleBatchSearch.bind(this)) // Search Analytics this.router.get('/facets', this._handleGetFacets.bind(this)) this.router.get('/suggestions', this._handleGetSuggestions.bind(this)) this.router.get('/stats', this._handleGetSearchStats.bind(this)) this.router.post('/explain', this._handleExplainQuery.bind(this)) // Search Management this.router.post('/index/rebuild', this._handleRebuildIndex.bind(this)) this.router.get('/index/status', this._handleGetIndexStatus.bind(this)) this.router.post('/cache/warm', this._handleWarmSearchCache.bind(this)) } /** * Setup middleware for search endpoints */ _setupMiddleware() { // Search request logging this.router.use((req, res, next) => { const start = Date.now() if (this.metrics) { this.metrics.recordSearchRequest(req.path, req.body?.query) } res.on('finish', () => { const duration = Date.now() - start logger.debug(`Search ${req.path}: ${duration}ms`) if (this.metrics) { this.metrics.recordSearchResponse(req.path, res.statusCode, duration, res.locals.resultCount) } }) next() }) // Search timeout this.router.use((req, res, next) => { req.setTimeout(this.options.searchTimeout, () => { res.status(408).json({ error: 'Search timeout', message: `Search exceeded ${this.options.searchTimeout}ms timeout` }) }) next() }) } /** * Unified search combining all search strategies */ async _handleUnifiedSearch(req, res) { try { const { query, limit = this.options.defaultLimit, offset = 0, searchTypes = Object.keys(this.options.searchTypes), includeScores = true, includeExplanation = false, filters = {}, sortBy = 'relevance' } = req.body if (!query || typeof query !== 'string') { return res.status(400).json({ error: 'Invalid query', message: 'Query parameter is required and must be a non-empty string' }) } const searchOptions = { limit: Math.min(parseInt(limit), this.options.maxLimit), offset: parseInt(offset), searchTypes, includeScores, includeExplanation, filters, sortBy } // Check cache first const cacheKey = this._generateCacheKey('unified', query, searchOptions) if (this.cache) { const cached = await this.cache.get(cacheKey) if (cached) { res.locals.resultCount = cached.results.length return res.json(cached) } } // Perform unified search const results = await this._performUnifiedSearch(query, searchOptions) // Cache results if (this.cache) { await this.cache.set(cacheKey, results, this.options.cacheTimeouts.entitySearch) } res.locals.resultCount = results.results.length res.json(results) } catch (error) { logger.error('Unified search failed:', error) res.status(500).json({ error: 'Search failed', message: 'Failed to perform unified search' }) } } /** * Semantic similarity search using vector embeddings */ async _handleSemanticSearch(req, res) { try { const { query, limit = this.options.defaultLimit, threshold = 0.7, nodeTypes = ['ragno:Unit', 'ragno:Attribute', 'ragno:CommunityElement'], includeMetadata = true } = req.body if (!query) { return res.status(400).json({ error: 'Invalid query', message: 'Query parameter is required' }) } // Generate query embedding const queryEmbedding = await this.options.embeddingHandler.generateEmbedding(query) if (!queryEmbedding) { return res.status(500).json({ error: 'Embedding failed', message: 'Failed to generate query embedding' }) } // Perform vector search const vectorResults = await this.options.vectorIndex.search( queryEmbedding, Math.min(parseInt(limit), this.options.maxLimit), threshold ) // Filter by node types and enhance with metadata const results = [] for (const result of vectorResults) { if (nodeTypes.includes(result.metadata?.nodeType)) { const enhancedResult = { id: result.id, similarity: result.distance, type: result.metadata.nodeType, ...result.metadata } if (includeMetadata) { enhancedResult.details = await this._getNodeDetails(result.id) } results.push(enhancedResult) } } res.json({ query, results, metadata: { totalFound: results.length, threshold, searchTypes: nodeTypes, executionTime: Date.now() - req.startTime } }) } catch (error) { logger.error('Semantic search failed:', error) res.status(500).json({ error: 'Search failed', message: 'Failed to perform semantic search' }) } } /** * Entity-focused search with relationship traversal */ async _handleEntitySearch(req, res) { try { const { query, limit = this.options.defaultLimit, includeRelationships = true, includeAttributes = true, traversalDepth = 1, entityTypes = [] } = req.body if (!query) { return res.status(400).json({ error: 'Invalid query', message: 'Query parameter is required' }) } const entities = await this._searchEntities(query, { limit: Math.min(parseInt(limit), this.options.maxLimit), entityTypes }) // Enhance with relationships and attributes const enhancedEntities = [] for (const entity of entities) { const enhanced = { ...entity } if (includeRelationships) { enhanced.relationships = await this._getEntityRelationships(entity.id, traversalDepth) } if (includeAttributes) { enhanced.attributes = await this._getEntityAttributes(entity.id) } enhancedEntities.push(enhanced) } res.json({ query, results: enhancedEntities, metadata: { totalFound: enhancedEntities.length, includeRelationships, includeAttributes, traversalDepth } }) } catch (error) { logger.error('Entity search failed:', error) res.status(500).json({ error: 'Search failed', message: 'Failed to perform entity search' }) } } /** * Graph traversal search using Personalized PageRank */ async _handleGraphTraversal(req, res) { try { const { seedEntities, limit = this.options.defaultLimit, alpha = 0.5, iterations = 3, nodeTypes = Object.keys(this.options.searchTypes), includeScores = true } = req.body if (!seedEntities || !Array.isArray(seedEntities) || seedEntities.length === 0) { return res.status(400).json({ error: 'Invalid seed entities', message: 'seedEntities parameter is required and must be a non-empty array' }) } // Use PersonalizedPageRank for graph traversal const ppr = new PersonalizedPageRank() const dataset = await this._getCurrentDataset() const traversalResults = await ppr.computePersonalizedPageRank(dataset, { seedEntities, alpha, iterations, topK: Math.min(parseInt(limit), this.options.maxLimit) }) // Filter by node types and enhance results const results = [] for (const [nodeUri, score] of traversalResults.scores) { const nodeType = await this._getNodeType(nodeUri) if (nodeTypes.includes(nodeType)) { const result = { id: nodeUri, type: nodeType, pprScore: score } if (includeScores) { result.details = await this._getNodeDetails(nodeUri) } results.push(result) } } res.json({ seedEntities, results, metadata: { algorithm: 'PersonalizedPageRank', parameters: { alpha, iterations }, nodeTypes, totalFound: results.length } }) } catch (error) { logger.error('Graph traversal search failed:', error) res.status(500).json({ error: 'Search failed', message: 'Failed to perform graph traversal search' }) } } /** * Faceted search with dynamic facet generation */ async _handleFacetedSearch(req, res) { try { const { query, facets = ['type', 'confidence', 'timestamp'], filters = {}, limit = this.options.defaultLimit } = req.body if (!query) { return res.status(400).json({ error: 'Invalid query', message: 'Query parameter is required' }) } // Perform base search const baseResults = await this._performUnifiedSearch(query, { limit: Math.min(parseInt(limit) * 2, this.options.maxLimit * 2), // Get more for faceting filters }) // Generate facets const facetResults = {} for (const facetName of facets) { facetResults[facetName] = await this._generateFacet(baseResults.results, facetName) } // Apply facet filters const filteredResults = this._applyFacetFilters(baseResults.results, filters) res.json({ query, results: filteredResults.slice(0, parseInt(limit)), facets: facetResults, metadata: { totalFound: filteredResults.length, facetsGenerated: facets, filtersApplied: Object.keys(filters) } }) } catch (error) { logger.error('Faceted search failed:', error) res.status(500).json({ error: 'Search failed', message: 'Failed to perform faceted search' }) } } /** * Get available search facets */ async _handleGetFacets(req, res) { try { const cacheKey = 'search-facets-available' if (this.cache) { const cached = await this.cache.get(cacheKey) if (cached) { return res.json(cached) } } const facets = await this._getAvailableFacets() if (this.cache) { await this.cache.set(cacheKey, facets, this.options.cacheTimeouts.facets) } res.json(facets) } catch (error) { logger.error('Failed to get facets:', error) res.status(500).json({ error: 'Failed to retrieve facets', message: 'Unable to get available search facets' }) } } /** * Get search suggestions for query completion */ async _handleGetSuggestions(req, res) { try { const { partial, limit = 10, types = ['entity'] } = req.query if (!partial || partial.length < 2) { return res.json({ suggestions: [] }) } const suggestions = await this._getSearchSuggestions(partial, { limit: Math.min(parseInt(limit), 50), types }) res.json({ suggestions }) } catch (error) { logger.error('Failed to get suggestions:', error) res.status(500).json({ error: 'Failed to get suggestions', message: 'Unable to generate search suggestions' }) } } /** * Get search analytics and statistics */ async _handleGetSearchStats(req, res) { try { const stats = { timestamp: new Date().toISOString(), searchMetrics: this.metrics ? await this.metrics.getSearchMetrics() : null, indexStatus: this.options.vectorIndex ? await this.options.vectorIndex.getStatistics() : null, cacheStats: this.cache ? await this.cache.getStatistics() : null } res.json(stats) } catch (error) { logger.error('Failed to get search stats:', error) res.status(500).json({ error: 'Failed to get statistics', message: 'Unable to retrieve search statistics' }) } } /** * Helper method to perform unified search */ async _performUnifiedSearch(query, options) { const startTime = Date.now() // Use DualSearch for comprehensive search const dualResults = await this.dualSearch.search(query, { limit: options.limit, searchTypes: options.searchTypes, includeScores: options.includeScores }) // Apply additional filtering and sorting let results = dualResults.results || [] if (options.filters) { results = this._applyFilters(results, options.filters) } if (options.sortBy && options.sortBy !== 'relevance') { results = this._sortResults(results, options.sortBy) } // Apply pagination const paginatedResults = results.slice(options.offset, options.offset + options.limit) return { query, results: paginatedResults, metadata: { totalFound: results.length, executionTime: Date.now() - startTime, searchStrategies: dualResults.metadata?.searchStrategies || [], offset: options.offset, limit: options.limit } } } /** * Generate cache key for search requests */ _generateCacheKey(type, query, options) { const keyData = { type, query: query.toLowerCase().trim(), ...options } return `search:${type}:${Buffer.from(JSON.stringify(keyData)).toString('base64')}` } /** * Get the Express router */ getRouter() { return this.router } /** * Initialize the search API */ async initialize() { logger.info('Initializing Enhanced Search API...') if (this.cache) { await this.cache.initialize() } if (this.metrics) { await this.metrics.initialize() } await this.dualSearch.initialize() logger.info('Enhanced Search API initialized successfully') } /** * Cleanup resources */ async shutdown() { logger.info('Shutting down Enhanced Search API...') if (this.cache) { await this.cache.shutdown() } if (this.metrics) { await this.metrics.shutdown() } await this.dualSearch.shutdown() logger.info('Enhanced Search API shutdown complete') } } export default SearchAPIEnhanced