feat(consume): fuse concept signals by RRF with declared tie-breaks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0550870ae2
commit
f4b1f4ef0b
2 changed files with 120 additions and 0 deletions
|
|
@ -380,3 +380,61 @@ def test_document_scores_are_identical_across_two_calls() -> None:
|
||||||
assert okf_consume.document_scores(FIXTURE, question) == okf_consume.document_scores(
|
assert okf_consume.document_scores(FIXTURE, question) == okf_consume.document_scores(
|
||||||
FIXTURE, question
|
FIXTURE, question
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Step 6: stage-two concept ranking, fused by RRF --------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _fixture_concepts() -> list[okf_consume.Concept]:
|
||||||
|
return [
|
||||||
|
okf_consume.read_concept(
|
||||||
|
FIXTURE / f"{concept_id}.md",
|
||||||
|
bundle_root=FIXTURE,
|
||||||
|
root_bundle_id="consume-fixture",
|
||||||
|
)
|
||||||
|
for concept_id in okf_consume.enumerate_concepts(FIXTURE)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_concepts_tying_on_every_signal_come_back_in_concept_id_order() -> None:
|
||||||
|
concepts = _fixture_concepts()
|
||||||
|
# A question matching nothing makes every signal identical, so the ONLY
|
||||||
|
# thing left deciding the order is the declared tie-break.
|
||||||
|
ranked = okf_consume.concept_scores(concepts, "zzzz qqqq", {})
|
||||||
|
ids = [concept.concept_id for concept, _ in ranked]
|
||||||
|
assert ids == sorted(ids)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reversing_the_input_order_does_not_change_the_output_order() -> None:
|
||||||
|
concepts = _fixture_concepts()
|
||||||
|
forward = [c.concept_id for c, _ in okf_consume.concept_scores(concepts, "zzzz qqqq", {})]
|
||||||
|
backward = [
|
||||||
|
c.concept_id
|
||||||
|
for c, _ in okf_consume.concept_scores(list(reversed(concepts)), "zzzz qqqq", {})
|
||||||
|
]
|
||||||
|
assert forward == backward
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_concept_in_a_high_scoring_document_outranks_an_equally_lexical_one() -> None:
|
||||||
|
concepts = _fixture_concepts()
|
||||||
|
question = "Hvordan skal prisene fylles ut?"
|
||||||
|
lifted = okf_consume.concept_scores(concepts, question, {"krav": 10.0, "dyp": 0.0})
|
||||||
|
dropped = okf_consume.concept_scores(concepts, question, {"krav": 0.0, "dyp": 10.0})
|
||||||
|
krav_first = [c.concept_id for c, _ in lifted].index("krav/prissammenstilling")
|
||||||
|
krav_later = [c.concept_id for c, _ in dropped].index("krav/prissammenstilling")
|
||||||
|
assert krav_first < krav_later
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_ranked_order_is_identical_across_two_calls() -> None:
|
||||||
|
concepts = _fixture_concepts()
|
||||||
|
scores = okf_consume.document_scores(FIXTURE, "Hvordan skal prisene fylles ut?")
|
||||||
|
first = okf_consume.concept_scores(concepts, "Hvordan skal prisene fylles ut?", scores)
|
||||||
|
second = okf_consume.concept_scores(concepts, "Hvordan skal prisene fylles ut?", scores)
|
||||||
|
assert [c.concept_id for c, _ in first] == [c.concept_id for c, _ in second]
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_price_concept_leads_on_the_price_question_in_the_fixture() -> None:
|
||||||
|
concepts = _fixture_concepts()
|
||||||
|
scores = okf_consume.document_scores(FIXTURE, "Hvordan skal prisene fylles ut?")
|
||||||
|
ranked = okf_consume.concept_scores(concepts, "Hvordan skal prisene fylles ut?", scores)
|
||||||
|
assert ranked[0][0].concept_id == "krav/prissammenstilling"
|
||||||
|
|
|
||||||
|
|
@ -576,3 +576,65 @@ def document_scores(
|
||||||
continue
|
continue
|
||||||
scores[document] += float(_overlap(question_tokens, entry.label))
|
scores[document] += float(_overlap(question_tokens, entry.label))
|
||||||
return scores
|
return scores
|
||||||
|
|
||||||
|
|
||||||
|
# --- Stage two: which concepts inside those documents -------------------------
|
||||||
|
|
||||||
|
#: Reciprocal Rank Fusion's smoothing constant. Lifted IN METHOD from the
|
||||||
|
#: reference implementation this repository read (`wiki-advise/rank.mjs:82`),
|
||||||
|
#: not in code and not as a quality claim: that engine's recall figures were
|
||||||
|
#: measured on an LLM candidate-generation pass this pre-pass deliberately does
|
||||||
|
#: not have, so its numbers say nothing about this one.
|
||||||
|
#:
|
||||||
|
#: RRF is chosen because it consumes RANKS ONLY. There is no score
|
||||||
|
#: normalisation to get wrong, no weight to tune against a test set, and the
|
||||||
|
#: fusion is invariant to any monotone transform of the individual signals.
|
||||||
|
RRF_K = 60
|
||||||
|
|
||||||
|
|
||||||
|
def concept_scores(
|
||||||
|
concepts: Sequence[Concept],
|
||||||
|
question: str,
|
||||||
|
document_score: Mapping[str, float],
|
||||||
|
) -> list[tuple[Concept, float]]:
|
||||||
|
"""Every concept, ordered best first, fused from three signals by RRF.
|
||||||
|
|
||||||
|
The signals: (1) the question against the concept's title and the segments
|
||||||
|
of its id, (2) the question against the body, (3) the stage-one score of the
|
||||||
|
document the concept belongs to.
|
||||||
|
|
||||||
|
**Ties break lexicographically by `concept_id`, at both the per-signal sort
|
||||||
|
and the fused sort.** Declared rather than inherited from dict insertion
|
||||||
|
order, so reproducibility is a property of the input SET and not of the
|
||||||
|
order it happened to arrive in.
|
||||||
|
|
||||||
|
**No float reaches the payload.** These scores order the cut; only ranks and
|
||||||
|
whole byte counts are emitted.
|
||||||
|
"""
|
||||||
|
question_tokens = normalise(question)
|
||||||
|
signals: list[dict[str, float]] = [
|
||||||
|
{
|
||||||
|
concept.concept_id: float(
|
||||||
|
_overlap(question_tokens, f"{concept.title} {concept.concept_id.replace('/', ' ')}")
|
||||||
|
)
|
||||||
|
for concept in concepts
|
||||||
|
},
|
||||||
|
{
|
||||||
|
concept.concept_id: float(_overlap(question_tokens, concept.body))
|
||||||
|
for concept in concepts
|
||||||
|
},
|
||||||
|
{
|
||||||
|
concept.concept_id: document_score.get(concept.concept_id.split("/", 1)[0], 0.0)
|
||||||
|
for concept in concepts
|
||||||
|
},
|
||||||
|
]
|
||||||
|
fused: dict[str, float] = {concept.concept_id: 0.0 for concept in concepts}
|
||||||
|
for signal in signals:
|
||||||
|
# Sort by score descending, then by id ascending -- the declared
|
||||||
|
# tie-break, applied before a rank is ever read.
|
||||||
|
order = sorted(signal, key=lambda key: (-signal[key], key))
|
||||||
|
for position, concept_id in enumerate(order, start=1):
|
||||||
|
fused[concept_id] += 1.0 / (RRF_K + position)
|
||||||
|
by_id = {concept.concept_id: concept for concept in concepts}
|
||||||
|
ranked_ids = sorted(fused, key=lambda key: (-fused[key], key))
|
||||||
|
return [(by_id[concept_id], fused[concept_id]) for concept_id in ranked_ids]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue