Skip to content

Validate NameRes ES: group its differences from Solr, and let a target declare it has no blocklist - #137

Open
gaurav wants to merge 6 commits into
mainfrom
validate-nameres-es
Open

Validate NameRes ES: group its differences from Solr, and let a target declare it has no blocklist#137
gaurav wants to merge 6 commits into
mainfrom
validate-nameres-es

Conversation

@gaurav

@gaurav gaurav commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Validating the Elasticsearch-backed NameRes (namelookup-es.ci.transltr.io) turned up a
large, undifferentiated pile of failures. This branch turns it into named failure classes
with reproducers — filed as #131-#136 under the
NameRes ES validation, September 2026
milestone — and makes the changes to this repo that the investigation needed.

The findings

Running the same 2076 sheet rows against ci-es and against ci — the Solr-backed NameRes
on the same Babel data, which is what separates a backend difference from a data one:

Outcome ES CI (Solr)
expected CURIE is the top hit 58 127
rank 2-6 (reported as a silent xfail) 59 34
absent from the top 20 32 7
below rank 6 14 4
blocklisted CURIE returned 10 0

Three mechanisms account for nearly all of it — #131 (alphanumeric tokens are split, so
?string=BRCA1 and ?string=BRCA return byte-identical lists and NCBIGene:672 is
unreachable), #132 (longer names containing the query outrank the concept), #133 (candidates
tie on score and the canonical concept falls out of the window). #134 records the eight rows
where ES is already ahead of Solr, so they do not get "fixed" back.

What changed here

NameResHasBlocklist, a new per-target capability. namelookup-es implements no
blocklist, so all 313 blocklist-sheet entries and every negative-flagged sheet row failed
by construction — the same known thing, daily, drowning the findings that matter.
targets.ini already exists to record this kind of difference rather than infer it from the
hostname. --target ci-es tests/nameres/ goes from 164 failures to 58; --target ci is
unchanged.

Filtering is on the negative flag, not the "Blocklist Nov 2023" category: only 16 rows
carry the flag, 5 of them live in "Unit Tests", and row 175 (hypersexuality) is an ordinary
positive row sitting in the Blocklist category. Category would have been wrong both ways.

expected_rank is now recorded. test_label calls pytest.xfail() imperatively when
the expected CURIE is inside NameResXFailIfInTop but not first, so a demotion from rank 1
to rank 2 never reaches the failure count. That hid the single largest regression: 46 rows
demoted, against 32 failing outright. Recording the rank as a user property puts it in
--report-jsonl where a comparison can see it. NameResXFailIfInTop is deliberately not
raised — that would make every other environment noisier to fix one.

Two new assertion handlers. The vocabulary could not express either thing this
validation most needs to say. SearchByName is top-N with N fixed at 5 by targets.ini, so
it passes on both services for all 46 demoted rows; SearchByNameTopResult passes only on
rank 1. DoesNotSearchByName is the first negative NameRes assertion of any kind, and makes
a blocklisted term expressible.

Gotchas worth keeping

  • The issues are prose and curl only, deliberately. An issue's assertions run against
    every --target with no way to scope them, and an open issue whose assertions all pass is
    a strict XPASS failure. An assertion that fails on ES and passes on Solr would turn
    prod, test, ci, dev and exp red daily. Scope an issue's assertions to particular targets #136 tracks the scoping that unblocks this.
  • DoesNotSearchByName treats a CURIE it cannot normalize as a failure, not a pass.
    "We could not look it up" and "it was not returned" both end with the CURIE absent, so the
    other choice lets a typo'd blocklist assertion succeed forever while testing nothing.
    DoesNotResolve takes the opposite line because there the CURIE failing to resolve is
    the assertion.
  • Two of the new tests catch BaseException rather than Exception. pytest's outcome
    exceptions derive from it, and catching Failed specifically would be worse than useless:
    a regressed skip would propagate and mark the test itself skipped, which reads as
    passing.
  • A capability skip has to live in the test body — target_info is parametrized by the root
    conftest and does not exist in pytest_generate_tests. That is test_blocklist.py makes an HTTP request at collection time (before any target is selected) #91, now on the milestone.
  • black was not run on the two nameres test files; it wanted to rewrite 236 and 101 lines
    and would have buried the change.

Before merging

  • Settle the final form of the two new assertions. SearchByNameTopResult and
    DoesNotSearchByName are unit-tested against fakes only and have never run against a
    live service, because no issue can carry them until Scope an issue's assertions to particular targets #136 lands. Worth deciding now
    whether the shapes are right — in particular whether top-1 should instead be a
    parameter on SearchByName ([query, curie, rank]) rather than a separate assertion
    name, and whether DoesNotSearchByName should honour NameResXFailIfInTop as its N
    or take its own.

  • Decide when NameResHasBlocklist = false comes back out, and schedule it. The
    flag is a stopgap, not the answer: it stops ci-es reporting the same 10 known
    failures daily, but it also means that target has no blocklist coverage, and nothing
    enforces its removal. The real fix is
    NCATSTranslator/Babel#738
    filter blocked synonyms in Babel so they are never written out, rather than having each
    consumer delete them from its own index — now decided and on Babel v1.19. If #738 lands
    well after this PR, this flag is the thing most likely to be forgotten, and ci-es
    silently keeps its exemption long after it has stopped needing one. Worth agreeing the
    trigger for removing it (Babel v1.19 shipping? the ci-es index rebuilt from it?) before
    this merges. Tracked in Decide the trigger for removing NameResHasBlocklist = false from [ci-es] #144, which lists the candidate triggers and what "done" means;
    note that the most accurate of them is itself blocked on NameRes ES: /status reports numDocs 0 and no babel_version #135, since NameRes ES
    /status cannot currently say which Babel release an index was built from.

Testing

gaurav and others added 6 commits September 1, 2026 15:48
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
namelookup-es implements no blocklist at all. Every `negative`-flagged sheet row
and all 313 blocklist-sheet entries therefore failed against --target ci-es,
which is ten to three hundred assertions a day all saying the same known thing
and drowning the findings that matter.

targets.ini already exists to record this kind of per-target capability rather
than infer it from the hostname, so add NameResHasBlocklist there, defaulting to
true in [DEFAULT] and false in [ci-es]. A target that forgets to declare it keeps
its blocklist coverage rather than silently losing it.

Filter on the `negative` flag rather than on the "Blocklist Nov 2023" category:
only 16 rows carry the flag, 5 of them live in "Unit Tests", and row 175
(hypersexuality) is an ordinary positive row that happens to sit in the Blocklist
category. Category would have been wrong in both directions.

test_blocklist.py skips in the test body rather than in pytest_generate_tests,
because target_info is parametrized by the root conftest and does not exist at
generation time.

--target ci-es tests/nameres/: 164 failed -> 58 failed, 329 skipped.
--target ci is unchanged: 196 passed, 117 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test_label calls pytest.xfail() imperatively when the expected CURIE is in the
top NameResXFailIfInTop (5) but not first, so a demotion from rank 1 to rank 2
is reported as an xfail and never appears in the failure count. Against
namelookup-es that hides the single largest regression there is: 46 rows moved
from rank 1 into ranks 2-6, against 32 that fail outright.

Recording the rank as a user_property puts it in --report-jsonl, where a
cross-environment comparison can see it. Raising NameResXFailIfInTop instead
would have made every other environment noisier to fix one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The assertion vocabulary could not express either of the two things the NameRes
ES validation most needs to say.

SearchByName is top-N, with N fixed at 5 by NameResXFailIfInTop in targets.ini,
so it cannot distinguish "the concept wins" from "the concept is somewhere in the
top five". That is exactly the largest regression against namelookup-es: 46 rows
moved from rank 1 into ranks 2-6, and a SearchByName assertion passes on both
services for every one of them. SearchByNameTopResult passes only on rank 1, and
it still asks for the top N so a failure can say whether the CURIE was at rank 4
(a ranking problem) or absent (a retrieval one) — different symptoms with
different owners.

DoesNotSearchByName is the first negative NameRes assertion of any kind; until
now only NodeNorm had DoesNotResolve/DoesNotResolveWith. It makes a blocklisted
term expressible: the term stays searchable, but the concept must not come back.

A CURIE it cannot normalize is a failure there, not a pass. "We could not look it
up" and "it was not returned" both end with the CURIE absent from the results, so
the other choice would let a typo'd blocklist assertion succeed forever while
testing nothing. DoesNotResolve takes the opposite line (VALIDATE_CURIES = False)
because there the CURIE failing to resolve is the assertion.

Both format untrusted params with !r, per the rule in CLAUDE.md — the neighbouring
SearchByNameHandler predates it and still uses plain quotes.

Neither is used by any filed issue yet: an issue's assertions run against every
--target with no way to scope them, so one that fails on ES and passes on Solr
would report a strict XPASS failure on every other environment daily.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The NameResHasBlocklist skips had no test, which is the wrong way round: a
capability that reads false when it should read true does not fail, it *skips*,
and the run stays green while testing nothing. So assert the checked-in
targets.ini directly (every section parses, only ci-es opts out, an absent key
keeps its coverage) and drive both skips against a synthetic target.

The last two tests catch BaseException rather than Exception, deliberately.
pytest's outcome exceptions derive from BaseException, so `pytest.raises(Exception)`
misses them entirely; and catching Failed specifically would be worse than
useless, because a regressed skip would then propagate and mark the test itself
skipped, which reads as passing.

Also pins expected_rank, added in b065aa1, by faking the lookup: the rank-2-6
branch calls pytest.xfail(), so recording the rank before it is the only reason a
comparison can see a demotion from rank 1.

CLAUDE.md gets the two things this session cost real time to work out: that a
NameRes gsheet run's failure count understates regressions, with the classification
needed to read --report-jsonl properly, and that an -es target must be compared
against ci rather than dev or exp, which are newer Babel releases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md requires untrusted text to be formatted with %r/!r, and the search
query is untrusted: it comes from a GitHub issue body and is the one param
curie_params() excludes from CURIE validation, because it is free text bound for
a URL query string.

Not a live hole — AssertionHandler._rejection() rejects a non-printable param
before any handler sees it, and it is deliberately the one check that runs over
every param for exactly this reason. But it is the only thing standing between an
issue body and these messages, repr is the belt to its braces, and having
SearchByName quote its query one way while SearchByNameTopResult and
DoesNotSearchByName quote it another invites the next handler to copy the wrong
one.

Message punctuation changes; no test asserts on it, and none is added — an
assertion that a message contains a particular kind of quote restates the
implementation, and the escaping this guards against is unreachable past
_rejection().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant