Skip to content

Speed up Dirichlet character searches using mathematical relations - #7143

Open
roed-math wants to merge 4 commits into
LMFDB:mainfrom
roed-math:ai/t01-dirichlet-timeouts
Open

roed-math wants to merge 4 commits into
LMFDB:mainfrom
roed-math:ai/t01-dirichlet-timeouts

Conversation

@roed-math

Copy link
Copy Markdown
Contributor

Closes #6733.
Closes #6422.

Diagnosis of the recurring Dirichlet character search timeouts (562M-row char_dirichlet): the plain-column cases from the issue threads are already index-fixed by the indexes Andrew added, and no new index is needed. The surviving timeout class is an indexed constraint plus a sparse boolean, e.g. conductor=17 & primitive walks all 230,769 conductor-17 rows to find 4 primitive ones (cancelled after 180s on devmirror; 87s in edgarcosta's EXPLAIN on beta).

The fix is mathematical, in the search parser:

  • Primitivity is equivalent to conductor = modulus, so constraints on either are mirrored onto the other (including ranges and comma-list $or branches), turning that query into a 45ms two-column index scan.
  • Characters induced from psi inherit order, parity, realness and conductor from psi, so inducing searches now pin those (enabling better indexes), and contradictions short-circuit.
  • Provably empty searches (odd order with odd parity, real with order > 2, conductor not dividing modulus, and so on) return immediately with an explanatory message instead of scanning the table.

Verified with EXPLAIN ANALYZE on devmirror (>180s cancelled, now 44.8ms for the issue query), 15 URLs through the test client covering every new path, and three new tests; pyflakes clean.


Ported from roed-math#6, where the full write-up and comment history live.

🤖 Generated with Claude Code

roed314 and others added 3 commits July 19, 2026 00:56
…MFDB#6733, LMFDB#6422)

With the new indexes, remaining production timeouts come from an indexed
column combined with a sparse boolean filter, e.g. conductor=17 &
is_primitive=yes walks all 230K conductor-17 rows to find the 4
primitive ones (>180s on devmirror).  Since a character is primitive if
and only if its conductor equals its modulus, mirror conductor/modulus
constraints when is_primitive=yes so the query is answered from the
(conductor, modulus, orbit) index (45ms).  Searches for characters
induced by psi now inherit psi's order, parity and realness, and
searches that are provably empty (odd order with odd parity, is_real
with order > 2, conductor not dividing modulus, etc.) short-circuit
with an explanatory message instead of scanning the table.

Verified with EXPLAIN ANALYZE on devmirror and new tests in
lmfdb/characters/test_characters.py (3 passed); pyflakes clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up on the previous commit, which normalized realness and
parity only when a top-level order constraint already settled the
question.

is_real is now completely replaced by an order constraint rather than
merely dropped when it was redundant: is_real=yes imposes order <= 2,
is_real=no imposes order >= 3, the interval is intersected with any
order the user asked for, and the boolean is removed.  This covers the
searches that previously kept the sparse boolean, in particular
conductor=17&is_real=yes, whose count went from an index scan of every
conductor-17 row filtering is_real (cost 899,246) to an index only scan
with Index Cond conductor = 17 AND "order" <= 2 (cost 14,620).

The odd-order/parity and inducing-order facts are now also applied
inside the $or branches where parse_ints stores comma separated inputs,
so order=3,5&parity=odd, is_real=yes&order=3,5 and inducing=3.b&order=3,5
short-circuit like their scalar counterparts, and order=2,3&parity=odd
drops just the impossible branch.  A comma separated conductor is
likewise pruned against the conductor of an inducing character, which
the existing check could not see.

The constraint arithmetic is factored out as intersect_ints, which also
replaces the bound juggling in refine_primitive_search, together with
prune_or_branches (narrow branches by the top level constraint, drop the
impossible ones) and collapse_single_branch (fold a lone survivor back
into the top level).

Verified on devmirror with EXPLAIN plans for the searches above and with
new tests: a DirichletSearchQueryTest class asserting the normalized
query dictionary, a check that six rewritten queries return exactly the
labels the booleans they replace would select, and coverage of the
result_count, list and random search modes.  pyflakes and ruff clean.

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

Copy link
Copy Markdown
Contributor Author

Autonomous review by an OpenAI dot

Reviewed by dot, powered by OpenAI; exact model variant was not recorded.

Outcome: Review incomplete / further validation needed.

The current-main merge conflicts need resolving while preserving both sets of tests: this PR's query-rewrite tests and main's field-column/download tests, plus their imports. Please validate the resolved version against current main before merging.

I reviewed both changed files, interval/list rewrites, inducing-character invariants, search/count/random/download paths, and the linked issue and fork discussions. The mathematical implications check out, including retaining the primitive filter for non-singleton ranges.

Existing character-test logs show all 34 tests passing, including the nine added tests; lint passed too. Those checks tested an older synthetic merge, before main's subsequent character-page changes and Sage 10.6 upgrade. They do not validate the pending conflict resolution. No new Sage runs, live-database queries, browser checks, or performance measurements were performed; production query-plan improvements remain author-reported.

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.

TImeout on Dirichlet characters Dirichlet Character search timeouts

2 participants