Description
The Content Drive search box has one behavior: every term runs a global, all-content search. There is no way to say "I only care about the title". Authors who know the name of what they are looking for get back everything whose body, blocks or metadata happens to contain the word, and the row they wanted is buried.
Add a scope selector to the right of the search input, inside the same rounded box, with two options:
| Option |
Behavior |
| Title |
Match against the contentlet title (and folder/file names) only |
| All Content |
Current behavior: title plus every indexed field |
The control is a small dropdown anchored to the search box, showing the active scope as its label and a check next to the selected option:
┌──────────────────────────────────────────────┬───────────┐
│ 🔍 Search by title │ Title ▲ │
└──────────────────────────────────────────────┴───────────┘
┌──────────────────┐
│ Title ✓ │
│ All Content │
└──────────────────┘
The placeholder follows the scope ("Search by title" vs "Search all content"), so the box says what it will do before the user types.
This is scoped to Content Drive search (POST /api/v1/drive/search). It is not a rewrite of the search strategy, it is a switch that selects between the existing all-content query and a narrower title-only one.
Why this also matters for performance
The all-content query is the expensive one, and today every keystroke pays for it. GlobalSearchAttributeStrategy builds a mandatory gate of:
+(catchall:<value>*^10 OR title_dotraw:*<value>*^2)
Two costs sit in that single line:
catchall aggregates every field of the document (body, Story Block content, metadata). A common word matches a large slice of the index, so the term is cheap to look up and enormously expensive in what it returns.
title_dotraw:*<value>* is a leading wildcard, which forces a scan across every distinct raw title in the index rather than a prefix seek.
In the drive path the ES hit set is not the end of the work. BrowserAPIImpl feeds those matches through DB hydration and per-chunk permission filtering (BROWSER_CONTENT_CHUNK_SIZE, default 900) before a page can be returned, so a broad match multiplies DB round trips and permission checks, not just ES time. A title-only query that drops the catchall clause and gates on a title prefix (+title:<value>*, keeping title_dotraw as a boost or fallback rather than as the gate) shrinks the candidate set at the source, and everything downstream gets cheaper with it.
So the scope selector is worth building twice over: it is the result quality users are asking for, and it gives them a fast path that avoids the most expensive clause in the query.
Where to point
Frontend
| File |
What changes |
core-web/libs/ui/src/lib/components/dot-search-input/dot-search-input.component.ts / .html |
Shared presentational box. Add the scope dropdown as an optional input (plus a scopeChange output) so the AssetPicker toolbar, which reuses this component, is untouched unless it opts in. |
core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-search-input/dot-content-drive-search-input.component.ts |
Store adapter. Opts the dropdown in, binds the current scope, forwards changes. |
.../store/dot-content-drive.store.ts — setGlobalSearch (~line 228) |
Carry the scope alongside the title filter; resetting pagination the same way. |
.../store/dot-content-drive.store.ts — $request computed (~line 119) |
Send the scope in the request payload next to filters.text. |
.../lib/utils/functions.ts |
Filter encode/decode, so the scope survives a URL restore and Back/Forward like every other filter. |
core-web/libs/dotcms-models/src/lib/dot-content-drive.model.ts |
DotContentDriveSearchRequest / filters model. |
Backend
| File |
What changes |
dotCMS/src/main/java/com/dotcms/rest/api/v1/drive/AbstractQueryFilters.java |
Natural home for the new field, next to text() and filterFolders(). Default it to the current behavior so existing callers are unaffected. |
.../v1/drive/ContentDriveHelper.java (~lines 179–183) |
Where filters.text() is wired into BrowserQuery.withFilter() and useElasticsearchFiltering(true) is flipped on. The scope rides along here. |
dotCMS/src/main/java/com/dotcms/browser/BrowserQuery.java |
Carry the scope through to the API. |
dotCMS/src/main/java/com/dotcms/browser/BrowserAPIImpl.java — buildBaseESQuery (~line 1193) |
The branch point: today it unconditionally calls GlobalSearchAttributeStrategy. Title mode selects the narrower query instead. Note buildPureESQuery (~line 663) still holds an older title:'*x*'^5 OR catchall:*x*^3 OR fileName:*x*^2 shape — worth confirming whether the drive reaches it. |
.../v1/content/search/strategies/GlobalSearchAttributeStrategy.java |
The current all-content query. Leave it as the All Content path; add a title-scoped sibling rather than branching inside it. |
Related history: #36688 replaced the old broad catchall:*kw* leading wildcard with the current strategy for exactly these reasons, and #36814 tracks search performance at scale.
Acceptance Criteria
Open decisions
- Default scope. The mock shows Title selected with a "Search by title" placeholder. Defaulting to Title gives everyone the fast path, but silently changes behavior for existing users. Defaulting to All Content is the no-regression choice. Needs a call before implementation.
- File names in Title mode. For file assets, the file name is usually what the user means by "title". Should Title mode also cover
fileName / metadata.name, or stay strictly on title?
- Stickiness. Is the scope per-search (resets on reload beyond the URL), or remembered per user?
- Sorting. Results are currently sorted by score descending whenever a term is present. Confirm that holds in Title mode.
Additional Context
Behavior surfaced from the attached mock. Frontend and backend both change; the frontend cannot narrow the query on its own because the Lucene string is built server-side in BrowserAPIImpl.
Description
The Content Drive search box has one behavior: every term runs a global, all-content search. There is no way to say "I only care about the title". Authors who know the name of what they are looking for get back everything whose body, blocks or metadata happens to contain the word, and the row they wanted is buried.
Add a scope selector to the right of the search input, inside the same rounded box, with two options:
The control is a small dropdown anchored to the search box, showing the active scope as its label and a check next to the selected option:
The placeholder follows the scope ("Search by title" vs "Search all content"), so the box says what it will do before the user types.
This is scoped to Content Drive search (
POST /api/v1/drive/search). It is not a rewrite of the search strategy, it is a switch that selects between the existing all-content query and a narrower title-only one.Why this also matters for performance
The all-content query is the expensive one, and today every keystroke pays for it.
GlobalSearchAttributeStrategybuilds a mandatory gate of:Two costs sit in that single line:
catchallaggregates every field of the document (body, Story Block content, metadata). A common word matches a large slice of the index, so the term is cheap to look up and enormously expensive in what it returns.title_dotraw:*<value>*is a leading wildcard, which forces a scan across every distinct raw title in the index rather than a prefix seek.In the drive path the ES hit set is not the end of the work.
BrowserAPIImplfeeds those matches through DB hydration and per-chunk permission filtering (BROWSER_CONTENT_CHUNK_SIZE, default 900) before a page can be returned, so a broad match multiplies DB round trips and permission checks, not just ES time. A title-only query that drops thecatchallclause and gates on a title prefix (+title:<value>*, keepingtitle_dotrawas a boost or fallback rather than as the gate) shrinks the candidate set at the source, and everything downstream gets cheaper with it.So the scope selector is worth building twice over: it is the result quality users are asking for, and it gives them a fast path that avoids the most expensive clause in the query.
Where to point
Frontend
core-web/libs/ui/src/lib/components/dot-search-input/dot-search-input.component.ts/.htmlscopeChangeoutput) so the AssetPicker toolbar, which reuses this component, is untouched unless it opts in.core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-search-input/dot-content-drive-search-input.component.ts.../store/dot-content-drive.store.ts—setGlobalSearch(~line 228)titlefilter; resetting pagination the same way..../store/dot-content-drive.store.ts—$requestcomputed (~line 119)filters.text..../lib/utils/functions.tscore-web/libs/dotcms-models/src/lib/dot-content-drive.model.tsDotContentDriveSearchRequest/ filters model.Backend
dotCMS/src/main/java/com/dotcms/rest/api/v1/drive/AbstractQueryFilters.javatext()andfilterFolders(). Default it to the current behavior so existing callers are unaffected..../v1/drive/ContentDriveHelper.java(~lines 179–183)filters.text()is wired intoBrowserQuery.withFilter()anduseElasticsearchFiltering(true)is flipped on. The scope rides along here.dotCMS/src/main/java/com/dotcms/browser/BrowserQuery.javadotCMS/src/main/java/com/dotcms/browser/BrowserAPIImpl.java—buildBaseESQuery(~line 1193)GlobalSearchAttributeStrategy. Title mode selects the narrower query instead. NotebuildPureESQuery(~line 663) still holds an oldertitle:'*x*'^5 OR catchall:*x*^3 OR fileName:*x*^2shape — worth confirming whether the drive reaches it..../v1/content/search/strategies/GlobalSearchAttributeStrategy.javaRelated history: #36688 replaced the old broad
catchall:*kw*leading wildcard with the current strategy for exactly these reasons, and #36814 tracks search performance at scale.Acceptance Criteria
catchallclause and no leading-wildcard term as its mandatory gatePOST /api/v1/drive/searchin both scopes, including the omitted-field defaultOpen decisions
fileName/metadata.name, or stay strictly ontitle?Additional Context
Behavior surfaced from the attached mock. Frontend and backend both change; the frontend cannot narrow the query on its own because the Lucene string is built server-side in
BrowserAPIImpl.