OpenMetadata SearchSeparation 端到端测试套件:锁定 Tag 字段分离契约与重索引一致性

📅 发布时间:2026/9/14 20:38:03
OpenMetadata SearchSeparation 端到端测试套件:锁定 Tag 字段分离契约与重索引一致性
OpenMetadata SearchSeparation 端到端测试套件锁定 Tag 字段分离契约与重索引一致性【免费下载链接】OpenMetadataThe Open Context Layer for Data and AI , OpenMetadata is the open platform for building trusted data context and business semantics for humans, AI assistants, and agents.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMetadata导读在 OpenMetadata 中实体的标签、Tier 与认证Certification信息会写入 Elasticsearch/OpenSearch 索引用于 Explore 页面的筛选。为保证「实时索引Live Indexing」与「SearchIndexApp 重建索引Reindex」两条写入路径产出的文档结构完全一致仓库维护了一套名为 SearchSeparation 的 Playwright 端到端测试套件。本文将以 SearchSeparation 套件 README 为骨架结合套件源码、Java 侧搜索实现与单元测试完整讲解这套测试的设计思路、字段分离契约、运行机制、扩展方法及尚未覆盖的边界。一、背景为什么需要「字段分离」契约在 OpenMetadata 的搜索索引文档中标签相关的字段被刻意拆分成多个独立槽位而不是全部塞进一个tags[]大袋子字段含义tags[]仅承载分类标签Classification与术语表标签Glossary的 TagLabel 数组tier被「提升」出来的 Tier TagLabel如Tier.Tier1certification结构化的AssetCertification对象内部通过certification.tagLabel.tagFQN定位classificationTags分类标签 FQN 的反规范化列表glossaryTags术语表标签 FQN 的反规范化列表这一分离设计在 Java 侧有明确的定义。位于 TaggableIndex.java 的applyTagFields()是两条写入路径共同收敛的方法实时索引路径SearchRepository#updateEntityIndexSearchIndexApp 重建索引路径BulkSink.addEntity。方法内部通过ParseTags一次性写齐tags、tier、classificationTags、glossaryTags四个字段。消费方如 UI应当使用专用字段tier.tagFQN、certification.tagLabel.tagFQN、classificationTags、glossaryTags进行筛选而不是把tags[]当作包罗万象的集合。子级标签列、Schema 字段则通过 mergeChildTags() 在扁平化子结构时合并且仅在buildSearchIndexDocInternal中执行一次。风险点在于实时索引的标签变更走的是 Painless 脚本原地更新ctx._source层面而重建索引走的是整文档重建applyTagFields。如果某个 Painless 脚本只改了tags[].tagFQN而没有同步重推tier/classificationTags/glossaryTags两条路径的文档形状就会漂移Explore 专用字段筛选随即失配。SearchSeparation 套件的职责就是把「两条路径产出的分离形状完全一致」固化为可回归的端到端契约。二、套件总览双通道一致性验证SearchSeparation 套件位于 openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchSeparation包含共享工厂SearchSeparationSuite.ts——registerFilterSeparationSuite通用注册器每实体规格ExploreFilterSeparation.spec.tsTable、Dashboard.spec.ts、Topic.spec.ts、Pipeline.spec.ts、MlModel.spec.ts、Container.spec.ts、ApiEndpoint.spec.ts、StoredProcedure.spec.ts、Metric.spec.ts、Database.spec.ts、DatabaseSchema.spec.tsPainless 级联专项GlossaryRenameCascade.spec.ts说明文档本 README。每个实体规格对同一个全新的实体实例连续跑两遍任何一遍失败都会在断言消息中点名出问题的 facet实时索引通道Live Indexing对实体执行 PATCH依次追加 Tier、Certification、分类标签与术语表标签随后进入 Explore 页面分别按四个专用字段tier.tagFQN、certification.tagLabel.tagFQN、tags.tagFQN×2过滤实体必须出现在每个过滤结果中。重建索引通道Recreate Reindex对实体POST /api/v1/search/reindexEntities?recreatetrue轮询搜索文档直到确认分离被保留Tier 在tier.tagFQN、Cert 在certification.tagLabel.tagFQN、tags[]中没有 Tier 泄漏然后重跑全部四个 Explore 过滤。三、核心实现registerFilterSeparationSuite 逐段解析SearchSeparationSuite.ts 是套件的核心。它注册一个test.describe.serial串行块避免同一实体的两步测试并行打架。3.1 结构化实体契约export type FilterSeparationEntity EntityClass { entityResponseData: { id: string; fullyQualifiedName?: string }; serviceResponseData?: { name?: string; displayName?: string }; create(apiContext: APIRequestContext): Promiseunknown; delete(apiContext: APIRequestContext): Promiseunknown; patch(opts: { apiContext: APIRequestContext; patchData: Operation[]; }): Promiseunknown; };该类型不直接继承抽象基类EntityClass而是通过交叉类型补足结构因为create/delete/patch声明在具体子类上。patch必须是对象式签名patch({ apiContext, patchData })若某实体类仍在使用旧的定位参数签名patch(apiContext, payload)需先将其归一化DatabaseClass/DatabaseSchemaClass已如此处理再接入套件。3.2 选项与夹具生命周期export interface FilterSeparationOptions { suiteName: string; // Playwright 输出中的套件名通常用实体类型显示名 reindexEntityType: string; // 重索引载荷 type须匹配 ENTITY_PATH 取值如 dashboard、topic entityFactory: () FilterSeparationEntity; }beforeAll中一次性创建分类Classification、分类标签Tag、术语表Glossary、术语表术语GlossaryTerm及目标实体并调用applyAllFacets完成 PATCHafterAll逆序清理。由于每轮套件包含实体创建、PATCH、多次 ES 轮询和 Explore UI 断言远超出 Playwright 默认 30 秒单测超时代码显式配置了两处超时test.describe.configure({ timeout: 180_000 }); test.beforeAll(async () { test.setTimeout(180_000); ... });注释特别说明不要在 hook 内依赖test.slow()来延长 hook 超时这是不受支持的做法必须像上面那样分别对 describe 和 hook 设置超时。3.3 四条 facet 的 PATCH 载荷applyAllFacets通过一次 JSON Patch 追加四段数据SearchSeparationSuite.ts/tags/0Tier1tagFQN: Tier.Tier1source: Classification/tags/1分类标签tagFQN取测试运行期新建分类的 FQN含每轮 UUID保证全局唯一/tags/2术语表术语source: Glossary/certification{ tagLabel: { tagFQN: Certification.Gold, ... } }。四个常量的关键设计TIER_FQN Tier.Tier1与CERTIFICATION_FQN Certification.Gold是所有并行测试运行共享的固定值而分类标签/术语表术语 FQN 因包含每轮 UUID 而唯一——这一差异决定了下方断言策略的分支。3.4 实时索引通道等待与筛选断言waitForLiveIndex使用expect.poll轮询/api/v1/search/query?qfullyQualifiedName:...indexdataAsset60 秒超时直到_source.tier.tagFQN Tier.Tier1同时顺带捞取service.displayName备用。assertAllFourFiltersWork依据是否有 service 展示名分两条路径SearchSeparationSuite.ts有 service 名先按service.displayName.keyword收敛到该服务再逐个叠加共享过滤Tier、Certification与唯一过滤分类标签、术语表标签无 service 名共享过滤先以本运行唯一的分类标签 FQN 预收敛保证目标实体一定在第一页不受其他并行运行的实体也携带Tier.Tier1/Certification.Gold影响唯一过滤因 FQN 全局唯一而可独立断言。过滤交互复用了 utils/explore.ts 中的searchAndClickOnOption、clickUpdateButtonIfVisible配合page.waitForResponse(/api/v1/search/query?*)等待过滤真正生效最终断言table-data-card_${fullyQualifiedName}可见并清理筛选 chips 后回到实体页。3.5 重建索引通道reindexEntities 与文档形状断言第二个测试SearchSeparationSuite.ts通过createAdminApiContext获取管理员 JWT/api/v1/search/reindexEntities需要 admin 权限然后const reindexRes await apiContext.post( /api/v1/search/reindexEntities?recreatetrue, { data: [ { id: entity.entityResponseData.id, type: reindexEntityType, fullyQualifiedName: entity.entityResponseData.fullyQualifiedName, }, ], } ); expect(reindexRes.status()).toBeLessThan(400);随后assertReindexedDocPreservesSeparation轮询搜索文档断言五点SearchSeparationSuite.ts{ tier: source.tier?.tagFQN, // 必须等于 Tier.Tier1 certification: source.certification?.tagLabel?.tagFQN, // 必须等于 Certification.Gold hasClassificationTag: ..., // tags[] 中含分类标签 FQN hasGlossaryTag: ..., // tags[] 中含术语表 FQN tierNotInTagsBag: ..., // tags[] 中不得出现 Tier.* }最后assertAllFourFiltersWorkWithRetry以最多 3 次、每次间隔 8 秒的重试在 Explore 页面重跑四个过滤——重建索引是异步任务首轮 UI 断言可能恰好落在索引就绪之前重试用于吸收这种时序抖动。四、实体接入矩阵README 中的当前矩阵各 Spec 文件均可直接验证EntitySpecTableExploreFilterSeparation.spec.tsDashboardDashboard.spec.tsTopicTopic.spec.tsPipelinePipeline.spec.tsMlModelMlModel.spec.tsContainerContainer.spec.tsApiEndpointApiEndpoint.spec.tsStoredProcedureStoredProcedure.spec.tsMetricMetric.spec.tsDatabaseDatabase.spec.tsDatabaseSchemaDatabaseSchema.spec.ts以 Table 为例ExploreFilterSeparation.spec.ts 全文仅 31 行——套件的可扩展性正是体现在这里import { TableClass } from ../../../support/entity/TableClass; import { test } from ../../../support/fixtures/base; import { registerFilterSeparationSuite } from ./SearchSeparationSuite; test.use({ storageState: playwright/.auth/admin.json }); registerFilterSeparationSuite({ suiteName: Table, reindexEntityType: table, entityFactory: () new TableClass(), });test.use({ storageState: playwright/.auth/admin.json })声明整组测试以管理员登录态运行。五、如何新增一个实体README 给出的接入模板完整如下import { test } from playwright/test; import { MyEntityClass } from ../../../support/entity/MyEntityClass; import { registerFilterSeparationSuite } from ./searchSeparationSuite; test.use({ storageState: playwright/.auth/admin.json }); registerFilterSeparationSuite({ suiteName: MyEntity, reindexEntityType: myEntity, // matches ENTITY_PATH value entityFactory: () new MyEntityClass(), });前置条件结合源码确认实体类必须暴露create(apiContext)、delete(apiContext)与patch({ apiContext, patchData })若实体类仍在用旧的定位参数签名patch(apiContext, payload)先归一化为对象式签名参照DatabaseClass/DatabaseSchemaClass的做法reindexEntityType必须与重索引载荷中的ENTITY_PATH取值一致如table、dashboard、topic实体类需提供entityResponseData含id、fullyQualifiedName与可选的serviceResponseData新建实体需实现TaggableIndex见 TaggableIndex.java从而在索引文档上具备tier/tags/classificationTags/glossaryTags字段。接入一个新实体类型到覆盖矩阵就是约 20 行、调用一次registerFilterSeparationSuite的 Spec。六、Painless 级联专项GlossaryRenameCascade.spec.ts常规的双通道测试覆盖「新建→PATCH→重索引」路径而 GlossaryRenameCascade.spec.ts 专门锁定实时更新的级联路径——重命名术语表术语触发的是 Painless 原地更新脚本而不是整文档重建。场景流程创建术语表 术语表术语 TablePATCH 上 Tier、术语表术语、Certification先断言「重命名前」的文档形状tier.tagFQN、certification.tagLabel.tagFQN、术语 FQN 同时出现在tags[]与glossaryTags[]、tags[]无 Tier 泄漏对术语表术语执行PATCH /name重命名FQN 随之变化再断言「重命名后」的文档形状。该用例背后的机制位于 SearchClient.java服务端对每个携带该术语的文档执行UPDATE_GLOSSARY_TERM_TAG_FQN_BY_PREFIX_SCRIPT脚本遍历tags[]中source Glossary的条目、按前缀改写 FQN并在脚本末尾追加TAG_RESEPARATION_SCRIPT重新推导tier、classificationTags、glossaryTags。TAG_RESEPARATION_SCRIPT的完整逻辑SearchClient.java值得细读它精确复刻了TaggableIndex.applyTagFields的语义def newTags new ArrayList(); def tier null; def classTags new ArrayList(); def glossTags new ArrayList(); if (ctx._source.containsKey(tags) ctx._source.tags ! null) { for (def t : ctx._source.tags) { if (t null || !t.containsKey(tagFQN) || t.tagFQN null) { continue; } if (t.tagFQN.startsWith(Tier.)) { tier t; } else { newTags.add(t); } if (t.containsKey(source)) { if (t.source Classification) { classTags.add(t.tagFQN); } else if (t.source Glossary) { glossTags.add(t.tagFQN); } } } ctx._source.tags newTags; if (tier ! null) { ctx._source.tier tier; } ctx._source.classificationTags classTags; ctx._source.glossaryTags glossTags; }三个值得注意的实现细节containsKey(tags)守卫UPDATE_FQN_PREFIX_SCRIPT会作用到全局搜索别名含tag_search_index这类没有tags字段的索引若四个反规范化写入无条件执行会污染文档因此全部写入被包在守卫内if (tier ! null)保护applyTagFields在索引期已把 Tier 从tags[]提升到tier字段因此被 Painless 触碰的文档几乎不会在tags[]中携带 Tier若脚本在循环未命中任何Tier.*时无条件执行ctx._source.tier tier会把 null 写进去、抹掉实时索引的专用字段。守卫保证「没看到 Tier 就不动tier」后缀拼接约束TAG_RESEPARATION_SCRIPT必须作为脚本的最后一段避免后续追加的标签变更逻辑再次破坏分离。凡是对ctx._source.tags做写操作的脚本如REMOVE_TAGS_CHILDREN_SCRIPT、UPDATE_CLASSIFICATION_TAG_FQN_BY_PREFIX_SCRIPT、UPDATE_FQN_PREFIX_SCRIPT、UPDATE_ADDED_DELETE_GLOSSARY_TAGS都以它收尾。如果 reseparation 片段从任一改标签脚本中被丢弃本 Spec 会立刻失败glossaryTags[]保留旧 FQN 而tags[]已是新 FQN专用字段查询随即失配。七、单元测试兜底SearchClientTagScriptSeparationTest端到端套件之外仓库还提供了静态兜底SearchClientTagScriptSeparationTest.java。它不执行真正的索引操作而是以字符串级断言锁定脚本契约assertEndsWithReseparation校验REMOVE_TAGS_CHILDREN_SCRIPT、UPDATE_GLOSSARY_TERM_TAG_FQN_BY_PREFIX_SCRIPT、UPDATE_CLASSIFICATION_TAG_FQN_BY_PREFIX_SCRIPT、UPDATE_FQN_PREFIX_SCRIPT、UPDATE_ADDED_DELETE_GLOSSARY_TAGS均以TAG_RESEPARATION_SCRIPT结尾后缀匹配而非contains防止未来补丁在 reseparation 之后又追加改标签逻辑tagReseparationScriptSkipsDocsWithoutTagsField确认tags/tier/classificationTags/glossaryTags四类写入全部位于containsKey(tags)守卫之内tagReseparationScriptLiftsTierAndPopulatesDenormalizations确认脚本会赋值tier、classificationTags、glossaryTags且通过startsWith(Tier.)过滤 Tier 防止泄漏进tags[]tagReseparationScriptOnlyOverwritesTierWhenFoundInTagsBag确认ctx._source.tier tier由if (tier ! null)保护。README 中说明得也很直白这是按名字守护契约的静态测试但不会真正端到端执行真正把这条级联路径跑通的是GlossaryRenameCascade.spec.ts。两者一静一动共同把分离契约锁死。八、尚未覆盖的边界README 明确列出了两类暂不适用分离契约的实体扩展套件前应先确认目标实体不落在这两类里Service 级实体DatabaseService、DashboardService等它们没有独立于子实体的 Tier/Cert/Tag/Glossary 表面其文档在子实体 Spec 运行时已被传递覆盖。README 建议若出现仅与 Service 过滤相关的回归再为这类实体显式添加 Spec时序型实体testCaseResolutionStatus、testCaseResult它们不实现TaggableIndex没有 Tier/Cert/Tag/Glossary 表面因此分离契约不适用。结语SearchSeparation 套件是 OpenMetadata 搜索索引「doc-shape 契约」在 UI 层的端到端守护者它以双通道实时索引 vs.recreatetrue重建索引 四 facetTier、Certification、分类标签、术语表标签的一致性断言配合TAG_RESEPARATION_SCRIPT的 Painless 级联专项与 Java 单测的静态校验确保任何一条写入路径都不会悄悄破坏tags[]与专用字段之间的分离。对需要接入新实体、调试 Explore 过滤失配或修改标签脚本的开发者而言SearchSeparationSuite.ts 是理解全部机制的最佳起点。【免费下载链接】OpenMetadataThe Open Context Layer for Data and AI , OpenMetadata is the open platform for building trusted data context and business semantics for humans, AI assistants, and agents.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMetadata创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考