|
1 | 1 | # @corbits/reranking |
2 | 2 |
|
3 | | -Cross-encoder rerank client. Given a query and a candidate set, `rerankDocuments` |
4 | | -posts them to a reranker, maps the reply back to your own document ids, and |
5 | | -returns `{id, score}` sorted descending. |
| 3 | +Cross-encoder reranking for retrieval pipelines. Send a query plus candidates, get scored ids back in ranked order. |
6 | 4 |
|
7 | | -## Install |
| 5 | +Give `rerankDocuments` a query and your own `{id, text}` docs; it posts them to the reranker, maps the reply back to your ids, and returns `{id, score}` sorted descending. |
8 | 6 |
|
9 | | -Not published to npm yet. Until a registry publish, `npm install @corbits/reranking` |
10 | | -(and `bun add @corbits/reranking`) 404. Git is the install path. |
| 7 | +## Runtime support |
11 | 8 |
|
12 | | -```bash |
13 | | -bun add github:corbitsdev/corbits-reranking |
14 | | -``` |
| 9 | +Runs on Bun >= 1.2 or Node >= 24. The built `dist/` entry is the default import. |
15 | 10 |
|
16 | | -Unlike embeddings, reranking has **no OpenAI-compatible standard** and is not |
17 | | -converging on one — TEI does not serve an OpenAI-shaped rerank route at all |
18 | | -([text-embeddings-inference#683](https://github.com/huggingface/text-embeddings-inference/issues/683)). |
19 | | -So this is a real adapter boundary, not a knob: |
20 | | - |
21 | | -| `apiStyle` | endpoint | request | response | |
22 | | -| ---------- | ------------ | -------------------- | --------------------------------------- | |
23 | | -| `tei` | `/rerank` | `{query, texts}` | `[{index, score}]` | |
24 | | -| `cohere` | `/v2/rerank` | `{query, documents}` | `{results: [{index, relevance_score}]}` | |
25 | | -| `voyage` | `/v1/rerank` | `{query, documents}` | `{data: [{index, relevance_score}]}` | |
| 11 | +## Quickstart |
26 | 12 |
|
27 | | -Jina's `/rerank` uses Cohere's shape and is served by the `cohere` adapter. |
| 13 | +```bash |
| 14 | +bun add @corbits/reranking |
| 15 | +``` |
28 | 16 |
|
29 | | -Transport, error classification and retry come from `@intx/inference` rather |
30 | | -than being reimplemented: requests are `BuiltRequest`s sent through the single |
31 | | -`deps.fetch` path, failures become an `InferenceError` through the same |
32 | | -classifiers a chat call uses, and `createDefaultRetryPolicy` decides whether to |
33 | | -back off or abort. |
| 17 | +`rerankDocuments(query, docs, config, options)` validates config with `RerankConfigSchema`, short-circuits empty input with no request, and returns ranked results. Options carry `deps`, with optional `retryPolicy`, `registry`, and `signal`. Config takes `baseURL` and `apiStyle`, with optional `model` (used by Cohere and Voyage), `apiKey`, and `timeoutMs`. |
34 | 18 |
|
35 | | -`RerankAdapter` deliberately mirrors inference's `ProviderAdapter` — same |
36 | | -`buildRequest` / `parseResponse` members, same optional `extractRetryAfterMs` |
37 | | -for a provider that signals pacing its own way. Styles resolve through |
38 | | -`createRerankAdapterRegistry`, which closes over a private `Map` copy so an |
39 | | -`apiStyle` arriving from config or an environment variable cannot reach an |
40 | | -inherited member of `Object.prototype` and fail later as an opaque `TypeError`. |
41 | | -Pass your own `registry` to add a house format without forking the package. |
| 19 | +A retrieval pipeline reads its reranker endpoint from host config at boot — a half-configured reranker should fail boot, not degrade silently later — then reranks each query's fused candidates, falling back to the caller's own ordering on failure so a reranker outage never fails search: |
42 | 20 |
|
43 | 21 | ```ts |
44 | 22 | import { createDefaultScheduler } from "@intx/inference"; |
45 | | -import { rerankDocuments } from "@corbits/reranking"; |
| 23 | +import { |
| 24 | + rerankDocuments, |
| 25 | + type RerankDoc, |
| 26 | + type RerankResult, |
| 27 | +} from "@corbits/reranking"; |
46 | 28 |
|
47 | | -// Only `fetch` and `scheduler` are needed — a full harness `Dependencies` |
48 | | -// also satisfies this if you already have one. |
49 | 29 | const deps = { fetch, scheduler: createDefaultScheduler() }; |
50 | 30 |
|
51 | | -const ranked = await rerankDocuments( |
52 | | - "how do I deploy to staging?", |
53 | | - [ |
54 | | - { id: "doc-1", text: "Staging deploys run from main…" }, |
55 | | - { id: "doc-2", text: "On-call rotation is weekly…" }, |
56 | | - ], |
57 | | - { baseURL: "http://localhost:8085", apiStyle: "tei" }, |
58 | | - { deps }, |
59 | | -); |
| 31 | +function requireEnv(name: string): string { |
| 32 | + const value = process.env[name]; |
| 33 | + if (!value) throw new Error(`${name} is required`); |
| 34 | + return value; |
| 35 | +} |
| 36 | + |
| 37 | +const baseURL = requireEnv("RERANK_BASE_URL"); |
| 38 | +const model = process.env.RERANK_MODEL; |
| 39 | + |
| 40 | +export async function rerankFusedCandidates( |
| 41 | + query: string, |
| 42 | + candidates: readonly RerankDoc[], |
| 43 | + onError: (error: unknown) => void, |
| 44 | +): Promise<RerankResult[]> { |
| 45 | + try { |
| 46 | + return await rerankDocuments( |
| 47 | + query, |
| 48 | + candidates, |
| 49 | + { |
| 50 | + baseURL, |
| 51 | + apiStyle: "tei", |
| 52 | + ...(model !== undefined ? { model } : {}), |
| 53 | + }, |
| 54 | + { deps }, |
| 55 | + ); |
| 56 | + } catch (error) { |
| 57 | + // Reranker unavailable or timed out — report it, then degrade to the |
| 58 | + // fused order the caller already computed rather than fail the search. |
| 59 | + onError(error); |
| 60 | + return candidates.map((doc, i) => ({ |
| 61 | + id: doc.id, |
| 62 | + score: candidates.length - i, |
| 63 | + })); |
| 64 | + } |
| 65 | +} |
60 | 66 | ``` |
61 | 67 |
|
62 | | -## API |
63 | | - |
64 | | -- `rerankDocuments(query, docs, config, options)` — rerank `RerankDoc[]` |
65 | | - (`{id, text}`) against the query and return `RerankResult[]` |
66 | | - (`{id, score}`), sorted descending. Empty input short-circuits with no |
67 | | - request. Config is validated by `RerankConfigSchema` (`RerankConfig`); |
68 | | - options (`RerankOptions`) take `deps`, an optional `retryPolicy`, |
69 | | - `registry`, and `signal`. |
70 | | -- Adapters — `RerankAdapter` (`buildRequest` / `parseResponse`, optional |
71 | | - `extractRetryAfterMs`), `RerankAPIStyle` (`tei` | `cohere` | `voyage`), |
72 | | - `rerankAdapters`, `rerankAdapterRegistry`, and |
73 | | - `createRerankAdapterRegistry` for house formats. Supporting types: |
74 | | - `RerankDoc`, `RerankResult`, `RerankRequestConfig`, |
75 | | - `RerankRequestBuilder`, `RerankResponseParser`, `RerankAdapterRegistry`. |
76 | | -- Transport — `runJSONRequest` sends the built request through `deps.fetch` |
77 | | - with retry; `extractRetryAfterMs` and `RetryAfterExtractor` handle |
78 | | - provider pacing; `ModelRequestError` carries the classified |
79 | | - `InferenceError` as `reason` with the URL (`RunRequestOptions` configures |
80 | | - the call). |
| 68 | +## How it works |
81 | 69 |
|
82 | | -## Scores are mapped by index, never by position |
| 70 | +Reranking has no single OpenAI-style standard, so this package draws the adapter boundary explicitly: |
83 | 71 |
|
84 | | -Every one of these protocols addresses documents by their position in the |
85 | | -_request_ array, and TEI's reply is explicitly unordered. Results are therefore |
86 | | -mapped back through `docs[index]`, and an out-of-range index raises rather than |
87 | | -silently attaching a score to the wrong document. |
88 | | - |
89 | | -`RerankDoc.id` exists for this reason: the wire formats only speak in array |
90 | | -offsets, so translating back to a stable identifier is this package's job rather |
91 | | -than the caller's. |
| 72 | +| `apiStyle` | Endpoint | Request shape | Response shape | |
| 73 | +| ---------- | ------------ | -------------------- | --------------------------------------- | |
| 74 | +| `tei` | `/rerank` | `{query, texts}` | `[{index, score}]` | |
| 75 | +| `cohere` | `/v2/rerank` | `{query, documents}` | `{results: [{index, relevance_score}]}` | |
| 76 | +| `voyage` | `/v1/rerank` | `{query, documents}` | `{data: [{index, relevance_score}]}` | |
92 | 77 |
|
93 | | -## Degrading soft is the caller's decision |
| 78 | +Jina's `/rerank` follows the Cohere shape and is served by the `cohere` adapter. |
94 | 79 |
|
95 | | -This package raises on failure. It does not swallow errors, because a reranker |
96 | | -outage should be a policy decision at the call site — a retrieval pipeline |
97 | | -typically wants to fall back to its fused ordering and carry on, which it cannot |
98 | | -do if the failure is hidden here. |
| 80 | +`RerankAdapter` mirrors inference's `ProviderAdapter` with `buildRequest` / `parseResponse` plus optional `extractRetryAfterMs`. Styles resolve through `createRerankAdapterRegistry`, which keeps a private `Map` copy so config-supplied style names resolve safely. Pass a custom `registry` to add a house format alongside the built-ins (`rerankAdapters`, `rerankAdapterRegistry`). |
99 | 81 |
|
100 | | -## Errors |
| 82 | +Every protocol addresses documents by position in the request array, and TEI replies unordered. Results map back through `docs[index]`, with an out-of-range index raising so a score always lands on the right document. `RerankDoc.id` carries your stable identifier across that index-based wire, and the final list sorts by score descending. |
101 | 83 |
|
102 | | -Every failure — transport, HTTP status, or a 200 whose body is not JSON — |
103 | | -raises `ModelRequestError`, carrying the classified `InferenceError` as `reason` |
104 | | -and the URL. |
| 84 | +This package raises on failure, leaving fallback policy at the call site. Retrieval pipelines commonly fall back to their fused ordering when the reranker is unavailable and carry on serving. |
105 | 85 |
|
106 | | -Note that `@corbits/embedding` and `@corbits/reranking` each carry their own |
107 | | -copy of this class until the shared transport is upstreamed, so `instanceof` |
108 | | -does not hold across the two. Catching both? Discriminate on |
109 | | -`error.name === "ModelRequestError"`. |
| 86 | +Every failure mode — transport, HTTP status, or a 200 with an unexpected body — raises `ModelRequestError`, carrying the classified `InferenceError` as `reason` plus the request URL. Transport, classification, and retry come from `@intx/inference`: built requests travel the shared `deps.fetch` path with `createDefaultRetryPolicy` guiding backoff. The embedding and reranking packages each carry their own copy of this class while the shared transport is upstreamed, so code catching both discriminates on `error.name === "ModelRequestError"`. |
110 | 87 |
|
111 | | -## Versioning |
| 88 | +Transport helpers (`runJSONRequest`, `extractRetryAfterMs`, `ModelRequestError`, `RunRequestOptions`, `RetryAfterExtractor`) are also re-exported for sibling one-shot JSON clients. |
112 | 89 |
|
113 | | -Semver. Released manually with `bun run build && npm publish` once CI is green. |
| 90 | +Interchange retrieval pipelines call `rerankDocuments` after hybrid search to lift the strongest candidates to the top. The shared `@intx/inference` transport keeps rerank retries and error taxonomy consistent with chat and embeddings across the hub. |
114 | 91 |
|
115 | 92 | ## Development |
116 | 93 |
|
117 | | -Requires Node.js >= 24 and Bun >= 1.2.0. |
118 | | - |
119 | 94 | ```bash |
120 | 95 | bun install |
121 | 96 | bun test ./src |
|
0 commit comments