Trained retrieval pulls a 'cabinet and drawer bar pull' into the top 10 for '3 1/2 inch drawer pull' that chapter 10 never returned. The learned branch supplies this candidate; the joint score is reused for the listings both chapters share. Open the row to see the trained terms.
Level 11 · Two-tower retrieval
Teach the vectors what this catalog means
Train separate query and item headsA small learned layer on top of a frozen text encoder. Chapter 11 learns one projection for queries and a separate one for items. Only the query head runs when a search arrives; item vectors are precomputed. on graded preferencesTraining examples built from the answer key: for one training query, a listing with a higher relevance grade is preferred to one with a lower grade. Unlike clicks, this target matches what NDCG rewards. from training query groups. Keep title information prominent in item vectorsA list of a few hundred numbers that a neural network produces from a text. Texts with a similar meaning get vectors that point in a similar direction, even when they share no words.. Retrieve new neighboursA nearby item in the trained vector space. The neighbour budget limits how many of them may enter the candidate list, so validation can trade extra recall against extra cost. and reuse chapter 10's joint scores. ValidationExamples used to choose settings, not to fit the model or to report the final score. Chapter 11 keeps query groups separate: training fits the towers, validation selects the epoch, budget and score weight, and test queries only measure the result. selects the candidate budget and a global weightThe number a feature is multiplied by when the score is built. Chapter 7 picks these numbers by hand; chapter 8 learns them from clicks. for adding trained similarity to joint relevance. Open a result to see the two weighted terms.
Where to look: match awaits chapter 10, retrieves trained neighboursA nearby item in the trained vector space. The neighbour budget limits how many of them may enter the candidate list, so validation can trade extra recall against extra cost. and scores only new pairs. combineTowerScores adds the validation-weighted similarity. Open projectVector to follow the learned query headA small learned layer on top of a frozen text encoder. Chapter 11 learns one projection for queries and a separate one for items. Only the query head runs when a search arrives; item vectors are precomputed..
Guided example
Prerequisites
npm run data:embeddingsAdd supervisedTraining that uses known answers. Chapter 11 learns from graded preferences: for a training query, a higher-graded listing should score above a lower-graded one. retrieval to chapter 11. Reuse joint scores and combine them with trained similarity using one validation-selected weightThe number a feature is multiplied by when the score is built. Chapter 7 picks these numbers by hand; chapter 8 learns them from clicks..
import { level6Content } from "../../content/levels/level-6.ts";import { } from "../query-understanding/understand.ts";import { } from "../signals/build-signals.ts";import { } from "../text/item-text.ts";import { } from "../two-tower/combine-scores.ts";import { , type TwoTowerRetrieval } from "../two-tower/model.ts";import { } from "../two-tower/projection.ts";import type { CorpusItem } from "../types/corpus.ts";import type { AsyncLevelRanker, CrossEncoderDeps, EvolutionLevel } from "../types/level.ts";import type { QueryUnderstanding } from "../types/query.ts";import type { ScoredCandidate } from "../types/ranking.ts";import { } from "../vector/vector-retrieval.ts";import { as , as } from "./level-5-cross-encoder.ts";interface TwoTowerDeps extends CrossEncoderDeps { readonly twoTower: TwoTowerRetrieval;}/** Keep the preceding cascade. Spend additional joint inference only on new candidates. */export function createRanker(deps: TwoTowerDeps): AsyncLevelRanker { const previous = (deps); const available = new Map(deps.index.availableItems.map((item) => [item.id, item])); const scoredByQuery = new WeakMap<QueryUnderstanding, ReadonlyMap<string, ScoredCandidate>>(); return { async match(understanding) { if (understanding.queryVector === null) { throw new Error("Two-tower retrieval requires the frozen query embedding."); } const queryVector = (understanding.queryVector, deps.twoTower.queryWeights); const hits = ( queryVector, deps.twoTower.itemVectors, deps.twoTower.retrievalLimit, ); const baseline = await previous.match(understanding); const scored = new Map( baseline.items.map((item) => [item.id, previous.score(understanding, item)]), ); const additions = hits.flatMap((hit) => { const item = available.get(hit.itemId); return item === undefined || scored.has(item.id) ? [] : [item]; }); await (understanding, additions, deps, scored); for (const [rank, hit] of hits.entries()) { const entry = scored.get(hit.itemId); if (entry !== undefined) { scored.set(hit.itemId, { ...entry, signals: [ ...entry.signals, ...buildSignals(understanding, entry.item, [ { name: "twoTowerRank", value: rank + 1 }, ]), ], }); } } for (const [id, entry] of scored) { scored.set(id, (understanding, entry, queryVector, deps.twoTower)); } scoredByQuery.set(understanding, scored); return { items: [...scored.values()].map((entry) => entry.item) }; }, score(understanding, candidate) { const result = scoredByQuery.get(understanding)?.get(candidate.id); if (result === undefined) { throw new Error( "Two-tower scoring requires an awaited match for this query and candidate.", ); } return result; }, };}async function scoreAdditions( understanding: QueryUnderstanding, additions: readonly CorpusItem[], deps: TwoTowerDeps, scored: Map<string, ScoredCandidate>,): Promise<void> { if (additions.length === 0) { return; } const jointScores = await deps.crossEncoder.scorePairs( (understanding.rawText), additions.map(), ); if (jointScores.length !== additions.length || !jointScores.every(Number.isFinite)) { throw new Error("Two-tower additions require one finite cross-encoder score per listing."); } for (const [index, score] of jointScores.entries()) { const item = additions[index]; if (item === undefined) { throw new Error("Missing learned candidate for joint score."); } scored.set(item.id, { item, score, signals: (understanding, item, [ { name: "crossEncoder", value: score }, { name: "learnedRetrievalOnly", value: 1 }, ]), }); }}