UNPKG

knowledgegraph-mcp

Version:

MCP server for enabling persistent knowledge storage for Claude through a knowledge graph with multiple storage backends

205 lines 9.4 kB
/** * Main search orchestrator that manages different search strategies */ export class SearchManager { config; strategy; constructor(config, strategy) { this.config = config; this.strategy = strategy; } async search(query, entities, options, project) { // Handle multiple queries if (Array.isArray(query)) { return this.searchMultiple(query, entities, options, project); } // Single query - use existing logic return this.searchSingle(query, entities, options, project); } async searchMultiple(queries, entities, options, project) { let allResults = []; const existingEntityNames = new Set(); // Process each query and merge results with deduplication for (const query of queries) { const results = await this.searchSingle(query, entities, options, project); // Add only new entities (deduplication) const newEntities = results.filter(e => !existingEntityNames.has(e.name)); allResults.push(...newEntities); // Update the set of existing entity names newEntities.forEach(e => existingEntityNames.add(e.name)); } return allResults; } async searchSingle(query, entities, options, project) { // Handle exact search mode if (options?.searchMode === 'exact') { return this.exactSearch(query, entities); } // Handle fuzzy search mode const threshold = options?.fuzzyThreshold || this.config.threshold; try { // Try database search first if available if (this.strategy.canUseDatabase()) { return await this.strategy.searchDatabase(query, threshold, project); } } catch (error) { console.warn('Database search failed, falling back to client-side:', error); // Fall back to client-side if database search fails if (this.config.clientSideFallback) { return this.strategy.searchClientSide(entities, query); } throw error; } // Use client-side search return this.strategy.searchClientSide(entities, query); } exactSearch(query, entities) { const lowerQuery = query.toLowerCase(); return entities.filter(e => { const entityTags = e.tags || []; return (e.name.toLowerCase().includes(lowerQuery) || e.entityType.toLowerCase().includes(lowerQuery) || e.observations.some(o => o.toLowerCase().includes(lowerQuery)) || entityTags.some(tag => tag.toLowerCase().includes(lowerQuery))); }); } /** * Paginated search with database-level pagination when possible */ async searchPaginated(query, pagination, options, project) { // Handle multiple queries if (Array.isArray(query)) { return this.searchMultiplePaginated(query, pagination, options, project); } // Single query return this.searchSinglePaginated(query, pagination, options, project); } async searchSinglePaginated(query, pagination, options, project) { // Handle exact tags search - this takes priority over other search modes if (options?.exactTags && options.exactTags.length > 0) { return this.exactTagsSearchPaginated(options.exactTags, options.tagMatchMode || 'any', pagination, project); } // Handle exact search mode with database-level pagination if (options?.searchMode === 'exact') { // Try database-level exact search pagination if available if (this.strategy.searchExactPaginated) { try { return await this.strategy.searchExactPaginated(query, pagination, project); } catch (error) { console.warn('Database exact search pagination failed, falling back to client-side:', error); } } // Fallback to client-side exact search with post-search pagination return this.exactSearchPaginated(query, pagination, project); } // Handle fuzzy search mode with database-level pagination const threshold = options?.fuzzyThreshold || this.config.threshold; try { // Try database search pagination first if available if (this.strategy.canUseDatabase() && this.strategy.searchDatabasePaginated) { return await this.strategy.searchDatabasePaginated(query, threshold, pagination, project); } } catch (error) { console.warn('Database search pagination failed, falling back to client-side:', error); } // Fallback to client-side search with post-search pagination return this.clientSideSearchPaginated(query, pagination, options, project); } async searchMultiplePaginated(queries, pagination, options, project) { // Handle exact tags search - this takes priority over other search modes if (options?.exactTags && options.exactTags.length > 0) { return this.exactTagsSearchPaginated(options.exactTags, options.tagMatchMode || 'any', pagination, project); } // For multiple queries, we need to aggregate results and then paginate // This is complex with database-level pagination, so we'll use post-search pagination // Get all results first (without pagination) const allResults = await this.searchMultiple(queries, [], options, project); // Apply post-search pagination return this.applyPostSearchPagination(allResults, pagination); } /** * Client-side search with post-search pagination */ async clientSideSearchPaginated(query, pagination, options, project) { // Load all entities first const entities = await this.strategy.getAllEntities(project); // Perform client-side search const searchResults = await this.searchSingle(query, entities, options, project); // Apply post-search pagination return this.applyPostSearchPagination(searchResults, pagination); } /** * Exact tags search with pagination support */ async exactTagsSearchPaginated(exactTags, tagMatchMode, pagination, project) { // Load all entities first (we need to do client-side filtering for exact tags) const entities = await this.strategy.getAllEntities(project); // Apply exact tag filtering using the same logic as in core.ts const filteredEntities = entities.filter(entity => { const entityTags = entity.tags || []; // Ensure entity has tags array (backward compatibility) if (entityTags.length === 0) return false; if (tagMatchMode === 'all') { return exactTags.every(tag => entityTags.includes(tag)); } else { return exactTags.some(tag => entityTags.includes(tag)); } }); // Apply post-search pagination return this.applyPostSearchPagination(filteredEntities, pagination); } /** * Exact search with database-level pagination fallback */ async exactSearchPaginated(query, pagination, project) { // Try database-level pagination for getAllEntities if available if (this.strategy.getAllEntitiesPaginated) { try { const paginatedEntities = await this.strategy.getAllEntitiesPaginated(pagination, project); // Apply exact search to the paginated entities const searchResults = this.exactSearch(query, paginatedEntities.data); // Note: This approach may not give accurate total counts for exact search // but it's a reasonable fallback for memory efficiency return { data: searchResults, pagination: paginatedEntities.pagination }; } catch (error) { console.warn('Database getAllEntities pagination failed, falling back to full load:', error); } } // Fallback to loading all entities and post-search pagination const entities = await this.strategy.getAllEntities(project); const searchResults = this.exactSearch(query, entities); return this.applyPostSearchPagination(searchResults, pagination); } /** * Apply pagination to search results (post-search pagination) */ applyPostSearchPagination(results, pagination) { const page = pagination.page || 0; const pageSize = pagination.pageSize || 100; const offset = page * pageSize; const totalCount = results.length; const totalPages = Math.ceil(totalCount / pageSize); const paginatedData = results.slice(offset, offset + pageSize); return { data: paginatedData, pagination: { currentPage: page, pageSize: pageSize, totalCount: totalCount, totalPages: totalPages, hasNextPage: page < totalPages - 1, hasPreviousPage: page > 0 } }; } } //# sourceMappingURL=search-manager.js.map