8000 Add a harness to evaluate images in Seed Search by daoudclarke · Pull Request #611 · mwmbl/mwmbl · GitHub
[go: up one dir, main page]

Skip to content

Add a harness to evaluate images in Seed Search - #611

Merged
daoudclarke merged 5 commits into
wikicommons_image_searchfrom
seed-images-eval-harness
Nov 10, 2026
Merged

daoudclarke merged 5 commits into
wikicommons_image_searchfrom
seed-images-eval-harness

Conversation

@daoudclarke
@daoudclarke daoudclarke commented Nov 9, 2026 •
Copy link
Copy Markdown
Contributor

Stack

PR 1 of 7 in the #610 evaluation stack: #611 ← #612 ← #613 ← #616 ← #615 ← #617 ← #614. Merge in that order.

The base is wikicommons_image_search, not main. That branch holds mwmbl/imagesearch/. It is unmerged, and its PR (#607) is closed.

What changed

This adds mwmbl/rankeval/seed_images/, an offline harness for testing whether Wikimedia Commons images belong in Seed Search (Combined Search, ordered by Jev). It has four resumable steps. Each caches its output under the gitignored devdata/seed_images/.

  1. queries: builds two query sets, each with the web top ten it is served. train is the 849 LLM-labelled queries, in Staan's order. engb is the 295 en-gb evaluation queries, in production order (Jev composite + Staan rank). It also writes the en-gb top tens as a holistic_judge lists file for the baseline.
  2. collect: fetches the top 20 Commons candidates per query with image search's own search_commons. That reuses its User-Agent, concurrency cap and back-off, and adds a retry that waits out the cooldown.
  3. local_verdicts: runs image search's own layers on every distinct candidate: the deny rules, NudeNet and SigLIP2. Thumbnails are held in memory only, as in the pipeline. It keeps the scores as well as the verdicts.
  4. run_jev: asks Jev in "separate" mode and records each request's latency:

jev_images also defines the image-relevance question and combined_request, the single request production would send. It reuses jev_order's retry and request-keyed disk cache. pyproject.toml leaves mwmbl/rankeval/seed_images/* out of coverage.

Results

The harness ran end to end on all 1,144 queries. These counts and latencies are from its output in devdata/seed_images/.

Data Count
Queries (849 train + 295 en-gb) 1,144
Queries with Commons candidates (NSFW requests) 950
Candidates (top 20 per query) 16,512
Distinct images, all checked by the local layers 15,658
Thumbnails fetched 15,658
Local layer Flags (of 15,658)
Deny rules 231
NudeNet 331 NSFW, 96 uncertain
SigLIP2 3,525
Jev request (separate mode) Requests p50 p95 Worst Over 1.5 s
NSFW, per query's candidates 950 0.33 s 0.56 s 1.81 s 2
Image need, per query 1,144 0.28 s 0.40 s 1.34 s 0

The later PRs use this data. Of the 16,512 candidates, 15,846 score below 1.0 on Jev NSFW (#612).

How verified

  • make check passes. test/test_seed_images_eval.py (new, no network) and test/test_image_search.py pass.
  • A code-review subagent and a trust-and-safety subagent reviewed the change. Fixes that followed:
    • A response read from jev_cache (a killed run, or an identical request) was timed at file-read speed. It is now recorded with a null latency.
    • run_jev set up Django before importing jev_rank, which failed with AppRegistryNotReady outside pytest.
  • Coverage: mwmbl/rankeval/seed_images/* is omitted from coverage, set in this PR, because it is offline evaluation code.
  • Running the harness against a shared Redis would set image search's Commons cooldown key there.

Decisions already made

  • Only images that pass the Jev NSFW check (score below 1.0) are shown or indexed. There is no additional pixel check behind it.

Nothing outside mwmbl/rankeval imports this code, so production is unchanged.

Part of #610

🤖 Generated with Claude Code

daoudclarke and others added 2 commits November 9, 2026 15:49
Collects each query's top 20 Wikimedia Commons candidates for the 849
training and 295 en-gb queries, runs image search's own safety layers
(deny rules, NudeNet, SigLIP2) on each, and asks Jev the text-only NSFW
question from #610 and a new "would images improve this page" question.
Also defines the image relevance question and the combined request
production would send. Everything is cached under devdata/seed_images/.

Part of #610

No functional changes to the crawler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jev_rank reaches Django models at import time, so run_jev failed with
AppRegistryNotReady outside pytest.

Part of #610

No functional changes to the crawler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov
codecov Bot commented Nov 9, 2026 •
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

A response already in jev_cache, left by a killed run or sent for an
identical request body, was timed at file-read speed and recorded as if
it were a real call's latency.

Part of #610

No functional changes to the crawler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
They are offline evaluation tools for #610, not served code, so their
patch coverage shouldn't gate the pull requests that add them.

No functional changes to the crawler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


def image_from_json(fields: dict) -> SourceImage:
parsed = {"categories": tuple(fields["categories"]), "uploaded_at": datetime.fromisoformat(fields["uploaded_at"])}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Shouldn't we add validation here? What if there's a field missing or an invalid format?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If that was the case it would have been found when running the evaluation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Probably a good idea to still add simple validation here (fail-fast, defense in depth, etc.). If there's a missing or invalid field, we'd rather know sooner than later.

"Judging only by its Wikimedia Commons file name, description and categories, how likely is it that the "
"image `image` shows nudity under `policy`?"
)
NSFW_CRITERIA = [
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Most other systems (e.g. SafeSearch, models we looked at) use a three-level classification system. Are you sure we want four? Should we combine the first/lowest two?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reply from Claude: The four levels are the criteria for a Jev score, not output classes. Jev returns a continuous score from 0 to 3, and we apply one threshold: an image passes only if it scores below 1.0. The three-way safe/uncertain/NSFW verdict comes from the local layers, not this scale.

Keeping the two lowest levels apart is what makes the gate strict. If we merged them, "a person may appear, but nothing suggests they are unclothed" would sit at the bottom of the scale, many more pictures of people would pass under the same threshold, and we'd have to measure a new threshold. The question and criteria are also the #610 safety benchmark's, word for word, and the threshold was measured on them. So I've left it as it is.

start = time.monotonic()
try:
result = fetch()
except ImageSourceUnavailable as e:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should separately catch SourceRejectedRequest, which is a subclass of this exception, and propagate it immediately. SourceRejectedRequest is a permanent failure, not a transient one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reply from Claude: Done in d90d71c. with_backoff now re-raises SourceRejectedRequest straight away instead of retrying it, with tests for both the retry path and the rejection path. The candidates already collected don't change.

def image_text(image: SourceImage) -> str:
description = image.description[:DESCRIPTION_CHARS]
categories = " | ".join(image.categories)
return f"File name: {image.title}\nDescription: {description}\nCategories: {categories}"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These three fields are specified by uploaders *unknown) and should be treated as potentially unsafe data. We should tell Jev that this data is untrusted user input and cannot/should-not be treated as instructions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reply from Claude: Agreed. Production should tell Jev that the file name, description and categories are untrusted uploader text and must not be followed as instructions. I haven't changed the prompt here, because every Jev score in this stack was measured with it. Changing it now would put the code out of step with the results. Instead it's in the eval doc's production recommendations (#614), next to the prompt-injection residual risk. It applies to the relevance question as well as NSFW. The new prompt's threshold must be re-checked on the benchmark, and tested separately with descriptions that address Jev.

SourceRejectedRequest subclasses ImageSourceUnavailable, so the collection
retry treated a permanent refusal as a back-off and retried it ten times.
The collected candidates are unchanged.

No functional changes to the crawler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
daoudclarke added a commit that referenced this pull request Nov 9, 2026
… production list

Production must tell Jev that Commons file names, descriptions and categories
are untrusted uploader text, and re-check the NSFW threshold with that prompt
(raised in review on #611). The wrong-sense item now says how: give Jev's
image-relevance question the top web result. The production list also carries
the holistic comparison's escaping and Wikipedia-card items, and the gate
recommendation links the holistic section instead of naming a PR.

No functional changes to the crawler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@daoudclarke
daoudclarke merged commit 0b161ab into wikicommons_image_search Nov 10, 2026
22 checks passed
@daoudclarke
daoudclarke deleted the seed-images-eval-harness branch November 10, 2026 06:13
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.

2 participants

0