@unrdf/dark-matter 5.0.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.
@@ -0,0 +1,242 @@
1
+ /**
2
+ * @file Index Advisor - Recommend indexes based on query patterns
3
+ * @module @unrdf/dark-matter/index-advisor
4
+ */
5
+
6
+ import { z } from 'zod';
7
+ import { analyzeSparqlQuery } from './query-analyzer.mjs';
8
+
9
+ /**
10
+ * @typedef {import('n3').Store} Store
11
+ */
12
+
13
+ /**
14
+ * Index recommendation schema
15
+ */
16
+ const IndexRecommendationSchema = z.object({
17
+ type: z.enum(['predicate', 'subject_predicate', 'object', 'composite']),
18
+ priority: z.enum(['low', 'medium', 'high', 'critical']),
19
+ estimatedBenefit: z.number().min(0).max(100),
20
+ reason: z.string(),
21
+ indexConfig: z.object({
22
+ fields: z.array(z.string()),
23
+ unique: z.boolean().optional(),
24
+ }),
25
+ });
26
+
27
+ /**
28
+ * Analyze index needs based on query log
29
+ * @param {Store} store - RDF store
30
+ * @param {Array<string>} queryLog - Array of executed queries
31
+ * @returns {Array<Object>} Index recommendations
32
+ *
33
+ * @throws {TypeError} If store or queryLog is invalid
34
+ *
35
+ * @example
36
+ * const recommendations = analyzeIndexNeeds(store, [query1, query2]);
37
+ * recommendations.forEach(r => console.log(r.type, r.priority));
38
+ */
39
+ export function analyzeIndexNeeds(store, queryLog) {
40
+ if (!store || typeof store.getQuads !== 'function') {
41
+ throw new TypeError('analyzeIndexNeeds: store must be a valid Store instance');
42
+ }
43
+
44
+ if (!Array.isArray(queryLog)) {
45
+ throw new TypeError('analyzeIndexNeeds: queryLog must be an array');
46
+ }
47
+
48
+ const recommendations = [];
49
+ const predicateFrequency = new Map();
50
+ const subjectPredicateFrequency = new Map();
51
+
52
+ // Analyze query patterns
53
+ for (const query of queryLog) {
54
+ if (typeof query !== 'string') {
55
+ continue;
56
+ }
57
+
58
+ const analysis = analyzeSparqlQuery(query);
59
+
60
+ for (const pattern of analysis.patterns) {
61
+ // Track predicate frequency
62
+ if (!pattern.predicate.startsWith('?')) {
63
+ const count = predicateFrequency.get(pattern.predicate) || 0;
64
+ predicateFrequency.set(pattern.predicate, count + 1);
65
+ }
66
+
67
+ // Track subject+predicate combinations
68
+ if (!pattern.subject.startsWith('?') && !pattern.predicate.startsWith('?')) {
69
+ const key = `${pattern.subject}|${pattern.predicate}`;
70
+ const count = subjectPredicateFrequency.get(key) || 0;
71
+ subjectPredicateFrequency.set(key, count + 1);
72
+ }
73
+ }
74
+ }
75
+
76
+ // Generate predicate index recommendations
77
+ for (const [predicate, frequency] of predicateFrequency.entries()) {
78
+ if (frequency >= 3) {
79
+ const priority = frequency >= 10 ? 'high' : frequency >= 5 ? 'medium' : 'low';
80
+ const estimatedBenefit = Math.min(frequency * 10, 100);
81
+
82
+ recommendations.push({
83
+ type: 'predicate',
84
+ priority,
85
+ estimatedBenefit,
86
+ reason: `Predicate ${predicate} queried ${frequency} times`,
87
+ indexConfig: {
88
+ fields: ['predicate'],
89
+ unique: false,
90
+ },
91
+ });
92
+ }
93
+ }
94
+
95
+ // Generate composite index recommendations
96
+ for (const [_key, frequency] of subjectPredicateFrequency.entries()) {
97
+ if (frequency >= 2) {
98
+ const priority = frequency >= 5 ? 'high' : 'medium';
99
+ const estimatedBenefit = Math.min(frequency * 15, 100);
100
+
101
+ recommendations.push({
102
+ type: 'subject_predicate',
103
+ priority,
104
+ estimatedBenefit,
105
+ reason: `Subject+Predicate combination queried ${frequency} times`,
106
+ indexConfig: {
107
+ fields: ['subject', 'predicate'],
108
+ unique: false,
109
+ },
110
+ });
111
+ }
112
+ }
113
+
114
+ // Sort by estimated benefit
115
+ recommendations.sort((a, b) => b.estimatedBenefit - a.estimatedBenefit);
116
+
117
+ return recommendations.map(r => IndexRecommendationSchema.parse(r));
118
+ }
119
+
120
+ /**
121
+ * Suggest index for specific pattern
122
+ * @param {Object} pattern - Triple pattern
123
+ * @returns {Object} Index suggestion
124
+ *
125
+ * @throws {TypeError} If pattern is invalid
126
+ *
127
+ * @example
128
+ * const suggestion = suggestIndexForPattern({
129
+ * subject: '?s',
130
+ * predicate: '<http://xmlns.com/foaf/0.1/name>',
131
+ * object: '?name'
132
+ * });
133
+ */
134
+ export function suggestIndexForPattern(pattern) {
135
+ if (!pattern || typeof pattern !== 'object') {
136
+ throw new TypeError('suggestIndexForPattern: pattern must be an object');
137
+ }
138
+
139
+ const { subject, predicate, object } = pattern;
140
+
141
+ if (!subject || !predicate || !object) {
142
+ throw new TypeError('suggestIndexForPattern: pattern must have subject, predicate, and object');
143
+ }
144
+
145
+ // Specific predicate - recommend predicate index
146
+ if (!predicate.startsWith('?')) {
147
+ return {
148
+ type: 'predicate',
149
+ priority: 'high',
150
+ estimatedBenefit: 70,
151
+ reason: 'Specific predicate benefits from dedicated index',
152
+ indexConfig: {
153
+ fields: ['predicate'],
154
+ },
155
+ };
156
+ }
157
+
158
+ // Specific subject - recommend subject index
159
+ if (!subject.startsWith('?')) {
160
+ return {
161
+ type: 'subject_predicate',
162
+ priority: 'medium',
163
+ estimatedBenefit: 50,
164
+ reason: 'Specific subject can use subject-based index',
165
+ indexConfig: {
166
+ fields: ['subject'],
167
+ },
168
+ };
169
+ }
170
+
171
+ // Specific object - recommend object index
172
+ if (!object.startsWith('?')) {
173
+ return {
174
+ type: 'object',
175
+ priority: 'low',
176
+ estimatedBenefit: 30,
177
+ reason: 'Specific object may benefit from object index',
178
+ indexConfig: {
179
+ fields: ['object'],
180
+ },
181
+ };
182
+ }
183
+
184
+ // All wildcards - no specific index recommended
185
+ return {
186
+ type: 'composite',
187
+ priority: 'low',
188
+ estimatedBenefit: 10,
189
+ reason: 'Pattern too general for specific index',
190
+ indexConfig: {
191
+ fields: ['subject', 'predicate', 'object'],
192
+ },
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Calculate index benefit for pattern
198
+ * @param {Object} pattern - Triple pattern
199
+ * @param {Object} indexConfig - Index configuration
200
+ * @returns {number} Benefit score 0-100
201
+ *
202
+ * @throws {TypeError} If parameters are invalid
203
+ *
204
+ * @example
205
+ * const benefit = calculateIndexBenefit(pattern, {
206
+ * fields: ['predicate'],
207
+ * unique: false
208
+ * });
209
+ */
210
+ export function calculateIndexBenefit(pattern, indexConfig) {
211
+ if (!pattern || typeof pattern !== 'object') {
212
+ throw new TypeError('calculateIndexBenefit: pattern must be an object');
213
+ }
214
+
215
+ if (!indexConfig || typeof indexConfig !== 'object') {
216
+ throw new TypeError('calculateIndexBenefit: indexConfig must be an object');
217
+ }
218
+
219
+ const { subject, predicate, object } = pattern;
220
+ const { fields } = indexConfig;
221
+
222
+ if (!Array.isArray(fields)) {
223
+ throw new TypeError('calculateIndexBenefit: indexConfig.fields must be an array');
224
+ }
225
+
226
+ let benefit = 0;
227
+
228
+ // Check if indexed fields are bound (not variables)
229
+ for (const field of fields) {
230
+ if (field === 'subject' && !subject.startsWith('?')) {
231
+ benefit += 30;
232
+ }
233
+ if (field === 'predicate' && !predicate.startsWith('?')) {
234
+ benefit += 40;
235
+ }
236
+ if (field === 'object' && !object.startsWith('?')) {
237
+ benefit += 30;
238
+ }
239
+ }
240
+
241
+ return Math.min(benefit, 100);
242
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * @file Dark Matter 80/20 Query Optimization - Main Export
3
+ * @module dark-matter
4
+ *
5
+ * @description
6
+ * Main entry point for Dark Matter 80/20 query optimization system.
7
+ * Provides integrated query analysis, critical path identification,
8
+ * and query optimization following the 80/20 principle.
9
+ */
10
+
11
+ import { QueryAnalyzer, createQueryAnalyzer } from './query-analyzer.mjs';
12
+ import { CriticalPathIdentifier, createCriticalPathIdentifier } from './critical-path.mjs';
13
+ import { DarkMatterOptimizer, createDarkMatterOptimizer } from './optimizer.mjs';
14
+
15
+ /**
16
+ * Integrated Dark Matter query optimization system
17
+ */
18
+ export class DarkMatterQuerySystem {
19
+ /**
20
+ * Create a new Dark Matter query system
21
+ * @param {Object} [config] - Configuration
22
+ */
23
+ constructor(config = {}) {
24
+ this.analyzer = createQueryAnalyzer(config.analyzer);
25
+ this.criticalPath = createCriticalPathIdentifier(config.criticalPath);
26
+ this.optimizer = createDarkMatterOptimizer(config.optimizer);
27
+
28
+ this.config = {
29
+ enableAutoOptimization: config.enableAutoOptimization !== false,
30
+ complexityThreshold: config.complexityThreshold || 100,
31
+ ...config,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Analyze a query
37
+ * @param {string} query - SPARQL query
38
+ * @param {string} [queryId] - Optional query identifier
39
+ * @returns {Object} Analysis result
40
+ */
41
+ analyze(query, queryId = null) {
42
+ return this.analyzer.analyze(query, queryId);
43
+ }
44
+
45
+ /**
46
+ * Log query execution for critical path analysis
47
+ * @param {string} queryId - Query identifier
48
+ * @param {string} query - SPARQL query
49
+ * @param {number} executionTime - Execution time in ms
50
+ * @param {Object} [metadata] - Optional metadata
51
+ */
52
+ logExecution(queryId, query, executionTime, metadata = {}) {
53
+ this.criticalPath.logExecution(queryId, query, executionTime, metadata);
54
+ }
55
+
56
+ /**
57
+ * Identify critical queries
58
+ * @returns {Object} Critical path analysis
59
+ */
60
+ identifyCriticalQueries() {
61
+ return this.criticalPath.identify();
62
+ }
63
+
64
+ /**
65
+ * Optimize a query
66
+ * @param {string} query - SPARQL query
67
+ * @param {Object} [analysis] - Optional pre-computed analysis
68
+ * @returns {Object} Optimization result
69
+ */
70
+ optimize(query, analysis = null) {
71
+ // Analyze first if not provided
72
+ if (!analysis) {
73
+ analysis = this.analyzer.analyze(query);
74
+ }
75
+
76
+ // Only optimize if above complexity threshold
77
+ if (analysis.complexity.score < this.config.complexityThreshold) {
78
+ return {
79
+ original: query,
80
+ optimized: query,
81
+ rules: [],
82
+ estimatedImprovement: {
83
+ before: analysis.complexity.score,
84
+ after: analysis.complexity.score,
85
+ percentageGain: 0,
86
+ },
87
+ timestamp: Date.now(),
88
+ skipped: true,
89
+ reason: 'Query complexity below threshold',
90
+ };
91
+ }
92
+
93
+ return this.optimizer.optimize(query, analysis);
94
+ }
95
+
96
+ /**
97
+ * Analyze and optimize a query in one step
98
+ * @param {string} query - SPARQL query
99
+ * @param {string} [queryId] - Optional query identifier
100
+ * @returns {Object} Combined analysis and optimization
101
+ */
102
+ analyzeAndOptimize(query, queryId = null) {
103
+ const analysis = this.analyze(query, queryId);
104
+ const optimization = this.optimize(query, analysis);
105
+
106
+ return {
107
+ analysis,
108
+ optimization,
109
+ shouldOptimize: !optimization.skipped,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Process query execution: analyze, log, and optionally optimize
115
+ * @param {string} query - SPARQL query
116
+ * @param {number} executionTime - Execution time in ms
117
+ * @param {string} [queryId] - Optional query identifier
118
+ * @returns {Object} Processing result
119
+ */
120
+ processExecution(query, executionTime, queryId = null) {
121
+ const analysis = this.analyze(query, queryId);
122
+ const id = queryId || analysis.queryId;
123
+
124
+ // Log execution
125
+ this.logExecution(id, query, executionTime, {
126
+ complexity: analysis.complexity.score,
127
+ expensiveOps: analysis.expensiveOperations.length,
128
+ });
129
+
130
+ // Auto-optimize if enabled and above threshold
131
+ let optimization = null;
132
+ if (this.config.enableAutoOptimization) {
133
+ optimization = this.optimize(query, analysis);
134
+ }
135
+
136
+ return {
137
+ queryId: id,
138
+ analysis,
139
+ optimization,
140
+ logged: true,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Get comprehensive statistics
146
+ * @returns {Object} Statistics
147
+ */
148
+ getStats() {
149
+ let criticalPathMetrics = null;
150
+
151
+ try {
152
+ criticalPathMetrics = this.criticalPath.identify().metrics;
153
+ } catch (error) {
154
+ // Not enough data yet for critical path analysis
155
+ criticalPathMetrics = {
156
+ error: error.message,
157
+ totalQueries: 0,
158
+ criticalQueryCount: 0,
159
+ criticalQueryPercentage: 0,
160
+ totalExecutionTime: 0,
161
+ criticalExecutionTime: 0,
162
+ impactRatio: 0,
163
+ avgExecutionTime: 0,
164
+ p50: 0,
165
+ p90: 0,
166
+ p99: 0,
167
+ };
168
+ }
169
+
170
+ return {
171
+ analyzer: this.analyzer.getStats(),
172
+ criticalPath: criticalPathMetrics,
173
+ optimizer: this.optimizer.getStats(),
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Generate full report
179
+ * @returns {string} Markdown report
180
+ */
181
+ getReport() {
182
+ let report = '# Dark Matter 80/20 Query Optimization Report\n\n';
183
+
184
+ // Analyzer stats
185
+ const analyzerStats = this.analyzer.getStats();
186
+ report += '## Query Analysis\n\n';
187
+ report += `- **Total Queries Analyzed**: ${analyzerStats.totalAnalyzed}\n`;
188
+ report += `- **Complex Queries**: ${analyzerStats.complexQueries}\n`;
189
+ report += `- **Simple Queries**: ${analyzerStats.simpleQueries}\n`;
190
+ report += `- **Complexity Ratio**: ${(analyzerStats.complexQueryRatio * 100).toFixed(1)}%\n`;
191
+ report += `- **Average Complexity**: ${analyzerStats.avgComplexity.toFixed(2)}\n\n`;
192
+
193
+ // Critical path
194
+ try {
195
+ const criticalPathReport = this.criticalPath.getReport();
196
+ report += criticalPathReport + '\n\n';
197
+ } catch (error) {
198
+ report += '## Critical Path Analysis\n\n';
199
+ report += `*Insufficient data for analysis: ${error.message}*\n\n`;
200
+ }
201
+
202
+ // Optimizer stats
203
+ const optimizerStats = this.optimizer.getStats();
204
+ report += '## Optimization Statistics\n\n';
205
+ report += `- **Total Optimizations**: ${optimizerStats.totalOptimizations}\n`;
206
+ report += '- **Rules Applied**:\n';
207
+
208
+ for (const [rule, count] of Object.entries(optimizerStats.rulesApplied)) {
209
+ report += ` - ${rule}: ${count}\n`;
210
+ }
211
+
212
+ return report;
213
+ }
214
+
215
+ /**
216
+ * Clear all data
217
+ */
218
+ clear() {
219
+ this.analyzer.resetStats();
220
+ this.criticalPath.clearLogs();
221
+ this.optimizer.resetStats();
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Create a Dark Matter query system
227
+ * @param {Object} [config] - Configuration
228
+ * @returns {DarkMatterQuerySystem} Query system
229
+ */
230
+ export function createDarkMatterQuerySystem(config = {}) {
231
+ return new DarkMatterQuerySystem(config);
232
+ }
233
+
234
+ // Re-export individual components
235
+ export {
236
+ QueryAnalyzer,
237
+ createQueryAnalyzer,
238
+ CriticalPathIdentifier,
239
+ createCriticalPathIdentifier,
240
+ DarkMatterOptimizer,
241
+ createDarkMatterOptimizer,
242
+ };
243
+
244
+ export default DarkMatterQuerySystem;