Skip to content

fix(search): drop subquery wrapper that leaked LIMIT into COUNT#28

Merged
abdout merged 1 commit into
mainfrom
fix/search-trigram-count-bug
May 10, 2026
Merged

fix(search): drop subquery wrapper that leaked LIMIT into COUNT#28
abdout merged 1 commit into
mainfrom
fix/search-trigram-count-bug

Conversation

@abdout
Copy link
Copy Markdown
Contributor

@abdout abdout commented May 10, 2026

Hotfix. Verified live regression introduced by #26.

The bug

After #26 deployed, every autocomplete result returns listingCount = limit instead of the true count:

$ curl -s 'https://mk.databayt.org/api/search/locations?q=port&limit=1'
{"data":[{"city":"Port Sudan", ..., "listingCount": 1}]}    ❌

$ curl -s 'https://mk.databayt.org/api/search/locations?q=port&limit=10'
{"data":[{"city":"Port Sudan", ..., "listingCount": 10}]}   ❌

$ curl -s 'https://mk.databayt.org/api/search/locations?q=port&limit=20'
{"data":[{"city":"Port Sudan", ..., "listingCount": 20}]}   ❌

$ curl -s 'https://mk.databayt.org/api/search/locations?q='   # popular path
{"data":[{"city":"Port Sudan", ..., "listingCount": 100}]}  ✅

The popular-locations path (single SELECT, no subquery wrapper) returns 100 correctly against the same data. So the data and the JOIN are fine — the bug is in the trigram subquery shape.

Root cause

#26 wrapped the aggregation in a subquery to keep similarity(l.city, $q) out of the GROUP BY (so it could be ranked in the outer ORDER BY):

-- broken
SELECT city, state, country, n, sim FROM (
  SELECT l.city, l.state, l.country,
         COUNT(li.id)::bigint AS n,
         GREATEST(similarity(...), ...) AS sim
  FROM ...
  GROUP BY l.city, l.state, l.country
) sub
ORDER BY n DESC, sim DESC
LIMIT $limit;

Under that shape the planner pushes the outer LIMIT into the inner GROUP BY's COUNT — a Postgres planner pathology that surfaces with GIN-indexed predicates plus an outer LIMIT over aggregates. Result: COUNT(*) ≡ LIMIT.

Fix

Drop the subquery. Aggregate similarity with MAX(GREATEST(...)) in the same SELECT as COUNT:

-- correct
SELECT l.city, l.state, l.country,
       COUNT(li.id)::bigint AS n,
       MAX(GREATEST(
         similarity(l.city,    $1),
         similarity(l.state,   $1),
         similarity(l.country, $1)
       )) AS sim
FROM "Location" l
JOIN "Listing" li ON li."locationId" = l.id
WHERE ...
GROUP BY l.city, l.state, l.country
ORDER BY n DESC, sim DESC
LIMIT $5;

similarity(grouped_col, const) is functionally constant per group, so MAX is idempotent — same ranking semantics, no subquery, no planner bug.

Validation

  • pnpm typecheck → exit 0
  • pnpm vitest run tests/actions/search-actions.test.ts → 19 / 19 pass
  • After Vercel deploys this PR, expect ?q=port&limit=1listingCount=100

🤖 Generated with Claude Code

After deploying the trigram autocomplete (PR #26) to prod, every
result returned listingCount = limit instead of the true count:

  /api/search/locations?q=port&limit=1   → listingCount=1   ❌
  /api/search/locations?q=port&limit=10  → listingCount=10  ❌
  /api/search/locations?q=port&limit=20  → listingCount=20  ❌

Same prod data, only 1 grouped row, COUNT(*) varying with the outer
LIMIT clause. The popular-locations path (no subquery wrapper)
correctly returned 100 across the same data.

Root cause: the subquery wrapper from PR #26 was added to keep
similarity() outside the GROUP BY for clean ORDER BY ranking. But
the planner under that shape pushed the outer LIMIT into the
inner GROUP BY's COUNT — a known Postgres planner pathology with
GIN-indexed predicates and outer LIMITs over aggregates.

Fix: drop the subquery. Aggregate similarity with MAX(GREATEST(...))
in the same SELECT as COUNT. similarity(grouped_col, const) is
functionally constant per group, so MAX is idempotent — same
ranking semantics, no subquery, no planner bug.

Verified locally with the same 19 tests; will verify prod after
deploy reaches READY:
  curl '...?q=port&limit=1'  → expect listingCount=100
  curl '...?q=port&limit=10' → expect listingCount=100

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mkan Ready Ready Preview, Comment May 10, 2026 6:17am

@abdout abdout merged commit 2656756 into main May 10, 2026
8 checks passed
@abdout abdout deleted the fix/search-trigram-count-bug branch May 10, 2026 06:18
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