Skip to content

feat(retrieval): second meta-belief consumer — adaptive bm25f_anchor_weight (#757) - #787

Merged
robotrocketscience merged 5 commits into
mainfrom
feat/issue-757-bm25f-anchor-weight-meta
May 14, 2026
Merged

feat(retrieval): second meta-belief consumer — adaptive bm25f_anchor_weight (#757)#787
robotrocketscience merged 5 commits into
mainfrom
feat/issue-757-bm25f-anchor-weight-meta

Conversation

@robotrocketscience

Copy link
Copy Markdown
Owner

Closes #757. Sub-task C of umbrella #480 — pattern reuse of #756 (just shipped via #784), scoped to the actual single-knob BM25F that aelfrice has rather than the multi-field BM25F the issue body assumed.

Scope-down call (ratified before any code)

#757's issue body assumed a per-field BM25F with weights for title/body/tags. The actual code in src/aelfrice/bm25.py has one tunable: anchor_weight (DEFAULT_ANCHOR_WEIGHT = 3), the weight on the incoming-anchor token stream from #148 R3. There are no title/body/tags fields. Scope was ratified down to anchor_weight only before any code was written; this PR honours that.

Commit layout (5 atomic, all SSH-signed)

  1. feat(retrieval): log-linear bounded bm25f_anchor_weight encoding + meta-belief env flag — constants, decoder, env-flag predicate. Pure additions, no caller change.
  2. feat(retrieval): install_bm25f_anchor_weight_meta_belief helper + meta-aware resolver — installer + 3-tier resolver. Still no caller change.
  3. feat(retrieval): wire meta-belief anchor_weight into BM25F + bm25_l0_ratio signal update_l1_hits BM25F branch reads through the resolver when no cache is supplied; emits bm25_l0_ratio evidence per query when the flag is on.
  4. test(retrieval): coverage for #757 bm25f_anchor_weight meta-belief consumer — 30 new tests mirroring tests/test_temporal_half_life_meta.py.
  5. docs(changelog): note adaptive bm25f_anchor_weight consumer (#757) — Unreleased entry.

Default-OFF byte-identical contract

Without AELFRICE_META_BELIEF_BM25F_ANCHOR_WEIGHT, the read path collapses to BM25IndexCache(store, anchor_weight=3) via the resolver's fallback to bm25.DEFAULT_ANCHOR_WEIGHT — identical to the pre-#757 BM25IndexCache(store) call. Signal updates are gated on the same flag. Caller-supplied bm25f_cache is honoured unconditionally so the bench harness pin contract is preserved.

Cold-start parity

static_default = 0.5 decodes through decode_meta_bm25f_anchor_weight to round(sqrt(1*10)) = round(3.162) = 3 = DEFAULT_ANCHOR_WEIGHT. Installing the meta-belief preserves byte-identical retrieval order until evidence actually moves the posterior.

Signal

bm25_l0_ratio = fraction of store-locked beliefs that BM25F's top-K also surfaced. Updated per query when both the flag is on and the store has at least one locked belief. Empty L0 → skip (undefined metric). Per the same D4 split #756 followed, the relevance signal stays deferred to #779.

Test plan

…ta-belief env flag

First slice of #757 consumer wiring. Pure additions — no caller
changes yet, so retrieval is byte-identical until the read-path
commits land.

Adds:

- META_BM25F_ANCHOR_WEIGHT_KEY, BM25F_ANCHOR_WEIGHT_FLOOR (1),
  BM25F_ANCHOR_WEIGHT_CEIL (10), META_BM25F_ANCHOR_WEIGHT_STATIC_DEFAULT
  (0.5), META_BM25F_ANCHOR_WEIGHT_POSTERIOR_DECAY_SECONDS (30d).
- ENV_META_BELIEF_BM25F_ANCHOR_WEIGHT = "AELFRICE_META_BELIEF_BM25F_ANCHOR_WEIGHT",
  ships default-OFF behind the #437 A/B bench-gate clause.
- decode_meta_bm25f_anchor_weight(v) — log-linear interpolation
  [0, 1] -> [1, 10], rounded to int (BM25Index.build replicates
  the anchor token stream by an int count, see bm25.py:243). v=0.5
  decodes to 3, matching DEFAULT_ANCHOR_WEIGHT exactly so the
  cold-start install preserves byte-identical retrieval order.
- is_meta_belief_bm25f_anchor_weight_enabled() — truthy-token resolver
  mirroring is_meta_belief_half_life_enabled(), including the
  '=enabled' spelling from the umbrella #480 issue body.

Encoding lives in the consumer per the 2026-05-13 #756 ratification.
The substrate stays pattern-uniform across #480 sub-tasks B–F; each
consumer picks its own bounds at integration time.

Scope-down note: #757's body asked for per-field weights, but
aelfrice's BM25F has one tunable (anchor_weight). Scope was ratified
down to anchor_weight only before any code was written.
…a-aware resolver

Second slice of #757. Still no caller change in retrieve() or
_l1_hits — the new resolve_bm25f_anchor_weight_with_meta() function
exists in parallel, ready for the third commit to wire in at the
BM25IndexCache construction site.

Adds:

- install_bm25f_anchor_weight_meta_belief(store, *, now_ts) — idempotent
  install of the #757 row with the v3.x ratified defaults: bm25_l0_ratio
  signal only (relevance deferred to #779 per D4, same split as #756),
  30d posterior decay, static_default=0.5 (decodes to 3, matching
  bm25.DEFAULT_ANCHOR_WEIGHT exactly).

- resolve_bm25f_anchor_weight_with_meta(store, *, now_ts, explicit=None)
  — three-tier precedence: explicit kwarg > meta-belief (gated on the
  env flag) > bm25.DEFAULT_ANCHOR_WEIGHT. Unlike the #756 resolver this
  one has no env-var or TOML override layer because anchor_weight has
  never had a user-facing config knob — it has been a code constant
  since v1.5/#148 R3. Keeping the surface narrow avoids introducing a
  public config that we'd have to keep stable.

The bm25.DEFAULT_ANCHOR_WEIGHT import is local (inside both helpers) to
avoid a top-of-module aelfrice.retrieval ↔ aelfrice.bm25 cycle on the
forward-ref direction — bm25 already imports nothing from retrieval,
but a top-level reverse import would force ordering constraints under
test collection.

Smoke-checked the precedence ladder against an in-memory store: no
store → 3, store + flag-off → 3, store + flag-on + cold meta-belief → 3
(byte-identical to baseline at cold start), explicit=7 always wins,
re-install returns False.
…ratio signal update

Closes the #757 loop in _l1_hits. The BM25F branch's BM25IndexCache
construction now drives anchor_weight through
resolve_bm25f_anchor_weight_with_meta when the caller hasn't supplied
a cache, and on each query (when the meta feature flag is on) records
a bm25_l0_ratio sub-posterior update: evidence is the fraction of
store-locked beliefs that appeared in BM25F's top-K for the query.

Wiring:

- Caller-supplied bm25f_cache stays the unconditional override. The
  bench harness and unit tests still pin specific anchor_weights via
  the cache, so determinism contracts there are untouched.
- The signal update fires inside _l1_hits because both the BM25F
  raw scored_pairs (top-K bid list) and the store handle are local
  there; threading them to retrieve_v2's tail would have been the
  same logic at one more level of indirection.
- update_meta_belief is wrapped in try/except — store errors are
  printed to stderr and swallowed, mirroring the #756 latency-signal
  posture in retrieve_v2 (commit 521f668). Retrieval must never
  raise on a meta-belief write failure.
- No locked beliefs in the store means no signal update (skip;
  evidence would be 0/0 division otherwise). That's the right
  fail-soft: empty L0 is uninformative for an L0-coverage metric.

Default-OFF byte-identical contract: with the env flag unset, the
read path collapses to BM25IndexCache(store, anchor_weight=3) via
the resolver's fallback to bm25.DEFAULT_ANCHOR_WEIGHT, identical
to the pre-#757 BM25IndexCache(store) call. Signal updates are
fully gated on the same env flag.

All 27 BM25-touching tests still pass under -k bm25.
…nsumer

Mirrors tests/test_temporal_half_life_meta.py from PR #784 (#756),
scoped to the smaller surface #757 exposes:

- Log-linear encoding: floor / ceil / mid=DEFAULT_ANCHOR_WEIGHT
  parity / out-of-range clamping / int-valued return /
  monotonic non-decreasing over the unit interval.
- Env feature flag: default-off / truthy + 'enabled' / falsy.
- Constants sanity: pinned to the #757 ratified defaults so a
  careless edit surfaces in this file, not in a bench surprise.
- Install helper: first-call True / second-call no-op
  (idempotency contract).
- Resolver precedence: no-store fallback / flag-off ignores
  installed meta-belief / flag-on cold-start matches static
  default / explicit kwarg wins / flag-on-no-install falls
  through without materialising a phantom row.
- _l1_hits integration: default-OFF byte-identical posture
  (no signal recorded with flag-off even when row is installed) /
  flag-on records a bm25_l0_ratio sub-posterior / flag-on no-install
  no-crash / caller-supplied bm25f_cache bypasses the resolver
  (bench harness pin contract).

30 tests, all green under uv run pytest.
@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@robotrocketscience has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 52 minutes and 8 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ea6aa360-4f77-4bae-9806-783fc7d6a8a2

📥 Commits

Reviewing files that changed from the base of the PR and between f004ede and 8361770.

⛔ Files ignored due to path filters (1)
  • CHANGELOG.md is excluded by !**/CHANGELOG.md
📒 Files selected for processing (2)
  • src/aelfrice/retrieval.py
  • tests/test_bm25f_anchor_weight_meta.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-757-bm25f-anchor-weight-meta

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 549 changed lines (limit: 200)
  • 3 changed files (limit: 3)

Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated attn:merge-conflict cycles (see #602). When practical, split into smaller PRs that each touch a focused surface.

This is advisory only — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the size:override label and this comment will be removed on the next push.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @robotrocketscience, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@robotrocketscience
robotrocketscience merged commit 8361770 into main May 14, 2026
21 checks passed
@robotrocketscience
robotrocketscience deleted the feat/issue-757-bm25f-anchor-weight-meta branch May 14, 2026 15:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v3.x] C: meta:retrieval.bm25f_field_weights — adaptive per-field weights (#480 sub-task)

1 participant