chore(FEEDBACK-0001): linting
This commit is contained in:
@@ -102,21 +102,21 @@ async vectorSearch(
|
||||
|
||||
```typescript
|
||||
function reciprocalRankFusion(
|
||||
...rankings: Array<Array<{ id: string; score: number }>>
|
||||
...rankings: Array<Array<{ id: string; score: number }>>
|
||||
): Array<{ id: string; rrfScore: number }> {
|
||||
const K = 60; // RRF constant (standard value)
|
||||
const scores = new Map<string, number>();
|
||||
const K = 60; // RRF constant (standard value)
|
||||
const scores = new Map<string, number>();
|
||||
|
||||
for (const ranking of rankings) {
|
||||
ranking.forEach(({ id }, rank) => {
|
||||
const current = scores.get(id) ?? 0;
|
||||
scores.set(id, current + 1 / (K + rank + 1));
|
||||
});
|
||||
}
|
||||
for (const ranking of rankings) {
|
||||
ranking.forEach(({ id }, rank) => {
|
||||
const current = scores.get(id) ?? 0;
|
||||
scores.set(id, current + 1 / (K + rank + 1));
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(scores.entries())
|
||||
.map(([id, rrfScore]) => ({ id, rrfScore }))
|
||||
.sort((a, b) => b.rrfScore - a.rrfScore);
|
||||
return Array.from(scores.entries())
|
||||
.map(([id, rrfScore]) => ({ id, rrfScore }))
|
||||
.sort((a, b) => b.rrfScore - a.rrfScore);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -126,65 +126,62 @@ function reciprocalRankFusion(
|
||||
|
||||
```typescript
|
||||
export interface HybridSearchOptions {
|
||||
repositoryId: string;
|
||||
versionId?: string;
|
||||
type?: 'code' | 'info';
|
||||
limit?: number;
|
||||
alpha?: number; // 0.0 = FTS5 only, 1.0 = vector only, 0.5 = balanced
|
||||
repositoryId: string;
|
||||
versionId?: string;
|
||||
type?: 'code' | 'info';
|
||||
limit?: number;
|
||||
alpha?: number; // 0.0 = FTS5 only, 1.0 = vector only, 0.5 = balanced
|
||||
}
|
||||
|
||||
export class HybridSearchService {
|
||||
constructor(
|
||||
private db: BetterSQLite3.Database,
|
||||
private searchService: SearchService,
|
||||
private embeddingProvider: EmbeddingProvider | null,
|
||||
) {}
|
||||
constructor(
|
||||
private db: BetterSQLite3.Database,
|
||||
private searchService: SearchService,
|
||||
private embeddingProvider: EmbeddingProvider | null
|
||||
) {}
|
||||
|
||||
async search(
|
||||
query: string,
|
||||
options: HybridSearchOptions
|
||||
): Promise<SnippetSearchResult[]> {
|
||||
const limit = options.limit ?? 20;
|
||||
const alpha = options.alpha ?? 0.5;
|
||||
async search(query: string, options: HybridSearchOptions): Promise<SnippetSearchResult[]> {
|
||||
const limit = options.limit ?? 20;
|
||||
const alpha = options.alpha ?? 0.5;
|
||||
|
||||
// Always run FTS5 search
|
||||
const ftsResults = this.searchService.searchSnippets(query, {
|
||||
repositoryId: options.repositoryId,
|
||||
versionId: options.versionId,
|
||||
type: options.type,
|
||||
limit: limit * 3, // get more candidates for fusion
|
||||
});
|
||||
// Always run FTS5 search
|
||||
const ftsResults = this.searchService.searchSnippets(query, {
|
||||
repositoryId: options.repositoryId,
|
||||
versionId: options.versionId,
|
||||
type: options.type,
|
||||
limit: limit * 3 // get more candidates for fusion
|
||||
});
|
||||
|
||||
// If no embedding provider or alpha = 0, return FTS5 results directly
|
||||
if (!this.embeddingProvider || alpha === 0) {
|
||||
return ftsResults.slice(0, limit);
|
||||
}
|
||||
// If no embedding provider or alpha = 0, return FTS5 results directly
|
||||
if (!this.embeddingProvider || alpha === 0) {
|
||||
return ftsResults.slice(0, limit);
|
||||
}
|
||||
|
||||
// Embed the query and run vector search
|
||||
const [queryEmbedding] = await this.embeddingProvider.embed([query]);
|
||||
const vectorResults = await this.vectorSearch(
|
||||
queryEmbedding.values,
|
||||
options.repositoryId,
|
||||
limit * 3
|
||||
);
|
||||
// Embed the query and run vector search
|
||||
const [queryEmbedding] = await this.embeddingProvider.embed([query]);
|
||||
const vectorResults = await this.vectorSearch(
|
||||
queryEmbedding.values,
|
||||
options.repositoryId,
|
||||
limit * 3
|
||||
);
|
||||
|
||||
// Normalize result lists for RRF
|
||||
const ftsRanked = ftsResults.map((r, i) => ({
|
||||
id: r.snippet.id,
|
||||
score: i,
|
||||
}));
|
||||
const vecRanked = vectorResults.map((r, i) => ({
|
||||
id: r.snippetId,
|
||||
score: i,
|
||||
}));
|
||||
// Normalize result lists for RRF
|
||||
const ftsRanked = ftsResults.map((r, i) => ({
|
||||
id: r.snippet.id,
|
||||
score: i
|
||||
}));
|
||||
const vecRanked = vectorResults.map((r, i) => ({
|
||||
id: r.snippetId,
|
||||
score: i
|
||||
}));
|
||||
|
||||
// Apply RRF
|
||||
const fused = reciprocalRankFusion(ftsRanked, vecRanked);
|
||||
// Apply RRF
|
||||
const fused = reciprocalRankFusion(ftsRanked, vecRanked);
|
||||
|
||||
// Fetch full snippet data for top results
|
||||
const topIds = fused.slice(0, limit).map(r => r.id);
|
||||
return this.fetchSnippetsByIds(topIds, options.repositoryId);
|
||||
}
|
||||
// Fetch full snippet data for top results
|
||||
const topIds = fused.slice(0, limit).map((r) => r.id);
|
||||
return this.fetchSnippetsByIds(topIds, options.repositoryId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -197,9 +194,9 @@ The hybrid search alpha value can be set per-request or globally via settings:
|
||||
```typescript
|
||||
// Default config stored in settings table under key 'search_config'
|
||||
export interface SearchConfig {
|
||||
alpha: number; // 0.5 default
|
||||
maxResults: number; // 20 default
|
||||
enableHybrid: boolean; // true if embedding provider is configured
|
||||
alpha: number; // 0.5 default
|
||||
maxResults: number; // 20 default
|
||||
enableHybrid: boolean; // true if embedding provider is configured
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user