(query: string, optionsOrTopK?: number | SearchQueryOptions)
| 488 | } |
| 489 | |
| 490 | async search(query: string, optionsOrTopK?: number | SearchQueryOptions): Promise<SearchResult[]> { |
| 491 | const options = resolveSearchOptions(optionsOrTopK); |
| 492 | const [queryVec] = await fetchEmbedding(query); |
| 493 | const queryTerms = new Set(splitCamelCase(query)); |
| 494 | const scores: { |
| 495 | idx: number; |
| 496 | score: number; |
| 497 | semanticScore: number; |
| 498 | keywordScore: number; |
| 499 | matchedSymbols: string[]; |
| 500 | matchedSymbolLocations: string[]; |
| 501 | }[] = []; |
| 502 | |
| 503 | for (let i = 0; i < this.vectors.length; i++) { |
| 504 | if (!this.vectors[i]) continue; |
| 505 | const doc = this.documents[i]; |
| 506 | const semanticScore = cosine(queryVec, this.vectors[i]); |
| 507 | const matchedEntries = doc.symbolEntries ? getMatchedSymbolEntries(doc.symbolEntries, queryTerms) : []; |
| 508 | const matchedSymbols = matchedEntries.length > 0 |
| 509 | ? matchedEntries.map((entry) => entry.name) |
| 510 | : getMatchedSymbols(doc.symbols, queryTerms); |
| 511 | const matchedSymbolLocations = matchedEntries.map((entry) => `${entry.name}@${formatLineRange(entry.line, entry.endLine)}`); |
| 512 | const keywordScore = computeKeywordScore(query, queryTerms, doc, matchedSymbols); |
| 513 | const score = computeCombinedScore(semanticScore, keywordScore, options); |
| 514 | |
| 515 | if (options.requireSemanticMatch && semanticScore <= 0) continue; |
| 516 | if (options.requireKeywordMatch && keywordScore <= 0) continue; |
| 517 | if (Math.max(semanticScore, 0) < options.minSemanticScore) continue; |
| 518 | if (keywordScore < options.minKeywordScore) continue; |
| 519 | if (score < options.minCombinedScore) continue; |
| 520 | |
| 521 | scores.push({ idx: i, score, semanticScore, keywordScore, matchedSymbols, matchedSymbolLocations }); |
| 522 | } |
| 523 | |
| 524 | return scores |
| 525 | .sort((a, b) => b.score - a.score || b.keywordScore - a.keywordScore || b.semanticScore - a.semanticScore) |
| 526 | .slice(0, options.topK) |
| 527 | .map(({ idx, score, semanticScore, keywordScore, matchedSymbols, matchedSymbolLocations }) => { |
| 528 | const doc = this.documents[idx]; |
| 529 | return { |
| 530 | path: doc.path, |
| 531 | score: Math.round(score * 1000) / 10, |
| 532 | semanticScore: Math.round(Math.max(semanticScore, 0) * 1000) / 10, |
| 533 | keywordScore: Math.round(keywordScore * 1000) / 10, |
| 534 | header: doc.header, |
| 535 | matchedSymbols, |
| 536 | matchedSymbolLocations, |
| 537 | }; |
| 538 | }); |
| 539 | } |
| 540 | |
| 541 | getDocumentCount(): number { |
| 542 | return this.documents.length; |
no test coverage detected