Skip to content

feat: add configurable FTS5 tokenizer for CJK text support#41810

Closed
carrotRakko wants to merge 2 commits into
openclaw:mainfrom
delight-co:feat/fts5-tokenizer-config
Closed

feat: add configurable FTS5 tokenizer for CJK text support#41810
carrotRakko wants to merge 2 commits into
openclaw:mainfrom
delight-co:feat/fts5-tokenizer-config

Conversation

@carrotRakko

Copy link
Copy Markdown
Contributor

Summary

  • Problem: FTS5 full-text search uses the default unicode61 tokenizer, which tokenizes by word boundaries. CJK text (Chinese, Japanese, Korean) has no word boundaries — entire sentences become single tokens. BM25 ranking returns zero results for CJK queries.
  • Why it matters: memorySearch is unusable for any agent handling CJK text. Memory entries in Japanese/Chinese/Korean cannot be found.
  • What changed: New config field store.fts.tokenizer allows selecting the FTS5 tokenizer. Setting it to "trigram" enables character n-gram tokenization that works for CJK. Query expansion logic adapted to handle trigram tokenizer behavior.
  • What did NOT change: Default tokenizer remains unicode61 for backward compatibility. Existing memory databases are not migrated automatically.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

New config field store.fts.tokenizer accepts "unicode61" (default) or "trigram". When set to "trigram", memorySearch works with CJK text. Requires creating a new memory database (or deleting the existing FTS table) for the tokenizer change to take effect.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Linux
  • Runtime/container: Node.js
  • Model/provider: Any
  • Integration/channel: Any
  • Relevant config: store.fts.tokenizer: "trigram"

Steps

  1. Set store.fts.tokenizer: "trigram" in config
  2. Create memory entries with CJK text
  3. Search for CJK terms using memorySearch

Expected

  • Matching entries are returned with BM25 ranking.

Actual

  • Before: Zero results for any CJK query.
  • After: CJK queries return relevant results.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

New tests in query-expansion.test.ts verify trigram tokenizer query generation and CJK text handling.

Human Verification (required)

  • Verified scenarios: Tested on a fork with Japanese text. Memory search returns correct results with trigram tokenizer.
  • Edge cases checked: Query expansion handles trigram-specific syntax. Unicode61 tokenizer behavior unchanged.
  • What you did not verify: Chinese and Korean text specifically (but trigram tokenization is language-agnostic for CJK).

Review Conversations

N/A — fresh PR, no review conversations yet.

Compatibility / Migration

  • Backward compatible? Yes — default tokenizer unchanged
  • Config/env changes? New optional field store.fts.tokenizer
  • Migration needed? Yes — changing tokenizer requires rebuilding the FTS table. New databases use the configured tokenizer automatically. Existing databases keep their tokenizer unless the FTS table is dropped and recreated.
  • Upgrade steps: Set store.fts.tokenizer: "trigram" in config, then delete the existing memory database or drop the FTS table to trigger recreation.

Failure Recovery (if this breaks)

  • How to disable/revert: Remove store.fts.tokenizer from config to revert to unicode61.
  • Files/config to restore: src/memory/manager.ts, src/memory/memory-schema.ts
  • Known bad symptoms: Memory search returning no results (tokenizer mismatch between FTS table and query).

Risks and Mitigations

  • Risk: Tokenizer mismatch if config changes but database is not rebuilt.
    • Mitigation: Logged warning when configured tokenizer differs from existing FTS table. Documentation notes the rebuild requirement.
  • Risk: Trigram tokenizer produces more tokens, increasing index size.
    • Mitigation: Expected and documented trade-off for CJK support.

✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)

Add a `memorySearch.store.fts.tokenizer` config option that allows
selecting the FTS5 tokenizer. Default remains `unicode61` (backward
compatible). Setting `"trigram"` enables substring matching for CJK
(Chinese, Japanese, Korean) text that unicode61 cannot tokenize.

Changes:
- Add `store.fts.tokenizer` to MemorySearchConfig type and Zod schema
- Pass tokenizer to ensureMemoryIndexSchema for FTS table creation
- Detect tokenizer changes in MemoryIndexMeta and trigger full reindex
- Reset FTS table with DROP + re-CREATE on tokenizer change
- Skip CJK bigram splitting in query-expansion when using trigram
- Auto-apply `case_sensitive 0` when trigram is selected

Known limitation: trigram does not match queries shorter than 3
characters via FTS MATCH. In hybrid mode, vector search covers
these cases.

✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Mar 10, 2026
@greptile-apps

greptile-apps Bot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a configurable FTS5 tokenizer (unicode61 or trigram) to enable CJK full-text search, wiring the option through config/schema, persisting it in index metadata, and triggering a full reindex on tokenizer change. The plumbing is solid, but there are two logic bugs in the query-expansion layer that would leave Chinese search broken even after enabling the trigram tokenizer.

Key issues found:

  • Chinese unigrams incompatible with trigram FTS5 (src/memory/query-expansion.ts lines 699–710): In trigram mode, the Chinese CJK branch emits individual 1-character tokens. SQLite's trigram FTS5 tokenizer requires at least 3 characters per query term; shorter terms silently return no results. The Japanese kanji path correctly keeps the whole contiguous block (e.g. "経済政策"), but the Chinese path falls back to unigrams. Chinese search will therefore still return zero results in trigram mode.
  • expandQueryWithLlm fallback ignores tokenizer (src/memory/query-expansion.ts line 817): The function signature has no opts parameter, so when the LLM expander fails the fallback extractKeywords(query) always uses unicode61 behaviour (producing CJK bigrams). While this function is not currently called in production code, the export makes it part of the public API and the oversight should be fixed.

Confidence Score: 2/5

  • Not safe to merge as-is: the primary CJK use-case (Chinese text search) remains broken in trigram mode due to unigram query tokens that the FTS5 trigram tokenizer cannot match.
  • The config plumbing, schema changes, reindex logic, and backward-compatibility are all correct. However the core goal of the PR — making Chinese text searchable — is not achieved: the Chinese tokenization path in trigram mode produces 1-character tokens that SQLite's trigram FTS5 silently ignores, so Chinese queries still return no results. A second logic bug exists in expandQueryWithLlm. The two issues are in src/memory/query-expansion.ts and must be fixed for the feature to work as intended.
  • src/memory/query-expansion.ts — both the Chinese CJK tokenization branch (lines 699–710) and the expandQueryWithLlm fallback (line 817) need fixes.

Comments Outside Diff (2)

  1. src/memory/query-expansion.ts, line 699-710 (link)

    Chinese unigrams in trigram mode will not match FTS5

    In trigram mode, the Japanese kanji path (lines 688–694) correctly preserves the whole kanji block (e.g. "経済政策" = 4 chars), which generates valid trigrams in SQLite FTS5. However, the Chinese path still pushes individual characters — chars is an array of single CJK codepoints, each exactly 1 character. SQLite's trigram FTS5 tokenizer has an effective minimum match length of 3 characters; queries shorter than that silently return no results.

    The result is that in trigram mode, a Chinese query like "之前讨论的那个方案" is tokenised into unigrams ["之","前","讨","论"…], each of which produces no FTS hits, so search is still broken for Chinese text even after enabling the trigram tokenizer.

    The fix should mirror the Japanese kanji path: push the whole contiguous CJK block as the query token instead of individual characters when useTrigram is true.

  2. src/memory/query-expansion.ts, line 817 (link)

    expandQueryWithLlm fallback ignores tokenizer option

    expandQueryWithLlm does not accept a tokenizer option, so its fallback extractKeywords call at line 817 always uses unicode61 behaviour (generating CJK bigrams). If this function is called when the trigram tokenizer is configured and the LLM expander fails or is absent, the returned keywords will include CJK bigrams that the trigram FTS5 tokenizer cannot match, producing zero results.

    The function signature and its internal fallback both need to accept and propagate opts:

Last reviewed commit: 4bb8da8

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4bb8da8539

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/memory/query-expansion.ts Outdated
Chinese tokenization path was emitting individual characters (unigrams)
in trigram mode, but SQLite FTS5 trigram tokenizer requires >= 3 chars
per query term. Single characters silently return no results, breaking
Chinese search entirely.

Now mirrors the Japanese kanji path: pushes the whole contiguous CJK
block as a single token. Also propagates ftsTokenizer option through
expandQueryForFts and expandQueryWithLlm fallback paths.

✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)
@carrotRakko

Copy link
Copy Markdown
Contributor Author

Addressing the two issues flagged by Greptile (both in "Comments Outside Diff"):

1. Chinese unigrams incompatible with trigram FTS5 (lines 699–710)

Fixed in 92be66c. The Chinese tokenization path now mirrors the Japanese kanji path: in trigram mode, it pushes the whole contiguous CJK block as a single token instead of individual characters. SQLite FTS5 trigram tokenizer requires >= 3 characters per query term; single characters silently return no results.

Before: "之前讨论的那个方案"["之", "前", "讨", "论", ...] (no FTS5 match)
After: "之前讨论的那个方案"["之前讨论的那个方案"] (valid trigram matches)

2. expandQueryWithLlm fallback ignores tokenizer (line 817)

Fixed in 92be66c. Both expandQueryForFts and expandQueryWithLlm now accept and propagate the ftsTokenizer option to extractKeywords.

✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92be66cd05

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/memory/query-expansion.ts
@laolin5564

Copy link
Copy Markdown

Real-world validation from a heavy CJK user

I've been running OpenClaw with Chinese memory for months and independently discovered the same FTS5 issue. Here's our production data:

Before fix (unicode61 tokenizer)

  • Search hit rate: 0/7 for typical Chinese queries
  • FTS5 tokenizes entire Chinese sentences as single tokens (e.g., 元宝老婆刚怀孕 = 1 token)
  • MATCH '怀孕' returns 0 results; only prefix match '怀孕*' works partially

Workaround we deployed (3 months ago)

Since we couldn't wait for upstream, we applied a multi-layered fix:

  1. Write-side: space-separated Chinese keywords in memoryFlush prompt (forces FTS5 to tokenize per-word)
  2. Tags line: every memory entry starts with tags: keyword1 keyword2 keyword3
  3. Rebalanced weights: vectorWeight: 0.6, textWeight: 0.4 to lean on vector search
  4. MMR dedup: lambda: 0.7 to avoid redundant results

After fix

  • Search hit rate: 7/7 (same 7 test queries)
  • Top score: 0.46 → 0.83

Supporting this PR

The trigram tokenizer approach is cleaner than our workaround. A few suggestions:

  • Consider documenting the migration path (users need to rebuild FTS index)
  • Adding a cjk_auto option that detects CJK content and applies trigram automatically would be ideal
  • The write-side space-separation trick could be kept as defense-in-depth even with trigram

Strong +1 for merging this. CJK users are effectively flying blind without it. 🦞

@carrotRakko

Copy link
Copy Markdown
Contributor Author

@laolin5564 Thank you for the detailed real-world validation — production data with before/after hit rates is exactly the kind of evidence that helps upstream maintainers evaluate a PR. Much appreciated.

Responding to each suggestion:

1. Migration path: The code handles this automatically. manager-sync-ops.ts detects a tokenizer mismatch between the stored index metadata and the configured tokenizer, drops the FTS table, and rebuilds it on next sync. Users just need to set store.fts.tokenizer: "trigram" in config — no manual reindex step required.

2. cjk_auto option: Interesting idea. Auto-detecting CJK content and switching tokenizer would be convenient, but it adds complexity around mixed-content thresholds and per-entry vs per-index decisions (FTS5 tokenizer is index-wide, not per-row). An explicit config is the right starting point; auto-detection could be a future enhancement.

3. Write-side space-separation as defense-in-depth: Your workaround was clever and effective under unicode61 constraints. With the trigram tokenizer, FTS5 handles sub-string matching natively, so the write-side trick becomes unnecessary. That said, the space-separated tags approach has independent value for readability — no reason to remove it if it is already in place.

Your 0/7 → 7/7 hit rate improvement is a compelling data point. Thanks for sharing it here.

✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)

@steipete

Copy link
Copy Markdown
Contributor

Maintainer follow-up:

I pulled/rebased this locally, fixed the remaining short-CJK trigram gap, and verified the memory-search surface. I cannot update the PR branch directly because the PR head is on delight-co/openclaw-public and GitHub returns 403 Permission denied to steipete for that fork.

Commits prepared locally:

  • 4d531fe78838b5a5f4daefa82f2d8273a91fe5bc fix: support short CJK trigram memory queries
  • 2e0d3e4219b2da7e3f96544e2ac012eecf1a25d0 test: cover additional short CJK trigram queries

Suggested apply path from the PR branch:

git fetch https://github.com/openclaw/openclaw.git feat/fts5-tokenizer-config
git cherry-pick 4d531fe78838b5a5f4daefa82f2d8273a91fe5bc 2e0d3e4219b2da7e3f96544e2ac012eecf1a25d0

What this fixes:

  • trigram-backed memory search now falls back to substring matching for 1-2 character Chinese, Japanese, and Korean queries
  • mixed long trigram terms plus short CJK terms still work in one query
  • config schema baseline updated for memorySearch.store.fts.tokenizer

Focused verification I ran:

pnpm test -- extensions/memory-core/src/memory/manager-search.test.ts packages/memory-host-sdk/src/host/query-expansion.test.ts src/config/schema.base.generated.test.ts
pnpm config:schema:check
pnpm config:docs:check

New/expanded regression coverage includes:

  • 1-char Chinese
  • 2-char Chinese
  • 2-char Japanese
  • 2-char Korean
  • mixed long MATCH + short LIKE query

Refactor suggestion after landing:

  • move trigram query planning into one shared helper that returns a query plan (matchTokens, substringTokens) so tokenizer-aware keyword extraction and tokenizer-aware search stay in one place instead of being split across query expansion and SQL search code.

Once those two commits are on this PR branch, the remaining behavior gap I found should be closed.

@steipete

Copy link
Copy Markdown
Contributor

Closing this as implemented after Codex review.

Current main already ships this feature and the follow-up short-CJK trigram handling, so this PR is obsolete on main.

What I checked:

So I’m closing this as already implemented rather than keeping a duplicate issue open.

Review notes: reviewed against 7ac35b4f6900; fix evidence: release v2026.4.5.

@steipete steipete closed this Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memorySearch BM25 (FTS5) does not work for CJK text

3 participants