Skip to content

Support CJK search on SQLite and preserve Unicode queries - #2337

Open
Maklu wants to merge 9 commits into
basecamp:mainfrom
Maklu:fix-cjk-search
Open

Maklu wants to merge 9 commits into
basecamp:mainfrom
Maklu:fix-cjk-search

Conversation

@Maklu

@Maklu Maklu commented Jan 10, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Queries such as 中文, 日本語, 한국어, and hälsa are still stripped by the ASCII-only query sanitizer on main. SQLite's existing tokenizer also treats contiguous CJK text as one token, so a query such as 中文 cannot find 中文测试卡片.

Changes

This implementation uses the current ActiveSearch architecture:

  • Preserve Unicode letters, numbers, and combining marks in queries. Preserve and NFC-normalize Unicode tokens in the MySQL adapter too.
  • Add a SQLite adapter that separates CJK characters for indexing and quotes contiguous CJK queries as phrases, preserving character order. Keep SQLite's existing porter tokenizer for English; Ruby does not stem indexed SQLite content.
  • Share the existing source-record loading and highlight formatting between the adapters. SQLite highlights and snippets operate on original text, so results display Card to delete and 日本語, never stemmed or space-separated index content. Overlapping terms are merged before inserting ActiveSearch markers; ActiveSearch handles HTML escaping and per-field formatting.
  • Reindex existing cards and comments in batches through ActiveSearch in a data migration. Both SQLite CJK content and MySQL accented content need rebuilding. The migration is irreversible; reverting requires restoring the previous application version and running search:reindex.

Scope

CJK character-level search is supported on SQLite. MySQL keeps whole Unicode words and its existing full-text token-size limits; this does not add CJK substring search or the MySQL ngram parser. MySQL ngram support remains a separate change.

Validation

  • GitHub CI passed on a453c2665: full application and system tests on SQLite and MySQL, plus lint, security, Gemfile drift, and GitHub Actions audit.
  • SQLite search regression suite: 89 tests, 318 assertions, no failures or errors (4 adapter-specific skips).
  • MySQL 8.4 search regression suite: 88 tests, 320 assertions, no failures or errors (3 SQLite-only skips). The additional Japanese combining-mark highlighter test passed in the SQLite run.
  • Covers CJK cards and comments, mixed scripts, CJK phrase order, original display text, accented/decomposed queries, overlapping highlights, snippets, HTML escaping, pagination, board access, reindex jobs/tasks, and upgrading legacy CJK index contents.
  • Full RuboCop: 901 files, no offenses. Zeitwerk check passed.

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

Copy link
Copy Markdown

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: 13e2a7efc1

ℹ️ 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 app/models/search/record/sqlite.rb Outdated
@Maklu
Maklu marked this pull request as draft February 23, 2026 11:41
Fix search functionality for CJK languages by addressing three issues:

1. Query sanitization: Switch from ASCII-only \w to Unicode-aware \p{L}\p{N}
2. Stemmer: Add character-level tokenization for CJK (e.g., "日本語" → "日 本 語")
3. Highlighter: Add CJK-aware highlighting without word boundaries

For SQLite FTS5: Store original content in main table, stemmed content in
FTS table. Use Search::Highlighter instead of FTS5's highlight() function
to display original text in search results.
@Maklu
Maklu marked this pull request as ready for review February 23, 2026 11:46

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

Copy link
Copy Markdown

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: fd83c659ba

ℹ️ 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 app/models/search/record/sqlite.rb Outdated
Comment thread app/models/search/highlighter.rb Outdated
… CJK boundary matching

- Stem highlighter terms so searches like "running" highlight "run" in results
- Add stem_query to preserve quoted phrases in SQLite MATCH queries
- Fix word boundary regex failing at CJK/Latin transitions (Ruby \b issue)
- Use case-insensitive matching for CJK snippet positioning

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

Copy link
Copy Markdown

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: aa20528f9e

ℹ️ 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 app/models/search/stemmer.rb Outdated
Comment thread app/models/search/highlighter.rb Outdated
…hting

- Unquoted CJK terms like 中文 now become "中 文" (phrase) in MATCH queries
  to preserve character order
- CJK highlight regex is now case-insensitive for mixed CJK/Latin terms
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@Maklu

Maklu commented Feb 23, 2026

Copy link
Copy Markdown
Contributor Author

@jorgemanrubia This is a re-implementation of CJK search support, addressing the issues from the previous attempt (#2321). Would appreciate your review when you have a chance. Thanks!

@jorgemanrubia jorgemanrubia left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great work here @Maklu.

This looks better to me and works well in my tests 👍. I would love to get @djmb input here too.

Comment thread app/models/search/record/sqlite.rb Outdated
Comment thread app/models/search/highlighter.rb Outdated
Comment thread app/models/search/highlighter.rb Outdated

@djmb djmb 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.

A few thoughts:

This won't work with MySQL because by default it requires tokens to be 3 characters or more. We should check that it doesn't break anything though.

We are now going to double stem data, once with mittens in Ruby and then a second time with the porter stemmer in the FTS5 SQLite implementation. This maybe works fine, but it seems a bit iffy, for example if the two stemming algorithms do not work the same way.

Ideally we should change the table to tokenize='unicode61' to avoid the SQLite tokenization.

I think we also will need to do a full reindex in a migration so the correctly stemmed data is stored.

Finally we should add some tests with CJK data to searches_controller_test.rb for a more complete testing. The setup is quite slow for those tests because they are not transactional so we might want a single test that tests a bunch of different queries.

Comment thread app/models/search/highlighter.rb Outdated
Copilot AI review requested due to automatic review settings March 4, 2026 04:16
@Maklu

Maklu commented Mar 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review @djmb, and the feedback @jorgemanrubia!

Updated in 973ff66:

  • FTS5 tokenize='unicode61' — this avoids the double-stemming (Mittens + porter). Added a migration that recreates the table and reindexes.
  • Unified snippet — single character-based method, no more cjk_dominant? branching. Also resolves the x3 and bytesize questions since both are gone now.
  • highlight_snippet / highlight_full — applied to both SQLite and Trilogy modules.
  • CJK controller test — single test covering Chinese, Japanese, Korean, and mixed queries.
  • MySQL — CJK single-char tokens fall below innodb_ft_min_token_size, so CJK search on MySQL would need a different approach (ngram tokenizer etc.), but that's a separate concern. Existing non-CJK behavior is unaffected.

Copilot AI 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.

Pull request overview

This PR re-implements CJK (Chinese/Japanese/Korean) search support by separating stemmed storage/indexing from user-facing display, and replacing SQLite FTS5 highlight()/snippet() usage with an application-level Search::Highlighter to avoid displaying stemmed text.

Changes:

  • Add CJK-aware stemming/tokenization and query stemming to support CJK matching in SQLite FTS.
  • Update highlighting/snippet generation to be CJK-aware and to operate on original (display) text rather than FTS-returned stemmed text.
  • Switch SQLite FTS tokenizer to unicode61 and add/adjust tests covering CJK search/highlighting/snippet behavior.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
app/models/search.rb Introduces shared CJK_PATTERN constant for cross-component CJK detection.
app/models/search/highlighter.rb Adds CJK-aware highlight/snippet logic and improves term handling (stemming-aware).
app/models/search/query.rb Updates sanitization to preserve Unicode letters/numbers (enables CJK queries).
app/models/search/stemmer.rb Implements character-level tokenization for CJK plus stem_query to produce FTS5-friendly queries.
app/models/search/record/sqlite.rb Uses Search::Highlighter for display and stores stemmed content in the FTS table; stems queries for matching.
app/models/search/record/trilogy.rb Refactors highlight helpers to highlight_full/highlight_snippet for consistency with SQLite path.
db/migrate/20260304120000_change_search_fts_tokenizer_to_unicode61.rb Migrates SQLite FTS table to unicode61 and reindexes.
db/schema_sqlite.rb Updates schema version and FTS virtual table definition to unicode61.
db/cable_schema.rb Contains unrelated schema-dump changes not mentioned in the PR description.
test/models/search/stemmer_test.rb Adds coverage for CJK stemming, mixed content, and stem_query behavior.
test/models/search/highlighter_test.rb Adds CJK highlight/snippet tests and switches snippet tests to max_chars.
test/models/card/searchable_test.rb Updates SQLite FTS expectation to reflect stemmed content storage.
test/controllers/searches_controller_test.rb Adds integration coverage for CJK search results and highlight rendering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread db/migrate/20260304120000_change_search_fts_tokenizer_to_unicode61.rb Outdated
Comment thread db/cable_schema.rb Outdated
Copilot AI review requested due to automatic review settings March 4, 2026 04:29
@Maklu
Maklu requested a review from djmb March 4, 2026 04:33

Copilot AI 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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/models/search/highlighter.rb Outdated
Comment thread app/models/search/stemmer.rb Outdated
- Switch FTS5 tokenizer from porter to unicode61 to eliminate
  double-stemming (Ruby Mittens + SQLite porter). Added migration
  with full reindex.
- Unify snippet into a single character-based method, removing the
  separate CJK/western branches and cjk_dominant? check.
- Split highlight(text, show:) into highlight_snippet/highlight_full
  in both SQLite and Trilogy modules.
- Add CJK integration test to searches_controller_test (SQLite only,
  MySQL fulltext requires 3+ character tokens).
- Revert unrelated cable_schema.rb changes.

Copilot AI 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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/models/search/record/sqlite.rb Outdated
Comment thread app/models/search/highlighter.rb Outdated
@BANG88

BANG88 commented Mar 26, 2026

Copy link
Copy Markdown

@Maklu Any updates ? I need this too. Thank you.

Resolve conflict in SQLite matching scope: adopt the explicit
INNER JOIN from main while keeping stemmed query from this branch.
@Maklu

Maklu commented Mar 27, 2026

Copy link
Copy Markdown
Contributor Author

@BANG88 Just rebased to resolve conflicts. Still waiting on maintainer review — hopefully it gets picked up soon.

@BANG88

BANG88 commented Mar 27, 2026

Copy link
Copy Markdown

@BANG88 Just rebased to resolve conflicts. Still waiting on maintainer review — hopefully it gets picked up soon.

Bro. Thank you very much.

BANG88 added a commit to PuffinStudio/fizzy that referenced this pull request Mar 29, 2026
* pr-2337-temp:
  Address review feedback: switch FTS5 to unicode61, unify snippet logic
  Wrap CJK tokens as phrases in FTS5 queries and fix mixed-term highlighting
  Address review feedback: fix stemmed highlighting, phrase search, and CJK boundary matching
  Add CJK (Chinese, Japanese, Korean) search support
@tushabe

tushabe commented May 19, 2026

Copy link
Copy Markdown

Hello @Maklu , I am wondering why this PR has not been merged. I would like to submit PRs to the main branch in hopes of it ending up in the https://www.fizzy.do/ deployment but I am not sure if that actually ever happens with PRs submitted.

Thoughts?

@Maklu

Maklu commented May 20, 2026

Copy link
Copy Markdown
Contributor Author

@tushabe All the review feedback was addressed back in early March, so it's been waiting on a maintainer re-review for a couple of months now. Keeping the PR focused and responsive to review is about all we can control on our end.

@djmb thanks again for the detailed review earlier. No rush at all, but whenever you have some time, I'd really appreciate another look — everything you flagged has been addressed since early March.

@AndreasNasman

Copy link
Copy Markdown

Would this PR also add support for other characters, like åäö? I noticed the searching being broken for Swedish characters as well.

Maklu added 2 commits July 10, 2026 14:43
# Conflicts:
#	db/schema.rb
#	db/schema_sqlite.rb
Query sanitization used to strip non-ASCII letters, breaking search
for accented characters (å, ä, ö, é, ...). The switch to \p{L}\p{N}
in Search::Query fixed this alongside CJK; cover it with a test that
runs on both SQLite and MySQL.

Claude-Session: https://claude.ai/code/session_01AHkZVHPX1kNFJU5fYu7o41
Copilot AI review requested due to automatic review settings July 10, 2026 06:34
@Maklu

Maklu commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

@AndreasNasman Yes, this fixes that too. On main, query sanitization strips any non-ASCII letter before the search runs (Ruby's \w is ASCII-only), so "hälsa" becomes "h lsa" — same root cause as the CJK breakage. This PR switches the sanitizer to Unicode-aware character classes (\p{L}\p{N}), which fixes accented Latin characters as well. I've added a test with Swedish content (runs on both SQLite and MySQL) to keep it covered.

One limitation: stemming is English-only, so inflected forms won't match each other (e.g. "hälsa" vs "hälsan"), but exact word matches work.

Copilot AI 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.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Comment thread app/models/search/highlighter.rb Outdated
Comment thread app/models/search/highlighter.rb Outdated
Comment thread app/models/search/query.rb
Comment thread app/models/search/stemmer.rb Outdated
highlight() inserted real <mark> HTML during per-term gsub passes, so a
later term matching the markup itself (e.g. "class", "text") corrupted
the output. Wrap matches in private-use sentinel characters instead and
swap in the real markup once all terms are processed.

Query sanitization and stemmer tokenization also treated combining marks
(\p{M}) as separators, splitting decomposed accents (NFD "café") and
scripts like Devanagari into fragments. Keep marks with their base
characters on both the indexing and query side.

Claude-Session: https://claude.ai/code/session_01AHkZVHPX1kNFJU5fYu7o41
Copilot AI review requested due to automatic review settings July 10, 2026 07:23

Copilot AI 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.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Comment thread app/models/search/stemmer.rb Outdated
Comment thread test/controllers/searches_controller_test.rb
@AndreasNasman

AndreasNasman commented Jul 12, 2026 •

Copy link
Copy Markdown

Awesome @Maklu! 💪
Let's hope @djmb or @jorgemanrubia has time to look through the PR at some point 😊

@Maklu Maklu changed the title Add CJK (Chinese, Japanese, Korean) search support Support CJK search on SQLite and preserve Unicode queries Sep 28, 2026
@Maklu

Maklu commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Updated in a453c26 to work with the latest main and its ActiveSearch migration.

  • Adapted CJK indexing and original-text highlighting, with a migration to reindex existing content.
  • Added regression coverage for CJK phrase order, comments, mixed scripts, and legacy index contents.
  • All CI checks pass, including application and system tests on SQLite and MySQL.

CJK substring search remains SQLite-only; MySQL ngram support is outside this PR’s scope. Ready for another review.

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.

7 participants