Skip to content

fix(search): recover exact-name and mid-token matches in global search #36791 - #36793

Open
ihoffmann-dot wants to merge 5 commits into
mainfrom
issue-36791-global-search-exact-name-match
Open

fix(search): recover exact-name and mid-token matches in global search #36791#36793
ihoffmann-dot wants to merge 5 commits into
mainfrom
issue-36791-global-search-exact-name-match

Conversation

@ihoffmann-dot

@ihoffmann-dot ihoffmann-dot commented Jul 29, 2026

Copy link
Copy Markdown
Member

Problem

GlobalSearchAttributeStrategy — shared by the Content Search portlet's main search box, Content Drive's keyword search, the Relationships field's dynamic search dialog, and any other Lucene Query Builder consumer — gates matching on a single mandatory clause: +catchall:<term>* (a prefix match). A term that lands mid-token, or spans a tokenizer boundary, never satisfies that gate — even a content's own exact full name can fail to match.

Example: a FileAsset named IMG_1004.jpeg tokenizes to img_1004 + jpeg (the standard tokenizer splits on . but not _). Searching IMG/img/IMG_1004/jpeg works (genuine prefixes), but 1004 (mid-token), 1004.jpeg (boundary-spanning), and the exact full name IMG_1004.jpeg do not match — confirmed identically in both the Content Search portlet (via the real LuceneQueryBuilder + ContentletAPI.search path) and Content Drive.

Found while working on #36688 (PR #36767), which intentionally aligned Content Drive with this same shared strategy for consistency/performance — surfacing this pre-existing, platform-wide gap.

Fix

Widen the mandatory gate from +catchall:<term>* alone to +(catchall:<term>*^10 OR fieldName_dotraw:*<term>*^2).

  • fieldName_dotraw (e.g. title_dotraw) already exists in the index mapping (keyword type, no reindex) and is scoped to that one field only — unlike catchall, which aggregates every field of the document.
  • This recovers mid-token and exact-full-value matches without reintroducing an unscoped, whole-document wildcard like the old broad catchall:*term* that [BUG] Content Drive: keyword/title search returns inconsistent or incoherent results #36688 deliberately removed (that one matched a term anywhere in any field, including unrelated body text — slow and irrelevant).
  • Asymmetric boosts (^10 vs ^2) are deliberate: a genuine token-prefix hit must outrank a raw-substring hit that can land anywhere, including mid-word. Without this, a document found only via the substring fallback could outrank one whose title actually starts with the search term.
  • Query-construction only. No schema/mapping/reindex change → rollback-safe.

Matching matrix (validated)

Seeding a FileAsset named IMG_1004.jpeg, every substring shape a user might type now matches — token prefixes, mid-token fragments, boundary-spanning fragments and the exact full name, in any case:

Term Before After Matches via
IMG / img / Img / IMG_ / IMG_1004 / jpeg catchall token prefix
1004 (mid-token) _dotraw substring
1004.jpeg (spans the .) _dotraw substring
MG_100 (pure mid-substring) _dotraw substring
G_1004.jpe / g_1004.jpe _dotraw substring
.jpeg _dotraw substring
IMG_1004.jpeg / img_1004.jpeg (exact name) _dotraw substring
Unrelated item with no 1004 anywhere not returned still not returned negative control — no reintroduction of #36688's whole-body matching

Validation

GlobalSearchAttributeStrategyMatchingTest (integration, 7/7 green) covers the matrix above plus relevance ordering.

Ranking — important caveat. A genuine token-prefix match ranks above a substring-only match, which is the reason for the asymmetric boosts. This was verified with both items in the same Content Type and the prefix item seeded older than the substring one — so a recency-driven order would rank it last; it ranking first can only come from the score. Verified stable across 3 consecutive runs.

However, relevance ordering only applies when a content type is selected: LuceneQueryBuilder.getOrderByClause() downgrades the default "score,modDate desc" to plain "modDate desc" whenever contentTypeIds is empty. So in the unfiltered global search box, ordering is by modification date and these boosts do not change it — they matter for the content-type-scoped searches (and for which documents match at all, everywhere). That pre-existing behavior is untouched by this PR and is worth a separate conversation if relevance ordering is wanted in the unfiltered case.

Tests scope their query to their own test site; without that, ranking assertions pick up whatever other content is in the shared test index and flip depending on which test classes ran in the same JVM.

LuceneQueryBuilderTest's existing global-search test case updated to the new query string.

Note (pre-existing, unrelated): running the full LuceneQueryBuilderTest suite surfaces one already-failing test (testSearchableFieldsByContentType[With Binary Field]) — its hardcoded expected string doesn't match BinaryFieldStrategy's current output (which already emits a _dotraw clause, added in unrelated commit b335f43d83). This branch's diff against BinaryFieldStrategy.java is empty and the failure reproduces on main. Flagged for visibility, not fixed here.

Scope

Closes #36791. Separate from #36688/#36767 (Content Drive keyword search fix) — this is shared, platform-wide search code, opened as its own issue/PR per team discussion. As a side effect, once merged, Content Drive (via the same shared strategy) also gains exact-name/mid-token matching.

Out of scope / follow-ups

  • Load/perf benchmarking of the _dotraw wildcard at scale (expected cheaper than the old catchall wildcard — much smaller term dictionary — but not empirically benchmarked here).
  • The pre-existing, unrelated Binary Field test failure noted above.

🤖 Generated with Claude Code

@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @ihoffmann-dot's task in 3m 5s —— View job


Code Review — recover exact-name and mid-token matches in global search

  • Read production change (GlobalSearchAttributeStrategy.java)
  • Read test additions/updates
  • Check for bugs, escaping, and regressions

The production change is sound: the mandatory gate is widened from +catchall:value* to +(catchall:value*^10 OR title_dotraw:*value*^2), keeping the fast token-prefix path while recovering mid-token/exact-name matches via the field-scoped _dotraw keyword. Escaping behavior is unchanged from the prior code (the catchall/_dotraw clauses used the raw value before this PR too — the _dotraw clause was simply moved from an optional SHOULD clause into the mandatory gate), and it correctly avoids reintroducing the unscoped catchall:*value* wildcard #36688 removed. Query-construction-only, no mapping change — rollback-safe as described.

New Issues

  • 🟡 Medium: dotcms-integration/src/test/java/com/dotcms/rest/api/v1/content/search/strategies/GlobalSearchAttributeStrategyMatchingTest.java:62 — The unrelatedBody field comment states it is "an unrelated content item whose BODY (not title) contains the searched substring — proves the fix stays scoped to title/name and does not reintroduce whole-document body matching." But the item actually seeded (lines 127–132) sets only title = "Unrelated report " + uniqueId and puts 1004 nowhere — not in the title and not in any body field (the second comment at 124–132 correctly says "no 1004 anywhere"). So the negative control never exercises the thing the field comment claims: an item whose body contains 1004 but whose title does not. Because [BUG] Content Drive: keyword/title search returns inconsistent or incoherent results #36688-style whole-document body matching is an explicitly stated regression concern, this guard is weaker than advertised — it would pass even if body text were being matched. To actually prove title-scoping, seed an item whose body/other field contains 1004 while its title does not, and assert it is not returned. At minimum, fix the contradictory field comment so it matches the data. Fix this →

Everything else checks out — the LuceneQueryBuilderTest expected-string update matches the new output exactly, and the MainSuite3a registration is correct. The pre-existing BinaryFieldStrategy test failure and leading-wildcard perf are correctly flagged as out of scope.
issue-36791-global-search-exact-name-match

@mergify

mergify Bot commented Jul 30, 2026

Copy link
Copy Markdown

Queued — the merge queue status continues in this comment ↓.

@ihoffmann-dot
ihoffmann-dot 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 no response for status checks Jul 31, 2026
@ihoffmann-dot
ihoffmann-dot added this pull request to the merge queue Jul 31, 2026
@mergify

mergify Bot commented Jul 31, 2026

Copy link
Copy Markdown

Merge Queue Status

This pull request spent 1 hour 48 minutes 13 seconds in the queue, including 1 hour 47 minutes 18 seconds running CI.

Required conditions to merge

Reason

Pull request #36793 has been dequeued

GitHub refused to merge the pull request. Pull Request is in the merge queue. This is usually enforced by a branch protection or ruleset rule.

Hint

You should look at the reason for the failure and decide if the pull request needs to be fixed or if you want to requeue it.
If you do update this pull request, it will automatically be requeued once the queue conditions match again.
If you think this was a flaky issue instead, you can requeue the pull request, without updating it, by posting a @mergifyio queue comment.

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

  • Requeue this pull request

@mergify mergify Bot added the queued label Jul 31, 2026
@mergify mergify Bot added dequeued and removed queued labels Jul 31, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Jul 31, 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 dequeued Team : Scout

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Global/Content search: exact-name and mid-token terms don't match (prefix-only catchall gate)

2 participants