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
JavaScript
/**
* 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