fix(search): prevent ES/OS document rejections via engine-native mapping hardening#28671
fix(search): prevent ES/OS document rejections via engine-native mapping hardening#28671mohityadav766 wants to merge 42 commits into
Conversation
…ing hardening Documents were being silently rejected by Elasticsearch/OpenSearch (immense-term on keyword > 32766 bytes, malformed numbers/dates, nested/depth explosion) and dead-lettered by the retry worker. Root cause: 66% of keyword field defs had no ignore_above, no numeric/date guards, and unbounded recursive flattening. Harden mappings once at index creation (zero per-document cost; the engine enforces the bounds): - SearchIndexSettings.harden injects ignore_above (keyword + multi-fields + flattened), ignore_malformed (numeric/date/boolean), depth_limit (flattened), and tunable index.mapping.*.limit guardrails; never overwrites existing values. - Applied on both the direct createIndex and the index-template paths (ES + OS). - OsUtils strips ES-only ignore_above/depth_limit when converting flattened to flat_object for OpenSearch. Cap structural explosion at the source (the one thing engines cannot truncate): - ColumnIndex/ColumnSearchIndex: depth + column-count caps. - New SchemaFieldFlattener: shared depth + field-count cap for Topic and APIEndpoint schemaFields (dedupes two identical copies). Limits are config-tunable via ElasticSearchConfiguration.searchIndexingLimits (enableMappingHardening, keywordMaxBytes, mappingDepthLimit, nestedObjectsLimit, totalFieldsLimit, maxColumns). Tests: SearchIndexSettingsTest (per-type hardening incl. all 16 field types + flattened/extension), OsUtilsTest (flat_object strip), ColumnIndexLimitTest, SchemaFieldFlattenerTest; IndexingLimitsIT proves raw mappings reject and hardened mappings accept against the real engine (per ES/OS profile). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR hardens Elasticsearch/OpenSearch index mappings at creation time (to prevent document rejections) and adds configurable caps to recursive search-document flattening (columns and schemaFields) to avoid structural explosions during indexing.
Changes:
- Introduces
searchIndexingLimitsconfiguration (mapping hardening + engine limit settings + max columns cap). - Adds
SearchIndexSettings+SearchFieldLimitsto injectignore_above,ignore_malformed,depth_limit, andindex.mapping.*.limitsettings into mappings on index/template creation (ES + OS). - Caps recursive flattening for
columnsandschemaFields(Topic/APIEndpoint) and adds unit + integration coverage.
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| openmetadata-spec/src/main/resources/json/schema/configuration/elasticSearchConfiguration.json | Adds searchIndexingLimits config block and defaults for mapping/indexing limits. |
| openmetadata-service/src/main/java/org/openmetadata/service/search/SearchIndexSettings.java | Implements create-time mapping hardening (ignore_above / ignore_malformed / mapping limits). |
| openmetadata-service/src/main/java/org/openmetadata/service/search/SearchFieldLimits.java | Resolves limits from config (with defaults) and exposes derived thresholds/caps. |
| openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OsUtils.java | Ensures ES-only flattened params are removed when converting to OpenSearch flat_object. |
| openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchIndexManager.java | Applies mapping hardening prior to OpenSearch mapping transformation on index creation. |
| openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchGenericManager.java | Applies mapping hardening prior to OpenSearch mapping transformation on template creation. |
| openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchIndexManager.java | Applies mapping hardening on Elasticsearch index creation. |
| openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchGenericManager.java | Applies mapping hardening on Elasticsearch index-template creation. |
| openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/SchemaFieldFlattener.java | New shared, bounded flattener for schemaFields used by Topic/APIEndpoint indexing. |
| openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TopicIndex.java | Switches schemaFields flattening to the shared bounded flattener. |
| openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/APIEndpointIndex.java | Switches schemaFields flattening to the shared bounded flattener. |
| openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/ColumnSearchIndex.java | Adds depth + max-columns caps to static column flattening. |
| openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/ColumnIndex.java | Adds depth + max-columns caps to column flattening during index-doc building. |
| openmetadata-service/src/test/java/org/openmetadata/service/search/SearchIndexSettingsTest.java | Unit tests for mapping hardening behavior across field types/settings. |
| openmetadata-service/src/test/java/org/openmetadata/service/search/opensearch/OsUtilsTest.java | Verifies ES-only flattened params are stripped for flat_object. |
| openmetadata-service/src/test/java/org/openmetadata/service/search/indexes/SchemaFieldFlattenerTest.java | Verifies schemaFields flattening stops at depth + count caps. |
| openmetadata-service/src/test/java/org/openmetadata/service/search/indexes/ColumnIndexLimitTest.java | Verifies column flattening stops at depth + count caps (interface + static). |
| openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/IndexingLimitsIT.java | Integration test proving hardened mappings accept docs that raw mappings reject. |
| private static int clampKeywordBytes(Integer value) { | ||
| int resolved = orDefault(value, LUCENE_KEYWORD_MAX_BYTES); | ||
| return Math.min(resolved, LUCENE_KEYWORD_MAX_BYTES); | ||
| } |
| @Execution(ExecutionMode.CONCURRENT) | ||
| public class IndexingLimitsIT { | ||
|
|
||
| private static final List<String> CREATED_INDICES = new ArrayList<>(); | ||
|
|
✅ TypeScript Types Auto-UpdatedThe generated TypeScript types have been automatically updated based on JSON schema changes in this PR. |
- SearchFieldLimits.loadActive: don't permanently cache defaults when IndexMappingLoader isn't initialized yet (would silently ignore configured limits for the JVM lifetime); only cache once the config is resolvable. - SearchFieldLimits.clampKeywordBytes: floor keywordMaxBytes at 4 so ignore_above (= value/4) can never be 0 (which would disable keyword indexing); schema minimum bumped to 4. - ColumnIndex / SchemaFieldFlattener: pass the fully-qualified name (not the local name) into the recursion so deeply nested (>2 levels) columns/fields get correct dotted paths (a.b.c, not b.c). - IndexingLimitsIT: CopyOnWriteArrayList for CREATED_INDICES (was a plain ArrayList mutated under @execution(CONCURRENT)). - Schema docs: clarify nested_objects.limit rejects (not truncates) and that maxColumns also caps schema-field flattening. - Tests: FQN-path assertions for columns and schema fields; tiny keywordMaxBytes ignore_above >= 1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| Column col = columns.get(index); | ||
| if (col.getTags() != null) { | ||
| tags = col.getTags(); | ||
| } | ||
| String columnName = addFlattenColumn(col, optParentColumn, tags, flattenColumns); |
| Field field = fields.get(index); | ||
| if (field.getTags() != null) { | ||
| tags = field.getTags(); | ||
| } | ||
| String fieldName = addFlattenField(field, optParentField, tags, flattenSchemaFields); |
✅ TypeScript Types Auto-UpdatedThe generated TypeScript types have been automatically updated based on JSON schema changes in this PR. |
🔴 Playwright Results — 12 failure(s), 48 flaky✅ 3597 passed · ❌ 12 failed · 🟡 48 flaky · ⏭️ 39 skipped
Genuine Failures (failed on all attempts)❌
|
| Optional<String> optParentField = | ||
| Optional.ofNullable(parentField).filter(Predicate.not(String::isEmpty)); | ||
| List<TagLabel> tags = new ArrayList<>(); | ||
| int index = 0; | ||
| boolean capReached = false; | ||
| while (index < fields.size() && !capReached) { | ||
| if (flattenSchemaFields.size() >= limits.getMaxColumns()) { | ||
| LOG.warn( | ||
| "Reached max indexed schema fields {}; dropping remaining under '{}'", | ||
| limits.getMaxColumns(), | ||
| parentField); | ||
| capReached = true; | ||
| } else { | ||
| Field field = fields.get(index); | ||
| if (field.getTags() != null) { | ||
| tags = field.getTags(); | ||
| } | ||
| String fieldName = addFlattenField(field, optParentField, tags, flattenSchemaFields); |
| Optional<String> optParentColumn = | ||
| Optional.ofNullable(parentColumn).filter(Predicate.not(String::isEmpty)); | ||
| List<TagLabel> tags = new ArrayList<>(); | ||
| for (Column col : columns) { | ||
| String columnName = col.getName(); | ||
| if (optParentColumn.isPresent()) { | ||
| columnName = FullyQualifiedName.add(optParentColumn.get(), columnName); | ||
| } | ||
| if (col.getTags() != null) { | ||
| tags = col.getTags(); | ||
| int index = 0; | ||
| boolean capReached = false; | ||
| while (index < columns.size() && !capReached) { | ||
| if (flattenColumns.size() >= limits.getMaxColumns()) { | ||
| LOG.warn( | ||
| "Reached max indexed columns {}; dropping remaining columns under '{}'", | ||
| limits.getMaxColumns(), | ||
| parentColumn); | ||
| capReached = true; | ||
| } else { | ||
| Column col = columns.get(index); | ||
| if (col.getTags() != null) { | ||
| tags = col.getTags(); | ||
| } | ||
| String columnName = addFlattenColumn(col, optParentColumn, tags, flattenColumns); | ||
| if (col.getChildren() != null) { |
|
The Java checkstyle failed. Please run You can install the pre-commit hooks with |
The status indexer stripped only the free-form `config` map from each pipeline run status; `metadata` is the same arbitrary per-run map and carried the identical exposure — its keys were dynamically mapped, risking string-then-object type conflicts and pushing the index past total_fields.limit (a hard document rejection). Strip both via a single NON_SEARCHABLE_STATUS_FIELDS set; searchable fields (state, runId, timestamps) are preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restructure the page to match the established settings-page conventions: a header with right-aligned Save/Reset actions, an info banner, a toolbar card with labelled responsive language + (searchable) entity-type selectors, and a bordered editor card showing the active entity/language with a Format action, loader, and empty state. Heights and layout move to a dedicated .less using design tokens. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tsIT The rest5 low-level client surfaces a rejected write as the Response carrying the 4xx rather than always throwing, so rejects() — which relied on catching an exception — always returned false, failing every "raw must reject" assertion on both engines. Inspect the status code (and still unwrap a thrown ResponseException), mirroring how ElasticSearchClient reads e.getResponse(); accepts() already used the status code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… mapping The recursive column/struct flatten that builds columnNamesFuzzy was capped at the global depth limit (20), so a column nested deeper than 20 was never searchable and could not be recovered. Resolve the depth per entity type from the stored, admin-editable index mapping (settings.index.mapping.depth.limit) via SearchFieldLimits.forEntity(...), memoized and invalidated when the mapping setting changes. Raising the depth through the search-index-mappings API and reindexing now makes deeper column names searchable; the default 20 still protects against explosion. SearchIndexImmenseTermIT is reworked to prove this end to end (default depth drops a 25-level column, raising it via the API restores searchability), and the Playwright counterpart asserts searchability via an in-limit column while still guarding that the oversized deeply nested column never breaks indexing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
buildEntityMapping fed the request-supplied `language` path parameter straight into a classpath resource path (getIndexMappingFile -> getResource AsStream), which CodeQL flagged as path injection. Validate it against the trusted IndexMappingLanguage allowlist first; an unknown language now yields no mapping (404 on the endpoint) instead of an arbitrary resource read. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review 👍 Approved with suggestions 5 resolved / 7 findingsHardens search index mappings with engine-native limits and introduces persistent, UI-editable mapping management to prevent document rejection. Ensure the read-modify-write cycle in 💡 Bug: Read-modify-write race can drop concurrent mapping edits📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/SystemRepository.java:393-407
This is admin-only and low frequency, so impact is limited, but the lost-update is silent. Consider serializing these updates (e.g. optimistic version check on the settings row, or a DB-side JSON merge) so concurrent edits to different slices don't clobber each other. 💡 Quality: updateSearchIndexMapping does not validate entityType exists📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/system/SystemResource.java:367-381 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/SystemRepository.java:378-381
Suggested fix: in Validate entityType against the known index registry before persisting.✅ 5 resolved✅ Bug: SearchFieldLimits caches defaults permanently if loaded pre-init
✅ Bug: updateIndex (PutMapping) path bypasses mapping hardening
✅ Edge Case: Removing mustExist(false) reintroduces TOCTOU failure in swapAliases
✅ Bug: Stored mapping shadows bundled resource mappings on upgrade
✅ Edge Case: enableMappingHardening=false has no effect once mappings are seeded
🤖 Prompt for agentsOptionsDisplay: compact → Showing less information. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |
…ply on upgrade Addresses review feedback that the searchIndexMappings setting, once seeded with the full default blob, permanently shadowed the bundled resource: after seeding, readIndexMapping always preferred the stored (now stale) slice, so a later release shipping mapping improvements was silently ignored until an admin manually reset each entity, with no signal one was needed. The enableMappingHardening flag was likewise a no-op once seeded. The setting now holds only entities an admin has explicitly edited. Un-edited entities resolve from the current bundled hardened resource (the fallback that already existed), so upgrades apply automatically on the next reindex and drift is detected by IndexMappingVersionTracker. seedIfAbsent initializes an empty override store; listSearchIndexMappings now enumerates editable entities from the bundled registry rather than the stored keys (same response shape, so the UI is unaffected). Hardening now runs at index-create time for un-edited entities — cheap (per index, not per doc). Also validates the entityType on update against the known index-mapping registry (was previously unchecked, persisting dead data for typos). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review 👍 Approved with suggestions 6 resolved / 7 findingsHardens search index mappings with engine-native bounds and adds an admin-managed persistence layer to prevent document rejections. Address the identified read-modify-write race condition in 💡 Bug: Read-modify-write race can drop concurrent mapping edits📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/SystemRepository.java:393-407
This is admin-only and low frequency, so impact is limited, but the lost-update is silent. Consider serializing these updates (e.g. optimistic version check on the settings row, or a DB-side JSON merge) so concurrent edits to different slices don't clobber each other. ✅ 6 resolved✅ Bug: SearchFieldLimits caches defaults permanently if loaded pre-init
✅ Bug: updateIndex (PutMapping) path bypasses mapping hardening
✅ Edge Case: Removing mustExist(false) reintroduces TOCTOU failure in swapAliases
✅ Bug: Stored mapping shadows bundled resource mappings on upgrade
✅ Edge Case: enableMappingHardening=false has no effect once mappings are seeded
...and 1 more resolved from earlier reviews 🤖 Prompt for agentsOptionsDisplay: compact → Showing less information. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |
…QL path-injection The prior allowlist guard (supportedLanguages().contains(language)) did not clear CodeQL alert #2213 (java/path-injection): a List.contains() check is not recognized as a sanitizer, and the raw request-supplied language was still passed into the classpath resource path. Resolve the language to the matching IndexMappingLanguage constant's own value and build the path from that, so the value reaching getResourceAsStream originates from the enum rather than the request. Unknown languages still yield no mapping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ngLimitsIT Once the rejects() fix let the test reach the hardened-accept assertion, the boolean case failed on OpenSearch: OsUtils strips ignore_malformed from boolean (OpenSearch does not support it), so a hardened boolean index there still rejects a malformed value. Assert acceptance conditionally — every guarded type accepts on both engines except boolean on OpenSearch, which still rejects — instead of assuming universal acceptance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…zzy match The deep-column-searchability assertion was flaky: a 25-level leaf buried in a large concatenated columnNamesFuzzy is reliably *indexed* once the container is built at the raised depth, but the fuzzy query matching it is subject to ES relevance/refresh timing that is unrelated to the depth this test exercises (one run matched instantly, another never did within 120s). Assert the deterministic signal instead: the container indexed at the default depth (20) drops the 25-level leaf from columnNamesFuzzy, and one indexed after raising the depth via the search-index-mappings API carries it. The leaf name is checked against the indexed _source.columnNamesFuzzy value (located by the container's unique description), and a short letters-only token keeps the field small. Runs in ~5s, deterministically. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback to use @openmetadata/ui-core-components instead of Ant Design: Button/Select/Card/Alert/Badge replace the AntD primitives (with Tailwind tw: utilities for layout), TitleBreadcrumb is replaced by the core-components-backed HeaderBreadcrumb, and the custom SelectOption interface is dropped in favour of the library's SelectItemType. The .less now imports variables with (reference) so it does not duplicate their output. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "indexed before delete" precondition in recursiveHardDelete_serviceSubtree_leavesNoOrphansAndSearchClean used a tight 60s, while the post-delete checks in the same method already allow 90s/180s. Secondary docs (e.g. column_search_index) are written on the async per-entity indexing lane and can sit briefly behind a concurrent full-reindex alias swap under full IT load, flaking the 60s bound. Raise it to 120s. Robustness-only: it cannot turn a real regression into a false pass, since the asserted post-delete cleanup is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review 👍 Approved with suggestions 6 resolved / 7 findingsHardens search index mappings with engine-native bounds and adds an admin-managed persistence layer to prevent document rejections. Address the identified read-modify-write race condition in 💡 Bug: Read-modify-write race can drop concurrent mapping edits📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/SystemRepository.java:393-407
This is admin-only and low frequency, so impact is limited, but the lost-update is silent. Consider serializing these updates (e.g. optimistic version check on the settings row, or a DB-side JSON merge) so concurrent edits to different slices don't clobber each other. ✅ 6 resolved✅ Bug: SearchFieldLimits caches defaults permanently if loaded pre-init
✅ Bug: updateIndex (PutMapping) path bypasses mapping hardening
✅ Edge Case: Removing mustExist(false) reintroduces TOCTOU failure in swapAliases
✅ Bug: Stored mapping shadows bundled resource mappings on upgrade
✅ Edge Case: enableMappingHardening=false has no effect once mappings are seeded
...and 1 more resolved from earlier reviews 🤖 Prompt for agentsOptionsDisplay: compact → Showing less information. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |
|
|



Problem
Search documents were being silently rejected by Elasticsearch/OpenSearch and dead-lettered by
SearchIndexRetryWorker(4xx marked non-retryable). Root causes found in the mappings:ignore_above→ a value > 32,766 bytes throws an immense-termIllegalArgumentExceptionand the whole document is rejected.columnsandschemaFields→ deep/wide structures explode the document.pipelineStatuses(config,metadata) → arbitrary keys dynamically mapped, causing string-then-object type conflicts and field-count blowups at reindex.Approach — engine-native hardening (zero per-document cost)
Harden the mapping once at index creation and let the engine enforce the bounds, instead of walking every document at index time (doesn't scale to large docs).
SearchIndexSettings.harden(content, limits):ignore_above(= byte-safe 8,191 = keywordMaxBytes/4) on keyword fields + keyword multi-fields +flattenedignore_malformedon numeric/date/booleandepth_limitonflattenedindex.mapping.{depth,nested_objects,total_fields}.limitApplied on both the direct
createIndexand the index-template paths, for ES and OS.OsUtilsstrips the ES-onlyignore_above/depth_limit(andignore_malformedonboolean) when convertingflattened → flat_objectfor OpenSearch.Structural explosion (the one thing engines can't truncate gracefully) is capped at the source:
ColumnIndex/ColumnSearchIndex— depth + column-count caps (global, not per-level)SchemaFieldFlattener— shared depth + field-count cap for Topic & APIEndpointschemaFields(also dedupes two identical copies)IngestionPipelineIndex— drops the free-formconfigandmetadatamaps from each run status before indexing (searchable fields — state, runId, timestamps — are preserved)Limits are config-tunable via
ElasticSearchConfiguration.searchIndexingLimits(enableMappingHardening,keywordMaxBytes,mappingDepthLimit,nestedObjectsLimit,totalFieldsLimit,maxColumns).Admin-editable, persisted mappings (DB-backed + UI)
The hardened mapping is no longer a read-only classpath resource — it is seeded into settings and is the effective mapping used at index creation:
searchIndexMappingssetting (SEARCH_INDEX_MAPPINGS) stores mappings keyed by language → entity type, hardened once at seed time (SearchIndexMappingsSeeder, insert-if-absent so admin edits are never clobbered). Seeded on fresh install and via the v2.0.0 upgrade migration.SearchRepository.readIndexMapping()prefers the stored slice and falls back to the hardened resource (fresh-install first boot / newly added entity types).IndexMappingVersionTrackercomputes drift against the effective mapping, so an admin edit (or a code-default change) surfaces as a reindex-required drift.SystemResource: list, get (?fallback), update (PUT, re-hardened before storage), reset-to-default (PUT).Coverage (exhaustive)
columns,schemaFields) — both capped.flattenedfields hardened;extension(andcolumns.extension,dataModel.columns.extension) is switched to a non-indexedobject(enabled:false) across all four languages — arbitrary custom-property values can no longer reject a document. Behaviour change:extension.*contents are no longer searchable; see Edge cases.mcp_*,testSuites, …) bounded by entity size +nested_objects.limit+ leafignore_above.Edge cases analysed
A focused audit of the hardening, persistence, and OS-transform paths confirmed the design holds; verdicts:
harden()is idempotent / non-destructive — never overwrites an existingignore_above/ignore_malformed/limit; recurses into nestedpropertiesand keyword multi-fields; no-ops safely when a mapping has no top-levelmappings.flattened → flat_object+ boolean-ignore_malformedstrip is applied to the effective (stored) mapping at both create and update, so ES-only params never reach OS.maxColumnsitems are kept.extension→enabled:falseis consistent acrossen/jp/ru/zh. The tradeoff:extension.<prop>fields configured as searchable in Search Settings will return no matches after reindex. This is intentional (arbitrary custom-property values were the prime immense-term/explosion source) — called out here for release notes.pipelineStatuseswas only half-guarded — the original change strippedconfigbut leftmetadata, an identically free-form per-runmap. It carried the same dynamic type-conflict exposure and could still push the index pasttotal_fields.limit(a hard rejection on ES7/OS). Now both are stripped.searchIndexMappingslimits are boot-time config (not a runtime-editable setting), so the cachedSearchFieldLimits.active()cannot go stale without a restart.Tests
SearchIndexSettingsTest(25) — per-type hardening across all 16 field types, multi-fields, flattened + column-level extension, no-override, settings-limit injectionOsUtilsTest(+1) —flat_objecttransform strips ES-only paramsColumnIndexLimitTest(4),SchemaFieldFlattenerTest(2) — depth + count capsIngestionPipelineIndexTest— asserts bothconfigandmetadataare stripped while searchable status fields are keptSearchIndexMappingsSeederTest,IndexMappingVersionTrackerTest— seeding + effective-mapping driftIndexingLimitsIT— against the real engine (both ES and OS via the two CI profiles): over-limit keyword and malformed numbers are rejected on a raw mapping and accepted on a hardened oneOut of scope (follow-ups)
searchIndexingLimits(they are config-only today)dynamic: strict(onedynamic:truehole:test_case_resolution_status:testCaseResolutionStatusDetails)suggestfields)🤖 Generated with Claude Code