// Search service for US1 (T034)
// Combines tokenization, scoring, ranking with freshness flags
import { tokenize } from '../core/tokenize.mjs';
import { rank } from '../core/score.mjs';
import { ValidationError } from '../utils/errors.mjs';
import { createTokenizationCache } from '../utils/tokenizationCache.mjs';
export function createSearchService({ index, config, logger, useCache = true }) {
if (!index) throw new ValidationError('index required');
if (!config) throw new ValidationError('config required');
logger = logger || { log: () => {} };
// Create tokenization cache for this service instance
const tokenCache = useCache ? createTokenizationCache() : null;
// Resource management for concurrent queries
let activeQueries = 0;
const MAX_CONCURRENT_QUERIES = 10;
const queryQueue = [];
// Performance monitoring
const performanceMetrics = {
averageResponseTime: 0,
totalQueries: 0,
fastQueries: 0, // queries < 1s
slowQueries: 0 // queries > 3s
};
// Queue management for concurrent queries
async function processQuery(query, options, resolve, reject) {
try {
activeQueries++;
const result = await executeSearch(query, options);
resolve(result);
} catch (error) {
reject(error);
} finally {
activeQueries--;
// Process next query in queue
if (queryQueue.length > 0) {
const nextQuery = queryQueue.shift();
setImmediate(() => processQuery(
nextQuery.query,
nextQuery.options,
nextQuery.resolve,
nextQuery.reject
));
}
}
}
async function search(query, options = {}) {
// Implement graceful degradation under high load
if (activeQueries >= MAX_CONCURRENT_QUERIES) {
// Standard queries get queued, emergency queries get priority
const isEmergency = options.priority === 'emergency';
if (!isEmergency) {
return new Promise((resolve, reject) => {
queryQueue.push({ query, options, resolve, reject });
});
}
}
return new Promise((resolve, reject) => {
processQuery(query, options, resolve, reject);
});
}
async function executeSearch(query, options = {}) {
const searchStartTime = Date.now();
if (!query || typeof query !== 'string') {
throw new ValidationError('query string required');
}
const topK = options.topK || config.topKDefault || 5;
// Optimized tokenization with caching
const tokens = tokenCache ? tokenCache.tokenize(query) : tokenize(query);
if (tokens.length === 0) {
return { results: [], query, tokens: [], total: 0 };
}
// Performance optimization: limit search scope for standard queries
const isStandardQuery = query.length < 50 && tokens.length <= 5;
const searchChunks = isStandardQuery ?
index.chunks.slice(0, Math.min(index.chunks.length, 100)) : // Limit search for standard queries
index.chunks;
// Enhanced scoring with intent keywords and performance optimization
const intentKeywords = options.intentKeywords || extractIntentKeywords(query);
const scored = rank(query, searchChunks, {
intentKeywords,
fastMode: isStandardQuery // Enable fast mode for standard queries
});
// Take top K and enhance with document metadata
const results = scored.slice(0, topK).map(chunk => {
const doc = index.documents.find(d => d.id === chunk.docId);
return {
chunkId: chunk.id,
docId: chunk.docId,
title: doc?.title || 'Unknown',
score: chunk.score,
text: chunk.text,
heading: chunk.heading,
stale: !!(doc?.freshness?.stale),
risks: doc?.risks || [],
severity: doc?.severity
};
});
const searchTime = Date.now() - searchStartTime;
// Update performance metrics
performanceMetrics.totalQueries++;
performanceMetrics.averageResponseTime =
(performanceMetrics.averageResponseTime * (performanceMetrics.totalQueries - 1) + searchTime) /
performanceMetrics.totalQueries;
if (searchTime < 1000) {
performanceMetrics.fastQueries++;
} else if (searchTime > 3000) {
performanceMetrics.slowQueries++;
}
logger.log('search.complete', {
query: '[REDACTED]', // FR-020 compliance
resultCount: results.length,
topScore: results[0]?.score,
searchTimeMs: searchTime,
isStandardQuery: isStandardQuery,
chunksSearched: searchChunks.length,
activeQueries: activeQueries,
queueLength: queryQueue.length
});
return {
results,
query,
tokens,
total: scored.length
};
}
// Extract intent keywords from query for enhanced scoring
function extractIntentKeywords(query) {
const keywords = [];
const lowercaseQuery = query.toLowerCase();
// Extract action verbs and key nouns
const actionPatterns = [
/\b(restart|reboot|stop|start|configure|install|debug|fix|check|verify|monitor)\b/g,
/\b(service|server|database|application|system|network|api|endpoint)\b/g,
/\b(error|issue|problem|timeout|failure|crash|slow|performance)\b/g
];
for (const pattern of actionPatterns) {
const matches = lowercaseQuery.match(pattern);
if (matches) {
keywords.push(...matches);
}
}
return [...new Set(keywords)]; // Remove duplicates
}
// Performance monitoring endpoint
function getPerformanceMetrics() {
return {
...performanceMetrics,
activeQueries,
queueLength: queryQueue.length,
fastQueryRate: performanceMetrics.totalQueries > 0 ?
(performanceMetrics.fastQueries / performanceMetrics.totalQueries * 100).toFixed(1) + '%' : 'N/A'
};
}
return Object.freeze({ search, getPerformanceMetrics });
}
export default { createSearchService };