Skip to content

Commit f425b49

Browse files
committed
refactor: update reranker
1 parent 629f436 commit f425b49

9 files changed

Lines changed: 106 additions & 117 deletions

File tree

README.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -208,14 +208,6 @@ await ctx.close();
208208
+------------------------------------------------------------------------+
209209
```
210210

211-
### Module Structure
212-
213-
- **Public API**: `Context`, `QueryOptions`, `QueryResult`, `Document`, `Loader`, `MarkdownLoader`, `JsonLoader`, `TextLoader`, `pathToId`
214-
- **Reranking**: `KeywordReranker`, `createReranker`, `Reranker`, `RerankCandidate`, `RerankResult`, `RerankOptions`
215-
- **Query Expansion**: `SynonymExpander`, `NoopExpander`, `QueryExpander`, `QueryExpansionOptions`
216-
- **Advanced API**: `Embedder`, `TransformersEmbedder`, `EmbedderManager`, `IZvecStore`, `ActualZvecStore`, `Store`
217-
218-
219211
## License
220212

221213
MIT

src/context.ts

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import {
55
ContextOptions,
66
QueryOptions,
77
QueryResult,
8-
Document,
98
LoadedDoc,
10-
LoadPhase,
11-
LoadProgress,
129
} from './types';
1310
import { Embedder } from './embedder';
1411

@@ -18,24 +15,18 @@ import { pathToId } from './utils/doc';
1815
import { Store } from './storage/store';
1916
import type { ZvecDoc } from './storage/zvec-store';
2017
import {
21-
createReranker,
2218
safeJsonParse,
2319
computeContentHash,
2420
loadSampleText,
2521
} from './utils';
2622
import { expand } from './expander';
27-
import type { Reranker, RerankCandidate } from './utils';
28-
29-
// ---------------------------------------------------------------------------
30-
// Context class
31-
// ---------------------------------------------------------------------------
23+
import { createReranker, applyRerank } from './reranker';
3224

3325
export class Context {
3426
private readonly options: ContextOptions;
3527
private readonly embedder: Embedder;
3628
readonly embedderInfo: EmbedderInfo;
3729
private readonly store: Store;
38-
private readonly reranker: Reranker | null;
3930

4031
private constructor(options: ContextOptions, embedder: Embedder, embedderInfo: EmbedderInfo) {
4132
this.options = {
@@ -46,7 +37,6 @@ export class Context {
4637
this.embedder = embedder;
4738
this.embedderInfo = embedderInfo;
4839
this.store = new Store(this.options.vectorsDir!, embedder, this.options);
49-
this.reranker = createReranker(this.options.rerankWeights);
5040
}
5141

5242
static async create(options: ContextOptions): Promise<Context> {
@@ -163,23 +153,9 @@ export class Context {
163153
};
164154
});
165155

166-
if (rerankEnabled && queryResults.length > topK) {
167-
const docs: RerankCandidate[] = queryResults.map((r) => ({
168-
id: r.id,
169-
content: r.content,
170-
score: r.score,
171-
}));
172-
173-
const reranked = await this.reranker!.rerank(text, docs);
174-
175-
const scoreMap = new Map(reranked.map((r) => [r.id, r.score]));
176-
for (const result of queryResults) {
177-
const newScore = scoreMap.get(result.id);
178-
if (newScore !== undefined) {
179-
result.score = newScore;
180-
result.scoreMode = 'reranked';
181-
}
182-
}
156+
if (rerankEnabled) {
157+
const reranker = createReranker(this.options.rerankWeights);
158+
await applyRerank(reranker, text, queryResults, topK);
183159
}
184160

185161
queryResults.sort((a, b) => b.score - a.score);

src/index.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,2 @@
1-
// ---------------------------------------------------------------------------
2-
// Public API
3-
// ---------------------------------------------------------------------------
4-
51
export { Context } from './context';
6-
export * from './types';
7-
8-
// Reranker
9-
export { KeywordReranker, createReranker } from './utils/reranker';
10-
export type { RerankCandidate, RerankResult } from './utils/reranker';
11-
12-
// Query expansion
13-
export { expand } from './expander';
14-
15-
// Embedder
16-
export { Embedder } from './embedder';
2+
export * from './types';

src/reranker/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { Reranker, RerankCandidate, RerankResult, RerankOptions } from './types';
2+
import { KeywordReranker } from './keyword-reranker';
3+
import { applyRerank } from './rerank';
4+
5+
export type { Reranker, RerankCandidate, RerankResult, RerankOptions };
6+
export { KeywordReranker, applyRerank };
7+
8+
/**
9+
* Create a reranker with optional weight configuration.
10+
*/
11+
export function createReranker(options?: RerankOptions): Reranker {
12+
return new KeywordReranker(options);
13+
}
Lines changed: 3 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,4 @@
1-
/**
2-
* Reranker — second-stage precision scoring for search results.
3-
*
4-
* The pipeline:
5-
* 1. Coarse search (vector / hybrid) → topK × rerankFactor candidates
6-
* 2. Reranker scores each candidate against the query
7-
* 3. Final sort by reranked score → topK results
8-
*/
9-
10-
// ---------------------------------------------------------------------------
11-
// Types
12-
// ---------------------------------------------------------------------------
13-
14-
/** A candidate document for reranking. */
15-
export interface RerankCandidate {
16-
id: string;
17-
content: string;
18-
/** Original score from the coarse search stage. */
19-
score: number;
20-
/** Heading path as a string (e.g. "Line Chart > Tooltip"). */
21-
headingPath?: string;
22-
}
23-
24-
/** A reranked result. */
25-
export interface RerankResult {
26-
id: string;
27-
/** Final score after reranking (higher is better). */
28-
score: number;
29-
}
30-
31-
/** Reranker type */
32-
export type Reranker = {
33-
rerank(query: string, candidates: RerankCandidate[]): Promise<RerankResult[]>;
34-
};
35-
36-
/** Configuration for reranking. */
37-
export interface RerankOptions {
38-
rerankFactor?: number;
39-
minCandidates?: number;
40-
phraseWeight?: number;
41-
phraseRepeatBonus?: number;
42-
termWeight?: number;
43-
termRepeatBonus?: number;
44-
substringWeight?: number;
45-
headingTermBonus?: number;
46-
headingPhraseBonus?: number;
47-
originalScoreCarry?: number;
48-
}
1+
import type { Reranker, RerankCandidate, RerankResult, RerankOptions } from './types';
492

503
// ---------------------------------------------------------------------------
514
// Defaults
@@ -72,7 +25,7 @@ const DEFAULT_WEIGHTS = {
7225
* All scoring weights are configurable via constructor options.
7326
* Scores are normalised to [0, 1] via min-max scaling.
7427
*/
75-
export class KeywordReranker {
28+
export class KeywordReranker implements Reranker {
7629
private readonly weights: Record<string, number>;
7730

7831
constructor(options?: RerankOptions) {
@@ -210,15 +163,4 @@ function isWordBoundary(
210163
const afterIdx = idx + term.length;
211164
const after = afterIdx >= text.length || /[\s\n.,;:!?"')\-_]/.test(text[afterIdx]);
212165
return before && after;
213-
}
214-
215-
// ---------------------------------------------------------------------------
216-
// Factory
217-
// ---------------------------------------------------------------------------
218-
219-
/**
220-
* Create a reranker with optional weight configuration.
221-
*/
222-
export function createReranker(options?: RerankOptions): Reranker {
223-
return new KeywordReranker(options);
224-
}
166+
}

src/reranker/rerank.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Reranker, RerankCandidate } from './types';
2+
import type { QueryResult } from '../types';
3+
4+
/**
5+
* Apply reranking to query results.
6+
*
7+
* @param reranker - The reranker instance
8+
* @param query - The original query text
9+
* @param results - Query results to rerank
10+
* @param topK - Original topK (only rerank if results > topK)
11+
* @returns The results with scores updated if reranking was applied
12+
*/
13+
export async function applyRerank(
14+
reranker: Reranker,
15+
query: string,
16+
results: QueryResult[],
17+
topK: number,
18+
): Promise<QueryResult[]> {
19+
if (results.length <= topK) return results;
20+
21+
const candidates: RerankCandidate[] = results.map((r) => ({
22+
id: r.id,
23+
content: r.content,
24+
score: r.score,
25+
}));
26+
27+
const reranked = await reranker.rerank(query, candidates);
28+
29+
const scoreMap = new Map(reranked.map((r) => [r.id, r.score]));
30+
for (const result of results) {
31+
const newScore = scoreMap.get(result.id);
32+
if (newScore !== undefined) {
33+
result.score = newScore;
34+
result.scoreMode = 'reranked';
35+
}
36+
}
37+
38+
return results;
39+
}

src/reranker/types.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Reranker — second-stage precision scoring for search results.
3+
*/
4+
5+
// ---------------------------------------------------------------------------
6+
// Types
7+
// ---------------------------------------------------------------------------
8+
9+
/** A candidate document for reranking. */
10+
export interface RerankCandidate {
11+
id: string;
12+
content: string;
13+
/** Original score from the coarse search stage. */
14+
score: number;
15+
/** Heading path as a string (e.g. "Line Chart > Tooltip"). */
16+
headingPath?: string;
17+
}
18+
19+
/** A reranked result. */
20+
export interface RerankResult {
21+
id: string;
22+
/** Final score after reranking (higher is better). */
23+
score: number;
24+
}
25+
26+
/** Reranker type */
27+
export type Reranker = {
28+
rerank(query: string, candidates: RerankCandidate[]): Promise<RerankResult[]>;
29+
};
30+
31+
/** Configuration for reranking. */
32+
export interface RerankOptions {
33+
rerankFactor?: number;
34+
minCandidates?: number;
35+
phraseWeight?: number;
36+
phraseRepeatBonus?: number;
37+
termWeight?: number;
38+
termRepeatBonus?: number;
39+
substringWeight?: number;
40+
headingTermBonus?: number;
41+
headingPhraseBonus?: number;
42+
originalScoreCarry?: number;
43+
}

src/utils/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,4 @@ export { safeJsonParse } from './common';
44
export { loadSampleText } from './sample';
55
export { isCJK, detectLanguage, tokenizerForLanguage, detectTokenizer } from './tokenizer';
66
export type { LanguageHint } from './tokenizer';
7-
export { containsCJK } from './str';
8-
export { KeywordReranker, createReranker } from './reranker';
9-
export type { Reranker, RerankCandidate, RerankResult } from './reranker';
7+
export { containsCJK } from './str';

test/reranker.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect } from 'vitest';
2-
import { KeywordReranker, createReranker } from '../src/utils/reranker';
3-
import type { RerankCandidate } from '../src/utils/reranker';
2+
import { KeywordReranker, createReranker } from '../src/reranker';
3+
import type { RerankCandidate } from '../src/reranker';
44

55
describe('KeywordReranker', () => {
66
const reranker = new KeywordReranker();

0 commit comments

Comments
 (0)