feat(server): faster reranking based on confidence (#12957)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Improved document reranking with a more streamlined and accurate
scoring system.
* Enhanced support for binary ("yes"/"no") document relevance judgments.

* **Improvements**
* Simplified user prompts and output formats for reranking tasks, making
results easier to interpret.
  * Increased reliability and consistency in document ranking results.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2025-06-28 11:41:53 +08:00
committed by GitHub
parent e6f91cced6
commit 9b881eb59a
5 changed files with 81 additions and 80 deletions
@@ -17,7 +17,6 @@ import {
import {
EMBEDDING_DIMENSIONS,
EmbeddingClient,
getReRankSchema,
type ReRankResult,
} from './types';
@@ -81,9 +80,9 @@ class ProductionEmbeddingClient extends EmbeddingClient {
}
private getTargetId<T extends ChunkSimilarity>(embedding: T) {
return 'docId' in embedding
return 'docId' in embedding && typeof embedding.docId === 'string'
? embedding.docId
: 'fileId' in embedding
: 'fileId' in embedding && typeof embedding.fileId === 'string'
? embedding.fileId
: '';
}
@@ -102,24 +101,19 @@ class ProductionEmbeddingClient extends EmbeddingClient {
throw new CopilotPromptNotFound({ name: RERANK_PROMPT });
}
const provider = await this.getProvider({ modelId: prompt.model });
const schema = getReRankSchema(embeddings.length);
const ranks = await provider.structure(
const ranks = await provider.rerank(
{ modelId: prompt.model },
prompt.finish({
query,
results: embeddings.map(e => ({
targetId: this.getTargetId(e),
chunk: e.chunk,
content: e.content,
})),
schema,
}),
{ maxRetries: 3, signal }
embeddings.map(e => prompt.finish({ query, doc: e.content })),
{ signal }
);
try {
return schema.parse(JSON.parse(ranks)).ranks;
return ranks.map((score, i) => ({
chunk: embeddings[i].content,
targetId: this.getTargetId(embeddings[i]),
score,
}));
} catch (error) {
this.logger.error('Failed to parse rerank results', error);
// silent error, will fallback to default sorting in parent method
@@ -176,9 +170,9 @@ class ProductionEmbeddingClient extends EmbeddingClient {
const highConfidenceChunks = ranks
.flat()
.toSorted((a, b) => b.scores.score - a.scores.score)
.filter(r => r.scores.score > 5)
.map(r => chunks[`${r.scores.targetId}:${r.scores.chunk}`])
.toSorted((a, b) => b.score - a.score)
.filter(r => r.score > 5)
.map(r => chunks[`${r.targetId}:${r.chunk}`])
.filter(Boolean);
this.logger.verbose(
@@ -177,11 +177,6 @@ export abstract class EmbeddingClient {
const ReRankItemSchema = z.object({
scores: z.object({
reason: z
.string()
.describe(
'Think step by step, describe in 20 words the reason for giving this score.'
),
chunk: z.string().describe('The chunk index of the search result.'),
targetId: z.string().describe('The id of the target.'),
score: z
@@ -194,11 +189,4 @@ const ReRankItemSchema = z.object({
}),
});
export const getReRankSchema = (size: number) =>
z.object({
ranks: ReRankItemSchema.array().describe(
`A array of scores. Make sure to score all ${size} results.`
),
});
export type ReRankResult = z.infer<ReturnType<typeof getReRankSchema>>['ranks'];
export type ReRankResult = z.infer<typeof ReRankItemSchema>['scores'][];