mirror of
https://github.com/zadam/trilium.git
synced 2025-12-06 07:24:25 +01:00
260 lines
9.6 KiB
TypeScript
260 lines
9.6 KiB
TypeScript
/**
|
|
* Unified Search Tool
|
|
*
|
|
* This tool combines semantic search, keyword search, and attribute search into a single
|
|
* intelligent search interface that automatically routes to the appropriate backend.
|
|
*/
|
|
|
|
import type { Tool, ToolHandler } from './tool_interfaces.js';
|
|
import log from '../../log.js';
|
|
import { SearchNotesTool } from './search_notes_tool.js';
|
|
import { KeywordSearchTool } from './keyword_search_tool.js';
|
|
import { AttributeSearchTool } from './attribute_search_tool.js';
|
|
|
|
/**
|
|
* Definition of the unified search tool
|
|
*/
|
|
export const unifiedSearchToolDefinition: Tool = {
|
|
type: 'function',
|
|
function: {
|
|
name: 'search',
|
|
description: 'Find notes intelligently. Example: search("machine learning") → finds related notes. Auto-detects search type (semantic/keyword/attribute).',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
query: {
|
|
type: 'string',
|
|
description: 'Search query. Can be: conceptual phrases ("machine learning algorithms"), exact terms in quotes ("meeting notes"), labels (#important), relations (~relatedTo), or attribute queries (label:todo)'
|
|
},
|
|
searchType: {
|
|
type: 'string',
|
|
description: 'Optional: Force specific search type. Auto-detected if not specified.',
|
|
enum: ['auto', 'semantic', 'keyword', 'attribute']
|
|
},
|
|
maxResults: {
|
|
type: 'number',
|
|
description: 'Maximum results to return (default: 10, max: 50)'
|
|
},
|
|
filters: {
|
|
type: 'object',
|
|
description: 'Optional filters for search',
|
|
properties: {
|
|
parentNoteId: {
|
|
type: 'string',
|
|
description: 'Limit search to children of this note'
|
|
},
|
|
includeArchived: {
|
|
type: 'boolean',
|
|
description: 'Include archived notes (default: false)'
|
|
},
|
|
attributeType: {
|
|
type: 'string',
|
|
description: 'For attribute searches: "label" or "relation"'
|
|
},
|
|
attributeValue: {
|
|
type: 'string',
|
|
description: 'Optional value for attribute searches'
|
|
}
|
|
}
|
|
}
|
|
},
|
|
required: ['query']
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Unified search tool implementation
|
|
*/
|
|
export class UnifiedSearchTool implements ToolHandler {
|
|
public definition: Tool = unifiedSearchToolDefinition;
|
|
private semanticSearchTool: SearchNotesTool;
|
|
private keywordSearchTool: KeywordSearchTool;
|
|
private attributeSearchTool: AttributeSearchTool;
|
|
|
|
constructor() {
|
|
this.semanticSearchTool = new SearchNotesTool();
|
|
this.keywordSearchTool = new KeywordSearchTool();
|
|
this.attributeSearchTool = new AttributeSearchTool();
|
|
}
|
|
|
|
/**
|
|
* Detect the search type from the query
|
|
*/
|
|
private detectSearchType(query: string): 'semantic' | 'keyword' | 'attribute' {
|
|
// Check for attribute patterns
|
|
if (query.startsWith('#') || query.startsWith('~')) {
|
|
return 'attribute';
|
|
}
|
|
|
|
// Check for label: or relation: patterns
|
|
if (query.match(/^(label|relation):/i)) {
|
|
return 'attribute';
|
|
}
|
|
|
|
// Check for exact phrase searches (quoted strings)
|
|
if (query.includes('"') && query.indexOf('"') !== query.lastIndexOf('"')) {
|
|
return 'keyword';
|
|
}
|
|
|
|
// Check for boolean operators
|
|
if (query.match(/\b(AND|OR|NOT)\b/)) {
|
|
return 'keyword';
|
|
}
|
|
|
|
// Check for special search operators
|
|
if (query.includes('note.') || query.includes('*=')) {
|
|
return 'keyword';
|
|
}
|
|
|
|
// Default to semantic search for natural language queries
|
|
return 'semantic';
|
|
}
|
|
|
|
/**
|
|
* Parse attribute search from query
|
|
*/
|
|
private parseAttributeSearch(query: string): { type: string, name: string, value?: string } | null {
|
|
// Handle #label or ~relation format
|
|
if (query.startsWith('#')) {
|
|
const parts = query.substring(1).split('=');
|
|
return {
|
|
type: 'label',
|
|
name: parts[0],
|
|
value: parts[1]
|
|
};
|
|
}
|
|
|
|
if (query.startsWith('~')) {
|
|
const parts = query.substring(1).split('=');
|
|
return {
|
|
type: 'relation',
|
|
name: parts[0],
|
|
value: parts[1]
|
|
};
|
|
}
|
|
|
|
// Handle label:name or relation:name format
|
|
const match = query.match(/^(label|relation):(\w+)(?:=(.+))?$/i);
|
|
if (match) {
|
|
return {
|
|
type: match[1].toLowerCase(),
|
|
name: match[2],
|
|
value: match[3]
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Execute the unified search tool
|
|
*/
|
|
public async execute(args: {
|
|
query: string,
|
|
searchType?: string,
|
|
maxResults?: number,
|
|
filters?: {
|
|
parentNoteId?: string,
|
|
includeArchived?: boolean,
|
|
attributeType?: string,
|
|
attributeValue?: string
|
|
}
|
|
}): Promise<string | object> {
|
|
try {
|
|
const { query, searchType = 'auto', maxResults = 10, filters = {} } = args;
|
|
|
|
log.info(`Executing unified search - Query: "${query}", Type: ${searchType}, MaxResults: ${maxResults}`);
|
|
|
|
// Detect search type if auto
|
|
let actualSearchType = searchType;
|
|
if (searchType === 'auto') {
|
|
actualSearchType = this.detectSearchType(query);
|
|
log.info(`Auto-detected search type: ${actualSearchType}`);
|
|
}
|
|
|
|
// Route to appropriate search tool
|
|
switch (actualSearchType) {
|
|
case 'semantic': {
|
|
log.info('Routing to semantic search');
|
|
const result = await this.semanticSearchTool.execute({
|
|
query,
|
|
parentNoteId: filters.parentNoteId,
|
|
maxResults,
|
|
summarize: false
|
|
});
|
|
|
|
// Add search type indicator
|
|
if (typeof result === 'object' && !Array.isArray(result)) {
|
|
return {
|
|
...result,
|
|
searchMethod: 'semantic',
|
|
tip: 'For exact matches, try keyword search. For tagged notes, try attribute search.'
|
|
};
|
|
}
|
|
return result;
|
|
}
|
|
|
|
case 'keyword': {
|
|
log.info('Routing to keyword search');
|
|
const result = await this.keywordSearchTool.execute({
|
|
query,
|
|
maxResults,
|
|
includeArchived: filters.includeArchived || false
|
|
});
|
|
|
|
// Add search type indicator
|
|
if (typeof result === 'object' && !Array.isArray(result)) {
|
|
return {
|
|
...result,
|
|
searchMethod: 'keyword',
|
|
tip: 'For conceptual matches, try semantic search. For tagged notes, try attribute search.'
|
|
};
|
|
}
|
|
return result;
|
|
}
|
|
|
|
case 'attribute': {
|
|
log.info('Routing to attribute search');
|
|
|
|
// Parse attribute from query if not provided in filters
|
|
const parsed = this.parseAttributeSearch(query);
|
|
if (!parsed) {
|
|
return {
|
|
error: 'Invalid attribute search format',
|
|
help: 'Use #labelname, ~relationname, label:name, or relation:name',
|
|
examples: ['#important', '~relatedTo', 'label:todo', 'relation:partOf=projectX']
|
|
};
|
|
}
|
|
|
|
const result = await this.attributeSearchTool.execute({
|
|
attributeType: filters.attributeType || parsed.type,
|
|
attributeName: parsed.name,
|
|
attributeValue: filters.attributeValue || parsed.value,
|
|
maxResults
|
|
});
|
|
|
|
// Add search type indicator
|
|
if (typeof result === 'object' && !Array.isArray(result)) {
|
|
return {
|
|
...result,
|
|
searchMethod: 'attribute',
|
|
tip: 'For content matches, try semantic or keyword search.'
|
|
};
|
|
}
|
|
return result;
|
|
}
|
|
|
|
default:
|
|
return {
|
|
error: `Unknown search type: ${actualSearchType}`,
|
|
validTypes: ['auto', 'semantic', 'keyword', 'attribute']
|
|
};
|
|
}
|
|
} catch (error: unknown) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
log.error(`Error executing unified search: ${errorMessage}`);
|
|
return `Error: ${errorMessage}`;
|
|
}
|
|
}
|
|
} |