* Find nodes by exact name match * * Used for hybrid search - looks up symbols by exact name or case-insensitive match. * Returns high-confidence matches for known symbol names extracted from query. * * @param names - Array of symbol names to look up * @param options - Search optio
(names: string[], options: SearchOptions = {})
| 1510 | * @returns SearchResult array with exact matches scored at 1.0 |
| 1511 | */ |
| 1512 | findNodesByExactName(names: string[], options: SearchOptions = {}): SearchResult[] { |
| 1513 | if (names.length === 0) return []; |
| 1514 | |
| 1515 | const { kinds, languages, limit = 50 } = options; |
| 1516 | |
| 1517 | // Two-pass approach to handle common names (e.g., "run" has 40+ matches): |
| 1518 | // Pass 1: Find which files contain distinctive (rare) symbols from the query. |
| 1519 | // Pass 2: Query each name, boosting results that co-locate with distinctive symbols. |
| 1520 | |
| 1521 | // Pass 1: Find files containing each queried name, identify distinctive names |
| 1522 | const nameToFiles = new Map<string, Set<string>>(); |
| 1523 | for (const name of names) { |
| 1524 | let sql = 'SELECT DISTINCT file_path FROM nodes WHERE name COLLATE NOCASE = ?'; |
| 1525 | const params: (string | number)[] = [name]; |
| 1526 | if (kinds && kinds.length > 0) { |
| 1527 | sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`; |
| 1528 | params.push(...kinds); |
| 1529 | } |
| 1530 | sql += ' LIMIT 100'; |
| 1531 | const rows = this.db.prepare(sql).all(...params) as { file_path: string }[]; |
| 1532 | nameToFiles.set(name.toLowerCase(), new Set(rows.map(r => r.file_path))); |
| 1533 | } |
| 1534 | |
| 1535 | // Distinctive names are those with fewer than 10 file matches (e.g., "scrapeLoop" = 1 file) |
| 1536 | const distinctiveFiles = new Set<string>(); |
| 1537 | for (const [, files] of nameToFiles) { |
| 1538 | if (files.size > 0 && files.size < 10) { |
| 1539 | for (const f of files) distinctiveFiles.add(f); |
| 1540 | } |
| 1541 | } |
| 1542 | |
| 1543 | // Pass 2: Query each name with per-name limit, scoring by co-location |
| 1544 | const perNameLimit = Math.max(8, Math.ceil(limit / names.length)); |
| 1545 | const allResults: SearchResult[] = []; |
| 1546 | const seenIds = new Set<string>(); |
| 1547 | |
| 1548 | for (const name of names) { |
| 1549 | let sql = ` |
| 1550 | SELECT nodes.*, 1.0 as score |
| 1551 | FROM nodes |
| 1552 | WHERE name COLLATE NOCASE = ? |
| 1553 | `; |
| 1554 | const params: (string | number)[] = [name]; |
| 1555 | |
| 1556 | if (kinds && kinds.length > 0) { |
| 1557 | sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`; |
| 1558 | params.push(...kinds); |
| 1559 | } |
| 1560 | |
| 1561 | if (languages && languages.length > 0) { |
| 1562 | sql += ` AND language IN (${languages.map(() => '?').join(',')})`; |
| 1563 | params.push(...languages); |
| 1564 | } |
| 1565 | |
| 1566 | // Fetch enough to find co-located results among common names |
| 1567 | sql += ' LIMIT ?'; |
| 1568 | params.push(Math.max(perNameLimit * 3, 50)); |
| 1569 |