fix: make FTS5 tokenizer configurable per collection - #2257
Conversation
🦋 Changeset detectedLatest commit: da8b64c The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
This is the right fix for #2245. Hard-coding porter unicode61 silently breaks CJK/Thai search because those scripts have no word separators, so making the FTS5 tokenizer a per-collection SearchConfig option is additive, backward-compatible, and fits the existing schema-driven search state machine.
I checked the tokenize plumbing end-to-end:
- API schema (
searchEnableBody) validates against the allowed built-in tokenizers before any DB work. - Runtime validation (
isSearchTokenizer/resolveSearchTokenizer) guardscreateFtsTable,rebuildIndex, andsetSearchConfig, so unsupported values can’t reach DDL. - The selected tokenizer is preserved through disable, re-enable, schema-triggered rebuilds, missing-index repair, and config parsing/serialization.
- Existing collections without a stored
tokenizecontinue to default toporter unicode61. - Tests cover the default tokenizer behavior, switching to
trigram, preserving weights through a tokenizer-only change, persistence across disable/schema/repair cycles, and rejection of injection-style tokenizer values. - Documentation in
rest-api.mdxis updated to explain the three options.
The only issue I see is a small ordering nit: in createFtsTable, rebuildIndex, and enableSearch, the tokenizer is validated before the SQLite-only guard. On a non-SQLite database this can produce a misleading “unsupported tokenizer” error instead of the intended “SQLite only” no-op/error, and it does work that the dialect is about to discard. Moving the guard above the validation keeps the behavior consistent with the rest of the file.
Once that ordering is fixed this looks good to merge.
| const tokenizer = resolveSearchTokenizer(tokenize); | ||
| if (!isSqlite(this.db)) return; |
There was a problem hiding this comment.
[suggestion] The tokenizer is validated before the SQLite-only early return. On a non-SQLite database this means an invalid tokenize value can throw an "Unsupported FTS5 tokenizer" error instead of the intended no-op (or the dedicated "only available with SQLite" error in enableSearch), and it performs validation work the dialect guard is about to discard.
Move the dialect check above the tokenize resolution so validation only runs when we’re actually going to build SQLite FTS DDL.
| const tokenizer = resolveSearchTokenizer(tokenize); | |
| if (!isSqlite(this.db)) return; | |
| ): Promise<void> { | |
| if (!isSqlite(this.db)) return; | |
| const tokenizer = resolveSearchTokenizer(tokenize); |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
What does this PR do?
FTSManager.createFtsTable()creates every SQLite FTS5 table withtokenize='porter unicode61'. Becauseunicode61treats contiguous non-spacing text as a single token and EmDash emits prefixMATCHexpressions, mid-sentence queries in Japanese, Chinese, Thai, Khmer, Lao, and Burmese silently miss indexed content. The tokenizer is not configurable, so there is no way out of it today.This adds a backward-compatible optional
tokenizefield to the exported per-collectionSearchConfig, constrained to the supported built-in configurations (porter unicode61,unicode61,trigram) so user input is never interpolated into FTS DDL unchecked. Omitting it retains the current English-stemming default, so existing collections are unaffected.The selected tokenizer is threaded through enable, explicit rebuild, schema-triggered rebuild, repair, disable/re-enable persistence, and config parsing, so posting an updated enabled configuration drops, recreates, and repopulates the existing FTS table rather than leaving its tokenizer sticky. The option is exposed through the validated search-enable REST body, with docs on when to choose each tokenizer (Porter's English-only stemming, trigram's three-character minimum).
Closes #2245
Type of change
Checklist
pnpm testpasses (or targeted tests for my change)pnpm formathas been runTwo checklist items are deliberately not ticked, with what I actually observed:
pnpm typecheck— fails in my checkout withTS2307: Cannot find module '@emdash-cms/blocks'/'@emdash-cms/plugin-types', i.e. unbuilt workspace siblings rather than anything in this diff. Worth a maintainer confirming it is green in CI.pnpm lint— fails ondemos/simple,fixtures/perf-site,infra/cache-demo,infra/do-solo-demo, andpackages/registry-clientwithno-redundant-type-constituents. I verified those same failures reproduce on a pristinemaincheckout, and thatoxlint --type-aware --deny-warningsis clean when run over only the files this PR changes.AI-generated code disclosure
AI was used for assistance.
Screenshots / test output
Targeted tests for this change: