- Add src/search/query-utils.ts with extractSearchTerms, scorePathRelevance, kindBonus, detectApiIntent, inferRouteDirectories - Add multi-signal scoring to searchNodes (kind bonus + path relevance) - Improve FTS query sanitization (strip :^ chars, filter boolean operators) - Add comprehensive search tests
130 lines
3.5 KiB
TypeScript
130 lines
3.5 KiB
TypeScript
/**
|
|
* Search Query Utilities
|
|
*
|
|
* Shared module for search term extraction, scoring, and intent detection.
|
|
*/
|
|
|
|
import * as path from 'path';
|
|
import { Node } from '../types';
|
|
|
|
/**
|
|
* Common stop words to filter from search queries
|
|
*/
|
|
export const STOP_WORDS = new Set([
|
|
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
|
|
'of', 'with', 'by', 'from', 'is', 'it', 'that', 'this', 'are', 'was',
|
|
'be', 'has', 'had', 'have', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
'should', 'may', 'might', 'can', 'shall', 'not', 'no', 'all', 'each',
|
|
'every', 'how', 'what', 'where', 'when', 'who', 'which', 'why',
|
|
'i', 'me', 'my', 'we', 'our', 'you', 'your', 'he', 'she', 'they',
|
|
'find', 'show', 'get', 'list', 'give', 'tell',
|
|
]);
|
|
|
|
/**
|
|
* Extract meaningful search terms from a natural language query
|
|
*/
|
|
export function extractSearchTerms(query: string): string[] {
|
|
return query
|
|
.toLowerCase()
|
|
.replace(/[^\w\s-]/g, ' ')
|
|
.split(/\s+/)
|
|
.filter(term => term.length > 1 && !STOP_WORDS.has(term));
|
|
}
|
|
|
|
/**
|
|
* Score path relevance to a query
|
|
* Higher score = more relevant path
|
|
*/
|
|
export function scorePathRelevance(filePath: string, query: string): number {
|
|
const terms = extractSearchTerms(query);
|
|
if (terms.length === 0) return 0;
|
|
|
|
const pathLower = filePath.toLowerCase();
|
|
const fileName = path.basename(filePath).toLowerCase();
|
|
const dirName = path.dirname(filePath).toLowerCase();
|
|
let score = 0;
|
|
|
|
for (const term of terms) {
|
|
// Exact filename match (strongest)
|
|
if (fileName.includes(term)) score += 10;
|
|
// Directory match
|
|
if (dirName.includes(term)) score += 5;
|
|
// General path match
|
|
else if (pathLower.includes(term)) score += 3;
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
/**
|
|
* Kind-based bonus for search ranking
|
|
* Functions and classes are typically more relevant than variables/imports
|
|
*/
|
|
export function kindBonus(kind: Node['kind']): number {
|
|
const bonuses: Record<string, number> = {
|
|
function: 10,
|
|
method: 10,
|
|
class: 8,
|
|
interface: 7,
|
|
type_alias: 6,
|
|
struct: 6,
|
|
trait: 6,
|
|
enum: 5,
|
|
component: 8,
|
|
route: 9,
|
|
module: 4,
|
|
property: 3,
|
|
field: 3,
|
|
variable: 2,
|
|
constant: 3,
|
|
import: 1,
|
|
export: 1,
|
|
parameter: 0,
|
|
namespace: 4,
|
|
file: 0,
|
|
protocol: 6,
|
|
enum_member: 3,
|
|
};
|
|
return bonuses[kind] ?? 0;
|
|
}
|
|
|
|
/**
|
|
* Detect if a query has API/endpoint intent
|
|
*/
|
|
export function detectApiIntent(query: string): boolean {
|
|
const apiPatterns = [
|
|
/\bapi\b/i, /\bendpoint/i, /\broute/i, /\bhandler/i,
|
|
/\bcontroller/i, /\bmiddleware/i, /\brest\b/i, /\bgraphql/i,
|
|
/\bget\s+\//, /\bpost\s+\//, /\bput\s+\//, /\bdelete\s+\//,
|
|
/\brequest/i, /\bresponse/i, /\bhttp/i,
|
|
];
|
|
return apiPatterns.some(p => p.test(query));
|
|
}
|
|
|
|
/**
|
|
* Infer route/controller directories from project structure
|
|
* Returns undefined if no route directories are detected
|
|
*/
|
|
export function inferRouteDirectories(files: string[]): string[] | undefined {
|
|
const routeDirs = new Set<string>();
|
|
const routePatterns = [
|
|
/routes?\//i, /controllers?\//i, /handlers?\//i,
|
|
/api\//i, /endpoints?\//i,
|
|
];
|
|
|
|
for (const file of files) {
|
|
for (const pattern of routePatterns) {
|
|
if (pattern.test(file)) {
|
|
const match = file.match(pattern);
|
|
if (match) {
|
|
const idx = file.indexOf(match[0]);
|
|
const dir = file.substring(0, idx + match[0].length - 1);
|
|
routeDirs.add(dir);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return routeDirs.size > 0 ? Array.from(routeDirs) : undefined;
|
|
}
|