fix(base): escalate fuzzy filter and search counts to capped-exact

Postgres has no per-expression stats for `cells->>'uuid' ILIKE '%…%'`,
`search_text ILIKE`, or `search_tsv @@`, so EXPLAIN Plan Rows falls
back to a default selectivity and is off by orders of magnitude — a
contains filter on a 10k-row base was reporting ~150 against thousands
of real matches. Auto-route any request whose filter tree contains a
contains/ncontains/startsWith/endsWith op or a search term to the
capped-exact path, even when the caller asked for an estimate.
This commit is contained in:
Philipinho
2026-04-25 01:05:02 +01:00
parent 72fc93dcc1
commit 18222d1142
@@ -30,6 +30,7 @@ import { generateJitteredKeyBetween } from 'fractional-indexing-jittered';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { BaseProperty } from '@docmost/db/types/entity.types';
import {
Condition,
FilterNode,
PropertySchema,
SearchSpec,
@@ -258,7 +259,14 @@ export class BaseRowService {
const filter = this.normaliseFilter({ filter: dto.filter });
const search = this.normaliseSearch(dto.search);
if (dto.exact) {
// Planner estimates are untrustworthy for ILIKE / FTS predicates on
// jsonb-extracted cells — PG falls back to a default selectivity and
// returns numbers off by orders of magnitude. Auto-escalate such
// requests to the capped-exact path even when the caller asked for
// an estimate.
const useExact = dto.exact || requiresExactCount(filter, search);
if (useExact) {
const { value, capped } = await this.baseRowRepo.countExact({
baseId: dto.baseId,
workspaceId,
@@ -393,3 +401,30 @@ export class BaseRowService {
return validatedCells;
}
}
// Any filter op whose SQL is ILIKE-based, or a present search term. The
// PG planner has no stats for `cells->>'uuid' ILIKE '%x%'` / `search_text
// ILIKE` / `search_tsv @@` and falls back to a default selectivity, so
// EXPLAIN Plan Rows is worthless here — route to the capped-exact count.
const FUZZY_OPS: ReadonlySet<string> = new Set([
'contains',
'ncontains',
'startsWith',
'endsWith',
]);
function requiresExactCount(
filter: FilterNode | undefined,
search: SearchSpec | undefined,
): boolean {
if (search) return true;
if (!filter) return false;
return filterHasFuzzyOp(filter);
}
function filterHasFuzzyOp(node: FilterNode): boolean {
if ('children' in node) {
return node.children.some(filterHasFuzzyOp);
}
return FUZZY_OPS.has((node as Condition).op);
}