Skip to content

Add tokenizer mode support for message search#33048

Open
shinaoka wants to merge 8 commits into
element-hq:developfrom
shinaoka:shinaoka/tokenizer-mode
Open

Add tokenizer mode support for message search#33048
shinaoka wants to merge 8 commits into
element-hq:developfrom
shinaoka:shinaoka/tokenizer-mode

Conversation

@shinaoka

@shinaoka shinaoka commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR ports the tokenizer-mode work to the monorepo layout and combines the previous web and desktop-side changes into a single branch.

It adds a user-facing tokenizer mode setting for local message search, passes the selected mode through the desktop layer to Seshat, and asks for confirmation before reindexing when the mode changes.

Related

Merge Status

matrix-org/seshat#150 is merged, but matrix-org/seshat#157 is still open and matrix-seshat@4.1.0 has not been released yet.

This PR should stay draft for now. Once matrix-seshat@4.1.0 is released, the dependency can be updated and this PR should be ready to merge.

Testing

  • pnpm --dir apps/desktop test:unit
  • pnpm --dir apps/web test -- --runTestsByPath test/unit-tests/async-components/dialogs/eventindex/ManageEventIndexDialog-test.tsx

shinaoka and others added 4 commits March 30, 2026 20:26
Add user-facing setting to choose tokenizer mode (language-based vs N-gram)
for local message search. N-gram tokenization supports CJK languages and
mixed-language content. Includes confirmation dialog for mode changes
(requires reindex) and automatic checkpoint recovery.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add N-gram tokenizer support to Seshat search index initialization.
Refactors initEventIndex into a testable module with dependency injection,
supporting both language-based and N-gram tokenization modes. Includes
automatic database recreation fallback and unit tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, any Seshat open failure that wasn't a ReindexError would
silently delete the EventStore and recreate it, losing the user's search
index on transient errors like filesystem locks or passphrase issues.

Restore the original behavior: only handle ReindexError with recovery
logic, and propagate all other errors to the caller (which shows an
error to the user via sendError).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the Z-Community-PR Issue is solved by a community member's PR label Apr 6, 2026
@aik2mlj

aik2mlj commented Apr 14, 2026

Copy link
Copy Markdown

Seshat has released v4.2.0. Perhaps now the dependency can be updated, and this pull request can proceed to be reviewed by the team. Great appreciation from a CJK language user.

@t3chguy

t3chguy commented Apr 16, 2026

Copy link
Copy Markdown
Member

#33168

@shinaoka

Copy link
Copy Markdown
Contributor Author

The default mode is "language-based."

image image

@shinaoka
shinaoka marked this pull request as ready for review May 16, 2026 04:51
@shinaoka
shinaoka requested a review from a team as a code owner May 16, 2026 04:51
@shinaoka

shinaoka commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@Half-Shot @MidhunSureshR How can I help you?

@Half-Shot

Copy link
Copy Markdown
Member

@Half-Shot @MidhunSureshR How can I help you?

Sorry, this is entirely blocked on us finding time to review. Thanks for the patience 🙏

@Half-Shot Half-Shot 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.

Sorry for the lateness on this review. I've asked the crypto team to lend a hand with the more technical aspect of the review, but given some pointers on the web side.

Comment on lines +9 to +14
* Create Seshat configuration based on tokenizer mode.
*
* @param tokenizerMode - The tokenizer mode: "ngram" for N-gram tokenization (CJK languages),
* or "language" for standard language-based tokenization.
* @returns Configuration object for Seshat initialization.
*/

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.

Your docstring has jumped up a few lines!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Moved the docstring back onto createSeshatConfig so it no longer documents the enum.

Language = "language",
}

export function createSeshatConfig(tokenizerMode?: string): {

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.

Should this throw if an unknown tokenizerMode is given?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated createSeshatConfig to default only when no tokenizerMode is provided, and to throw for unknown values. Added coverage for that case.

value={this.state.crawlerSleepTime.toString()}
onChange={this.onCrawlerSleepTimeChange}
/>
<Field

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.

So I think this bit of the UI is a bit clunky and could do with a refactor, how about using a Compound option component https://compound.element.io/?path=/docs/compound-web_form-controls-radio--docs instead which means:

  1. You can separate the labels and descriptions (so you don't need the big block of text below)
  2. Using Compound means it'll fit the rest of our app.

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.

Image

is a trivial example of what I mean.

The other thing I'd do is add a header with the `"settings|security|tokenizer_mode" string

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Refactored the tokenizer mode selector to Compound radio controls, added the tokenizer mode heading, and split the labels/descriptions so the explanatory text is attached to each option.

Comment thread apps/web/src/i18n/strings/en_EN.json Outdated
"strict_encryption": "Only send messages to verified users",
"tokenizer_mode": "Search tokenizer mode",
"tokenizer_mode_change_warning": "Changing the tokenizer mode requires deleting the existing search index. The index will be rebuilt automatically. Do you want to continue?",
"tokenizer_mode_description": "Language-based: Single language support fixed to UI language. Only works with some languages (English, German, etc.). N-gram: Supports all languages including those without word boundaries. Works with mixed languages. Changing this setting will rebuild your search index.",

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.

You probably don't need "Changing this setting will rebuild your search index" if we have a warning too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed the rebuild note from the option description. The destructive/rebuild behavior is now only in the confirmation warning.

public async initEventIndex(
userId: string,
deviceId: string,
tokenizerMode?: string,

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.

This should probably use the enum, or at the very least:

Suggested change
tokenizerMode?: string,
tokenizerMode?: "ngram"|"language",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Narrowed tokenizerMode through the web indexing manager API using a TokenizerMode enum.


if (!SettingsStore.getValueAt(SettingLevel.DEVICE, "enableEventIndexing")) {
const enableEventIndexing = SettingsStore.getValueAt(SettingLevel.DEVICE, "enableEventIndexing");
if (!enableEventIndexing) {

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.

This seems like an unrelated change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This change is needed to pass the selected tokenizer mode from device settings into the platform index manager. I kept it and narrowed the type so the intent is clearer.

public async initEventIndex(
userId: string,
deviceId: string,
tokenizerMode?: string,

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.

Also could do with a more narrow string type?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Narrowed this to the same TokenizerMode enum used by the base index manager API.


render(<ConfirmTokenizerChangeDialog onFinished={onFinished} />);

fireEvent.click(screen.getByRole("button", { name: /ok/i }));

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.

Anything wrong with just "ok"? And I think this needs awaiting?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the test to click the exact "OK" button text and await userEvent.click.


import BaseEventIndexManager from "../../../src/indexing/BaseEventIndexManager";

describe("BaseEventIndexManager", () => {

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.

I'm not sure this needs to even be tested to be honest

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed this test. It only asserted the base unimplemented method behavior and did not add useful coverage for tokenizer mode.


it("passes tokenizerMode to initEventIndex IPC call", async () => {
// IPCManager requires window.electron to exist.
window.electron = {

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.

I agree with SonarCloud :)

Suggested change
window.electron = {
globalThis.electron = {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the electron stub to use globalThis with a typed cast.

@Half-Shot

Copy link
Copy Markdown
Member

@shinaoka Are you able to continue with this PR?

@shinaoka

Copy link
Copy Markdown
Contributor Author

Oh, I am sorry. I just missed your messages. I will work on them.

hayaksi1 pushed a commit to hayaksi1/element-web that referenced this pull request Jun 27, 2026
@shinaoka

shinaoka commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review comments in b3df964:

  • Moved the Seshat config docstring and made unknown tokenizer modes throw.
  • Narrowed tokenizerMode through the web indexing path with a TokenizerMode enum.
  • Refactored the tokenizer mode setting UI to Compound radio controls with separate labels/descriptions.
  • Removed the duplicated rebuild warning copy from the option description.
  • Cleaned up the related tests.

Verification run locally:

  • Desktop vitest for seshat config/index tests: 8 passed
  • Web jest for event index dialog, SeshatIndexManager, and EventIndexPeg tests: 11 passed
  • Web and desktop TypeScript checks passed
  • ESLint passed for touched files
  • git diff --check passed

@Modificator

Copy link
Copy Markdown

Any progress? @Half-Shot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Z-Community-PR Issue is solved by a community member's PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants