Skip to content

feat(site-search): allow overriding the index analyzer via SITE_SEARCH_ANALYZER - #36840

Open
swicken wants to merge 5 commits into
mainfrom
issue-36839-sitesearch-analyzer-override
Open

feat(site-search): allow overriding the index analyzer via SITE_SEARCH_ANALYZER#36840
swicken wants to merge 5 commits into
mainfrom
issue-36839-sitesearch-analyzer-override

Conversation

@swicken

@swicken swicken commented Jul 31, 2026

Copy link
Copy Markdown
Member

Proposed Changes

  • Add a SITE_SEARCH_ANALYZER Config property (env: DOT_SITE_SEARCH_ANALYZER) naming any analyzer known to the search cluster — built-in language analyzers (cjk, arabic, thai, ...) or plugin-provided ones (kuromoji, nori, smartcn). When set, it is applied at index creation to the text fields of the site-search mapping (content, content_raw, title, description, author) on both the Elasticsearch and OpenSearch providers, and the content.ngram subfield's search_analyzer follows it so query-time tokenization stays consistent. Unset keeps the bundled defaults byte-for-byte.
  • Declare content_raw explicitly in both bundled mappings. The publishers add it to every text/* document but the mapping never declared it, so it was dynamically mapped with the standard analyzer — and since site-search queries use query_string with default_field:"*", it silently reintroduced CJK unigram false positives even with the override applied. Declared type matches the previous dynamic mapping, so the unset path is unchanged.
  • Centralize settings/mapping resource loading in SiteSearchIndexResources (also fixes the ES path reading resources via new File(url.getPath()), which breaks when packaged inside a JAR), with a loud DotSearchException on missing resources or a failed override instead of silently building an index with default analyzers.

Why

Site Search hardcodes an English-tuned analyzer chain (standard tokenizer + lowercase + asciifolding + English stemmer). CJK text degrades to single-character tokens (a query for 東京 matches any document containing 東 or 京 anywhere) and RTL languages get no normalization or stemming (كتاب does not match الكتاب). Resolves #36839.

Checklist

  • Tests
  • Translations (n/a — no user-facing strings)
  • Security Implications Contemplated (analyzer name is operator-controlled config, inserted via the JSON API — no injection path; value is trimmed and failures are loud)

Additional Info

Verified end-to-end against a live stack, not just unit tests: built the branch image, booted dotCMS + OpenSearch with DOT_SITE_SEARCH_ANALYZER=cjk, published Japanese HTML file assets, and ran a real run-now Site Search job:

  • The dotCMS-created index mapping carries cjk on all text fields, including content_raw.
  • Query 東京 (Tokyo) via the exact query_string/default_field:"*" shape Site Search uses returns only the document about Tokyo Tower; a control document containing 東 and 京 in unrelated words no longer matches (it did on the default mapping, at an identical score).
  • Control queries 建物 and 静か each return exactly their one matching document.
  • A misconfigured analyzer name fails index creation with the engine's analyzer [x] has not been configured in mappings error.

Scope notes:

  • One analyzer per index (a mixed-language site picks one tokenization) — per-language subfields/indices are deliberately out of scope until a customer needs them.
  • Plugin analyzers (kuromoji/nori/smartcn/icu) additionally require the plugin installed on every node of the ES/OS cluster; built-ins need nothing.
  • content_raw (raw un-stripped HTML, no readers anywhere) roughly 2–5×'s the indexed text per document; making it index: false is a good follow-up but changes default-path search behavior, so it is not part of this PR.

swicken added 2 commits July 31, 2026 09:37
…H_ANALYZER

Site Search hardcodes an English-tuned analyzer chain (standard
tokenizer + lowercase + asciifolding + English stemmer) into its
bundled index mapping, which tokenizes CJK text into single-character
unigrams and applies no RTL normalization, with no way to change it
short of repackaging the webapp.

Add a SITE_SEARCH_ANALYZER Config property (env:
DOT_SITE_SEARCH_ANALYZER) naming any analyzer known to the cluster —
built-in language analyzers (cjk, arabic, thai, ...) or plugin-provided
ones (kuromoji, nori, smartcn). When set, it is applied to the text
fields (content, title, description, author) of the site-search mapping
at index creation for both the ES and OS providers; the ngram
subfield's search_analyzer follows it so queries tokenize consistently.
Unset keeps the bundled defaults byte-for-byte.

Also fixes the ES path reading its resources via new File(url.getPath()),
which breaks when packaged inside a JAR.
…rom review

E2E testing against a live stack (DOT_SITE_SEARCH_ANALYZER=cjk, real
Site Search job over Japanese file assets) surfaced a gap: the
publishers add a content_raw field (raw pre-HTML-strip copy) to every
text/* document, but the bundled mapping never declared it. OpenSearch
dynamically mapped it with the standard analyzer, and since site search
queries use query_string with default_field:*, CJK unigram false
positives came back through that field even with the override applied
(query 東京 matched a doc containing only 東 and 京 in unrelated words,
via content_raw).

Declare content_raw explicitly as text in both bundled mappings — same
effective analysis as the previous dynamic mapping, so the unset path
is unchanged — and include it in the override's text-field list. With
the fix, the same E2E run returns only the genuinely matching document.

Also fold in review feedback: trim the configured analyzer value
(stray whitespace from env files otherwise reaches the cluster as an
unknown-analyzer error), guard the JSONObject fork's null-returning
toString(), log when the override is applied, correct the javadoc's
description of the default analyzers, and extend the unit test with
byte-identity and blank-value fallback cases.
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Claude finished @swicken's task in 2m 36s —— View job


Code Review — SITE_SEARCH_ANALYZER override

  • Read all 6 changed files
  • Traced SiteSearchIndexResources usage in both ES/OS providers
  • Verified UtilMethods.isSet(String) trims (blank → fallback), DotSearchException is unchecked, os-sitesearch-settings.json exists
  • Reviewed mapping JSON changes and test coverage

The design is sound and well-documented. The move to getResourceAsStream (JAR-safe), the loud DotSearchException on missing resources / serialization failure, and the content.ngram search_analyzer reasoning all check out. Tests cover unset/blank/override/missing-resource. Just one accuracy note below.

New Issues

  • 🟡 Medium: dotCMS/src/main/resources/es-sitesearch-mapping.json:21-23 (and os-sitesearch-mapping.json:21-23) — The PR states declaring content_raw as {"type":"text"} "matches the previous dynamic mapping, so the unset path is unchanged." That isn't quite exact: ES/OS dynamic string mapping produces text plus a content_raw.keyword sub-field (type: keyword, ignore_above: 256). The explicit declaration drops that sub-field, so the unset path is a small mapping change, not byte-identical to what dynamic mapping produced before.
    • Assumption: default dynamic mapping was in effect for content_raw (no template overriding it) — consistent with the PR's own description.
    • Impact: low — content_raw is raw un-stripped HTML; nothing in the codebase sorts/aggregates on content_raw.keyword, and ignore_above:256 would have discarded most HTML values anyway. Only new indices are affected (existing ones keep their mapping).
    • What to verify: confirm no downstream query/aggregation references content_raw.keyword. If confirmed, this is purely a wording nit in the PR description; if you want strict parity, add "fields": {"keyword": {"type": "keyword", "ignore_above": 256}}. Fix this →

No blocking issues. The two non-blocking points raised earlier by the author (mapping key order on the set path; content.ngram index/query divergence) are both correctly reasoned and don't need changes.
issue-36839-sitesearch-analyzer-override

@mergify

mergify Bot commented Jul 31, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@swicken

swicken commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

Re the two non-blocking notes:

  1. Key order on the set path — agreed and intended; the byte-for-byte guarantee is only claimed (and only matters) for the unset path. Mapping property order is not significant to either engine.

  2. content.ngram index/query divergence — verified: nothing in the codebase queries content.ngram (it is reachable only via the raw-JSON query path; grep shows no reader). The divergence is also deliberate defense, not an accident: with search_analyzer following the override, a CJK query produces bigrams that cannot match the standard-tokenized edge-ngram index, so the subfield is inert under query_string(default_field:"*"). Leaving its search_analyzer at standard_content would decompose CJK queries into unigrams that DO match single-character edge-grams — reintroducing the same false-positive side door this PR closes for content_raw, through a third field. The E2E result (東京 returning only the genuinely matching doc across all fields) covers this path.

@swicken
swicken enabled auto-merge July 31, 2026 18:02
@swicken
swicken added this pull request to the merge queue Jul 31, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Backend PR changes Java/Maven backend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Allow overriding the Site Search index analyzer via config to support CJK/RTL languages

2 participants