Search: each group counts its matches and then fetches them, two full scans per group with SQLite's Python LIKE #1116
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#1116
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What exists today
src/postulo/core/search.py:149-155take()returnsFound(total=rows.count(), hits=[build(row) for row in rows[:limit]]): oneCOUNTand one ordered slice per group.search()(:658) calls ten group functions (GROUPS,:640-651);search_career()(:550) loops over several career models, each with its owntake(). The rows are filtered withicontainsover several fields and ordered byranked()(:132), aCASE WHEN title icontains. On SQLite,core/sqlite_like.py:50-51replaceslikewith a Python function (#505, so that case folding works beyond ASCII), so every row of every filtered table is passed through Python once for the count and again for the ordered slice.Measured with
CaptureQueriesContexton the second GET ofcore:search?q=aperturewith 300 companies, under the test settings (probeaudit/m_scripts/test_probe_queries.py): 51 queries, 75 ms, two per table.What changed from the candidate: search does not run "per keystroke". The search-everything button in
templates/cotton/site_search.htmlis a plain submit tocore:search; only the table filters search live, and they do not usetake(). So the cost is per search, not per letter.What would change
One query per group: annotate the total with a window function over the filtered rows and take the first
limit(rows.annotate(total=Window(Count("pk")))[:limit]), reading the total off the first row, and 0 when there is none. Window functions run on SQLite 3.25+ and PostgreSQL, both supported. The "and N more" line keeps its exact number. On SQLite the Pythonlikestill runs once per row, but once rather than twice.What it saves or protects
small: about half the scans of a search (51 queries for 10 groups and the career sections, measured 75 ms with 300 companies), more on SQLite where every LIKE is a Python call
Accessibility
none
Decisions
limit + 1and say "more" without a number? Recommended: the window count, so nothing a person sees changes.Found by the efficiency and reliability audit of 2026-10-30 (finding Q5; index postulo/postulo#1108). Checked against
mainatdaee30b15(2026-10-30).