@danielsimonjr/memory-mcp 9.8.3 → 9.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +360 -1829
- package/dist/core/ManagerContext.d.ts +4 -0
- package/dist/core/ManagerContext.d.ts.map +1 -1
- package/dist/core/ManagerContext.js +6 -0
- package/dist/features/KeywordExtractor.d.ts +61 -0
- package/dist/features/KeywordExtractor.d.ts.map +1 -0
- package/dist/features/KeywordExtractor.js +126 -0
- package/dist/features/ObservationNormalizer.d.ts +90 -0
- package/dist/features/ObservationNormalizer.d.ts.map +1 -0
- package/dist/features/ObservationNormalizer.js +193 -0
- package/dist/features/index.d.ts +2 -0
- package/dist/features/index.d.ts.map +1 -1
- package/dist/features/index.js +3 -0
- package/dist/search/HybridSearchManager.d.ts +80 -0
- package/dist/search/HybridSearchManager.d.ts.map +1 -0
- package/dist/search/HybridSearchManager.js +187 -0
- package/dist/search/QueryAnalyzer.d.ts +76 -0
- package/dist/search/QueryAnalyzer.d.ts.map +1 -0
- package/dist/search/QueryAnalyzer.js +227 -0
- package/dist/search/QueryPlanner.d.ts +58 -0
- package/dist/search/QueryPlanner.d.ts.map +1 -0
- package/dist/search/QueryPlanner.js +137 -0
- package/dist/search/ReflectionManager.d.ts +71 -0
- package/dist/search/ReflectionManager.d.ts.map +1 -0
- package/dist/search/ReflectionManager.js +124 -0
- package/dist/search/SymbolicSearch.d.ts +61 -0
- package/dist/search/SymbolicSearch.d.ts.map +1 -0
- package/dist/search/SymbolicSearch.js +163 -0
- package/dist/search/index.d.ts +5 -0
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +8 -0
- package/dist/server/toolDefinitions.d.ts +1 -1
- package/dist/server/toolDefinitions.d.ts.map +1 -1
- package/dist/server/toolDefinitions.js +141 -1
- package/dist/server/toolHandlers.d.ts.map +1 -1
- package/dist/server/toolHandlers.js +167 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/types.d.ts +118 -0
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid Search Manager
|
|
3
|
+
*
|
|
4
|
+
* Phase 11: Orchestrates three-layer hybrid search combining
|
|
5
|
+
* semantic, lexical, and symbolic signals.
|
|
6
|
+
*
|
|
7
|
+
* @module search/HybridSearchManager
|
|
8
|
+
*/
|
|
9
|
+
import { SymbolicSearch } from './SymbolicSearch.js';
|
|
10
|
+
import { SEMANTIC_SEARCH_LIMITS } from '../utils/constants.js';
|
|
11
|
+
/**
|
|
12
|
+
* Default weights for hybrid search layers.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_HYBRID_WEIGHTS = {
|
|
15
|
+
semantic: 0.5,
|
|
16
|
+
lexical: 0.3,
|
|
17
|
+
symbolic: 0.2,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Hybrid Search Manager
|
|
21
|
+
*
|
|
22
|
+
* Combines three search layers:
|
|
23
|
+
* 1. Semantic: Vector similarity via embeddings
|
|
24
|
+
* 2. Lexical: Keyword matching via TF-IDF/BM25
|
|
25
|
+
* 3. Symbolic: Structured metadata filtering
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const hybrid = new HybridSearchManager(semanticSearch, rankedSearch);
|
|
30
|
+
* const results = await hybrid.search(graph, 'machine learning', {
|
|
31
|
+
* semanticWeight: 0.5,
|
|
32
|
+
* lexicalWeight: 0.3,
|
|
33
|
+
* symbolicWeight: 0.2,
|
|
34
|
+
* symbolic: { tags: ['ai'] }
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export class HybridSearchManager {
|
|
39
|
+
semanticSearch;
|
|
40
|
+
rankedSearch;
|
|
41
|
+
symbolicSearch;
|
|
42
|
+
constructor(semanticSearch, rankedSearch) {
|
|
43
|
+
this.semanticSearch = semanticSearch;
|
|
44
|
+
this.rankedSearch = rankedSearch;
|
|
45
|
+
this.symbolicSearch = new SymbolicSearch();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Perform hybrid search combining all three layers.
|
|
49
|
+
*
|
|
50
|
+
* @param graph - Knowledge graph to search
|
|
51
|
+
* @param query - Search query text
|
|
52
|
+
* @param options - Hybrid search options with weights
|
|
53
|
+
* @returns Combined and ranked results
|
|
54
|
+
*/
|
|
55
|
+
async search(graph, query, options = {}) {
|
|
56
|
+
const { semanticWeight = DEFAULT_HYBRID_WEIGHTS.semantic, lexicalWeight = DEFAULT_HYBRID_WEIGHTS.lexical, symbolicWeight = DEFAULT_HYBRID_WEIGHTS.symbolic, semantic = {}, lexical = {}, symbolic = {}, limit = SEMANTIC_SEARCH_LIMITS.DEFAULT_LIMIT, } = options;
|
|
57
|
+
// Normalize weights
|
|
58
|
+
const totalWeight = semanticWeight + lexicalWeight + symbolicWeight;
|
|
59
|
+
const normSemantic = semanticWeight / totalWeight;
|
|
60
|
+
const normLexical = lexicalWeight / totalWeight;
|
|
61
|
+
const normSymbolic = symbolicWeight / totalWeight;
|
|
62
|
+
// Execute searches in parallel
|
|
63
|
+
const [semanticResults, lexicalResults, symbolicResults] = await Promise.all([
|
|
64
|
+
this.executeSemanticSearch(graph, query, semantic, limit * 2),
|
|
65
|
+
this.executeLexicalSearch(query, lexical, limit * 2),
|
|
66
|
+
this.executeSymbolicSearch(graph.entities, symbolic),
|
|
67
|
+
]);
|
|
68
|
+
// Merge results
|
|
69
|
+
const merged = this.mergeResults(graph.entities, semanticResults, lexicalResults, symbolicResults, { semantic: normSemantic, lexical: normLexical, symbolic: normSymbolic });
|
|
70
|
+
// Sort by combined score and limit
|
|
71
|
+
return merged
|
|
72
|
+
.sort((a, b) => b.scores.combined - a.scores.combined)
|
|
73
|
+
.slice(0, limit);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Execute semantic search layer.
|
|
77
|
+
*/
|
|
78
|
+
async executeSemanticSearch(graph, query, options, limit) {
|
|
79
|
+
const results = new Map();
|
|
80
|
+
if (!this.semanticSearch) {
|
|
81
|
+
return results; // Semantic search not available
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const semanticResults = await this.semanticSearch.search(graph, query, options.topK ?? limit, options.minSimilarity ?? 0);
|
|
85
|
+
for (const result of semanticResults) {
|
|
86
|
+
results.set(result.entity.name, result.similarity);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Semantic search may fail if not indexed
|
|
91
|
+
}
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Execute lexical search layer (TF-IDF/BM25).
|
|
96
|
+
*/
|
|
97
|
+
async executeLexicalSearch(query, _options, limit) {
|
|
98
|
+
const results = new Map();
|
|
99
|
+
try {
|
|
100
|
+
const lexicalResults = await this.rankedSearch.searchNodesRanked(query, undefined, // tags
|
|
101
|
+
undefined, // minImportance
|
|
102
|
+
undefined, // maxImportance
|
|
103
|
+
limit);
|
|
104
|
+
// Normalize scores to 0-1 range
|
|
105
|
+
const maxScore = Math.max(...lexicalResults.map(r => r.score), 1);
|
|
106
|
+
for (const result of lexicalResults) {
|
|
107
|
+
results.set(result.entity.name, result.score / maxScore);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Lexical search may fail
|
|
112
|
+
}
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Execute symbolic search layer.
|
|
117
|
+
*/
|
|
118
|
+
executeSymbolicSearch(entities, filters) {
|
|
119
|
+
const results = new Map();
|
|
120
|
+
if (!filters || Object.keys(filters).length === 0) {
|
|
121
|
+
// No symbolic filters, give all entities base score
|
|
122
|
+
for (const entity of entities) {
|
|
123
|
+
results.set(entity.name, 0.5);
|
|
124
|
+
}
|
|
125
|
+
return results;
|
|
126
|
+
}
|
|
127
|
+
const symbolicResults = this.symbolicSearch.search(entities, filters);
|
|
128
|
+
for (const result of symbolicResults) {
|
|
129
|
+
results.set(result.entity.name, result.score);
|
|
130
|
+
}
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Merge results from all three layers.
|
|
135
|
+
*/
|
|
136
|
+
mergeResults(entities, semanticScores, lexicalScores, symbolicScores, weights) {
|
|
137
|
+
// Collect all unique entity names that have at least one non-zero score
|
|
138
|
+
const allNames = new Set([
|
|
139
|
+
...semanticScores.keys(),
|
|
140
|
+
...lexicalScores.keys(),
|
|
141
|
+
...symbolicScores.keys(),
|
|
142
|
+
]);
|
|
143
|
+
// Create entity lookup map
|
|
144
|
+
const entityMap = new Map(entities.map(e => [e.name, e]));
|
|
145
|
+
const results = [];
|
|
146
|
+
for (const name of allNames) {
|
|
147
|
+
const entity = entityMap.get(name);
|
|
148
|
+
if (!entity)
|
|
149
|
+
continue;
|
|
150
|
+
const semantic = semanticScores.get(name) ?? 0;
|
|
151
|
+
const lexical = lexicalScores.get(name) ?? 0;
|
|
152
|
+
const symbolic = symbolicScores.get(name) ?? 0;
|
|
153
|
+
const combined = semantic * weights.semantic +
|
|
154
|
+
lexical * weights.lexical +
|
|
155
|
+
symbolic * weights.symbolic;
|
|
156
|
+
const matchedLayers = [];
|
|
157
|
+
if (semantic > 0)
|
|
158
|
+
matchedLayers.push('semantic');
|
|
159
|
+
if (lexical > 0)
|
|
160
|
+
matchedLayers.push('lexical');
|
|
161
|
+
if (symbolic > 0)
|
|
162
|
+
matchedLayers.push('symbolic');
|
|
163
|
+
// Skip if no layers matched meaningfully
|
|
164
|
+
if (matchedLayers.length === 0)
|
|
165
|
+
continue;
|
|
166
|
+
results.push({
|
|
167
|
+
entity,
|
|
168
|
+
scores: { semantic, lexical, symbolic, combined },
|
|
169
|
+
matchedLayers,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return results;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Search with full entity resolution.
|
|
176
|
+
* Alias for search() since we now always resolve entities.
|
|
177
|
+
*/
|
|
178
|
+
async searchWithEntities(graph, query, options = {}) {
|
|
179
|
+
return this.search(graph, query, options);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get the symbolic search instance for direct access.
|
|
183
|
+
*/
|
|
184
|
+
getSymbolicSearch() {
|
|
185
|
+
return this.symbolicSearch;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Phase 11: Extracts structured information from natural language queries
|
|
5
|
+
* to enable intelligent search planning.
|
|
6
|
+
*
|
|
7
|
+
* @module search/QueryAnalyzer
|
|
8
|
+
*/
|
|
9
|
+
import type { QueryAnalysis } from '../types/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* Query Analyzer extracts structured information from queries.
|
|
12
|
+
*
|
|
13
|
+
* Uses rule-based heuristics for reliable extraction of:
|
|
14
|
+
* - Person names
|
|
15
|
+
* - Location names
|
|
16
|
+
* - Organization names
|
|
17
|
+
* - Temporal references
|
|
18
|
+
* - Question type
|
|
19
|
+
* - Query complexity
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const analyzer = new QueryAnalyzer();
|
|
24
|
+
* const analysis = analyzer.analyze(
|
|
25
|
+
* 'What projects did Alice work on last month?'
|
|
26
|
+
* );
|
|
27
|
+
* // { query: '...', entities: [...], persons: ['Alice'], temporalRange: { relative: 'last month' }, ... }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare class QueryAnalyzer {
|
|
31
|
+
private personIndicators;
|
|
32
|
+
private temporalKeywords;
|
|
33
|
+
private questionKeywords;
|
|
34
|
+
/**
|
|
35
|
+
* Analyze a query using rule-based heuristics.
|
|
36
|
+
* Main entry point - returns full QueryAnalysis.
|
|
37
|
+
*/
|
|
38
|
+
analyze(query: string): QueryAnalysis;
|
|
39
|
+
/**
|
|
40
|
+
* Calculate confidence score for the analysis.
|
|
41
|
+
*/
|
|
42
|
+
private calculateConfidence;
|
|
43
|
+
/**
|
|
44
|
+
* Extract person names from query.
|
|
45
|
+
*/
|
|
46
|
+
private extractPersons;
|
|
47
|
+
/**
|
|
48
|
+
* Extract location names from query.
|
|
49
|
+
*/
|
|
50
|
+
private extractLocations;
|
|
51
|
+
/**
|
|
52
|
+
* Extract organization names from query.
|
|
53
|
+
*/
|
|
54
|
+
private extractOrganizations;
|
|
55
|
+
/**
|
|
56
|
+
* Extract temporal range from query.
|
|
57
|
+
*/
|
|
58
|
+
private extractTemporalRange;
|
|
59
|
+
/**
|
|
60
|
+
* Detect the type of question.
|
|
61
|
+
*/
|
|
62
|
+
private detectQuestionType;
|
|
63
|
+
/**
|
|
64
|
+
* Estimate query complexity.
|
|
65
|
+
*/
|
|
66
|
+
private estimateComplexity;
|
|
67
|
+
/**
|
|
68
|
+
* Detect what types of information are being requested.
|
|
69
|
+
*/
|
|
70
|
+
private detectRequiredInfoTypes;
|
|
71
|
+
/**
|
|
72
|
+
* Decompose complex queries into sub-queries.
|
|
73
|
+
*/
|
|
74
|
+
private decomposeQuery;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=QueryAnalyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QueryAnalyzer.d.ts","sourceRoot":"","sources":["../../src/search/QueryAnalyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAkC,MAAM,mBAAmB,CAAC;AAEvF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,gBAAgB,CAA0C;IAClE,OAAO,CAAC,gBAAgB,CAKtB;IACF,OAAO,CAAC,gBAAgB,CAOtB;IAEF;;;OAGG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa;IAiCrC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAoB3B;;OAEG;IACH,OAAO,CAAC,cAAc;IAqBtB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAgBxB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAe5B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAsB5B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAS1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAc1B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAa/B;;OAEG;IACH,OAAO,CAAC,cAAc;CAYvB"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Phase 11: Extracts structured information from natural language queries
|
|
5
|
+
* to enable intelligent search planning.
|
|
6
|
+
*
|
|
7
|
+
* @module search/QueryAnalyzer
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Query Analyzer extracts structured information from queries.
|
|
11
|
+
*
|
|
12
|
+
* Uses rule-based heuristics for reliable extraction of:
|
|
13
|
+
* - Person names
|
|
14
|
+
* - Location names
|
|
15
|
+
* - Organization names
|
|
16
|
+
* - Temporal references
|
|
17
|
+
* - Question type
|
|
18
|
+
* - Query complexity
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const analyzer = new QueryAnalyzer();
|
|
23
|
+
* const analysis = analyzer.analyze(
|
|
24
|
+
* 'What projects did Alice work on last month?'
|
|
25
|
+
* );
|
|
26
|
+
* // { query: '...', entities: [...], persons: ['Alice'], temporalRange: { relative: 'last month' }, ... }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export class QueryAnalyzer {
|
|
30
|
+
personIndicators = ['Mr.', 'Mrs.', 'Ms.', 'Dr.', 'Prof.'];
|
|
31
|
+
temporalKeywords = [
|
|
32
|
+
'yesterday', 'today', 'tomorrow',
|
|
33
|
+
'last week', 'last month', 'last year',
|
|
34
|
+
'this week', 'this month', 'this year',
|
|
35
|
+
'next week', 'next month', 'next year',
|
|
36
|
+
];
|
|
37
|
+
questionKeywords = {
|
|
38
|
+
factual: ['what', 'who', 'where', 'which'],
|
|
39
|
+
temporal: ['when', 'how long', 'since', 'until'],
|
|
40
|
+
comparative: ['compare', 'difference', 'vs', 'versus', 'better', 'worse'],
|
|
41
|
+
aggregation: ['how many', 'count', 'total', 'sum', 'average'],
|
|
42
|
+
'multi-hop': ['and then', 'which means', 'therefore', 'related to'],
|
|
43
|
+
conceptual: ['explain', 'why', 'how does', 'what is the meaning', 'understand'],
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Analyze a query using rule-based heuristics.
|
|
47
|
+
* Main entry point - returns full QueryAnalysis.
|
|
48
|
+
*/
|
|
49
|
+
analyze(query) {
|
|
50
|
+
const lowerQuery = query.toLowerCase();
|
|
51
|
+
const persons = this.extractPersons(query);
|
|
52
|
+
const locations = this.extractLocations(query);
|
|
53
|
+
const organizations = this.extractOrganizations(query);
|
|
54
|
+
const questionType = this.detectQuestionType(lowerQuery);
|
|
55
|
+
const complexity = this.estimateComplexity(query);
|
|
56
|
+
// Build entities array from extracted names
|
|
57
|
+
const entities = [
|
|
58
|
+
...persons.map(name => ({ name, type: 'person' })),
|
|
59
|
+
...locations.map(name => ({ name, type: 'location' })),
|
|
60
|
+
...organizations.map(name => ({ name, type: 'organization' })),
|
|
61
|
+
];
|
|
62
|
+
// Calculate confidence based on extraction quality
|
|
63
|
+
const confidence = this.calculateConfidence(entities, complexity, questionType);
|
|
64
|
+
return {
|
|
65
|
+
query,
|
|
66
|
+
entities,
|
|
67
|
+
persons,
|
|
68
|
+
locations,
|
|
69
|
+
organizations,
|
|
70
|
+
temporalRange: this.extractTemporalRange(query) ?? null,
|
|
71
|
+
questionType,
|
|
72
|
+
complexity,
|
|
73
|
+
confidence,
|
|
74
|
+
requiredInfoTypes: this.detectRequiredInfoTypes(lowerQuery),
|
|
75
|
+
subQueries: this.decomposeQuery(query),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Calculate confidence score for the analysis.
|
|
80
|
+
*/
|
|
81
|
+
calculateConfidence(entities, complexity, questionType) {
|
|
82
|
+
let confidence = 0.5;
|
|
83
|
+
// Higher confidence for simple queries
|
|
84
|
+
if (complexity === 'low')
|
|
85
|
+
confidence += 0.3;
|
|
86
|
+
else if (complexity === 'medium')
|
|
87
|
+
confidence += 0.1;
|
|
88
|
+
// Higher confidence when entities are detected
|
|
89
|
+
if (entities.length > 0)
|
|
90
|
+
confidence += 0.1;
|
|
91
|
+
// Lower confidence for conceptual queries (harder to satisfy)
|
|
92
|
+
if (questionType === 'conceptual')
|
|
93
|
+
confidence -= 0.2;
|
|
94
|
+
return Math.max(0, Math.min(1, confidence));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Extract person names from query.
|
|
98
|
+
*/
|
|
99
|
+
extractPersons(query) {
|
|
100
|
+
const persons = [];
|
|
101
|
+
const words = query.split(/\s+/);
|
|
102
|
+
for (let i = 0; i < words.length; i++) {
|
|
103
|
+
const word = words[i];
|
|
104
|
+
// Check for titles followed by names
|
|
105
|
+
if (this.personIndicators.some(ind => word.startsWith(ind))) {
|
|
106
|
+
if (i + 1 < words.length) {
|
|
107
|
+
persons.push(words[i + 1].replace(/[^a-zA-Z]/g, ''));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Check for capitalized words that might be names
|
|
111
|
+
if (/^[A-Z][a-z]+$/.test(word) && i > 0 && !/^[A-Z]/.test(words[i - 1])) {
|
|
112
|
+
persons.push(word);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return [...new Set(persons)];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Extract location names from query.
|
|
119
|
+
*/
|
|
120
|
+
extractLocations(query) {
|
|
121
|
+
const locationIndicators = ['in', 'at', 'from', 'to', 'near'];
|
|
122
|
+
const locations = [];
|
|
123
|
+
const words = query.split(/\s+/);
|
|
124
|
+
for (let i = 0; i < words.length; i++) {
|
|
125
|
+
if (locationIndicators.includes(words[i].toLowerCase())) {
|
|
126
|
+
if (i + 1 < words.length && /^[A-Z]/.test(words[i + 1])) {
|
|
127
|
+
locations.push(words[i + 1].replace(/[^a-zA-Z]/g, ''));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...new Set(locations)];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Extract organization names from query.
|
|
135
|
+
*/
|
|
136
|
+
extractOrganizations(query) {
|
|
137
|
+
const orgIndicators = ['Inc.', 'Corp.', 'LLC', 'Ltd.', 'Company', 'Co.'];
|
|
138
|
+
const organizations = [];
|
|
139
|
+
for (const indicator of orgIndicators) {
|
|
140
|
+
const regex = new RegExp(`([A-Z][a-zA-Z]*)\\s*${indicator.replace('.', '\\.')}`, 'g');
|
|
141
|
+
const matches = query.match(regex);
|
|
142
|
+
if (matches) {
|
|
143
|
+
organizations.push(...matches);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return [...new Set(organizations)];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Extract temporal range from query.
|
|
150
|
+
*/
|
|
151
|
+
extractTemporalRange(query) {
|
|
152
|
+
const lowerQuery = query.toLowerCase();
|
|
153
|
+
for (const keyword of this.temporalKeywords) {
|
|
154
|
+
if (lowerQuery.includes(keyword)) {
|
|
155
|
+
return { relative: keyword };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Check for date patterns
|
|
159
|
+
const datePattern = /\d{4}-\d{2}-\d{2}/g;
|
|
160
|
+
const dates = query.match(datePattern);
|
|
161
|
+
if (dates && dates.length >= 1) {
|
|
162
|
+
return {
|
|
163
|
+
start: dates[0],
|
|
164
|
+
end: dates.length > 1 ? dates[1] : undefined,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Detect the type of question.
|
|
171
|
+
*/
|
|
172
|
+
detectQuestionType(query) {
|
|
173
|
+
for (const [type, keywords] of Object.entries(this.questionKeywords)) {
|
|
174
|
+
if (keywords.some(kw => query.includes(kw))) {
|
|
175
|
+
return type;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return 'factual';
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Estimate query complexity.
|
|
182
|
+
*/
|
|
183
|
+
estimateComplexity(query) {
|
|
184
|
+
const wordCount = query.split(/\s+/).length;
|
|
185
|
+
const hasConjunctions = /\b(and|or|but|then|therefore)\b/i.test(query);
|
|
186
|
+
const hasMultipleClauses = /[,;]/.test(query);
|
|
187
|
+
if (wordCount > 20 || (hasConjunctions && hasMultipleClauses)) {
|
|
188
|
+
return 'high';
|
|
189
|
+
}
|
|
190
|
+
if (wordCount > 10 || hasConjunctions || hasMultipleClauses) {
|
|
191
|
+
return 'medium';
|
|
192
|
+
}
|
|
193
|
+
return 'low';
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Detect what types of information are being requested.
|
|
197
|
+
*/
|
|
198
|
+
detectRequiredInfoTypes(query) {
|
|
199
|
+
const infoTypes = [];
|
|
200
|
+
if (/\b(who|person|people|name)\b/.test(query))
|
|
201
|
+
infoTypes.push('person');
|
|
202
|
+
if (/\b(where|location|place|city)\b/.test(query))
|
|
203
|
+
infoTypes.push('location');
|
|
204
|
+
if (/\b(when|date|time|year|month)\b/.test(query))
|
|
205
|
+
infoTypes.push('temporal');
|
|
206
|
+
if (/\b(how many|count|number|total)\b/.test(query))
|
|
207
|
+
infoTypes.push('quantity');
|
|
208
|
+
if (/\b(why|reason|because)\b/.test(query))
|
|
209
|
+
infoTypes.push('reason');
|
|
210
|
+
if (/\b(what|which|project|task)\b/.test(query))
|
|
211
|
+
infoTypes.push('entity');
|
|
212
|
+
return infoTypes;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Decompose complex queries into sub-queries.
|
|
216
|
+
*/
|
|
217
|
+
decomposeQuery(query) {
|
|
218
|
+
// Split on conjunctions
|
|
219
|
+
const parts = query.split(/\b(and then|and|but|or)\b/i)
|
|
220
|
+
.map(p => p.trim())
|
|
221
|
+
.filter(p => p && !/^(and then|and|but|or)$/i.test(p));
|
|
222
|
+
if (parts.length > 1) {
|
|
223
|
+
return parts;
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Planner
|
|
3
|
+
*
|
|
4
|
+
* Phase 11: Generates execution plans for queries based on analysis.
|
|
5
|
+
*
|
|
6
|
+
* @module search/QueryPlanner
|
|
7
|
+
*/
|
|
8
|
+
import type { QueryAnalysis, QueryPlan } from '../types/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* Query Planner generates execution plans from query analysis.
|
|
11
|
+
*
|
|
12
|
+
* Creates optimized plans that:
|
|
13
|
+
* - Select appropriate search layers
|
|
14
|
+
* - Determine execution strategy (parallel/sequential)
|
|
15
|
+
* - Set up query dependencies
|
|
16
|
+
* - Configure merge strategies
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const analyzer = new QueryAnalyzer();
|
|
21
|
+
* const planner = new QueryPlanner();
|
|
22
|
+
*
|
|
23
|
+
* const analysis = analyzer.analyze('Find projects by Alice');
|
|
24
|
+
* const plan = planner.createPlan('Find projects by Alice', analysis);
|
|
25
|
+
* // { originalQuery: '...', subQueries: [...], executionStrategy: 'iterative', ... }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare class QueryPlanner {
|
|
29
|
+
/**
|
|
30
|
+
* Create an execution plan from query analysis.
|
|
31
|
+
*/
|
|
32
|
+
createPlan(query: string, analysis: QueryAnalysis): QueryPlan;
|
|
33
|
+
/**
|
|
34
|
+
* Create sub-queries from analysis.
|
|
35
|
+
*/
|
|
36
|
+
private createSubQueries;
|
|
37
|
+
/**
|
|
38
|
+
* Select the most appropriate search layer.
|
|
39
|
+
*/
|
|
40
|
+
private selectLayer;
|
|
41
|
+
/**
|
|
42
|
+
* Select execution strategy based on sub-queries.
|
|
43
|
+
*/
|
|
44
|
+
private selectExecutionStrategy;
|
|
45
|
+
/**
|
|
46
|
+
* Select merge strategy based on question type.
|
|
47
|
+
*/
|
|
48
|
+
private selectMergeStrategy;
|
|
49
|
+
/**
|
|
50
|
+
* Build symbolic filters from analysis.
|
|
51
|
+
*/
|
|
52
|
+
private buildFilters;
|
|
53
|
+
/**
|
|
54
|
+
* Calculate plan complexity score.
|
|
55
|
+
*/
|
|
56
|
+
private calculateComplexity;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=QueryPlanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QueryPlanner.d.ts","sourceRoot":"","sources":["../../src/search/QueryPlanner.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAA6B,MAAM,mBAAmB,CAAC;AAE7F;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,YAAY;IACvB;;OAEG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,SAAS;IAc7D;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA6BxB;;OAEG;IACH,OAAO,CAAC,WAAW;IAanB;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAO/B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAQ3B;;OAEG;IACH,OAAO,CAAC,YAAY;IAepB;;OAEG;IACH,OAAO,CAAC,mBAAmB;CAQ5B"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Planner
|
|
3
|
+
*
|
|
4
|
+
* Phase 11: Generates execution plans for queries based on analysis.
|
|
5
|
+
*
|
|
6
|
+
* @module search/QueryPlanner
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Query Planner generates execution plans from query analysis.
|
|
10
|
+
*
|
|
11
|
+
* Creates optimized plans that:
|
|
12
|
+
* - Select appropriate search layers
|
|
13
|
+
* - Determine execution strategy (parallel/sequential)
|
|
14
|
+
* - Set up query dependencies
|
|
15
|
+
* - Configure merge strategies
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const analyzer = new QueryAnalyzer();
|
|
20
|
+
* const planner = new QueryPlanner();
|
|
21
|
+
*
|
|
22
|
+
* const analysis = analyzer.analyze('Find projects by Alice');
|
|
23
|
+
* const plan = planner.createPlan('Find projects by Alice', analysis);
|
|
24
|
+
* // { originalQuery: '...', subQueries: [...], executionStrategy: 'iterative', ... }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class QueryPlanner {
|
|
28
|
+
/**
|
|
29
|
+
* Create an execution plan from query analysis.
|
|
30
|
+
*/
|
|
31
|
+
createPlan(query, analysis) {
|
|
32
|
+
const subQueries = this.createSubQueries(query, analysis);
|
|
33
|
+
const executionStrategy = this.selectExecutionStrategy(subQueries);
|
|
34
|
+
const mergeStrategy = this.selectMergeStrategy(analysis);
|
|
35
|
+
return {
|
|
36
|
+
originalQuery: query,
|
|
37
|
+
subQueries,
|
|
38
|
+
executionStrategy,
|
|
39
|
+
mergeStrategy,
|
|
40
|
+
estimatedComplexity: this.calculateComplexity(subQueries),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create sub-queries from analysis.
|
|
45
|
+
*/
|
|
46
|
+
createSubQueries(query, analysis) {
|
|
47
|
+
const subQueries = [];
|
|
48
|
+
let id = 0;
|
|
49
|
+
// If query was decomposed, create sub-query for each part
|
|
50
|
+
if (analysis.subQueries && analysis.subQueries.length > 1) {
|
|
51
|
+
for (const sq of analysis.subQueries) {
|
|
52
|
+
subQueries.push({
|
|
53
|
+
id: `sq_${id++}`,
|
|
54
|
+
query: sq,
|
|
55
|
+
targetLayer: this.selectLayer(analysis),
|
|
56
|
+
priority: id === 1 ? 1 : 2,
|
|
57
|
+
dependsOn: id > 1 ? [`sq_${id - 2}`] : undefined,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Single query
|
|
63
|
+
subQueries.push({
|
|
64
|
+
id: `sq_${id}`,
|
|
65
|
+
query,
|
|
66
|
+
targetLayer: this.selectLayer(analysis),
|
|
67
|
+
priority: 1,
|
|
68
|
+
filters: this.buildFilters(analysis),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return subQueries;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Select the most appropriate search layer.
|
|
75
|
+
*/
|
|
76
|
+
selectLayer(analysis) {
|
|
77
|
+
// Use symbolic for tag/type/date filtered queries
|
|
78
|
+
if (analysis.temporalRange || analysis.requiredInfoTypes.includes('temporal')) {
|
|
79
|
+
return 'symbolic';
|
|
80
|
+
}
|
|
81
|
+
// Use semantic for complex concept queries
|
|
82
|
+
if (analysis.complexity === 'high' || analysis.questionType === 'comparative') {
|
|
83
|
+
return 'semantic';
|
|
84
|
+
}
|
|
85
|
+
// Use hybrid for balanced approach
|
|
86
|
+
return 'hybrid';
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Select execution strategy based on sub-queries.
|
|
90
|
+
*/
|
|
91
|
+
selectExecutionStrategy(subQueries) {
|
|
92
|
+
const hasDependencies = subQueries.some(sq => sq.dependsOn && sq.dependsOn.length > 0);
|
|
93
|
+
if (hasDependencies)
|
|
94
|
+
return 'sequential';
|
|
95
|
+
if (subQueries.length > 1)
|
|
96
|
+
return 'parallel';
|
|
97
|
+
return 'iterative';
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Select merge strategy based on question type.
|
|
101
|
+
*/
|
|
102
|
+
selectMergeStrategy(analysis) {
|
|
103
|
+
switch (analysis.questionType) {
|
|
104
|
+
case 'aggregation': return 'union';
|
|
105
|
+
case 'comparative': return 'intersection';
|
|
106
|
+
default: return 'weighted';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Build symbolic filters from analysis.
|
|
111
|
+
*/
|
|
112
|
+
buildFilters(analysis) {
|
|
113
|
+
const filters = {};
|
|
114
|
+
let hasFilters = false;
|
|
115
|
+
if (analysis.temporalRange) {
|
|
116
|
+
filters.dateRange = {
|
|
117
|
+
start: analysis.temporalRange.start || '',
|
|
118
|
+
end: analysis.temporalRange.end || '',
|
|
119
|
+
};
|
|
120
|
+
hasFilters = true;
|
|
121
|
+
}
|
|
122
|
+
return hasFilters ? filters : undefined;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Calculate plan complexity score.
|
|
126
|
+
*/
|
|
127
|
+
calculateComplexity(subQueries) {
|
|
128
|
+
let complexity = subQueries.length;
|
|
129
|
+
for (const sq of subQueries) {
|
|
130
|
+
if (sq.dependsOn)
|
|
131
|
+
complexity += sq.dependsOn.length * 0.5;
|
|
132
|
+
if (sq.filters)
|
|
133
|
+
complexity += 0.5;
|
|
134
|
+
}
|
|
135
|
+
return Math.min(complexity, 10);
|
|
136
|
+
}
|
|
137
|
+
}
|