Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d3d727a
Add SneakyBox to search boxes
rvisser7 Mar 11, 2026
0bc76de
Add multi-label search to search_wrapper
rvisser7 Mar 11, 2026
49439a5
Implements multi-label search functionality in number_field and searc…
rvisser7 Apr 10, 2026
53a422f
Add 'handle_multi_jump_search' function and update search array handling
rvisser7 Apr 10, 2026
7306887
Add tests for multi-label search
rvisser7 Apr 10, 2026
9571a66
Change "labels_knowl" to "label_knowl"
rvisser7 Apr 10, 2026
f8c122c
Merge branch 'main' into new_jump_box
rvisser7 Apr 10, 2026
7579f46
Correct number field multi-label jump test
rvisser7 Apr 11, 2026
af1edea
Some light edits to the multi-entry jump box search parsing
rvisser7 Apr 11, 2026
2332fac
Small typo fix in number field import
rvisser7 Apr 11, 2026
98e9e09
Small bug fixes
rvisser7 Apr 11, 2026
3879b6f
Added time_limit (with timer) to multi_entry_jump_search
rvisser7 Apr 11, 2026
6874f3b
Some minor extra comments added
rvisser7 Apr 11, 2026
4dfa224
Fix some typos in parse_labels function
rvisser7 Apr 11, 2026
f1bbfdc
Add split_top_level_commas function for parsing jump box input
rvisser7 Apr 11, 2026
8dbe81f
Moved split_top_level_commas function for jump box input parsing and …
rvisser7 Apr 11, 2026
255215f
Small change to parse_labels function
rvisser7 Apr 11, 2026
bcf1d9e
Small whitespace edit in number_field.py
rvisser7 Apr 11, 2026
3074823
Update multi_entry_jump_search docstring to clarify default values fo…
rvisser7 Apr 11, 2026
76d375a
Updated jump box time limit to 20 seconds, small edits to time limit …
rvisser7 May 5, 2026
5b5a2dc
Small update to timeout warning message
rvisser7 May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion lmfdb/number_fields/number_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
parse_floats, parse_subfield, search_wrap, parse_padicfields, integer_options,
raw_typeset, raw_typeset_poly, flash_info, input_string_to_poly,
raw_typeset_int, compress_poly_Q, compress_polynomial, CodeSnippet, redirect_no_cache)
from lmfdb.utils.search_wrapper import multi_entry_jump_search
from lmfdb.utils.web_display import compress_int
from lmfdb.utils.interesting import interesting_knowls
from lmfdb.utils.search_columns import SearchColumns, SearchCol, CheckCol, MathCol, ProcessedCol, MultiProcessedCol, CheckMaybeCol, PolynomialCol
Expand Down Expand Up @@ -817,6 +818,17 @@ def interesting():


def number_field_jump(info):
# If jump box input is a comma-separated list of labels/names/polynomials, then use multi_entry_jump_search to return a search page
multi_jump = multi_entry_jump_search(
info,
parse_entry=nf_string_to_label,
label_exists=db.nf_fields.label_exists,
index_endpoint=".number_field_render_webpage",
object_name="number fields",
)
if multi_jump is not None:
return multi_jump

query = {'label_orig': info['jump']}
try:
parse_nf_string(info, query, 'jump', name="Label", qfield='label')
Expand Down Expand Up @@ -1165,6 +1177,7 @@ def nf_code(**args):

class NFSearchArray(SearchArray):
noun = "field"
label_knowl = "nf.label"
sorts = [("", "degree", ['degree', 'disc_abs', 'disc_sign', 'iso_number']),
("signature", "signature", ['degree', 'r2', 'disc_abs', 'disc_sign', 'iso_number']),
("rd", "root discriminant", ['rd', 'degree', 'disc_abs', 'disc_sign', 'iso_number']),
Expand All @@ -1177,7 +1190,7 @@ class NFSearchArray(SearchArray):
jump_example = "x^7 - x^6 - 3 x^5 + x^4 + 4 x^3 - x^2 - x + 1"
jump_egspan = r"e.g. 2.2.5.1, Qsqrt5, x^2-5, or x^2-x-1 for \(\Q(\sqrt{5})\)"
jump_knowl = "nf.search_input"
jump_prompt = "Label, name, or polynomial"
jump_prompt = "Label, name, polynomial, or comma-separated list"
has_diagram = False

def __init__(self):
Expand Down
8 changes: 8 additions & 0 deletions lmfdb/number_fields/test_numberfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ def test_search_zeta(self):
def test_search_sqrt(self):
self.check_args('/NumberField/?jump=Qsqrt-163&search=Go', '41') # minpoly

def test_search_multiple_fields(self):
# Test comma-separated list of field labels
self.check_args('/NumberField/?jump=2.2.5.1%2c+3.3.49.1&search=Go', '2.2.5.1')
self.check_args('/NumberField/?jump=2.2.5.1%2c+3.3.49.1&search=Go', '3.3.49.1')
# Test comma-separated list with different input formats
self.check_args('/NumberField/?jump=Qsqrt5%2c+x%5E2-3&search=Go', '2.2.5.1')
self.check_args('/NumberField/?jump=Qsqrt5%2c+x%5E2-3&search=Go', '2.2.12.1')

def test_search_disc(self):
self.check_args('/NumberField/?discriminant=1988-2014', '401') # factor of one of the discriminants

Expand Down
4 changes: 3 additions & 1 deletion lmfdb/utils/search_boxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,7 @@ class SearchArray(UniqueRepresentation):
"""
_ex_col_width = 170 # only used for box layout
sort_knowl = None
label_knowl = None # Link to knowl for label (used for "labels" SneakyBox)
sorts = None # Provides an easy way to implement sort_order: a list of triples (name, display, sort -- as a list of columns or pairs (col, +-1)), or a dictionary indexed on the value of self._st()
null_column_explanations = {} # Can override the null warnings for a column by including False as a value, or customize the error message by giving a formatting string (see search_wrapper.py)
noun = "result"
Expand Down Expand Up @@ -711,7 +712,8 @@ def main_array(self, info):
if info is None:
return self.browse_array
else:
return self.refine_array
# Append a "Labels" search box allowing multiple labels to be passed via URL
return self.refine_array + [[SneakyTextBox(name="labels", label="Labels", knowl=self.label_knowl)]]

def _print_table(self, grid, info, layout_type):
if not grid:
Expand Down
136 changes: 134 additions & 2 deletions lmfdb/utils/search_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import time
from random import randrange
from flask import render_template, jsonify, redirect, request
from flask import render_template, jsonify, redirect, request, url_for
from urllib.parse import urlparse
from psycopg2.extensions import QueryCanceledError
from psycopg2.errors import NumericValueOutOfRange
Expand All @@ -8,7 +9,7 @@

from lmfdb.app import app, ctx_proc_userdata, is_debug_mode
from lmfdb.utils.search_parsing import parse_start, parse_count, SearchParsingError
from lmfdb.utils.utilities import flash_error, flash_info, flash_success, to_dict
from lmfdb.utils.utilities import flash_error, flash_warning, flash_info, flash_success, to_dict
from lmfdb.utils.completeness import results_complete

# For diagram search support in SearchWrapper:
Expand Down Expand Up @@ -38,6 +39,136 @@ def use_split_ors(info, query, split_ors, offset, table):
)


def split_top_level_commas(text):
"""
A function which takes an input string and returns a list of strings, splitting on commas that are not inside parentheses/brackets/braces.
Used as the default separator function when parsing jump box input for multiple entries.
"""

entries = []
chunk = []
depth = 0
for ch in text:
if ch in "([{":
depth += 1
chunk.append(ch)
elif ch in ")]}":
depth = max(depth - 1, 0)
chunk.append(ch)
elif ch == "," and depth == 0:
entry = "".join(chunk).strip()
if entry:
entries.append(entry)
chunk = []
else:
chunk.append(ch)

entry = "".join(chunk).strip()
if entry:
entries.append(entry)
return entries


def multi_entry_jump_search(info, parse_entry, label_exists, index_endpoint, input_key="jump", labels_key="labels",
sep=split_top_level_commas, object_name="records", time_limit=20):
"""
Generic handler for jump boxes that supports comma-separated input of various entries (labels/names/polynomials/equations etc.).

Returns ``None`` if there is at most one entry, allowing the caller's single-entry jump logic to run.
Otherwise returns a redirect to a search page of the given labels.

INPUT:

- ``info`` -- the info dictionary passed in from front end
- ``parse_entry`` -- a custom function which converts a string (e.g. polynomial, equation, nickname, etc.) to be parsed into a label
- ``label_exists`` -- a custom function which determines whether a given label exists in the database
- ``index_endpoint`` -- the input to "url_for" which returns the index homepage for this section
- ``input_key`` -- the dictionary key for the jump search box (default: "jump")
- ``labels_key`` -- the dictionary key for the labels search query (default: "labels")
- ``sep`` -- A function used to separate out jump box input into separate entries (default: split_top_level_commas)
- ``object_name`` -- The name of the objects in the database (e.g. "fields", "elliptic curves"). Used when flashing info or error messages.
- ``time_limit`` -- a time limit (in seconds) for the maximum total amount of time this query should take (default: 20)
"""

jump_input = info.get(input_key, "")
entries = [s.strip() for s in sep(jump_input) if s.strip()]
if len(entries) <= 1:
return None

# For each entry given in the comma-separated jump box input, we attempt to parse the entry using parse_entry (while skipping duplicates)
# If the user inputs a large number of entries, this may take a long time (e.g. for number fields, this might require calling Pari's polredabs on every entry)
# We start a timer, and stop parsing entries if after parsing i entries, it's predicted that parsing i+1 entries will exceed the time_limit (default: 20 seconds)

labels, seen = [], set()
not_parsed, not_found = 0, 0
start_timer = time.monotonic()
for i in range(len(entries)):
# Check if doing the (i+1)-th entry will exceed the time limit
if (i>0) and (time.monotonic() - start_timer > (i*time_limit)/(i+1)):
flash_warning("Search query timed out after processing the first %s out of %s entries in the input box. Only the first %s entries are included in the search results below.", i, len(entries), i)
break

# Attempt to parse entry
try:
label = parse_entry(entries[i])
except (SearchParsingError, ValueError):
not_parsed += 1
continue
if not label_exists(label):
not_found += 1
continue
if label not in seen:
labels.append(label)
seen.add(label)

# Flash error and return index page if no entries successfully parsed
if not labels:
flash_error("None of the %s entries matched %s in the database.", len(entries), object_name)
return redirect(url_for(index_endpoint))

# Otherwise flash info message with number of entries we are able to parse
ignored = not_parsed + not_found
duplicates = len(entries) - ignored - len(labels)
if ignored:
flash_info("Matched %s of %s entries; ignored %s unrecognized or missing entries.", len(labels), len(entries), ignored)
if duplicates > 0:
flash_info("Removed %s duplicate label(s).", duplicates)

return redirect(url_for(index_endpoint, **{labels_key: ",".join(labels)}))


def parse_labels(info, query, table, labels_key="labels"):
"""
Parse a list of labels from the URL "?labels=" query into a database query.
Mainly used when multiple entries are given in the search jump box.
"""

labels_input = info.get(labels_key)
if not labels_input or not hasattr(table, "_label_col"):
return

# Separate out labels from input, stripping whitespace and removing duplicates while preserving order
labels = list(set(label.strip() for label in labels_input.split(",")))
seen = set(labels)
if not labels:
return

label_col = table._label_col
existing = query.get(label_col)
if existing is None:
query[label_col] = {"$in": labels}
elif isinstance(existing, dict):
if "$in" in existing:
existing["$in"] = [label for label in existing["$in"] if label in seen]
else:
# Keep existing constraints and add an $in constraint as well.
existing["$in"] = labels
else:
# Existing exact match constraint: keep it only if it appears in labels.
if existing not in seen:
query[label_col] = {"$in": []}


class Wrapper:
def __init__(
self,
Expand Down Expand Up @@ -85,6 +216,7 @@ def make_query(self, info, random=False):
template_kwds = {key: info.get(key, val()) for key, val in self.kwds.items()}
try:
errpage = self.f(info, query)
parse_labels(info, query, self.table)
except Exception as err:
# Errors raised in parsing; these should mostly be SearchParsingErrors
if is_debug_mode():
Expand Down
Loading