perf(pfs): code→family index for anchor_metadata (refs #688)
code_family_index(families) builds a code -> family key map once so a restamp run of ~1.15M chunks doesn't rescan every family per code. anchor_metadata gains an optional code_index kwarg that looks family membership up through it instead. Ruling A5: families with fewer than two codes are excluded from the index and from the default unindexed scan — the derived registry is ~17,187 keys and all but a handful are single-code stem families, where a 'families' anchor would just echo the code already in 'codes'. Only real groupings (today: ACP, CCM, PCM, TCM, APCM) get a families anchor.
This commit is contained in:
@@ -11,26 +11,67 @@ from pfs.elements import parse_descriptor
|
||||
from pfs.families import FAMILIES, Family, find_codes
|
||||
|
||||
|
||||
def code_family_index(families: Mapping[str, Family]) -> dict[str, str]:
|
||||
"""Code -> family key, built once so ``anchor_metadata`` doesn't scan
|
||||
every family for every chunk (a restamp run calls it ~1.15M times).
|
||||
|
||||
Single-code families are excluded (Ruling A5): the derived registry
|
||||
is ~17,187 keys, all but a handful stem families with exactly one
|
||||
code, and a 'families' anchor on one of those would just echo the
|
||||
code already in 'codes'. Only families with two or more codes (today:
|
||||
ACP, CCM, PCM, TCM, APCM) are real groupings worth a separate anchor.
|
||||
|
||||
A code that belongs to two qualifying families keeps the first family
|
||||
reached in *families*' iteration (dict) order — same tie-break a
|
||||
caller would see from scanning in that order."""
|
||||
out: dict[str, str] = {}
|
||||
for key, fam in families.items():
|
||||
if len(fam.codes) < 2:
|
||||
continue
|
||||
for code in fam.codes:
|
||||
out.setdefault(code, key)
|
||||
return out
|
||||
|
||||
|
||||
def anchor_metadata(
|
||||
text: str, *, families: Mapping[str, Family] | None = None
|
||||
text: str,
|
||||
*,
|
||||
families: Mapping[str, Family] | None = None,
|
||||
code_index: Mapping[str, str] | None = None,
|
||||
) -> dict[str, str]:
|
||||
"""Extract codes, families, and elements from text.
|
||||
|
||||
Returns a dict with keys:
|
||||
- 'codes': space-joined sorted unique codes (from find_codes)
|
||||
- 'families': space-joined sorted unique family keys
|
||||
- 'families': space-joined sorted unique family keys (families with
|
||||
only one code are excluded — see ``code_family_index``)
|
||||
- 'elements': space-joined sorted unique element slugs (type=value, no detail)
|
||||
|
||||
Args:
|
||||
text: The text to analyze
|
||||
families: Optional mapping of family keys to Family objects. Defaults to pfs.families.FAMILIES
|
||||
code_index: Optional precomputed ``code_family_index(families)``. When
|
||||
given, family lookup goes through it instead of scanning every
|
||||
family for every code — same result, built once per run by a
|
||||
caller like the restamp backfill.
|
||||
|
||||
Returns:
|
||||
dict with 'codes', 'families', and 'elements' keys
|
||||
"""
|
||||
fams = FAMILIES if families is None else families
|
||||
codes = find_codes(text)
|
||||
keys = sorted({k for k, f in fams.items() for c in codes if c in f.codes})
|
||||
if code_index is not None:
|
||||
keys = sorted({code_index[c] for c in codes if c in code_index})
|
||||
else:
|
||||
keys = sorted(
|
||||
{
|
||||
k
|
||||
for k, f in fams.items()
|
||||
if len(f.codes) >= 2
|
||||
for c in codes
|
||||
if c in f.codes
|
||||
}
|
||||
)
|
||||
elements = sorted({f"{e.type.value}={e.value}" for e in parse_descriptor(text)})
|
||||
return {
|
||||
"codes": " ".join(codes),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""pfs.anchors — metadata for chunks: codes, families, and element slugs."""
|
||||
|
||||
from pfs.anchors import anchor_metadata
|
||||
from pfs.families import Family
|
||||
from pfs.anchors import anchor_metadata, code_family_index
|
||||
from pfs.families import FAMILIES, Family
|
||||
|
||||
|
||||
def test_codes_families_elements():
|
||||
@@ -23,5 +23,33 @@ def test_empty_and_unknown():
|
||||
"families": "",
|
||||
"elements": "",
|
||||
}
|
||||
fams = {"X": Family("X", "X fam", ("12345",), ("x fam",))}
|
||||
fams = {"X": Family("X", "X fam", ("12345", "23456"), ("x fam",))}
|
||||
assert anchor_metadata("code 12345", families=fams)["families"] == "X"
|
||||
|
||||
|
||||
def test_code_index_matches_unindexed_scan():
|
||||
text = "Use 99439 in conjunction with 99490; consent; per calendar month"
|
||||
index = code_family_index(FAMILIES)
|
||||
assert anchor_metadata(text, code_index=index) == anchor_metadata(text)
|
||||
|
||||
|
||||
def test_code_index_keeps_first_family_on_collision():
|
||||
fams = {
|
||||
"A": Family("A", "A fam", ("12345", "23456"), ("a fam",)),
|
||||
"B": Family("B", "B fam", ("12345", "34567"), ("b fam",)),
|
||||
}
|
||||
index = code_family_index(fams)
|
||||
assert index["12345"] == "A"
|
||||
|
||||
|
||||
def test_single_code_family_never_stamped():
|
||||
"""A stem-derived family of exactly one code (~17,182 of 17,187
|
||||
derived keys) is not a grouping worth its own anchor — it would just
|
||||
echo the code already in 'codes' (Ruling A5)."""
|
||||
fams = {"SOLO": Family("SOLO", "Solo", ("54321",), ("solo",))}
|
||||
assert "SOLO" not in code_family_index(fams)
|
||||
assert anchor_metadata("code 54321", families=fams)["families"] == ""
|
||||
index = code_family_index(fams)
|
||||
assert (
|
||||
anchor_metadata("code 54321", families=fams, code_index=index)["families"] == ""
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user