Search Evolution Explorer

Level 7 · Attribute boosting

src/domain/levels/level-2-boosting.ts · 87 lines
Back to search

Use what I meant, not only the words I typed

First work out what the query points at. A bare 'marble' defeats the guess and nothing is boosted; add a product word, such as 'marble end table', and the category resolves. Then add points to listings in that category and at a fitting price, plus points that do not depend on the query at all: a recent post, a longer title, and a price that sits high in its category.

Where to look: match is passed through unchanged from chapter 6. score takes the chapter 6 score and adds the named boostsA hand-chosen number added to the score when a listing has a wanted property, for example the category the query points at. to it.

Guided example

couchvs chapter 6

Category and static-quality boosts push the 'couch riser' accessory out of the top 10 for 'couch': it matches the word but sits in the wrong category and price band.

Prerequisites

Reuses the ranking fromChapter 6
Foundation · Inverted indexbuilt at start
Declared artifactsready
src/domain/levels/level-2-boosting.ts87 linessame module the pipeline ran

Same candidatesAll listings that matched the query at all, before they are ordered. Scoring can only reorder this set. It cannot add to it. as chapter 6. The order changes: the BM25The standard formula that scores how strongly a listing's text matches the query words. A word counts more when it is rare in the whole corpus and appears often in this listing. Very long listings are penalised a little. score gets boostsA hand-chosen number added to the score when a listing has a wanted property, for example the category the query points at. for query attributes and for static qualityA quality score that does not depend on the query, for example how new a listing is or how complete its title is. It can be computed once for every listing. of the listing.

import { level2Content } from "../../content/levels/level-2.ts";
import { } from "../signals/build-signals.ts";
import { } from "../signals/price-fit.ts";
import { } from "../signals/static-quality.ts";
import type { CorpusItem } from "../types/corpus.ts";
import type { EvolutionLevel, LevelDeps, LevelRanker } from "../types/level.ts";
import type { QueryUnderstanding } from "../types/query.ts";
import { as } from "./level-1-canonical.ts";
const CATEGORY_BOOST = 0.1;
const PRICE_FIT_BOOST = 0.05;
/** Recency and other static quality must not outweigh a real text match. */
const STATIC_QUALITY_WEIGHT = 0.01;
/** Duplication across levels is accepted so each evolution level stays independently readable. */
export function createRanker(deps: LevelDeps): LevelRanker {
const previous = (deps);
const quality = (deps.index.availableItems, deps.clock);
return {
match: previous.match,
score(understanding, candidate) {
const base = previous.score(understanding, candidate);
const categoryMatch = (understanding, candidate);
const priceFit = (understanding, candidate);
const staticQuality = quality.get(candidate.id);
const recency = staticQuality?.recency ?? 0;
const titleQuality = staticQuality?.titleQuality ?? 0;
const pricePercentile = staticQuality?.pricePercentile ?? 0;
const added =
* categoryMatch +
* priceFit +
* (recency + titleQuality + pricePercentile);
return {
item: candidate,
score: base.score + added,
signals: (understanding, candidate, [
// Preserve each carried weight: the spelling flag has none, so the printed
// total still matches the score for corrected queries.
...base.signals.map((signal) => ({
name: signal.name,
value: signal.value,
weight: signal.weight,
})),
{ name: "categoryMatch", value: categoryMatch, weight: },
{ name: "priceFit", value: priceFit, weight: },
{ name: "recency", value: recency, weight: },
{ name: "titleQuality", value: titleQuality, weight: },
{ name: "pricePercentile", value: pricePercentile, weight: },
]),
};
},
};
}
function categoryMatchScore(understanding: QueryUnderstanding, candidate: CorpusItem): number {
if (understanding.predictedCategoryId === null) {
return 0;
}
if (understanding.predictedCategoryId === candidate.categoryId) {
return 1;
}
return 0;
}
a numbered note, explained on the righta function you can open heredefined further down in this filewhere level 6 joins