Use index-backed RDKit operators and rank similarity search results - #196
Merged
Conversation
The component search predicates were built as function calls (substruct(...), tanimoto_sml(...) >= t). PostgreSQL only uses the rdkit GiST indexes through the matching operators, so each substructure/SMARTS/similarity search sequentially scanned rdkit.mols, evaluating the RDKit function on every row. Switch to the operator forms, mirroring the reaction-SMARTS query that already used @>: - SUBSTRUCTURE: mol @> %s::mol - SMARTS: mol @> %s::qmol - SIMILAR: morgan_bfp % morganbv_fp(%s) The similarity threshold and chirality flag can't be passed to those operators inline, so they are supplied out of band via the rdkit.tanimoto_threshold and rdkit.do_chiral_sss session settings (set_config) before the query runs; connections are per request, so the settings don't leak. Verified with EXPLAIN that the operator forms use the GiST indexes where the function forms could not. Also rank similarity results. When a query has exactly one similarity predicate, the matched reactions are scored by their greatest component Tanimoto similarity and ordered descending, with the LIMIT applied after scoring so it selects the most similar matches rather than an arbitrary subset. fetch_reactions now preserves the order of its input IDs so the ranking survives the proto fetch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…elper set_config(local=true) scopes the similarity/chirality settings to the current transaction (connections use autocommit=False, so the setting covers the query and resets at transaction end), which is safe even if the connection is later pooled. Rename the ranking test helper to _max_input_similarities to make its input-component scoping explicit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a target parameter to _max_similarities (mirroring ReactionComponentQuery._mols_join) so it generically recomputes INPUT- or OUTPUT-component similarities, per reviewer feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related search improvements, both in
ord_interface/api/queries.py.1. Index-backed RDKit operators (speedup)
The component predicates were built as function calls —
substruct(...),tanimoto_sml(...) >= t. PostgreSQL only uses therdkitGiST indexes (mol_index,morgan_bfp_index) through the matching operators, so each substructure/SMARTS/similarity search sequentially scannedrdkit.mols, evaluating the RDKit function on every row. (The reaction-SMARTS query already used@>; the component queries just hadn't followed suit.)substruct(mol, %s)mol @> %s::molsubstruct(mol, %s::qmol)mol @> %s::qmoltanimoto_sml(...) >= tmorgan_bfp % morganbv_fp(%s)substruct_chiral(...)@>+rdkit.do_chiral_sssThe operators can't take the similarity threshold or chirality flag inline, so those are supplied out of band via the
rdkit.tanimoto_threshold/rdkit.do_chiral_ssssession settings (set_config) before the query runs. Connections are per request, so the settings don't leak.Verified with
EXPLAINthat the operator forms use the GiST indexes where the function forms forced a seq scan. On a synthetic table up to 200k molecules, a selective substructure or similarity search runs ~15–50× faster, and the gap grows with table size (the function form is linear in row count).2. Rank similarity results
Previously each predicate collapsed to an unordered set of reaction IDs. Now, when a query has exactly one similarity predicate, the matched reactions are scored by their greatest component Tanimoto similarity and ordered descending. The
LIMITis applied after scoring, so it selects the globally most-similar matches rather than the top of an arbitrary subset.fetch_reactionsnow preserves the order of its input IDs, so the ranking survives the final proto fetch (both the sync/queryand asyncsubmit_querypaths).With zero or multiple similarity predicates, results are returned unordered (as before).
Tests
test_similarity_rankingindependently recomputes each reaction's max similarity and asserts the order is non-increasing and thatLIMITreturns the true top-N of the full ranking.All
ord_interface/apitests pass;ty/ruffclean.🤖 Generated with Claude Code
Greptile Summary
This PR improves component search in two ways: it replaces function-call predicates (
substruct,tanimoto_sml >= t) with the matching RDKit operators (@>,%) that the PostgreSQL GiST indexes understand, and adds a two-phase similarity ranking — filter with the index operator, then score the filtered set withtanimoto_sml()and applyLIMITafter sorting.rdkit.do_chiral_sss) and threshold (rdkit.tanimoto_threshold) are supplied via transaction-localset_config, so GUC values don't escape the request even with pooled connections.run_queriesdetects a single similarity predicate, skips the earlyLIMIT, and delegates ranking to_rank_by_similarityusingtanimoto_sml()directly.fetch_reactionsnow preserves input order so the ranking survives the final proto fetch.test_similarity_rankingindependently recomputes max similarities and asserts both non-increasing order and thatlimit=10returns exactly the true top-10.Confidence Score: 5/5
Safe to merge — the operator swap is semantically equivalent to the function-call forms, GUC settings are transaction-local, and the ranking/ordering logic is verified by the new test.
Both changes are well-contained: the index operator swap is validated by existing match-count tests, and the new ranking path is independently cross-checked by test_similarity_ranking. The fetch_reactions order-preservation fix is straightforward and the async Redis path correctly stores and restores the ranked ID list.
No files require special attention.
Important Files Changed
Reviews (3): Last reviewed commit: "Parametrize ranking test helper by targe..." | Re-trigger Greptile