Files
chemenu/tools/chemenu/search/fuse.py
T
torben 18ae28f918
CI / verify (push) Failing after 32s
Release / release (push) Successful in 38s
Chemenu 2.1.0 - deterministischer Wissenskompiler
Chemenu kompiliert Rohnotizen zu einem verlinkten, quellengebundenen Wiki:
raw/ -> types/ + tools/ -> kb/ -> reports/. Was mechanisch ist, macht
tools/wikitool; was Urteil braucht, macht ein Agent unter Contracts, deren
Grenzen in Code durchgesetzt sind statt im Prompt.

Dieser Commit ist der Startpunkt der oeffentlichen Historie. Die vorherige
Entwicklung fand in einer privaten Instanz statt und ist nicht Teil dieses
Repositorys; ihre Erzaehlung steht vollstaendig in CHANGES.md, das mit 44
Eintraegen von 0.1.0 bis 2.1.0 erhalten geblieben ist.

Der mitgelieferte Korpus ist ein Testbett und eine Demo: 170 Seiten ueber den
Stack selbst - Gates, Lint, Versionierung, Suche, das Wiki-Muster. Er
dokumentiert das Werkzeug mit den eigenen Mitteln des Werkzeugs.

Lizenz: AGPL-3.0 fuer den Stack (tools/, types/), CC-BY-4.0 fuer die Inhalte.
Die Grenze zwischen beiden ist der Dateiplan, den dist export berechnet -
siehe NOTICE.
2026-09-01 16:26:14 +02:00

42 lines
1.6 KiB
Python

"""Reciprocal Rank Fusion: merge several backends' rankings into one.
RRF is here before a second backend exists because it is what makes adding one
cheap. It needs no per-backend score calibration - only each hit's *rank* -
which is the property that lets a lexical and a semantic backend, whose scores
are not on any common scale, be combined without tuning weights.
"""
from __future__ import annotations
from chemenu.search.types import SearchHit
# Cormack et al.'s default. Large enough that the difference between rank 1 and
# rank 2 does not swamp agreement between backends further down the list.
RRF_K = 60
def reciprocal_rank_fusion(rankings: list[list[SearchHit]], k: int = RRF_K) -> list[SearchHit]:
"""Fuse ranked hit lists. A page found by two backends outranks a page
found by one, even if neither ranked it first."""
scored: dict[str, SearchHit] = {}
totals: dict[str, float] = {}
backends: dict[str, list[str]] = {}
for ranking in rankings:
for rank, hit in enumerate(ranking, start=1):
totals[hit.path] = totals.get(hit.path, 0.0) + 1.0 / (k + rank)
backends.setdefault(hit.path, []).append(hit.backend)
existing = scored.get(hit.path)
if existing is None:
scored[hit.path] = hit
elif hit.matches and not existing.matches:
scored[hit.path] = hit
fused: list[SearchHit] = []
for path, hit in scored.items():
hit.score = totals[path]
hit.backend = "+".join(dict.fromkeys(backends[path]))
fused.append(hit)
fused.sort(key=lambda h: (-h.score, h.title.lower()))
return fused