sweet-search 0.0.1 → 2.3.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/LICENSE +190 -0
- package/NOTICE +23 -0
- package/core/cli.js +51 -0
- package/core/config.js +27 -0
- package/core/embedding/embedding-cache.js +467 -0
- package/core/embedding/embedding-local-model.js +845 -0
- package/core/embedding/embedding-remote.js +492 -0
- package/core/embedding/embedding-service.js +712 -0
- package/core/embedding/embedding-telemetry.js +219 -0
- package/core/embedding/index.js +40 -0
- package/core/graph/community-detector.js +294 -0
- package/core/graph/graph-expansion.js +839 -0
- package/core/graph/graph-extractor.js +2304 -0
- package/core/graph/graph-search.js +2148 -0
- package/core/graph/hcgs-generator.js +666 -0
- package/core/graph/index.js +16 -0
- package/core/graph/leiden-algorithm.js +547 -0
- package/core/graph/relationship-resolver.js +366 -0
- package/core/graph/repo-map.js +408 -0
- package/core/graph/summary-manager.js +549 -0
- package/core/indexing/artifact-builder.js +1054 -0
- package/core/indexing/ast-chunker.js +709 -0
- package/core/indexing/chunking/chunk-builder.js +170 -0
- package/core/indexing/chunking/markdown-chunker.js +503 -0
- package/core/indexing/chunking/plaintext-chunker.js +104 -0
- package/core/indexing/dedup/dedup-phase.js +159 -0
- package/core/indexing/dedup/exemplar-selector.js +65 -0
- package/core/indexing/document-chunker.js +56 -0
- package/core/indexing/incremental-parser.js +390 -0
- package/core/indexing/incremental-tracker.js +761 -0
- package/core/indexing/index-codebase-v21.js +472 -0
- package/core/indexing/index-maintainer.mjs +1674 -0
- package/core/indexing/index.js +90 -0
- package/core/indexing/indexer-ann.js +1077 -0
- package/core/indexing/indexer-build.js +742 -0
- package/core/indexing/indexer-phases.js +800 -0
- package/core/indexing/indexer-pool.js +764 -0
- package/core/indexing/indexer-sparse-gram.js +98 -0
- package/core/indexing/indexer-utils.js +536 -0
- package/core/indexing/indexer-worker.js +148 -0
- package/core/indexing/li-skip-policy.js +225 -0
- package/core/indexing/merkle-tracker.js +244 -0
- package/core/indexing/model-pool.js +166 -0
- package/core/infrastructure/code-graph-repository.js +120 -0
- package/core/infrastructure/codebase-repository.js +131 -0
- package/core/infrastructure/config/dedup.js +54 -0
- package/core/infrastructure/config/embedding.js +298 -0
- package/core/infrastructure/config/graph.js +80 -0
- package/core/infrastructure/config/index.js +82 -0
- package/core/infrastructure/config/indexing.js +8 -0
- package/core/infrastructure/config/platform.js +254 -0
- package/core/infrastructure/config/ranking.js +221 -0
- package/core/infrastructure/config/search.js +396 -0
- package/core/infrastructure/config/translation.js +89 -0
- package/core/infrastructure/config/vector-store.js +114 -0
- package/core/infrastructure/constants.js +86 -0
- package/core/infrastructure/coreml-cascade.js +909 -0
- package/core/infrastructure/coreml-cascade.json +46 -0
- package/core/infrastructure/coreml-provider.js +81 -0
- package/core/infrastructure/db-utils.js +69 -0
- package/core/infrastructure/dedup-hashing.js +83 -0
- package/core/infrastructure/hardware-capability.js +332 -0
- package/core/infrastructure/index.js +104 -0
- package/core/infrastructure/language-patterns/maps.js +121 -0
- package/core/infrastructure/language-patterns/registry-core.js +323 -0
- package/core/infrastructure/language-patterns/registry-data-query.js +155 -0
- package/core/infrastructure/language-patterns/registry-object-oriented.js +285 -0
- package/core/infrastructure/language-patterns/registry-tooling.js +240 -0
- package/core/infrastructure/language-patterns/registry-web-style.js +143 -0
- package/core/infrastructure/language-patterns/registry.js +19 -0
- package/core/infrastructure/language-patterns.js +141 -0
- package/core/infrastructure/llm-provider.js +733 -0
- package/core/infrastructure/manifest.json +46 -0
- package/core/infrastructure/maxsim.wasm +0 -0
- package/core/infrastructure/model-fetcher.js +423 -0
- package/core/infrastructure/model-registry.js +214 -0
- package/core/infrastructure/native-inference.js +587 -0
- package/core/infrastructure/native-resolver.js +187 -0
- package/core/infrastructure/native-sparse-gram.js +257 -0
- package/core/infrastructure/native-tokenizer.js +160 -0
- package/core/infrastructure/onnx-mutex.js +45 -0
- package/core/infrastructure/onnx-session-utils.js +261 -0
- package/core/infrastructure/ort-pipeline.js +111 -0
- package/core/infrastructure/project-detector.js +102 -0
- package/core/infrastructure/quantization.js +410 -0
- package/core/infrastructure/simd-distance.js +502 -0
- package/core/infrastructure/simd-distance.wasm +0 -0
- package/core/infrastructure/tree-sitter-provider.js +665 -0
- package/core/infrastructure/webgpu-maxsim.js +222 -0
- package/core/query/index.js +35 -0
- package/core/query/intent-detector.js +201 -0
- package/core/query/intent-router.js +156 -0
- package/core/query/query-router-catboost.js +222 -0
- package/core/query/query-router-ml.js +266 -0
- package/core/query/query-router.js +213 -0
- package/core/ranking/cascaded-scorer.js +379 -0
- package/core/ranking/flashrank.js +810 -0
- package/core/ranking/index.js +49 -0
- package/core/ranking/late-interaction-index.js +2383 -0
- package/core/ranking/late-interaction-model.js +812 -0
- package/core/ranking/local-reranker.js +374 -0
- package/core/ranking/mmr.js +379 -0
- package/core/ranking/quality-scorer.js +363 -0
- package/core/search/context-expander.js +1167 -0
- package/core/search/dedup/sibling-expander.js +327 -0
- package/core/search/index.js +16 -0
- package/core/search/search-boost.js +259 -0
- package/core/search/search-cli.js +544 -0
- package/core/search/search-format.js +282 -0
- package/core/search/search-fusion.js +327 -0
- package/core/search/search-hybrid.js +204 -0
- package/core/search/search-pattern-chunks.js +337 -0
- package/core/search/search-pattern-planner.js +439 -0
- package/core/search/search-pattern-prefilter.js +412 -0
- package/core/search/search-pattern-ripgrep.js +663 -0
- package/core/search/search-pattern.js +463 -0
- package/core/search/search-postprocess.js +452 -0
- package/core/search/search-semantic.js +706 -0
- package/core/search/search-server.js +554 -0
- package/core/search/session-daemon-prewarm.mjs +164 -0
- package/core/search/session-warmup.js +595 -0
- package/core/search/sweet-search.js +632 -0
- package/core/search/warmup-metrics.js +532 -0
- package/core/start-server.js +6 -0
- package/core/training/query-router/features/extractor.js +762 -0
- package/core/training/query-router/features/multilingual-patterns.js +431 -0
- package/core/training/query-router/features/text-segmenter.js +303 -0
- package/core/training/query-router/features/unicode-utils.js +383 -0
- package/core/training/query-router/output/v45_router_d4.js +11521 -0
- package/core/training/query-router/output/v46_router_d4.js +11498 -0
- package/core/vector-store/binary-heap.js +227 -0
- package/core/vector-store/binary-hnsw-index.js +1004 -0
- package/core/vector-store/float-vector-store.js +234 -0
- package/core/vector-store/hnsw-index.js +580 -0
- package/core/vector-store/index.js +39 -0
- package/core/vector-store/seismic-index.js +498 -0
- package/core/vocabulary/index.js +84 -0
- package/core/vocabulary/vocab-constants.js +20 -0
- package/core/vocabulary/vocab-miner-extractors.js +375 -0
- package/core/vocabulary/vocab-miner-nl.js +404 -0
- package/core/vocabulary/vocab-miner-utils.js +146 -0
- package/core/vocabulary/vocab-miner.js +574 -0
- package/core/vocabulary/vocab-prewarm-cli.js +110 -0
- package/core/vocabulary/vocab-ranker.js +492 -0
- package/core/vocabulary/vocab-warmer.js +523 -0
- package/core/vocabulary/vocab-warmup-orchestrator.js +425 -0
- package/core/vocabulary/vocabulary-utils.js +704 -0
- package/crates/wasm-router/pkg/package.json +13 -0
- package/crates/wasm-router/pkg/query_router_wasm.d.ts +36 -0
- package/crates/wasm-router/pkg/query_router_wasm.js +271 -0
- package/crates/wasm-router/pkg/query_router_wasm_bg.wasm +0 -0
- package/crates/wasm-router/pkg/query_router_wasm_bg.wasm.d.ts +19 -0
- package/mcp/config-gen.js +121 -0
- package/mcp/server.js +335 -0
- package/mcp/tool-handlers.js +476 -0
- package/package.json +131 -9
- package/scripts/benchmark-harness.js +794 -0
- package/scripts/init.js +1058 -0
- package/scripts/smoke-test.js +435 -0
- package/scripts/uninstall.js +478 -0
- package/scripts/verify-runtime.js +176 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CatBoost Query Router - Production ML Router with Reject Option
|
|
3
|
+
*
|
|
4
|
+
* This router uses the trained CatBoost model (v45_depth4) with proper
|
|
5
|
+
* uncertainty-based reject option to improve utility accuracy.
|
|
6
|
+
*
|
|
7
|
+
* Performance: ~50μs inference (vs 1.8μs ID3, but higher accuracy)
|
|
8
|
+
* Utility Accuracy: 98%+ on 255-query benchmark (3-class, with reject option)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Import the trained CatBoost router
|
|
12
|
+
import { routeQueryCatBoost, LABELS } from '../training/query-router/output/v46_router_d4.js';
|
|
13
|
+
|
|
14
|
+
// Import the feature extractor (must match training features)
|
|
15
|
+
import { extractAllFeatures } from '../training/query-router/features/extractor.js';
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// REJECT OPTION CONFIGURATION
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// When model confidence/margin is low, fall back to HYBRID (safe max-recall)
|
|
21
|
+
// Thresholds tuned to maximize utility accuracy on 315-query benchmark
|
|
22
|
+
|
|
23
|
+
const REJECT_OPTION = {
|
|
24
|
+
enabled: true, // Improves utility from 92.7% to 94.3%
|
|
25
|
+
|
|
26
|
+
// Per-class thresholds: different classes need different treatment
|
|
27
|
+
// LEXICAL needs aggressive rejection (foreign identifiers look like English)
|
|
28
|
+
// STRUCTURAL needs strict thresholds (very few false positives)
|
|
29
|
+
// SEMANTIC is fairly safe, moderate thresholds
|
|
30
|
+
thresholds: {
|
|
31
|
+
LEXICAL: { confidence: 0.92, margin: 0.40 }, // Aggressive - catch foreign identifiers
|
|
32
|
+
SEMANTIC: { confidence: 0.75, margin: 0.25 }, // Moderate
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Maximum query length to prevent DoS attacks */
|
|
37
|
+
const MAX_QUERY_LENGTH = 100000; // 100KB
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// SOFTMAX AND POST-PROCESSING
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compute softmax probabilities from raw scores.
|
|
45
|
+
* Uses stable softmax to avoid numerical overflow.
|
|
46
|
+
*
|
|
47
|
+
* @param {number[]} scores - Raw scores from CatBoost
|
|
48
|
+
* @returns {number[]} - Probability distribution
|
|
49
|
+
*/
|
|
50
|
+
function softmax(scores) {
|
|
51
|
+
const maxScore = Math.max(...scores);
|
|
52
|
+
const expScores = scores.map(s => Math.exp(s - maxScore));
|
|
53
|
+
const sumExp = expScores.reduce((a, b) => a + b, 0);
|
|
54
|
+
return expScores.map(e => e / sumExp);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Apply reject option: route to HYBRID when uncertain.
|
|
59
|
+
*
|
|
60
|
+
* @param {{mode: string, confidence: number, scores: number[]}} result - CatBoost result
|
|
61
|
+
* @returns {string} - Final routing mode
|
|
62
|
+
*/
|
|
63
|
+
function applyRejectOption(result) {
|
|
64
|
+
if (!REJECT_OPTION.enabled) return result.mode;
|
|
65
|
+
|
|
66
|
+
const { mode, scores } = result;
|
|
67
|
+
|
|
68
|
+
// Already HYBRID - no change
|
|
69
|
+
if (mode === 'HYBRID') return mode;
|
|
70
|
+
|
|
71
|
+
// Compute softmax probabilities from scores
|
|
72
|
+
const probs = softmax(scores);
|
|
73
|
+
const modeIdx = LABELS.indexOf(mode);
|
|
74
|
+
const confidence = probs[modeIdx];
|
|
75
|
+
|
|
76
|
+
// Get thresholds for this prediction class
|
|
77
|
+
const thresholds = REJECT_OPTION.thresholds[mode];
|
|
78
|
+
if (!thresholds) return mode;
|
|
79
|
+
|
|
80
|
+
// Check confidence threshold
|
|
81
|
+
if (confidence < thresholds.confidence) {
|
|
82
|
+
return 'HYBRID';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check margin (difference between top-1 and top-2)
|
|
86
|
+
const sorted = [...probs].sort((a, b) => b - a);
|
|
87
|
+
const margin = sorted[0] - sorted[1];
|
|
88
|
+
if (margin < thresholds.margin) {
|
|
89
|
+
return 'HYBRID';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return mode;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// PUBLIC API
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Extract features for CatBoost routing.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} query - The query to route
|
|
103
|
+
* @returns {number[]|null} - Feature vector for CatBoost, or null for invalid input
|
|
104
|
+
*/
|
|
105
|
+
export function extractCatBoostFeatures(query) {
|
|
106
|
+
// Input validation
|
|
107
|
+
if (query == null || typeof query !== 'string') {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (query.length > MAX_QUERY_LENGTH) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return extractAllFeatures(query);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Route query using CatBoost ML model with reject option.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} query - The search query
|
|
120
|
+
* @returns {{mode: string, confidence: number, rawMode: string, rejected: boolean, scores: number[], error?: string}}
|
|
121
|
+
*/
|
|
122
|
+
export function routeQueryCatBoostWithReject(query) {
|
|
123
|
+
// Input validation
|
|
124
|
+
if (query == null || typeof query !== 'string') {
|
|
125
|
+
return {
|
|
126
|
+
mode: 'hybrid',
|
|
127
|
+
confidence: 0,
|
|
128
|
+
rawMode: 'hybrid',
|
|
129
|
+
rejected: false,
|
|
130
|
+
scores: [0, 0, 0],
|
|
131
|
+
probs: [0.33, 0.33, 0.34],
|
|
132
|
+
method: 'invalid_input',
|
|
133
|
+
error: 'Query must be a non-null string',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (query.length > MAX_QUERY_LENGTH) {
|
|
138
|
+
return {
|
|
139
|
+
mode: 'hybrid',
|
|
140
|
+
confidence: 0,
|
|
141
|
+
rawMode: 'hybrid',
|
|
142
|
+
rejected: false,
|
|
143
|
+
scores: [0, 0, 0],
|
|
144
|
+
probs: [0.33, 0.33, 0.34],
|
|
145
|
+
method: 'query_too_long',
|
|
146
|
+
error: `Query exceeds maximum length of ${MAX_QUERY_LENGTH} characters`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const trimmed = query.trim();
|
|
151
|
+
if (trimmed.length === 0) {
|
|
152
|
+
return {
|
|
153
|
+
mode: 'hybrid',
|
|
154
|
+
confidence: 0,
|
|
155
|
+
rawMode: 'hybrid',
|
|
156
|
+
rejected: false,
|
|
157
|
+
scores: [0, 0, 0],
|
|
158
|
+
probs: [0.33, 0.33, 0.34],
|
|
159
|
+
method: 'empty_query',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const features = extractAllFeatures(trimmed);
|
|
164
|
+
const result = routeQueryCatBoost(features);
|
|
165
|
+
|
|
166
|
+
// Compute proper confidence from softmax
|
|
167
|
+
const probs = softmax(result.scores);
|
|
168
|
+
const modeIdx = LABELS.indexOf(result.mode);
|
|
169
|
+
const confidence = probs[modeIdx];
|
|
170
|
+
|
|
171
|
+
// Apply reject option
|
|
172
|
+
const finalMode = applyRejectOption({ ...result, confidence });
|
|
173
|
+
const rejected = finalMode !== result.mode;
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
mode: finalMode.toLowerCase(),
|
|
177
|
+
confidence,
|
|
178
|
+
rawMode: result.mode.toLowerCase(),
|
|
179
|
+
rejected,
|
|
180
|
+
scores: result.scores,
|
|
181
|
+
probs,
|
|
182
|
+
method: 'catboost',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Route query using CatBoost (simplified interface, compatible with ID3 router).
|
|
188
|
+
*
|
|
189
|
+
* @param {number[]} features - Pre-extracted features (if available)
|
|
190
|
+
* @returns {{mode: string, confidence: number, method: string}}
|
|
191
|
+
*/
|
|
192
|
+
export function routeQueryML(features) {
|
|
193
|
+
const result = routeQueryCatBoost(features);
|
|
194
|
+
|
|
195
|
+
// Compute proper confidence from softmax
|
|
196
|
+
const probs = softmax(result.scores);
|
|
197
|
+
const modeIdx = LABELS.indexOf(result.mode);
|
|
198
|
+
const confidence = probs[modeIdx];
|
|
199
|
+
|
|
200
|
+
// Apply reject option
|
|
201
|
+
const finalMode = applyRejectOption({ ...result, confidence });
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
mode: finalMode.toLowerCase(),
|
|
205
|
+
confidence,
|
|
206
|
+
method: 'catboost',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Re-export LABELS for compatibility
|
|
211
|
+
export { LABELS };
|
|
212
|
+
|
|
213
|
+
// Confidence threshold for fallback (not used directly with reject option)
|
|
214
|
+
export const ML_CONFIDENCE_THRESHOLD = 0.5;
|
|
215
|
+
|
|
216
|
+
export default {
|
|
217
|
+
routeQueryCatBoostWithReject,
|
|
218
|
+
routeQueryML,
|
|
219
|
+
extractCatBoostFeatures,
|
|
220
|
+
LABELS,
|
|
221
|
+
ML_CONFIDENCE_THRESHOLD,
|
|
222
|
+
};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ML Query Router - Auto-generated from Decision Tree
|
|
3
|
+
*
|
|
4
|
+
* DO NOT EDIT - This file is auto-generated by training/models/decision-tree.js
|
|
5
|
+
* Generated: 2026-01-04
|
|
6
|
+
*
|
|
7
|
+
* Performance: ~0.11μs inference (500x faster than 50μs target)
|
|
8
|
+
* Accuracy: 94.26% on test set
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Feature indices for reference:
|
|
13
|
+
* 0: hasCamelBoundary
|
|
14
|
+
* 1: hasUnderscoreWord
|
|
15
|
+
* 2: startsWithUppercase
|
|
16
|
+
* 3: hasConsecutiveUppercase
|
|
17
|
+
* 4: isSingleToken
|
|
18
|
+
* 5: hasDotNotation
|
|
19
|
+
* 6: hasParentheses
|
|
20
|
+
* 7: hasFileExtension
|
|
21
|
+
* 8: hasPathSeparator
|
|
22
|
+
* 9: tokenCount
|
|
23
|
+
* 10: avgTokenLength
|
|
24
|
+
* 11: uppercaseRatio
|
|
25
|
+
* 12: digitRatio
|
|
26
|
+
* 13: nonAsciiRatio
|
|
27
|
+
* 14: hasNonLatinScript
|
|
28
|
+
* 15: hasCallerPattern
|
|
29
|
+
* 16: hasCalleePattern
|
|
30
|
+
* 17: hasImplementationPattern
|
|
31
|
+
* 18: hasImpactPattern
|
|
32
|
+
* 19: hasIdentifierInQuery
|
|
33
|
+
* 20: startsWithQuestionWord
|
|
34
|
+
* 21: endsWithQuestionMark
|
|
35
|
+
* 22: hasSemanticConceptWord
|
|
36
|
+
* 23: hasExplanationRequest
|
|
37
|
+
* 24: hasHowDoesWorkPattern
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extract all 25 features from a query for ML routing.
|
|
42
|
+
* @param {string} query - The query to extract features from
|
|
43
|
+
* @returns {Float32Array} - 25-element feature vector
|
|
44
|
+
*/
|
|
45
|
+
export function extractMLFeatures(query) {
|
|
46
|
+
const trimmed = query.trim();
|
|
47
|
+
const chars = trimmed.replace(/\s/g, '');
|
|
48
|
+
const tokens = trimmed.split(/\s+/);
|
|
49
|
+
const charLen = Math.max(1, chars.length);
|
|
50
|
+
const lower = trimmed.toLowerCase();
|
|
51
|
+
|
|
52
|
+
return new Float32Array([
|
|
53
|
+
// === Base Features (0-14) ===
|
|
54
|
+
/[a-z][A-Z]/.test(trimmed) ? 1 : 0, // 0: hasCamelBoundary
|
|
55
|
+
/_[a-z]/.test(trimmed) ? 1 : 0, // 1: hasUnderscoreWord
|
|
56
|
+
/^[A-Z]/.test(trimmed) ? 1 : 0, // 2: startsWithUppercase
|
|
57
|
+
/[A-Z]{2,}/.test(trimmed) ? 1 : 0, // 3: hasConsecutiveUppercase
|
|
58
|
+
!trimmed.includes(' ') && trimmed.length > 2 ? 1 : 0, // 4: isSingleToken
|
|
59
|
+
/\w\.\w/.test(trimmed) ? 1 : 0, // 5: hasDotNotation
|
|
60
|
+
/\(\)/.test(trimmed) ? 1 : 0, // 6: hasParentheses
|
|
61
|
+
/\.(java|ts|tsx|js|jsx|py|go|rs|cpp|c|h|rb|php|swift|kt|proto|yml|yaml|json|xml|md)$/i.test(trimmed) ? 1 : 0, // 7: hasFileExtension
|
|
62
|
+
/[/\\]/.test(trimmed) ? 1 : 0, // 8: hasPathSeparator
|
|
63
|
+
tokens.length, // 9: tokenCount
|
|
64
|
+
chars.length / Math.max(1, tokens.length), // 10: avgTokenLength
|
|
65
|
+
(chars.match(/[A-Z]/g) || []).length / charLen, // 11: uppercaseRatio
|
|
66
|
+
(chars.match(/[0-9]/g) || []).length / charLen, // 12: digitRatio
|
|
67
|
+
(chars.match(/[^\x00-\x7F]/g) || []).length / charLen, // 13: nonAsciiRatio
|
|
68
|
+
/[\u4e00-\u9fff\u0400-\u04ff\u0600-\u06ff\u0980-\u09ff\u3040-\u30ff\uac00-\ud7af]/.test(trimmed) ? 1 : 0, // 14: hasNonLatinScript
|
|
69
|
+
|
|
70
|
+
// === Structural Features (15-19) ===
|
|
71
|
+
/\b(callers?\s+of|what\s+calls|who\s+calls|usages?\s+of|references?\s+to|where\s+is\s+\w+\s+called)\b/.test(lower) ? 1 : 0, // 15: hasCallerPattern
|
|
72
|
+
/\b(callees?\s+of|what\s+does\s+\w+\s+call|dependencies\s+of|methods?\s+called\s+by)\b/.test(lower) ? 1 : 0, // 16: hasCalleePattern
|
|
73
|
+
/\b(implementations?\s+of|classes?\s+implementing|who\s+extends|subtypes?\s+of)\b/.test(lower) ? 1 : 0, // 17: hasImplementationPattern
|
|
74
|
+
/\b(impact\s+of|affected\s+by|depends?\s+on|downstream\s+effects?|will\s+break)\b/.test(lower) ? 1 : 0, // 18: hasImpactPattern
|
|
75
|
+
/[A-Z][a-z]+[A-Z]|[a-z]+[A-Z]/.test(query) ? 1 : 0, // 19: hasIdentifierInQuery
|
|
76
|
+
|
|
77
|
+
// === Semantic Features (20-24) ===
|
|
78
|
+
/^(how|what|why|where|when|which|who|can|does|is|are|should)\s/.test(lower) ? 1 : 0, // 20: startsWithQuestionWord
|
|
79
|
+
query.trim().endsWith('?') ? 1 : 0, // 21: endsWithQuestionMark
|
|
80
|
+
/\b(flow|process|mechanism|strategy|pattern|approach|logic|algorithm|system|detection|validation|authentication|handling)\b/.test(lower) ? 1 : 0, // 22: hasSemanticConceptWord
|
|
81
|
+
/\b(explain|describe|understand|overview|architecture|design)\b/.test(lower) ? 1 : 0, // 23: hasExplanationRequest
|
|
82
|
+
/how\s+does\s+.+\s+work/.test(lower) ? 1 : 0, // 24: hasHowDoesWorkPattern
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Route query using trained ML decision tree.
|
|
88
|
+
* @param {Float32Array|number[]} features - 25-element feature vector from extractMLFeatures()
|
|
89
|
+
* @returns {{mode: string, confidence: number, tier: number, method: string}}
|
|
90
|
+
*/
|
|
91
|
+
export function routeQueryML(features) {
|
|
92
|
+
if (features[4] /* isSingleToken */ <= 0.500000) {
|
|
93
|
+
if (features[0] /* hasCamelBoundary */ <= 0.500000) {
|
|
94
|
+
if (features[9] /* tokenCount */ <= 2.500000) {
|
|
95
|
+
if (features[22] /* hasSemanticConceptWord */ <= 0.500000) {
|
|
96
|
+
if (features[23] /* hasExplanationRequest */ <= 0.500000) {
|
|
97
|
+
if (features[13] /* nonAsciiRatio */ <= 0.500000) {
|
|
98
|
+
if (features[3] /* hasConsecutiveUppercase */ <= 0.500000) {
|
|
99
|
+
if (features[10] /* avgTokenLength */ <= 5.750000) {
|
|
100
|
+
return { mode: 'semantic', confidence: 0.5000, tier: 5, method: 'ml' };
|
|
101
|
+
} else {
|
|
102
|
+
return { mode: 'hybrid', confidence: 0.8649, tier: 5, method: 'ml' };
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
if (features[10] /* avgTokenLength */ <= 8.750000) {
|
|
109
|
+
if (features[10] /* avgTokenLength */ <= 7.750000) {
|
|
110
|
+
return { mode: 'hybrid', confidence: 0.6667, tier: 5, method: 'ml' };
|
|
111
|
+
} else {
|
|
112
|
+
return { mode: 'semantic', confidence: 0.5000, tier: 5, method: 'ml' };
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
if (features[2] /* startsWithUppercase */ <= 0.500000) {
|
|
123
|
+
if (features[10] /* avgTokenLength */ <= 9.250000) {
|
|
124
|
+
if (features[10] /* avgTokenLength */ <= 8.750000) {
|
|
125
|
+
if (features[10] /* avgTokenLength */ <= 7.750000) {
|
|
126
|
+
return { mode: 'semantic', confidence: 0.6250, tier: 5, method: 'ml' };
|
|
127
|
+
} else {
|
|
128
|
+
return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
return { mode: 'hybrid', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
if (features[13] /* nonAsciiRatio */ <= 0.051587) {
|
|
142
|
+
return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
143
|
+
} else {
|
|
144
|
+
if (features[13] /* nonAsciiRatio */ <= 0.566667) {
|
|
145
|
+
if (features[9] /* tokenCount */ <= 3.500000) {
|
|
146
|
+
return { mode: 'hybrid', confidence: 0.6667, tier: 5, method: 'ml' };
|
|
147
|
+
} else {
|
|
148
|
+
return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
if (features[2] /* startsWithUppercase */ <= 0.500000) {
|
|
157
|
+
if (features[9] /* tokenCount */ <= 3.500000) {
|
|
158
|
+
if (features[13] /* nonAsciiRatio */ <= 0.159091) {
|
|
159
|
+
return { mode: 'structural', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
160
|
+
} else {
|
|
161
|
+
if (features[13] /* nonAsciiRatio */ <= 0.346591) {
|
|
162
|
+
return { mode: 'hybrid', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
163
|
+
} else {
|
|
164
|
+
if (features[13] /* nonAsciiRatio */ <= 0.491379) {
|
|
165
|
+
return { mode: 'structural', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
166
|
+
} else {
|
|
167
|
+
if (features[10] /* avgTokenLength */ <= 7.666667) {
|
|
168
|
+
return { mode: 'hybrid', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
169
|
+
} else {
|
|
170
|
+
return { mode: 'structural', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
if (features[18] /* hasImpactPattern */ <= 0.500000) {
|
|
177
|
+
if (features[16] /* hasCalleePattern */ <= 0.500000) {
|
|
178
|
+
if (features[13] /* nonAsciiRatio */ <= 0.267857) {
|
|
179
|
+
return { mode: 'hybrid', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
180
|
+
} else {
|
|
181
|
+
if (features[13] /* nonAsciiRatio */ <= 0.592033) {
|
|
182
|
+
return { mode: 'structural', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
183
|
+
} else {
|
|
184
|
+
return { mode: 'hybrid', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
return { mode: 'structural', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
return { mode: 'structural', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
return { mode: 'hybrid', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
if (features[13] /* nonAsciiRatio */ <= 0.111111) {
|
|
200
|
+
if (features[10] /* avgTokenLength */ <= 23.000000) {
|
|
201
|
+
if (features[10] /* avgTokenLength */ <= 17.500000) {
|
|
202
|
+
return { mode: 'lexical', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
203
|
+
} else {
|
|
204
|
+
if (features[0] /* hasCamelBoundary */ <= 0.500000) {
|
|
205
|
+
if (features[2] /* startsWithUppercase */ <= 0.500000) {
|
|
206
|
+
return { mode: 'lexical', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
207
|
+
} else {
|
|
208
|
+
return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
return { mode: 'lexical', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
if (features[2] /* startsWithUppercase */ <= 0.500000) {
|
|
216
|
+
return { mode: 'lexical', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
217
|
+
} else {
|
|
218
|
+
return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
if (features[0] /* hasCamelBoundary */ <= 0.500000) {
|
|
223
|
+
if (features[10] /* avgTokenLength */ <= 7.500000) {
|
|
224
|
+
if (features[10] /* avgTokenLength */ <= 5.000000) {
|
|
225
|
+
return { mode: 'hybrid', confidence: 0.6000, tier: 5, method: 'ml' };
|
|
226
|
+
} else {
|
|
227
|
+
if (features[10] /* avgTokenLength */ <= 6.500000) {
|
|
228
|
+
return { mode: 'semantic', confidence: 0.6667, tier: 5, method: 'ml' };
|
|
229
|
+
} else {
|
|
230
|
+
return { mode: 'semantic', confidence: 0.7500, tier: 5, method: 'ml' };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
return { mode: 'hybrid', confidence: 1.0000, tier: 5, method: 'ml' };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* ML confidence threshold for accepting ML routing.
|
|
245
|
+
* Below this, fall back to hybrid mode.
|
|
246
|
+
*/
|
|
247
|
+
export const ML_CONFIDENCE_THRESHOLD = 0.7;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Route query end-to-end: extract features and route.
|
|
251
|
+
* @param {string} query - The query to route
|
|
252
|
+
* @returns {{mode: string, confidence: number, tier: number, method: string}}
|
|
253
|
+
*/
|
|
254
|
+
export function routeWithML(query) {
|
|
255
|
+
const features = extractMLFeatures(query);
|
|
256
|
+
const result = routeQueryML(features);
|
|
257
|
+
|
|
258
|
+
// Apply confidence threshold
|
|
259
|
+
if (result.confidence < ML_CONFIDENCE_THRESHOLD) {
|
|
260
|
+
return { mode: 'hybrid', confidence: result.confidence, tier: 6, method: 'ml_fallback' };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export default { extractMLFeatures, routeQueryML, routeWithML, ML_CONFIDENCE_THRESHOLD };
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Router - CatBoost WASM (Production)
|
|
3
|
+
*
|
|
4
|
+
* Single high-performance router using compiled CatBoost model in WebAssembly.
|
|
5
|
+
*
|
|
6
|
+
* Performance:
|
|
7
|
+
* - Latency: ~10μs per query (22x faster than JS)
|
|
8
|
+
* - Size: 225KB WASM (4.5x smaller than 1MB JS)
|
|
9
|
+
* - Accuracy: 100% match rate with reference implementation
|
|
10
|
+
* - Throughput: ~100k queries/sec
|
|
11
|
+
*
|
|
12
|
+
* The WASM module includes:
|
|
13
|
+
* - CatBoost model (499 trees, depth 4)
|
|
14
|
+
* - Feature extraction (50 features)
|
|
15
|
+
* - Reject option for uncertainty handling
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import { dirname, join } from 'path';
|
|
20
|
+
import { createRequire } from 'module';
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// CONSTANTS
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
/** Maximum query length to prevent DoS attacks */
|
|
29
|
+
const MAX_QUERY_LENGTH = 100000; // 100KB
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// WASM MODULE LOADING
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
let wasmModule = null;
|
|
36
|
+
let wasmLoadError = null;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load the WASM module synchronously on first use.
|
|
40
|
+
* The WASM file is loaded via require() for Node.js compatibility.
|
|
41
|
+
*/
|
|
42
|
+
function loadWasm() {
|
|
43
|
+
if (wasmModule) return wasmModule;
|
|
44
|
+
if (wasmLoadError) throw wasmLoadError;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// Use createRequire for ES module compatibility with CommonJS WASM glue
|
|
48
|
+
const require = createRequire(import.meta.url);
|
|
49
|
+
const wasmPath = join(__dirname, '..', '..', 'crates', 'wasm-router', 'pkg', 'query_router_wasm.js');
|
|
50
|
+
wasmModule = require(wasmPath);
|
|
51
|
+
return wasmModule;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
wasmLoadError = new Error(`Failed to load WASM router: ${err.message}`);
|
|
54
|
+
throw wasmLoadError;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// QUERY ROUTER CLASS
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
export class QueryRouter {
|
|
63
|
+
constructor(options = {}) {
|
|
64
|
+
this.wasm = null;
|
|
65
|
+
this.initialized = false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Ensure WASM module is loaded.
|
|
70
|
+
*/
|
|
71
|
+
ensureLoaded() {
|
|
72
|
+
if (!this.wasm) {
|
|
73
|
+
this.wasm = loadWasm();
|
|
74
|
+
this.initialized = true;
|
|
75
|
+
}
|
|
76
|
+
return this.wasm;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Route a query to the optimal search path.
|
|
81
|
+
*
|
|
82
|
+
* Pipeline:
|
|
83
|
+
* 1. Input validation - Handles null, undefined, empty, too-long queries
|
|
84
|
+
* 2. File path check (~0.1μs) - Fast path for file extensions
|
|
85
|
+
* 3. WASM CatBoost ML (~10μs) - 3-class routing (lexical/semantic/hybrid)
|
|
86
|
+
*
|
|
87
|
+
* Structural mode is opt-in only (explicit --structural flag).
|
|
88
|
+
*
|
|
89
|
+
* @param {string} query - The search query
|
|
90
|
+
* @returns {{mode: string, confidence: number, method: string, routingLatency_us: number}}
|
|
91
|
+
*/
|
|
92
|
+
route(query) {
|
|
93
|
+
const start = performance.now();
|
|
94
|
+
|
|
95
|
+
// === INPUT VALIDATION ===
|
|
96
|
+
// Handle null, undefined, and non-string inputs
|
|
97
|
+
if (query == null || typeof query !== 'string') {
|
|
98
|
+
return {
|
|
99
|
+
mode: 'hybrid',
|
|
100
|
+
confidence: 0,
|
|
101
|
+
method: 'invalid_input',
|
|
102
|
+
routingLatency_us: Math.round((performance.now() - start) * 1000),
|
|
103
|
+
error: 'Query must be a non-null string',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle queries exceeding maximum length (DoS protection)
|
|
108
|
+
if (query.length > MAX_QUERY_LENGTH) {
|
|
109
|
+
return {
|
|
110
|
+
mode: 'hybrid',
|
|
111
|
+
confidence: 0,
|
|
112
|
+
method: 'query_too_long',
|
|
113
|
+
routingLatency_us: Math.round((performance.now() - start) * 1000),
|
|
114
|
+
error: `Query exceeds maximum length of ${MAX_QUERY_LENGTH} characters`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const trimmed = query.trim();
|
|
119
|
+
|
|
120
|
+
// Handle empty or whitespace-only queries
|
|
121
|
+
if (trimmed.length === 0) {
|
|
122
|
+
return {
|
|
123
|
+
mode: 'hybrid',
|
|
124
|
+
confidence: 0,
|
|
125
|
+
method: 'empty_query',
|
|
126
|
+
routingLatency_us: Math.round((performance.now() - start) * 1000),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// === FILE PATH CHECK (~0.1μs) ===
|
|
131
|
+
// File extensions and paths are always lexical
|
|
132
|
+
if (/\.(java|js|jsx|ts|tsx|py|go|rs|kt|swift|rb|php|c|cpp|h|proto|json|xml|yml|yaml|md|sql)$/i.test(trimmed) ||
|
|
133
|
+
/[/\\]/.test(trimmed)) {
|
|
134
|
+
return {
|
|
135
|
+
mode: 'lexical',
|
|
136
|
+
confidence: 0.95,
|
|
137
|
+
method: 'file_pattern',
|
|
138
|
+
routingLatency_us: Math.round((performance.now() - start) * 1000),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// === WASM CATBOOST ML (~10μs) ===
|
|
143
|
+
let result = null;
|
|
144
|
+
try {
|
|
145
|
+
const wasm = this.ensureLoaded();
|
|
146
|
+
result = wasm.route_query(trimmed);
|
|
147
|
+
|
|
148
|
+
const mode = result.mode_str.toLowerCase();
|
|
149
|
+
const confidence = result.confidence;
|
|
150
|
+
const rejected = result.rejected;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
mode: rejected ? 'hybrid' : mode,
|
|
154
|
+
confidence,
|
|
155
|
+
method: rejected ? 'wasm_rejected' : 'wasm_catboost',
|
|
156
|
+
routingLatency_us: Math.round((performance.now() - start) * 1000),
|
|
157
|
+
};
|
|
158
|
+
} catch (err) {
|
|
159
|
+
// Fallback to hybrid if WASM fails
|
|
160
|
+
console.error(`[QueryRouter] WASM error: ${err.message}`);
|
|
161
|
+
return {
|
|
162
|
+
mode: 'hybrid',
|
|
163
|
+
confidence: 0.5,
|
|
164
|
+
method: 'fallback_error',
|
|
165
|
+
routingLatency_us: Math.round((performance.now() - start) * 1000),
|
|
166
|
+
};
|
|
167
|
+
} finally {
|
|
168
|
+
// Always free WASM result to prevent memory leak
|
|
169
|
+
if (result && typeof result.free === 'function') {
|
|
170
|
+
try {
|
|
171
|
+
result.free();
|
|
172
|
+
} catch {
|
|
173
|
+
// Ignore free errors (may already be freed or invalid)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// =============================================================================
|
|
181
|
+
// CONVENIENCE EXPORTS
|
|
182
|
+
// =============================================================================
|
|
183
|
+
|
|
184
|
+
// Default singleton instance
|
|
185
|
+
const defaultRouter = new QueryRouter();
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Route a query using the default router.
|
|
189
|
+
* @param {string} query - The search query
|
|
190
|
+
* @returns {{mode: string, confidence: number, method: string, routingLatency_us: number}}
|
|
191
|
+
*/
|
|
192
|
+
export function routeQuery(query) {
|
|
193
|
+
return defaultRouter.route(query);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Extract features from a query (for debugging/testing).
|
|
198
|
+
* @param {string} query - The query to extract features from
|
|
199
|
+
* @returns {Float32Array|null} - Feature vector, or null for invalid input
|
|
200
|
+
*/
|
|
201
|
+
export function extractFeatures(query) {
|
|
202
|
+
// Input validation
|
|
203
|
+
if (query == null || typeof query !== 'string') {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
if (query.length > MAX_QUERY_LENGTH) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const wasm = loadWasm();
|
|
210
|
+
return wasm.extract_features_js(query);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default QueryRouter;
|