Skip to content

feat(content-drive): field-filter resolver — Tag (DB) + index clauses via search strategies (#36384)#36474

Open
ihoffmann-dot wants to merge 1 commit into
36384-content-drive-field-search-contractfrom
36384-content-drive-field-search-resolver
Open

feat(content-drive): field-filter resolver — Tag (DB) + index clauses via search strategies (#36384)#36474
ihoffmann-dot wants to merge 1 commit into
36384-content-drive-field-search-contractfrom
36384-content-drive-field-search-resolver

Conversation

@ihoffmann-dot

Copy link
Copy Markdown
Member

Proposed Changes

Second slice (PR 2 of 3) of the Content Drive field-based search backend — issue #36384. Builds on #36463 (contract + plumbing) and implements the actual resolution of userSearchable field filters into DB predicates and index clauses, per the ADR-0018 routing contract.

Stacked on #36463. Base branch is 36384-content-drive-field-search-contract; review that one first. Once #36463 merges, this can be retargeted to main.

  • Index-routed clauses (reuse strategies)BrowserAPIImpl.buildBaseESQuery now appends the per-field Lucene clauses for index-routed criteria (Text/Textarea/WYSIWYG, Select/Radio, Multi-Select/Checkbox, Boolean, Date/Time/Date-Time, Category). The field-value → Lucene translation is delegated to the shared strategies in com.dotcms.rest.api.v1.content.search (FieldHandlerRegistry + FieldContext) so the syntax matches the modern content search — no hand-rolled Lucene. Clauses are injected into the existing hybrid per-chunk narrowing, so they AND with the text term and the DB candidate set.
  • DB-routed predicate: TagselectQuery now appends a tag/tag_inode sub-select for Tag criteria, resolved DB-side regardless of ES filtering to preserve read-your-writes. Tag names within a field combine with OR; criteria across fields combine with AND. Category stays index-only (no SQL tree resolution).
  • Value formatting — a small formatter maps each FieldSearchCriteria to the value string its strategy expects (scalar, comma-joined multi, true/false, from TO to range with * for open bounds).
  • ModelFieldSearchCriteria now carries its ContentType (needed to build the ct.field Lucene name and provide strategy context).
  • Routing preserved — index-routed criteria never touch SQL; Tag never touches the index; Category never hits SQL; no structural predicate is re-routed. Default heuristic stays HYBRID_SINGLE_CHUNKED_QUERY_ES.

Checklist

  • Tests — new ContentDriveFieldFilterTest (integration): text→index, tag→DB (read-your-writes), tag OR within field, date range→index, index+DB AND composition, and the three 400 validations. 8/8 passing locally against the integration environment (Postgres + OpenSearch).
  • Translations
  • Security Implications Contemplated — Category resolution reuses CategoryFieldStrategy's per-user permission filtering; all field values are validated against the content type schema (PR 1); tag/field values are bound as SQL parameters (no concatenation of user input into SQL).

Additional Info

  • /v1/drive/search is @Hidden → no openapi.yaml change.
  • Known semantic to confirm with FE: Multi-Select/Checkbox "in list" reuses the shared TextFieldStrategy, which ANDs the tokens (matches the current content-search behavior) rather than OR-ing them. Category and Tag are OR-in-list. If FE expects OR for multi-select, we can special-case it — flagged rather than silently diverging from content-search.
  • Tag matching is by name only (case-insensitive, exact), mirroring the existing text-filter tag join. Not scoped to tag_inode.field_var_name to avoid false negatives from legacy rows; a content type with two tag fields could cross-match — acceptable v1 limitation, easy to tighten later.
  • Relationship remains v1.1 (rejected at parse time) — PR 3.

Screenshots

N/A — backend only.

@mergify

mergify Bot commented Jul 8, 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

@claude

claude Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @ihoffmann-dot's task in 10m 36s —— View job


Code Review — Content Drive field-filter resolver (PR #36474)

Reviewed against origin/36384-content-drive-field-search-contract. Read the ES/DB routing in BrowserAPIImpl, the shared field strategies, and the resolver/model changes. Overall the routing contract is implemented cleanly, Tag SQL is parameterized (no injection), and the test coverage for the default HYBRID path is solid.

New Issues

  • 🟡 Medium: dotCMS/src/main/java/com/dotcms/browser/BrowserAPIImpl.java:550PURE_ES heuristic silently drops all field filters. buildBaseESQuery now appends the index-routed field clauses, but the alternate buildPureESQuery (used when BROWSE_API_HEURISTIC_TYPE=PURE_ES) was not updated and never calls buildFieldCriteriaESClauses. The pure-ES path also bypasses the DB selectQuery entirely, so the Tag predicate is skipped too. Net effect under that config: a userSearchable request returns unfiltered content (too many results) with no error. Default is HYBRID_SINGLE_CHUNKED_QUERY_ES, so this is config-gated, but the divergence is easy to miss.
    Assumption: PURE_ES remains a supported, operator-selectable heuristic. What to verify: whether Content Drive can ever run under PURE_ES; if so, mirror the field-clause + tag handling there (or reject field filters when the heuristic is PURE_ET). Fix this →

  • 🟡 Medium: dotCMS/src/main/java/com/dotcms/browser/BrowserAPIImpl.java:1915Tag predicate is not scoped to the field, so two Tag fields on the same type cross-match. appendTagQuery joins tag/tag_inode by tagname only, without constraining tag_inode.field_var_name. If the content type has two Tag fields, a value filtered on field A will also match content tagged via field B. The PR body acknowledges this as an accepted v1 limitation — flagging so it's a conscious sign-off rather than an oversight. Tightening later would require the field-scoped join.

  • 🟡 Medium: dotcms-integration/.../ContentDriveFieldFilterTest.java:564new Date(year-1900, 0, 1) uses the JVM default timezone, while the range bounds ("2023-01-01", "2025-01-01") are parsed by ES/Lucene. On a JVM running west of UTC, the 2024 content's stored instant can shift into the previous day but stays comfortably inside the 2023–2025 window, so the assertions still hold — but the deprecated Date(int,int,int) ctor makes the intent (and any tighter future range test) fragile. Consider an explicit UTC Calendar/Instant. Non-blocking.

Notes (non-blocking, confirm intent)

  • BrowserAPIImpl.java:139 fieldCriteriaLuceneValueBOOLEAN/Checkbox routes through TextFieldStrategy (+(ct.field:*true* ct.field_dotraw:*true*)), i.e. a substring match on true/false. This matches the shared content-search behavior for Checkbox, so it's consistent, but a stored value containing the substring could match. Confirm this is the desired boolean semantics (the PR body already flags the Multi-Select AND-vs-OR question in the same area).
  • Tag values are correctly bound via parameters (t.tagname ILIKE ? + parameters.add(...)) — no SQL injection. Category reuses CategoryFieldStrategy's per-user permission filtering. Good.

No blocking issues — the two Medium routing/scoping items are worth resolving or explicitly signing off before merge, but the default path is correct and tested.
· branch 36384-content-drive-field-search-resolver

@zJaaal

zJaaal commented Jul 8, 2026

Copy link
Copy Markdown
Member

Re: the "Known semantic to confirm with FE" note on Multi-Select/Checkbox —

On the FE these render as a multi-pick listbox: the user ticks several values expecting "match any of these" (OR). Two concerns with the current AND behavior (from reusing TextFieldStrategy):

  1. Inconsistency within the same feature — Tag and Category are already OR-in-list, so a Multi-Select/Checkbox row that ANDs its tokens would behave the opposite way from a Tag/Category row that looks identical in the UI.
  2. AND rarely matches intent for a filter — selecting A and B to mean "items that have both" is a narrow case; "items that have either" is the common expectation.

So the FE expectation is OR for Multi-Select/Checkbox. Could we special-case those two to OR the tokens rather than inheriting the text strategy's AND? If there's a reason to keep AND, happy to talk it through — just flagging before this merges since the behavior would diverge from what the UI implies.

Drafted by Claude Code on my behalf.

@fabrizzio-dotCMS fabrizzio-dotCMS 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.

Reviewed the resolver slice (stacked on #36463). Clean implementation — reusing the shared FieldStrategy handlers instead of hand-rolling Lucene is the right call, and the INDEX↔SQL routing is respected. One blocking item (test not wired into CI, inline) plus coverage gaps below.

Coverage gaps worth closing:

  • The new IT only exercises Text (index), Tag (DB), Date range and the 3 validations. Missing: Select/Radio SCALAR equals, Multi-Select/Checkbox MULTI (exactly the AND-vs-OR semantic you flagged — deserves a test pinning the current behavior), Boolean checkbox, and Category (the permission-filtered path the security note relies on). Also: open-ended range (* bound), out-of-scope type → 400, kind-mismatch → 400, Relationship → 400.
  • No test goes through JSON deserialization. The IT builds DriveRequestForm via the builder, so userSearchable (Map<String,Object>) never passes through Jackson. inferKind's Boolean/Map/Iterable branches are validated with Java types, not with what Jackson actually produces from a JSON body. A Postman test on POST /v1/drive/search would cover the real HTTP/JSON contract and the 400s — the existing ContentDriveResource.postman_collection.json currently has 0 references to userSearchable. Recommend adding one.
  • A pure unit test for ContentDriveFieldFilterResolver (see the note on #36463) still applies.

*/
@ApplicationScoped
@RunWith(DataProviderWeldRunner.class)
public class ContentDriveFieldFilterTest extends IntegrationTestBase {

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 IT isn't registered in any suite, so it won't run in CI. Failsafe in dotcms-integration/pom.xml only includes MainSuite*/Junit5Suite*/QuickSuite/OpenSearchUpgradeSuite; standalone classes aren't discovered. The sibling drive tests are wired into MainSuite3a (lines 70-71) — this one isn't, so the 8 "passing locally" tests never execute in the pipeline.

Fix: add ContentDriveFieldFilterTest.class to MainSuite3a. And since the Text/Date cases depend on the index, consider also adding it to OpenSearchUpgradeSuite so it's validated under the ES→OS migration matrix.

final Field field = criteria.getField();
final ContentType contentType = criteria.getContentType();
final String luceneFieldName = contentType.variable() + "." + field.variable();
final Function<FieldContext, String> handler = FieldHandlerRegistry.getHandler(field.type());

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.

getHandler returns getOrDefault(type, ctx -> BLANK), so an INDEX-routed field type without a registered handler silently produces no clause → the criterion returns everything unfiltered instead of failing. All 11 current index-routed types (PR 1's isIndexRouted) are registered, so this is correct today, but the coupling is silent. Consider a fail-fast/WARN when bucket == INDEX and the resolved handler yields BLANK for a non-empty value, so a future type added to isIndexRouted without a handler doesn't degrade to "returns everything".

? criteria.getRangeTo() : "*";
return from + " TO " + to;
case MULTI:
return String.join(",", criteria.getValues());

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.

MULTI joins values by comma into TextFieldStrategy, which ANDs the tokens (the semantic you flagged). Recommend confirming the OR-vs-AND expectation with FE before they consume it. Related and not called out in the description: Boolean Checkbox also routes through the TEXT handler group with "true"/"false" — worth a quick confirm that it matches an indexed boolean checkbox correctly.

"Field-filter test data ready under " + testAssetPath);
}

private static Date date(final int year) {

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.

new Date(year - 1900, 0, 1) is deprecated and JVM-local-timezone; the date-range boundary assertions could get flaky on a non-UTC CI agent. Prefer an explicit UTC Instant/ISO-8601 string.

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

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Backend] API support for Content Drive field-based search/filter criteria

3 participants