Skip to content

Tunable multi-hit name resolution with structure clustering + OPSIN - #2

Merged
simulkade merged 1 commit into
mainfrom
feature/tunable-multihit-search
Jun 12, 2026
Merged

simulkade merged 1 commit into
mainfrom
feature/tunable-multihit-search

Conversation

@simulkade

Copy link
Copy Markdown
Collaborator

Problem

Search returned exactly one merged hit per query, and for name queries that hit was often wrong. Root cause: each source was collapsed to a single candidate (rows[0] / first synonym) before consensus scoring, so a wrong #1 hit from one source could dominate and the correct compound (hit #2 elsewhere) was never seen.

What changed

  • Candidate pooling — collect top-k candidates per source instead of one pre-chosen winner.
  • Structure clustering — pool clustered into distinct compounds by InChIKey (skeleton-aware), so a wrong top hit no longer dominates.
  • Query-aware ranking — clusters ranked by a confidence combining method base, how well each candidate matches the query (name similarity / Tanimoto), and cross-source support. Exact-identifier paths reproduce the previous confidence exactly.
  • Multi-hit output — n_hits (int or "all") + min_confidence return ranked distinct compounds with a hit_rank column. Default stays one row per query. New columns: hit_rank, n_source_support, opsin_smiles (+ optional alternatives). DataFrame/file extra columns broadcast across hit rows.
  • PYOPSIN anchoring (opt-in, use_opsin=True) — IUPAC names parsed to SMILES offline and used as a high-confidence anchor; pulls the correct compound by InChIKey regardless of name spelling. Graceful fallback when Java/py2opsin is absent.
  • Tunable knobs (were hard-coded): top_k_per_source, cluster_by_skeleton, fuzzy_score_cutoff, fuzzy_scorer, consensus_compat_threshold, query_weight.
  • All resolvers (cas/name/smiles/inchi/inchikey/dtxsid/formula) flow through one pool → cluster → rank → finalise pipeline.

Validation

scripts/validate_name_search.py reproduces the CASRN→table→reproduce-by-name idea using CompTox as ground truth (correctness = InChIKey skeleton). On a hard 200-chemical synonym sample (all sources):

metric default tuned (fuzzy + OPSIN + top_k=10)
correct (structure reproduced) 16.0% 99.5%
wrong hit (different structure) 0.0% 0.5%¹
not found (recall gap) 84.0% 0.0%
precision when a hit returned 100% 99.5%

¹ the single tuned "wrong hit" is an inorganic salt (Cerium(3+) triiodate) reported at confidence 0.0.

The wrong-hit failure mode is eliminated in precision-first default mode; the remaining default gap is recall, which the tunables (esp. OPSIN) close.

Tests

  • tests/test_search_multihit.py — 25 unit tests (clustering, validation, ranking, multi-hit, broadcast, OPSIN).
  • tests/test_search_precision_regression.py — integration test asserting zero wrong hits vs CompTox ground truth (skips when the offline DB is absent).
  • Full search suite: 162 passed, 1 failed. The failure (test_exact_inchikey_with_low_consensus) is pre-existing — it fails identically on main.

Notes

  • uv.lock is intentionally not included (its platformdirs change pre-existed this branch and is unrelated).

🤖 Generated with Claude Code

Reworks Search name resolution to fix wrong-hit selection and add flexible,
tunable matching:

- Candidate pooling: collect top-k candidates per source instead of collapsing
  each source to one pre-chosen winner before consensus.
- Structure clustering: pool is clustered into distinct compounds by InChIKey
  (skeleton-aware), so a wrong #1 hit from one source no longer dominates.
- Query-aware ranking: clusters ranked by a confidence combining method base,
  how well each candidate matches the query (name similarity / Tanimoto), and
  cross-source support. Exact-identifier paths reproduce the old confidence.
- Multi-hit output: n_hits (int or "all") + min_confidence return ranked
  distinct compounds with a hit_rank column; default stays one row per query.
  New columns: hit_rank, n_source_support, opsin_smiles (+ optional
  alternatives). DataFrame/file extra columns broadcast across hit rows.
- PYOPSIN anchoring (opt-in, use_opsin): IUPAC names parsed to SMILES offline
  and used as a high-confidence structure anchor; pulls the correct compound
  by InChIKey regardless of name spelling. Graceful fallback without Java.
- Tunable knobs (were hard-coded): top_k_per_source, cluster_by_skeleton,
  fuzzy_score_cutoff, fuzzy_scorer, consensus_compat_threshold, query_weight.
- All resolvers (cas/name/smiles/inchi/inchikey/dtxsid/formula) flow through
  the same pool -> cluster -> rank -> finalise pipeline.

Validation (scripts/validate_name_search.py) against CompTox CASRN ground
truth: on a hard synonym sample the resolver returns 0 wrong hits at 100%
precision (default), and enabling OPSIN lifts recall from 16% to ~99.5% while
holding precision. Adds a precision regression test asserting zero wrong hits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@simulkade
simulkade merged commit f7a9fc4 into main Jun 12, 2026
2 checks passed
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