Skip to content

A TextField stored in a numeric column makes the whole contentlet unindexable #37272

Description

@dario-daza

Problem Statement

ESMappingAPIImpl.loadFields() decides how to serialize a field by its storage column
(field_contentlet) rather than its field type. When a TextField is backed by an integerN
or floatN column — which dotCMS allows and which exists in real content types — the String value
is handed to DecimalFormat.format() and throws:

WARN  business.ESMappingAPIImpl - Error indexing field: viewWeekTotal of contentlet: 3c3276a0-33d2-4c5e-80a8-09edeb11572f
java.lang.IllegalArgumentException: Cannot format given Object as a Number
    at java.base/java.text.DecimalFormat.format(DecimalFormat.java:584)
    at java.base/java.text.Format.format(Format.java:165)
    at com.dotcms.content.elasticsearch.business.ESMappingAPIImpl.loadFields(ESMappingAPIImpl.java:1118)
    at com.dotcms.content.elasticsearch.business.ESMappingAPIImpl.toMap(ESMappingAPIImpl.java:465)
    at com.dotcms.content.elasticsearch.business.ContentletIndexAPIImpl.addBulkRequest(ContentletIndexAPIImpl.java:2785)

The affected fields:

Content Type Field field_type field_contentlet
RptTopHits viewWeekTotal com.dotcms.contenttype.model.field.TextField integer1
Trending viewWeekTotal com.dotcms.contenttype.model.field.TextField integer1

Stored value: {"type": "Text", "value": "54"} — a String, correctly matching the declared
TextField. But the branch is chosen by the column name:

// ESMappingAPIImpl.java:1114
} else if (field.getFieldContentlet()
        .startsWith(ESMappingConstants.FIELD_ELASTIC_TYPE_FLOAT) || field
        .getFieldContentlet()
        .startsWith(ESMappingConstants.FIELD_ELASTIC_TYPE_INTEGER)) {
    contentletMap.put(keyName, valueObj);
    contentletMap.put(keyNameText, numFormatter.format(valueObj));   // String -> IllegalArgumentException
}

integer1 starts with integer, so the numeric branch is taken for a field whose type and value
are both textual.

The error handling contradicts itself

// ESMappingAPIImpl.java:1141
} catch (Exception e) {
    Logger.warn(ESMappingAPIImpl.class, "Error indexing field: " + field.getFieldName()
            + " of contentlet: " + contentlet.getInode(), e);
    throw new DotDataException(e.getMessage(), e);
}

It logs at WARN — the level that says "recoverable, carry on" — and then rethrows, aborting the
entire contentlet. One misconfigured field out of dozens makes the whole document unindexable. The
adjacent Date branch a few lines above does the opposite and degrades gracefully:

} catch(Exception ex) {
    contentletMap.put(keyName, valueObj);
    contentletMap.put(keyNameText, valueObj.toString());
}

A valueObj.toString() fallback here would have indexed "54" correctly, since that is what the
field declares itself to be.

How it surfaced

Found while reindexing 370 contentlets via POST /api/v1/content/_bulkrefresh: 364 succeeded, 6
failed — 4 from an unrelated serialization limit and these 2. Both are RptTopHits/Trending
content, so any reindex touching those types loses those documents. The failure is silent from the
operator's side: the UI reports a flat failure count with no field attribution (see the companion
issue on per-engine and per-document failure visibility).

Steps to Reproduce

  1. Create a content type with a Text field whose backing column is numeric. On an existing
    install this can be confirmed with:

    SELECT st.velocity_var_name AS content_type, f.velocity_var_name AS field,
           f.field_type, f.field_contentlet
    FROM field f JOIN structure st ON st.inode = f.structure_inode
    WHERE f.field_type LIKE '%TextField'
      AND (f.field_contentlet LIKE 'integer%' OR f.field_contentlet LIKE 'float%');
  2. Save a contentlet with a textual value in that field (e.g. "54").

  3. Reindex it — full reindex, POST /api/v1/esindex/reindex?contentType=<type>, or
    POST /api/v1/content/_bulkrefresh.

  4. Observe Cannot format given Object as a Number and the contentlet absent from the index.

  5. Note the value is a perfectly valid TextField value; nothing in the UI flags the field or
    the content type as invalid.

Acceptance Criteria

  • loadFields() selects the serialization branch by field type, not by storage column;
    or the numeric branch tolerates non-numeric values.
  • A field that cannot be serialized does not abort the whole contentlet — it is skipped (or
    indexed via toString()) and the rest of the document is indexed, consistent with how the
    Date branch already behaves.
  • If a field is genuinely unindexable, the reported failure names the field, not just the
    contentlet, so the content-type misconfiguration is actionable.
  • The log level matches the outcome: if the document is aborted it is an ERROR, not a WARN.
  • Consider validating type/column consistency when a content type is saved, so this cannot be
    modelled in the first place.

dotCMS Version

dotcms/dotcms:trunk. PostgreSQL 16, ~1.55 M contentlets, OpenSearch 1.3.20 + 3.4.0 in
PHASE_1_DUAL_WRITE_ES_READS.

Severity

Medium - Some functionality impacted

Medium — affects only content types with this modelling, but for those every document is silently
missing from the index, and the cause is not discoverable from the UI.

Links

  • ESMappingAPIImpl.java:1114 — branch selected by field_contentlet instead of field type
  • ESMappingAPIImpl.java:1118numFormatter.format(valueObj)
  • ESMappingAPIImpl.java:1141 — WARN followed by rethrow
  • ESMappingAPIImpl.java:1121 — the Date branch that degrades gracefully instead

Freshdesk ticket: NA — found during internal ES→OpenSearch migration testing.

Activity

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

Metadata

Metadata

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions