v3.10.27

ruvnet/ruflov3.10.27May 30, 2026by ruvnet

AI Summary

Honest negative result: RRF degrades nDCG@10 on both datasets because multi-field BM25 is weaker than Lucene's.

Key Highlights

  • RRF degrades nDCG@10 on both NFCorpus (-0.024) and SciFact (-0.057)
  • Recall@100 IS up (good foundation for stage 2 rerank)
  • Diagnosis: asymmetric input strength - weak BM25 dilutes strong dense
  • Ablation harness with bootstrap CI shipped
  • Bug fix: bge-cache per-dataset path

New Features

  • RRF ablation harness
  • Per-dataset cache path
  • Negative result documentation

Full Release Notes

## What ships

Honest negative result. The textbook "lowest-regret" first move — BM25+dense RRF k=60 — **degrades nDCG@10 on both NFCorpus and SciFact** because our multi-field BM25 is materially weaker than Lucene's. We ship the ablation harness + the finding anyway.

## Acceptance test outcome

"RRF improves or preserves nDCG@10 on both NFCorpus and SciFact, bootstrap CI does not undermine the claim, defaults fixed before viewing test result." **FAILS.**

| Config (BOTH datasets, fixed defaults BEFORE viewing) | NFCorpus | SciFact | Mean |
|---|---:|---:|---:|
| dense alone (BGE-base) | **0.352** | **0.626** | **0.489** |
| RRF k=60 equal (textbook default) | 0.328 ↓ | 0.569 ↓ | 0.449 ↓ |
| RRF k=30 equal (best ablation) | 0.335 ↓ | 0.582 ↓ | 0.459 ↓ |
| RRF k=60 dense=1.2, bm25=0.8 | 0.334 ↓ | 0.577 ↓ | 0.456 ↓ |
| RRF k=60 dense=0.8, bm25=1.2 | 0.323 ↓ | 0.558 ↓ | 0.441 ↓ |

**Every RRF variant underperforms dense-alone** on the 2-dataset mean (-0.04 nDCG@10 worse).

## What DID work — recall

Recall@100 IS up on both:

| Dataset | Dense R@100 | RRF R@100 | Δ |
|---|---:|---:|---:|
| NFCorpus | 0.305 | 0.321 | **+0.016** |
| SciFact | 0.828 | 0.951 | **+0.123** |

RRF surfaces more candidates correctly — it just ranks them worse at top-K. This is the right setup for **stage 2: cross-encoder rerank on the wider candidate pool** (ADR-088 / 3.10.28).

## Diagnosis (why RRF hurt)

The classic RRF win assumes **comparably-strong systems with different failure modes**. Our setup is asymmetric: BGE-base dense is strong (0.626 SciFact), our multi-field BM25 is weak (0.576 SciFact vs Lucene published 0.679). Pure BM25 nDCG@10 on NFCorpus: 0.279 vs Lucene 0.325 — we're 14% relative below.

When one input is weak, RRF averages its noise into top positions instead of cancelling it. The math works perfectly for the documented Lucene+strong-dense case; we don't match that profile yet.

## Bug found and fixed

`bge-cache/` was hardcoded to `/tmp/beir-nfcorpus/bge-cache/` — the SciFact run silently overwrote the NFCorpus cache. Caught only when the first RRF run returned nDCG=0.14 (random-noise level), forcing investigation. Now per-dataset path. **3.10.25 and 3.10.26 NFCorpus numbers were computed before the overwrite and are still valid.**

## What's in the box

1. **`scripts/run-beir-rrf-ablation.mjs`** — re-runnable ablation harness with bootstrap CI on the fixed default config + full ablation matrix.
2. **`scripts/run-beir-hybrid.mjs`** — full RRF + opt-in cross-encoder rerank runner (rerank wired but pending ADR-088 measurement).
3. **`bge-cache/` per-dataset path fix** in run-beir-bge.mjs.
4. **ADR-087** — full negative-result writeup with diagnosis + tracked next steps.
5. **Updated BEIR-MATRIX.md** with ablation rows + the honest 2-dataset mean comparison.
6. **No default change** — dense-only stays the BEIR runner default. RRF is opt-in for callers with Lucene-strength BM25.

## Next steps (already tracked)

- **ADR-088 / 3.10.28**: Cross-encoder rerank on RRF's wider candidate pool (Recall@100 0.951 on SciFact says the candidates ARE there).
- **Lucene-style BM25**: Porter/Snowball stemmer + Lucene stopword list + length norm. Would make RRF actually work as designed.
- **ruvnet/ruvector#524**: bundle BGE in ruvector so downstream packages stop hitting the sharp dependency.

## Reproduce

```bash
git clone https://github.com/ruvnet/ruflo && cd ruflo
npm install && ( cd v3/@claude-flow/cli && npx tsc )

mkdir -p /tmp/beir-nfcorpus && cd /tmp/beir-nfcorpus
curl -sL -o nf.zip 'https://public.ukp.informatik.tu-darmstadt.de/thakur/BEIR/datasets/nfcorpus.zip' && unzip -q nf.zip
node /path/to/v3/@claude-flow/cli/scripts/run-beir-bge.mjs              # ingest
node /path/to/v3/@claude-flow/cli/scripts/run-beir-rrf-ablation.mjs    # ablation matrix
```

## Install

```bash
npx ruflo@3.10.27    # latest / alpha / v3alpha all aligned
```

Full ADR: [v3/docs/adr/ADR-087-rrf-negative-result.md](v3/docs/adr/ADR-087-rrf-negative-result.md)