mirror of
https://github.com/docmost/docmost.git
synced 2026-08-31 02:46:27 +08:00
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:
@@ -30,6 +30,7 @@ import { generateJitteredKeyBetween } from 'fractional-indexing-jittered';
|
|||||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||||
import { BaseProperty } from '@docmost/db/types/entity.types';
|
import { BaseProperty } from '@docmost/db/types/entity.types';
|
||||||
import {
|
import {
|
||||||
|
Condition,
|
||||||
FilterNode,
|
FilterNode,
|
||||||
PropertySchema,
|
PropertySchema,
|
||||||
SearchSpec,
|
SearchSpec,
|
||||||
@@ -258,7 +259,14 @@ export class BaseRowService {
|
|||||||
const filter = this.normaliseFilter({ filter: dto.filter });
|
const filter = this.normaliseFilter({ filter: dto.filter });
|
||||||
const search = this.normaliseSearch(dto.search);
|
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({
|
const { value, capped } = await this.baseRowRepo.countExact({
|
||||||
baseId: dto.baseId,
|
baseId: dto.baseId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -393,3 +401,30 @@ export class BaseRowService {
|
|||||||
return validatedCells;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user