From 010ec5c9c5c6a4d921f095b1664009dfc9ea4359 Mon Sep 17 00:00:00 2001 From: Karan Hotchandani <33024356+karanh37@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:19:10 +0000 Subject: [PATCH] feat(search): add data_contract_search_index (#34304) * feat(search): register data_contract_search_index mappings Co-Authored-By: Claude Opus 5.5 (1M context) * feat(search): index data contracts via DataContractIndex Co-Authored-By: Claude Opus 5.5 (1M context) * test(search): cover data contract indexing in DataContractResourceIT Co-Authored-By: Claude Opus 5.5 (1M context) * feat(ingestion): map DataContract to data_contract_search_index Co-Authored-By: Claude Opus 5.5 (1M context) * feat(ui): data contract search index and query builder fields Co-Authored-By: Claude Opus 5.5 (1M context) * fix(search): keep contract owners on result updates; drop all alias Recording a contract result PATCHed a copy fetched without owners and reviewers, so the reindex blanked them in every contract doc. Reload the relationship fields first. Keep the index out of the all alias: team and user asset lists and the domain, tag and data product pickers query all, where contracts would show with no link and be accepted as assets they cannot hold. Lowercase-normalize the entity and testSuite FQN keywords so the asset autocomplete matches mixed-case FQNs. Co-Authored-By: Claude Opus 5.5 (1M context) * Fix UI checkstyle * feat(ui): let a query builder caller allow removing its last condition The canvas hides the delete control on the last remaining rule, so a builder can never be emptied. Callers that handle an emptied builder themselves (the Collate Automator resets to its 'Add Condition' state) opt in with allowRemoveLastRule; every other builder keeps the default. * fix(ui): offer data contract conditions only on fields contracts populate A contract has no tags, tier, domains, data products, certification or service, so conditions on those common fields matched no contract and reported 0 assets. They are no longer offered while data contracts are selected, alone or alongside other asset types, the same rule that limits entity-specific fields to the ones every selected index shares. * test(ui): removing the last allowed rule emits an empty tree, mounting never does * feat(ui): let a builder preset draw the rule remove button as a plain button Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 (1M context) Co-authored-by: Himanshu Khairajani <46777429+Khairajani@users.noreply.github.com> Co-authored-by: Khairajani (cherry picked from commit 5377edd9209bf1d0d4949eabd4f93f1c2f821fc1) --- ingestion/src/metadata/utils/constants.py | 2 + ingestion/src/metadata/utils/elasticsearch.py | 2 + ingestion/tests/unit/test_ometa_utils.py | 8 + .../it/tests/DataContractResourceIT.java | 71 +- .../service/jdbi3/DataContractRepository.java | 13 +- .../service/search/SearchIndexFactory.java | 3 + .../search/indexes/DataContractIndex.java | 47 ++ .../search/DataContractIndexMappingTest.java | 107 +++ .../search/SearchIndexFactoryTest.java | 4 + .../search/indexes/DataContractIndexTest.java | 129 ++++ .../search/indexes/EntityTypeNameTest.java | 2 + .../en/data_contract_index_mapping.json | 599 +++++++++++++++++ .../resources/elasticsearch/indexMapping.json | 7 + .../jp/data_contract_index_mapping.json | 604 +++++++++++++++++ .../ru/data_contract_index_mapping.json | 611 ++++++++++++++++++ .../zh/data_contract_index_mapping.json | 594 +++++++++++++++++ .../ui/src/enums/AdvancedSearch.enum.ts | 4 + .../resources/ui/src/enums/search.enum.ts | 1 + .../ui/src/interface/search.interface.ts | 5 + .../src/utils/AdvancedSearchClassBase.test.ts | 74 +++ .../ui/src/utils/AdvancedSearchClassBase.ts | 97 ++- .../ui/src/utils/SearchClassBase.test.ts | 13 + .../resources/ui/src/utils/SearchClassBase.ts | 2 + 23 files changed, 2988 insertions(+), 11 deletions(-) create mode 100644 openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/DataContractIndex.java create mode 100644 openmetadata-service/src/test/java/org/openmetadata/service/search/DataContractIndexMappingTest.java create mode 100644 openmetadata-service/src/test/java/org/openmetadata/service/search/indexes/DataContractIndexTest.java create mode 100644 openmetadata-spec/src/main/resources/elasticsearch/en/data_contract_index_mapping.json create mode 100644 openmetadata-spec/src/main/resources/elasticsearch/jp/data_contract_index_mapping.json create mode 100644 openmetadata-spec/src/main/resources/elasticsearch/ru/data_contract_index_mapping.json create mode 100644 openmetadata-spec/src/main/resources/elasticsearch/zh/data_contract_index_mapping.json diff --git a/ingestion/src/metadata/utils/constants.py b/ingestion/src/metadata/utils/constants.py index 2228d199a0f6..049fd6b151c8 100644 --- a/ingestion/src/metadata/utils/constants.py +++ b/ingestion/src/metadata/utils/constants.py @@ -21,6 +21,7 @@ from metadata.generated.schema.entity.data.dashboardDataModel import DashboardDataModel from metadata.generated.schema.entity.data.database import Database from metadata.generated.schema.entity.data.databaseSchema import DatabaseSchema +from metadata.generated.schema.entity.data.dataContract import DataContract from metadata.generated.schema.entity.data.directory import Directory from metadata.generated.schema.entity.data.file import File from metadata.generated.schema.entity.data.glossary import Glossary @@ -168,6 +169,7 @@ "metric": Metric, "glossary": Glossary, "glossaryTerm": GlossaryTerm, + "dataContract": DataContract, "eventSubscription": EventSubscription, } diff --git a/ingestion/src/metadata/utils/elasticsearch.py b/ingestion/src/metadata/utils/elasticsearch.py index 7e50d1c82fdd..fac971a30a5a 100644 --- a/ingestion/src/metadata/utils/elasticsearch.py +++ b/ingestion/src/metadata/utils/elasticsearch.py @@ -29,6 +29,7 @@ from metadata.generated.schema.entity.data.dashboardDataModel import DashboardDataModel from metadata.generated.schema.entity.data.database import Database from metadata.generated.schema.entity.data.databaseSchema import DatabaseSchema +from metadata.generated.schema.entity.data.dataContract import DataContract from metadata.generated.schema.entity.data.directory import Directory from metadata.generated.schema.entity.data.file import File from metadata.generated.schema.entity.data.glossary import Glossary @@ -80,6 +81,7 @@ Spreadsheet.__name__: "spreadsheet_search_index", Worksheet.__name__: "worksheet_search_index", Query.__name__: "query_search_index", + DataContract.__name__: "data_contract_search_index", ReportData.__name__: "entity_report_data_index", Metric.__name__: "metric_search_index", "web_analytic_user_activity_report": "web_analytic_user_activity_report_data_index", diff --git a/ingestion/tests/unit/test_ometa_utils.py b/ingestion/tests/unit/test_ometa_utils.py index 854c160eeb67..34db1cf998b5 100644 --- a/ingestion/tests/unit/test_ometa_utils.py +++ b/ingestion/tests/unit/test_ometa_utils.py @@ -25,6 +25,7 @@ from metadata.generated.schema.entity.data.dashboardDataModel import DashboardDataModel from metadata.generated.schema.entity.data.database import Database from metadata.generated.schema.entity.data.databaseSchema import DatabaseSchema +from metadata.generated.schema.entity.data.dataContract import DataContract from metadata.generated.schema.entity.data.directory import Directory from metadata.generated.schema.entity.data.mlmodel import MlModel from metadata.generated.schema.entity.data.pipeline import Pipeline @@ -58,6 +59,7 @@ model_str, ) from metadata.utils.constants import ENTITY_REFERENCE_CLASS_MAP +from metadata.utils.elasticsearch import ES_INDEX_MAP MOCK_TABLE = Table( id="c3eb265f-5445-4ad3-ba5e-797d3a3071bb", @@ -100,6 +102,12 @@ def test_get_entity_type(self): self.assertEqual(get_entity_type("hello"), "hello") self.assertEqual(get_entity_type(MlModel), "mlmodel") + def test_get_entity_type_data_contract(self): + self.assertEqual(get_entity_type(DataContract), "dataContract") + + def test_es_index_map_data_contract(self): + self.assertEqual(ES_INDEX_MAP[DataContract.__name__], "data_contract_search_index") + def test_get_entity_type_camel_case_consistency(self): """ Test that get_entity_type returns camelCase entity types diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataContractResourceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataContractResourceIT.java index 49819fe30d1b..fb2b6cda8e67 100644 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataContractResourceIT.java +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataContractResourceIT.java @@ -53,6 +53,7 @@ import org.openmetadata.schema.entity.services.ingestionPipelines.PipelineStatus; import org.openmetadata.schema.entity.services.ingestionPipelines.PipelineStatusType; import org.openmetadata.schema.entity.teams.Role; +import org.openmetadata.schema.entity.teams.User; import org.openmetadata.schema.tests.TestCase; import org.openmetadata.schema.tests.TestCaseParameterValue; import org.openmetadata.schema.tests.TestDefinition; @@ -100,7 +101,7 @@ public class DataContractResourceIT extends BaseEntityIT { + String response = searchForEntity(contract.getId().toString()); + assertTrue(response.contains(ownerRef.getId().toString()), "owner id indexed"); + assertTrue(response.contains(assetFqn), "entity.fullyQualifiedName indexed"); + assertTrue(response.contains("\"type\":\"table\""), "entity.type indexed"); + }); + } + + @Test + void addResult_keepsOwnersInSearchDoc(TestNamespace ns) throws Exception { + DataContract contract = createEntity(createMinimalRequest(ns)); + String ownerName = ("dc_result_owner_" + ns.uniqueShortId()).toLowerCase(Locale.ROOT); + User owner = + SdkClients.adminClient() + .users() + .create( + new CreateUser() + .withName(ownerName) + .withEmail(ownerName + "@test.openmetadata.org")); + String ownerId = owner.getId().toString(); + contract.setOwners(List.of(owner.getEntityReference())); + SdkClients.adminClient().dataContracts().update(contract.getId().toString(), contract); + + String marker = "keep-owners-" + ns.uniqueShortId(); + SdkClients.adminClient() + .dataContracts() + .addResult( + contract.getId(), + new DataContractResult() + .withDataContractFQN(contract.getFullyQualifiedName()) + .withTimestamp(System.currentTimeMillis()) + .withContractExecutionStatus(ContractExecutionStatus.Failed) + .withResult(marker) + .withExecutionTime(1L)); + + Awaitility.await("contract doc reflects the new result") + .pollInterval(Duration.ofSeconds(2)) + .atMost(Duration.ofSeconds(120)) + .ignoreExceptions() + .untilAsserted( + () -> assertTrue(searchForEntity(contract.getId().toString()).contains(marker))); + assertTrue( + searchForEntity(contract.getId().toString()).contains(ownerId), + "recording a result must not drop owners from the search doc"); + } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java index 22ef2060dfb6..a1853e118e1c 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java @@ -128,6 +128,7 @@ public DataContractRepository(OpenMetadataApplicationConfig config) { DATA_CONTRACT_UPDATE_FIELDS); this.ingestionPipelineMapper = new IngestionPipelineMapper(config); this.openMetadataApplicationConfig = config; + supportsSearch = true; } @Override @@ -1596,7 +1597,15 @@ private boolean semanticsRuleMatch(SemanticsRule rule1, SemanticsRule rule2) { private void updateLatestResult(DataContract dataContract, DataContractResult result) { try { - DataContract updated = JsonUtils.deepCopy(dataContract, DataContract.class); + // Reload with the relationship fields the search doc is built from: callers pass contracts + // fetched without owners/reviewers, and the reindex after this update would blank them. + DataContract original = + Entity.getEntity( + Entity.DATA_CONTRACT, + dataContract.getId(), + "owners,reviewers,extension", + Include.NON_DELETED); + DataContract updated = JsonUtils.deepCopy(original, DataContract.class); updated.setLatestResult( new LatestResult() .withTimestamp(result.getTimestamp()) @@ -1604,7 +1613,7 @@ private void updateLatestResult(DataContract dataContract, DataContractResult re .withMessage(result.getResult()) .withResultId(result.getId())); EntityRepository.EntityUpdater entityUpdater = - getUpdater(dataContract, updated, EntityRepository.Operation.PATCH, null); + getUpdater(original, updated, EntityRepository.Operation.PATCH, null); entityUpdater.update(); } catch (Exception e) { LOG.error( diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchIndexFactory.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchIndexFactory.java index 2f18298b8595..30ec57427a8e 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchIndexFactory.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchIndexFactory.java @@ -21,6 +21,7 @@ import org.openmetadata.schema.entity.data.ContextFile; import org.openmetadata.schema.entity.data.Dashboard; import org.openmetadata.schema.entity.data.DashboardDataModel; +import org.openmetadata.schema.entity.data.DataContract; import org.openmetadata.schema.entity.data.Database; import org.openmetadata.schema.entity.data.DatabaseSchema; import org.openmetadata.schema.entity.data.Directory; @@ -79,6 +80,7 @@ import org.openmetadata.service.search.indexes.DashboardDataModelIndex; import org.openmetadata.service.search.indexes.DashboardIndex; import org.openmetadata.service.search.indexes.DashboardServiceIndex; +import org.openmetadata.service.search.indexes.DataContractIndex; import org.openmetadata.service.search.indexes.DataProductIndex; import org.openmetadata.service.search.indexes.DatabaseIndex; import org.openmetadata.service.search.indexes.DatabaseSchemaIndex; @@ -215,6 +217,7 @@ public SearchIndex buildIndex(String entityType, Object entity) { case Entity.CONTEXT_FILE -> new ContextFileIndex((ContextFile) entity); case Entity.CONTEXT_MEMORY -> new ContextMemoryIndex((ContextMemory) entity); case Entity.PAGE -> new PageIndex((Page) entity); + case Entity.DATA_CONTRACT -> new DataContractIndex((DataContract) entity); case Entity.DATA_PRODUCT -> new DataProductIndex((DataProduct) entity); case Entity.METADATA_SERVICE -> new MetadataServiceIndex((MetadataService) entity); case Entity.ENTITY_REPORT_DATA -> new EntityReportDataIndex((ReportData) entity); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/DataContractIndex.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/DataContractIndex.java new file mode 100644 index 000000000000..c007801b6b42 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/DataContractIndex.java @@ -0,0 +1,47 @@ +package org.openmetadata.service.search.indexes; + +import java.util.Map; +import java.util.Set; +import org.openmetadata.schema.entity.data.DataContract; +import org.openmetadata.service.Entity; + +public record DataContractIndex(DataContract dataContract) implements SearchIndex { + /** Large or free-form contract blocks; indexing them would explode the dynamic mapping. */ + private static final Set EXCLUDED_FIELDS = + Set.of( + "schema", + "semantics", + "qualityExpectations", + "odcsQualityRules", + "odcsElementExtensions", + "contractUpdates", + "sla", + "security", + "termsOfUse"); + + @Override + public Object getEntity() { + return dataContract; + } + + @Override + public String getEntityTypeName() { + return Entity.DATA_CONTRACT; + } + + @Override + public Map buildSearchIndexDocInternal(Map doc) { + doc.put("entity", getEntityWithDisplayName(dataContract.getEntity())); + doc.put("testSuite", getEntityWithDisplayName(dataContract.getTestSuite())); + return doc; + } + + @Override + public Set getExcludedFields() { + return EXCLUDED_FIELDS; + } + + public static Map getFields() { + return SearchIndex.getDefaultFields(); + } +} diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/search/DataContractIndexMappingTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/search/DataContractIndexMappingTest.java new file mode 100644 index 000000000000..7618e2b4ad3c --- /dev/null +++ b/openmetadata-service/src/test/java/org/openmetadata/service/search/DataContractIndexMappingTest.java @@ -0,0 +1,107 @@ +package org.openmetadata.service.search; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DataContractIndexMappingTest { + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final List FREE_FORM_FIELDS = + List.of( + "schema", + "semantics", + "qualityExpectations", + "odcsQualityRules", + "odcsElementExtensions", + "contractUpdates", + "sla", + "security", + "termsOfUse"); + + private static JsonNode read(String path) throws IOException { + try (InputStream in = DataContractIndexMappingTest.class.getResourceAsStream(path)) { + assertNotNull(in, "missing resource " + path); + return MAPPER.readTree(in); + } + } + + private static JsonNode properties(String language) throws IOException { + return read("/elasticsearch/" + language + "/data_contract_index_mapping.json") + .path("mappings") + .path("properties"); + } + + @Test + void indexIsRegisteredWithoutParentAliases() throws IOException { + JsonNode entry = read("/elasticsearch/indexMapping.json").path("dataContract"); + assertEquals("data_contract_search_index", entry.path("indexName").asText()); + assertEquals( + "/elasticsearch/%s/data_contract_index_mapping.json", + entry.path("indexMappingFile").asText()); + assertEquals("dataContract", entry.path("alias").asText()); + // Not under `all`: owner/team asset lists and the domain/tag asset pickers query `all`, and a + // contract there would be listed with no link and accepted as a domain/tag asset it can't hold. + assertTrue(entry.path("parentAliases").isEmpty()); + assertTrue(entry.path("childAliases").isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {"en", "jp", "ru", "zh"}) + void contractFieldsAreMapped(String language) throws IOException { + JsonNode props = properties(language); + assertEquals("keyword", props.at("/entity/properties/type/type").asText()); + assertEquals( + "keyword", props.at("/entity/properties/fullyQualifiedName/fields/keyword/type").asText()); + assertEquals("keyword", props.at("/testSuite/properties/id/type").asText()); + assertEquals("keyword", props.at("/latestResult/properties/status/type").asText()); + assertEquals("keyword", props.at("/reviewers/properties/id/type").asText()); + assertEquals("nested", props.at("/owners/type").asText()); + assertEquals("keyword", props.at("/fqnHash/type").asText()); + } + + @ParameterizedTest + @ValueSource(strings = {"en", "jp", "ru", "zh"}) + void entityFqnKeywordHasNoIgnoreAbove(String language) throws IOException { + JsonNode keyword = + properties(language).at("/entity/properties/fullyQualifiedName/fields/keyword"); + assertTrue( + keyword.path("ignore_above").isMissingNode(), + "asset FQNs can exceed 256 chars; ignore_above would drop them from the filter"); + } + + @ParameterizedTest + @ValueSource(strings = {"en", "jp", "ru", "zh"}) + void referenceFqnKeywordsAreLowercaseNormalized(String language) throws IOException { + // Aggregation autocomplete lowercases the typed prefix; an un-normalized keyword never matches + // mixed-case FQNs once the user types. + JsonNode props = properties(language); + for (String ref : List.of("entity", "testSuite")) { + assertEquals( + "lowercase_normalizer", + props + .at("/" + ref + "/properties/fullyQualifiedName/fields/keyword/normalizer") + .asText()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"en", "jp", "ru", "zh"}) + void freeFormBlocksAndTagsAreNotMapped(String language) throws IOException { + JsonNode props = properties(language); + for (String field : FREE_FORM_FIELDS) { + assertFalse(props.has(field), field + " must be excluded, not mapped"); + } + assertFalse(props.has("tags"), "contracts have no tags"); + assertFalse(props.has("classificationTags"), "contracts have no tags"); + } +} diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/search/SearchIndexFactoryTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/search/SearchIndexFactoryTest.java index 3943d5cb45c9..1da5f937df42 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/search/SearchIndexFactoryTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/search/SearchIndexFactoryTest.java @@ -34,6 +34,7 @@ import org.openmetadata.schema.entity.data.Container; import org.openmetadata.schema.entity.data.Dashboard; import org.openmetadata.schema.entity.data.DashboardDataModel; +import org.openmetadata.schema.entity.data.DataContract; import org.openmetadata.schema.entity.data.Database; import org.openmetadata.schema.entity.data.DatabaseSchema; import org.openmetadata.schema.entity.data.Directory; @@ -91,6 +92,7 @@ import org.openmetadata.service.search.indexes.DashboardDataModelIndex; import org.openmetadata.service.search.indexes.DashboardIndex; import org.openmetadata.service.search.indexes.DashboardServiceIndex; +import org.openmetadata.service.search.indexes.DataContractIndex; import org.openmetadata.service.search.indexes.DataProductIndex; import org.openmetadata.service.search.indexes.DatabaseIndex; import org.openmetadata.service.search.indexes.DatabaseSchemaIndex; @@ -416,6 +418,8 @@ private static Stream supportedIndexMappings() { AiFrameworkControlIndex.class), Arguments.of( Entity.AUDIT_REPORT, (Supplier) AuditReport::new, AuditReportIndex.class), + Arguments.of( + Entity.DATA_CONTRACT, (Supplier) DataContract::new, DataContractIndex.class), Arguments.of(Entity.TAG, (Supplier) Tag::new, TagIndex.class), Arguments.of( Entity.CLASSIFICATION, diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/search/indexes/DataContractIndexTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/search/indexes/DataContractIndexTest.java new file mode 100644 index 000000000000..6f4c289b577a --- /dev/null +++ b/openmetadata-service/src/test/java/org/openmetadata/service/search/indexes/DataContractIndexTest.java @@ -0,0 +1,129 @@ +package org.openmetadata.service.search.indexes; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.InputStream; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.openmetadata.schema.api.data.ContractSLA; +import org.openmetadata.schema.api.data.ContractSecurity; +import org.openmetadata.schema.entity.data.DataContract; +import org.openmetadata.schema.entity.data.TermsOfUse; +import org.openmetadata.schema.type.EntityReference; +import org.openmetadata.service.Entity; +import org.openmetadata.service.search.SearchRepository; + +class DataContractIndexTest { + private static final List FREE_FORM_FIELDS = + List.of( + "schema", + "semantics", + "qualityExpectations", + "odcsQualityRules", + "odcsElementExtensions", + "contractUpdates", + "sla", + "security", + "termsOfUse"); + + private static SearchRepository previousRepository; + + @BeforeAll + static void initSearchRepository() { + previousRepository = Entity.getSearchRepository(); + Entity.setSearchRepository(mock(SearchRepository.class)); + } + + @AfterAll + static void restoreSearchRepository() { + Entity.setSearchRepository(previousRepository); + } + + private static EntityReference ref(String type, String name) { + return new EntityReference() + .withId(UUID.randomUUID()) + .withType(type) + .withName(name) + .withFullyQualifiedName("svc.db.schema." + name); + } + + private static DataContract contract() { + return new DataContract() + .withId(UUID.randomUUID()) + .withName("orders_contract") + .withFullyQualifiedName("svc.db.schema.orders.dataContract_orders_contract") + .withEntity(ref(Entity.TABLE, "orders")) + .withOwners(List.of(ref(Entity.USER, "alice"))); + } + + @Test + void buildDoc_setsEntityReferenceWithDisplayName() { + Map doc = new DataContractIndex(contract()).buildSearchIndexDoc(); + + EntityReference entity = (EntityReference) doc.get("entity"); + assertEquals(Entity.TABLE, entity.getType()); + assertEquals("orders", entity.getDisplayName(), "displayName falls back to name"); + assertEquals(Entity.DATA_CONTRACT, doc.get("entityType")); + assertEquals(List.of("alice"), doc.get("ownerName")); + } + + @Test + void buildDoc_minimalContract_hasNoNulls() { + DataContract minimal = + new DataContract() + .withId(UUID.randomUUID()) + .withName("c") + .withFullyQualifiedName("svc.db.schema.t.dataContract_c") + .withEntity(ref(Entity.TABLE, "t")); + + Map doc = new DataContractIndex(minimal).buildSearchIndexDoc(); + + assertNull(doc.get("testSuite")); + assertEquals(List.of(), doc.get("owners")); + assertEquals(List.of(), doc.get("reviewers")); + } + + @Test + void buildDoc_excludesFreeFormBlocks() { + DataContract heavy = + contract() + .withSla(new ContractSLA()) + .withSecurity(new ContractSecurity()) + .withTermsOfUse(new TermsOfUse().withContent("terms")); + + Map doc = new DataContractIndex(heavy).buildSearchIndexDoc(); + + for (String field : FREE_FORM_FIELDS) { + assertFalse(doc.containsKey(field), field + " must not reach the search doc"); + } + } + + @Test + void everyDocKeyIsMapped() throws Exception { + JsonNode properties; + try (InputStream in = + getClass().getResourceAsStream("/elasticsearch/en/data_contract_index_mapping.json")) { + properties = new ObjectMapper().readTree(in).path("mappings").path("properties"); + } + Set mapped = new HashSet<>(); + properties.fieldNames().forEachRemaining(mapped::add); + + Map doc = new DataContractIndex(contract()).buildSearchIndexDoc(); + + Set unmapped = new HashSet<>(doc.keySet()); + unmapped.removeAll(mapped); + assertTrue(unmapped.isEmpty(), "doc keys missing from mapping (dynamic mapping): " + unmapped); + } +} diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/search/indexes/EntityTypeNameTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/search/indexes/EntityTypeNameTest.java index 351107bafde1..eeb7d517b29d 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/search/indexes/EntityTypeNameTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/search/indexes/EntityTypeNameTest.java @@ -28,6 +28,7 @@ import org.openmetadata.schema.entity.data.Container; import org.openmetadata.schema.entity.data.Dashboard; import org.openmetadata.schema.entity.data.DashboardDataModel; +import org.openmetadata.schema.entity.data.DataContract; import org.openmetadata.schema.entity.data.Database; import org.openmetadata.schema.entity.data.DatabaseSchema; import org.openmetadata.schema.entity.data.Directory; @@ -120,6 +121,7 @@ static Stream indexEntityTypeProvider() { new AiFrameworkControlIndex(new AIFrameworkControl().withId(id())), Entity.AI_FRAMEWORK_CONTROL), Arguments.of(new AuditReportIndex(new AuditReport().withId(id())), Entity.AUDIT_REPORT), + Arguments.of(new DataContractIndex(new DataContract().withId(id())), Entity.DATA_CONTRACT), Arguments.of(new McpServerIndex(new McpServer().withId(id())), Entity.MCP_SERVER), Arguments.of( new PromptTemplateIndex(new PromptTemplate().withId(id())), Entity.PROMPT_TEMPLATE), diff --git a/openmetadata-spec/src/main/resources/elasticsearch/en/data_contract_index_mapping.json b/openmetadata-spec/src/main/resources/elasticsearch/en/data_contract_index_mapping.json new file mode 100644 index 000000000000..bbd4beb3e16d --- /dev/null +++ b/openmetadata-spec/src/main/resources/elasticsearch/en/data_contract_index_mapping.json @@ -0,0 +1,599 @@ +{ + "settings": { + "index": { + "max_ngram_diff": 17 + }, + "analysis": { + "tokenizer": { + "n_gram_tokenizer": { + "type": "ngram", + "min_gram": 3, + "max_gram": 20, + "token_chars": [ + "letter", + "digit" + ] + } + }, + "normalizer": { + "lowercase_normalizer": { + "type": "custom", + "char_filter": [], + "filter": [ + "lowercase", + "asciifolding" + ] + } + }, + "analyzer": { + "om_analyzer": { + "tokenizer": "standard", + "filter": [ + "word_delimiter_filter", + "lowercase", + "asciifolding", + "om_stemmer", + "om_plural_stemmer" + ] + }, + "om_ngram": { + "type": "custom", + "tokenizer": "n_gram_tokenizer", + "filter": [ + "lowercase", + "asciifolding" + ] + }, + "om_compound_analyzer": { + "tokenizer": "standard", + "filter": [ + "compound_word_delimiter_graph", + "lowercase", + "asciifolding", + "flatten_graph" + ] + } + }, + "filter": { + "om_stemmer": { + "type": "stemmer", + "name": "kstem" + }, + "word_delimiter_filter": { + "type": "word_delimiter", + "preserve_original": true + }, + "compound_word_delimiter_graph": { + "type": "word_delimiter_graph", + "generate_word_parts": true, + "generate_number_parts": true, + "split_on_case_change": true, + "split_on_numerics": true, + "catenate_words": false, + "catenate_numbers": false, + "catenate_all": false, + "preserve_original": true, + "stem_english_possessive": true + }, + "om_plural_stemmer": { + "type": "stemmer", + "name": "minimal_english" + } + } + } + }, + "mappings": { + "properties": { + "changeDescription": { + "enabled": false + }, + "incrementalChangeDescription": { + "enabled": false + }, + "id": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "name": { + "type": "text", + "analyzer": "om_analyzer", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256, + "normalizer": "lowercase_normalizer" + }, + "ngram": { + "type": "text", + "analyzer": "om_ngram" + }, + "compound": { + "type": "text", + "analyzer": "om_compound_analyzer" + } + } + }, + "fullyQualifiedName": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "displayName": { + "type": "text", + "analyzer": "om_analyzer", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + }, + "actualCase": { + "type": "keyword", + "ignore_above": 256 + }, + "ngram": { + "type": "text", + "analyzer": "om_ngram" + }, + "compound": { + "type": "text", + "analyzer": "om_compound_analyzer" + } + } + }, + "description": { + "type": "text", + "analyzer": "om_analyzer", + "similarity": "boolean", + "term_vector": "with_positions_offsets" + }, + "fqnParts": { + "type": "keyword" + }, + "version": { + "type": "float" + }, + "updatedAt": { + "type": "date", + "format": "epoch_second" + }, + "updatedBy": { + "type": "text" + }, + "href": { + "type": "text" + }, + "sourceUrl": { + "type": "text" + }, + "domains": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "extension": { + "type": "object", + "enabled": false + }, + "owners": { + "type": "nested", + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text" + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "deleted": { + "type": "boolean" + }, + "followers": { + "type": "keyword" + }, + "entityType": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "entityStatus": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "totalVotes": { + "type": "long", + "null_value": 0 + }, + "votes": { + "type": "object", + "dynamic": false, + "properties": { + "upVotes": { + "type": "integer" + }, + "downVotes": { + "type": "integer" + } + } + }, + "descriptionStatus": { + "type": "keyword" + }, + "descriptionSources": { + "type": "object", + "dynamic": false + }, + "certification": { + "type": "object", + "properties": { + "tagLabel": { + "type": "object", + "properties": { + "tagFQN": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "fields": { + "text": { + "type": "text", + "analyzer": "om_analyzer" + } + } + }, + "labelType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "source": { + "type": "keyword" + }, + "state": { + "type": "keyword" + } + } + }, + "appliedDate": { + "type": "date", + "format": "strict_date_optional_time||epoch_millis" + }, + "expiryDate": { + "type": "date", + "format": "strict_date_optional_time||epoch_millis" + } + } + }, + "customPropertiesTyped": { + "type": "nested", + "properties": { + "name": { + "type": "keyword" + }, + "propertyType": { + "type": "keyword" + }, + "stringValue": { + "type": "keyword" + }, + "textValue": { + "type": "text", + "analyzer": "om_analyzer" + }, + "longValue": { + "type": "long" + }, + "doubleValue": { + "type": "double" + }, + "start": { + "type": "long" + }, + "end": { + "type": "long" + }, + "refId": { + "type": "keyword" + }, + "refType": { + "type": "keyword" + }, + "refName": { + "type": "keyword" + }, + "refFqn": { + "type": "keyword" + } + } + }, + "ownerDisplayName": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "ownerName": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "fqnHash": { + "type": "keyword" + }, + "impersonatedBy": { + "type": "keyword" + }, + "reviewers": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text" + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "entity": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + } + } + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "testSuite": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + } + } + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "latestResult": { + "properties": { + "status": { + "type": "keyword" + }, + "timestamp": { + "type": "date", + "format": "epoch_millis" + }, + "resultId": { + "type": "keyword" + }, + "message": { + "type": "text" + } + } + }, + "effectiveFrom": { + "type": "date" + }, + "effectiveUntil": { + "type": "date" + }, + "createdAt": { + "type": "date", + "format": "epoch_millis" + }, + "createdBy": { + "type": "keyword" + }, + "inherited": { + "type": "boolean" + } + } + } +} diff --git a/openmetadata-spec/src/main/resources/elasticsearch/indexMapping.json b/openmetadata-spec/src/main/resources/elasticsearch/indexMapping.json index 8b913fb1b192..aa958c4d872d 100644 --- a/openmetadata-spec/src/main/resources/elasticsearch/indexMapping.json +++ b/openmetadata-spec/src/main/resources/elasticsearch/indexMapping.json @@ -601,6 +601,13 @@ ], "childAliases": [] }, + "dataContract": { + "indexName": "data_contract_search_index", + "indexMappingFile": "/elasticsearch/%s/data_contract_index_mapping.json", + "alias": "dataContract", + "parentAliases": [], + "childAliases": [] + }, "mcpServer": { "indexName": "mcp_server_search_index", "indexMappingFile": "/elasticsearch/%s/mcp_server_index_mapping.json", diff --git a/openmetadata-spec/src/main/resources/elasticsearch/jp/data_contract_index_mapping.json b/openmetadata-spec/src/main/resources/elasticsearch/jp/data_contract_index_mapping.json new file mode 100644 index 000000000000..21b325b1d869 --- /dev/null +++ b/openmetadata-spec/src/main/resources/elasticsearch/jp/data_contract_index_mapping.json @@ -0,0 +1,604 @@ +{ + "settings": { + "analysis": { + "normalizer": { + "lowercase_normalizer": { + "type": "custom", + "char_filter": [], + "filter": [ + "lowercase", + "asciifolding" + ] + } + }, + "analyzer": { + "om_analyzer": { + "tokenizer": "letter", + "filter": [ + "lowercase", + "asciifolding", + "om_stemmer", + "om_plural_stemmer" + ] + }, + "om_analyzer_jp": { + "tokenizer": "kuromoji_tokenizer", + "type": "custom", + "filter": [ + "kuromoji_baseform", + "kuromoji_part_of_speech", + "kuromoji_number", + "kuromoji_stemmer" + ] + }, + "om_ngram": { + "type": "custom", + "tokenizer": "n_gram_tokenizer", + "filter": [ + "lowercase", + "asciifolding" + ] + }, + "om_compound_analyzer": { + "tokenizer": "standard", + "filter": [ + "compound_word_delimiter_graph", + "lowercase", + "asciifolding", + "flatten_graph" + ] + } + }, + "filter": { + "om_stemmer": { + "type": "stemmer", + "name": "english" + }, + "compound_word_delimiter_graph": { + "type": "word_delimiter_graph", + "generate_word_parts": true, + "generate_number_parts": true, + "split_on_case_change": true, + "split_on_numerics": true, + "catenate_words": false, + "catenate_numbers": false, + "catenate_all": false, + "preserve_original": true, + "stem_english_possessive": true + }, + "om_plural_stemmer": { + "type": "stemmer", + "name": "minimal_english" + } + }, + "tokenizer": { + "n_gram_tokenizer": { + "type": "ngram", + "min_gram": 1, + "max_gram": 2, + "token_chars": [ + "letter", + "digit" + ] + } + } + }, + "index": { + "max_ngram_diff": 1 + } + }, + "mappings": { + "properties": { + "changeDescription": { + "enabled": false + }, + "incrementalChangeDescription": { + "enabled": false + }, + "id": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "name": { + "type": "text", + "analyzer": "om_analyzer_jp", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256, + "normalizer": "lowercase_normalizer" + }, + "ngram": { + "type": "text", + "analyzer": "om_ngram" + }, + "compound": { + "type": "text", + "analyzer": "om_compound_analyzer" + } + } + }, + "fullyQualifiedName": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "displayName": { + "type": "text", + "analyzer": "om_analyzer_jp", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + }, + "actualCase": { + "type": "keyword", + "ignore_above": 256 + }, + "ngram": { + "type": "text", + "analyzer": "om_ngram" + }, + "compound": { + "type": "text", + "analyzer": "om_compound_analyzer" + } + } + }, + "description": { + "type": "text", + "analyzer": "om_analyzer_jp", + "similarity": "boolean", + "term_vector": "with_positions_offsets" + }, + "fqnParts": { + "type": "keyword" + }, + "version": { + "type": "float" + }, + "updatedAt": { + "type": "date", + "format": "epoch_second" + }, + "updatedBy": { + "type": "text" + }, + "href": { + "type": "text" + }, + "sourceUrl": { + "type": "text" + }, + "domains": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "extension": { + "type": "object", + "enabled": false + }, + "owners": { + "type": "nested", + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text" + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "deleted": { + "type": "boolean" + }, + "followers": { + "type": "keyword" + }, + "entityType": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "entityStatus": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "totalVotes": { + "type": "long", + "null_value": 0 + }, + "votes": { + "type": "object", + "dynamic": false, + "properties": { + "upVotes": { + "type": "integer" + }, + "downVotes": { + "type": "integer" + } + } + }, + "descriptionStatus": { + "type": "keyword" + }, + "descriptionSources": { + "type": "object", + "dynamic": false + }, + "certification": { + "type": "object", + "properties": { + "tagLabel": { + "type": "object", + "properties": { + "tagFQN": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "fields": { + "text": { + "type": "text", + "analyzer": "om_analyzer" + } + } + }, + "labelType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "source": { + "type": "keyword" + }, + "state": { + "type": "keyword" + } + } + }, + "appliedDate": { + "type": "date", + "format": "strict_date_optional_time||epoch_millis" + }, + "expiryDate": { + "type": "date", + "format": "strict_date_optional_time||epoch_millis" + } + } + }, + "customPropertiesTyped": { + "type": "nested", + "properties": { + "name": { + "type": "keyword" + }, + "propertyType": { + "type": "keyword" + }, + "stringValue": { + "type": "keyword" + }, + "textValue": { + "type": "text", + "analyzer": "om_analyzer" + }, + "longValue": { + "type": "long" + }, + "doubleValue": { + "type": "double" + }, + "start": { + "type": "long" + }, + "end": { + "type": "long" + }, + "refId": { + "type": "keyword" + }, + "refType": { + "type": "keyword" + }, + "refName": { + "type": "keyword" + }, + "refFqn": { + "type": "keyword" + } + } + }, + "ownerDisplayName": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "ownerName": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "fqnHash": { + "type": "keyword" + }, + "impersonatedBy": { + "type": "keyword" + }, + "reviewers": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text" + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "entity": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + } + } + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "testSuite": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + } + } + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "latestResult": { + "properties": { + "status": { + "type": "keyword" + }, + "timestamp": { + "type": "date", + "format": "epoch_millis" + }, + "resultId": { + "type": "keyword" + }, + "message": { + "type": "text" + } + } + }, + "effectiveFrom": { + "type": "date" + }, + "effectiveUntil": { + "type": "date" + }, + "createdAt": { + "type": "date", + "format": "epoch_millis" + }, + "createdBy": { + "type": "keyword" + }, + "inherited": { + "type": "boolean" + } + } + } +} diff --git a/openmetadata-spec/src/main/resources/elasticsearch/ru/data_contract_index_mapping.json b/openmetadata-spec/src/main/resources/elasticsearch/ru/data_contract_index_mapping.json new file mode 100644 index 000000000000..ae9e6bf48458 --- /dev/null +++ b/openmetadata-spec/src/main/resources/elasticsearch/ru/data_contract_index_mapping.json @@ -0,0 +1,611 @@ +{ + "settings": { + "index": { + "max_ngram_diff": 17 + }, + "analysis": { + "tokenizer": { + "n_gram_tokenizer": { + "type": "ngram", + "min_gram": 3, + "max_gram": 20, + "token_chars": [ + "letter", + "digit" + ] + } + }, + "normalizer": { + "lowercase_normalizer": { + "type": "custom", + "char_filter": [], + "filter": [ + "lowercase", + "asciifolding" + ] + } + }, + "analyzer": { + "om_analyzer": { + "tokenizer": "standard", + "filter": [ + "word_delimiter_filter", + "lowercase", + "asciifolding", + "russian_stop", + "russian_snowball", + "english_stop", + "om_kstem" + ] + }, + "om_ngram": { + "type": "custom", + "tokenizer": "n_gram_tokenizer", + "filter": [ + "lowercase", + "asciifolding" + ] + }, + "om_compound_analyzer": { + "tokenizer": "standard", + "filter": [ + "compound_word_delimiter_graph", + "lowercase", + "asciifolding", + "flatten_graph" + ] + } + }, + "filter": { + "word_delimiter_filter": { + "type": "word_delimiter", + "preserve_original": true + }, + "compound_word_delimiter_graph": { + "type": "word_delimiter_graph", + "generate_word_parts": true, + "generate_number_parts": true, + "split_on_case_change": true, + "split_on_numerics": true, + "catenate_words": false, + "catenate_numbers": false, + "catenate_all": false, + "preserve_original": true, + "stem_english_possessive": true + }, + "russian_stop": { + "type": "stop", + "stopwords": "_russian_" + }, + "english_stop": { + "type": "stop", + "stopwords": "_english_" + }, + "russian_snowball": { + "name": "russian", + "type": "stemmer" + }, + "om_kstem": { + "type": "kstem" + }, + "asciifolding": { + "type": "asciifolding" + } + } + } + }, + "mappings": { + "properties": { + "changeDescription": { + "enabled": false + }, + "incrementalChangeDescription": { + "enabled": false + }, + "id": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "name": { + "type": "text", + "analyzer": "om_analyzer", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256, + "normalizer": "lowercase_normalizer" + }, + "ngram": { + "type": "text", + "analyzer": "om_ngram" + }, + "compound": { + "type": "text", + "analyzer": "om_compound_analyzer" + } + } + }, + "fullyQualifiedName": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "displayName": { + "type": "text", + "analyzer": "om_analyzer", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + }, + "actualCase": { + "type": "keyword", + "ignore_above": 256 + }, + "ngram": { + "type": "text", + "analyzer": "om_ngram" + }, + "compound": { + "type": "text", + "analyzer": "om_compound_analyzer" + } + } + }, + "description": { + "type": "text", + "analyzer": "om_analyzer", + "similarity": "boolean", + "term_vector": "with_positions_offsets" + }, + "fqnParts": { + "type": "keyword" + }, + "version": { + "type": "float" + }, + "updatedAt": { + "type": "date", + "format": "epoch_second" + }, + "updatedBy": { + "type": "text" + }, + "href": { + "type": "text" + }, + "sourceUrl": { + "type": "text" + }, + "domains": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "extension": { + "type": "object", + "enabled": false + }, + "owners": { + "type": "nested", + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text" + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "deleted": { + "type": "boolean" + }, + "followers": { + "type": "keyword" + }, + "entityType": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "entityStatus": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "totalVotes": { + "type": "long", + "null_value": 0 + }, + "votes": { + "type": "object", + "dynamic": false, + "properties": { + "upVotes": { + "type": "integer" + }, + "downVotes": { + "type": "integer" + } + } + }, + "descriptionStatus": { + "type": "keyword" + }, + "descriptionSources": { + "type": "object", + "dynamic": false + }, + "certification": { + "type": "object", + "properties": { + "tagLabel": { + "type": "object", + "properties": { + "tagFQN": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "fields": { + "text": { + "type": "text", + "analyzer": "om_analyzer" + } + } + }, + "labelType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "source": { + "type": "keyword" + }, + "state": { + "type": "keyword" + } + } + }, + "appliedDate": { + "type": "date", + "format": "strict_date_optional_time||epoch_millis" + }, + "expiryDate": { + "type": "date", + "format": "strict_date_optional_time||epoch_millis" + } + } + }, + "customPropertiesTyped": { + "type": "nested", + "properties": { + "name": { + "type": "keyword" + }, + "propertyType": { + "type": "keyword" + }, + "stringValue": { + "type": "keyword" + }, + "textValue": { + "type": "text", + "analyzer": "om_analyzer" + }, + "longValue": { + "type": "long" + }, + "doubleValue": { + "type": "double" + }, + "start": { + "type": "long" + }, + "end": { + "type": "long" + }, + "refId": { + "type": "keyword" + }, + "refType": { + "type": "keyword" + }, + "refName": { + "type": "keyword" + }, + "refFqn": { + "type": "keyword" + } + } + }, + "ownerDisplayName": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "ownerName": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "fqnHash": { + "type": "keyword" + }, + "impersonatedBy": { + "type": "keyword" + }, + "reviewers": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text" + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "entity": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + } + } + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "testSuite": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + } + } + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "latestResult": { + "properties": { + "status": { + "type": "keyword" + }, + "timestamp": { + "type": "date", + "format": "epoch_millis" + }, + "resultId": { + "type": "keyword" + }, + "message": { + "type": "text" + } + } + }, + "effectiveFrom": { + "type": "date" + }, + "effectiveUntil": { + "type": "date" + }, + "createdAt": { + "type": "date", + "format": "epoch_millis" + }, + "createdBy": { + "type": "keyword" + }, + "inherited": { + "type": "boolean" + } + } + } +} diff --git a/openmetadata-spec/src/main/resources/elasticsearch/zh/data_contract_index_mapping.json b/openmetadata-spec/src/main/resources/elasticsearch/zh/data_contract_index_mapping.json new file mode 100644 index 000000000000..6cfae1773bd7 --- /dev/null +++ b/openmetadata-spec/src/main/resources/elasticsearch/zh/data_contract_index_mapping.json @@ -0,0 +1,594 @@ +{ + "settings": { + "analysis": { + "normalizer": { + "lowercase_normalizer": { + "type": "custom", + "char_filter": [], + "filter": [ + "lowercase", + "asciifolding" + ] + } + }, + "analyzer": { + "om_analyzer": { + "tokenizer": "letter", + "filter": [ + "lowercase", + "asciifolding", + "om_stemmer", + "om_plural_stemmer" + ] + }, + "om_ngram": { + "type": "custom", + "tokenizer": "n_gram_tokenizer", + "filter": [ + "lowercase", + "asciifolding" + ] + }, + "om_compound_analyzer": { + "tokenizer": "standard", + "filter": [ + "compound_word_delimiter_graph", + "lowercase", + "asciifolding", + "flatten_graph" + ] + } + }, + "filter": { + "om_stemmer": { + "type": "stemmer", + "name": "english" + }, + "compound_word_delimiter_graph": { + "type": "word_delimiter_graph", + "generate_word_parts": true, + "generate_number_parts": true, + "split_on_case_change": true, + "split_on_numerics": true, + "catenate_words": false, + "catenate_numbers": false, + "catenate_all": false, + "preserve_original": true, + "stem_english_possessive": true + }, + "om_plural_stemmer": { + "type": "stemmer", + "name": "minimal_english" + } + }, + "tokenizer": { + "n_gram_tokenizer": { + "type": "ngram", + "min_gram": 2, + "max_gram": 3, + "token_chars": [ + "letter", + "digit" + ] + } + } + }, + "index": { + "max_ngram_diff": 1 + } + }, + "mappings": { + "properties": { + "changeDescription": { + "enabled": false + }, + "incrementalChangeDescription": { + "enabled": false + }, + "id": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "name": { + "type": "text", + "analyzer": "om_analyzer", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256, + "normalizer": "lowercase_normalizer" + }, + "ngram": { + "type": "text", + "analyzer": "om_ngram" + }, + "compound": { + "type": "text", + "analyzer": "om_compound_analyzer" + } + } + }, + "fullyQualifiedName": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "displayName": { + "type": "text", + "analyzer": "om_analyzer", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + }, + "actualCase": { + "type": "keyword", + "ignore_above": 256 + }, + "ngram": { + "type": "text", + "analyzer": "om_ngram" + }, + "compound": { + "type": "text", + "analyzer": "om_compound_analyzer" + } + } + }, + "description": { + "type": "text", + "analyzer": "om_analyzer", + "similarity": "boolean", + "term_vector": "with_positions_offsets" + }, + "fqnParts": { + "type": "keyword" + }, + "version": { + "type": "float" + }, + "updatedAt": { + "type": "date", + "format": "epoch_second" + }, + "updatedBy": { + "type": "text" + }, + "href": { + "type": "text" + }, + "sourceUrl": { + "type": "text" + }, + "domains": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "extension": { + "type": "object", + "enabled": false + }, + "owners": { + "type": "nested", + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text" + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "deleted": { + "type": "boolean" + }, + "followers": { + "type": "keyword" + }, + "entityType": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "entityStatus": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "totalVotes": { + "type": "long", + "null_value": 0 + }, + "votes": { + "type": "object", + "dynamic": false, + "properties": { + "upVotes": { + "type": "integer" + }, + "downVotes": { + "type": "integer" + } + } + }, + "descriptionStatus": { + "type": "keyword" + }, + "descriptionSources": { + "type": "object", + "dynamic": false + }, + "certification": { + "type": "object", + "properties": { + "tagLabel": { + "type": "object", + "properties": { + "tagFQN": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "fields": { + "text": { + "type": "text", + "analyzer": "om_analyzer" + } + } + }, + "labelType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "source": { + "type": "keyword" + }, + "state": { + "type": "keyword" + } + } + }, + "appliedDate": { + "type": "date", + "format": "strict_date_optional_time||epoch_millis" + }, + "expiryDate": { + "type": "date", + "format": "strict_date_optional_time||epoch_millis" + } + } + }, + "customPropertiesTyped": { + "type": "nested", + "properties": { + "name": { + "type": "keyword" + }, + "propertyType": { + "type": "keyword" + }, + "stringValue": { + "type": "keyword" + }, + "textValue": { + "type": "text", + "analyzer": "om_analyzer" + }, + "longValue": { + "type": "long" + }, + "doubleValue": { + "type": "double" + }, + "start": { + "type": "long" + }, + "end": { + "type": "long" + }, + "refId": { + "type": "keyword" + }, + "refType": { + "type": "keyword" + }, + "refName": { + "type": "keyword" + }, + "refFqn": { + "type": "keyword" + } + } + }, + "ownerDisplayName": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "ownerName": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + }, + "fqnHash": { + "type": "keyword" + }, + "impersonatedBy": { + "type": "keyword" + }, + "reviewers": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text" + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "entity": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + } + } + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "testSuite": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "keyword" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer" + } + } + }, + "description": { + "type": "text" + }, + "deleted": { + "type": "boolean" + }, + "href": { + "type": "text" + } + } + }, + "latestResult": { + "properties": { + "status": { + "type": "keyword" + }, + "timestamp": { + "type": "date", + "format": "epoch_millis" + }, + "resultId": { + "type": "keyword" + }, + "message": { + "type": "text" + } + } + }, + "effectiveFrom": { + "type": "date" + }, + "effectiveUntil": { + "type": "date" + }, + "createdAt": { + "type": "date", + "format": "epoch_millis" + }, + "createdBy": { + "type": "keyword" + }, + "inherited": { + "type": "boolean" + } + } + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/AdvancedSearch.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/AdvancedSearch.enum.ts index 8305e15be391..13d32559ce4b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/AdvancedSearch.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/AdvancedSearch.enum.ts @@ -93,6 +93,10 @@ export enum EntityFields { PARENT = 'parent.displayName.keyword', DIRECTORY = 'directory.displayName.keyword', FILE_TYPE = 'fileType', + DATA_CONTRACT_ENTITY_TYPE = 'entity.type', + DATA_CONTRACT_ENTITY_FQN = 'entity.fullyQualifiedName.keyword', + DATA_CONTRACT_LATEST_RESULT_STATUS = 'latestResult.status', + REVIEWERS = 'reviewers.displayName.keyword', FILE_EXTENSION = 'fileExtension', LAST_MODIFIED_BY = 'lastModifiedBy', SPREADSHEET = 'spreadsheet.displayName.keyword', diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/search.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/search.enum.ts index b37e361b8743..8827d555e699 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/search.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/search.enum.ts @@ -28,6 +28,7 @@ export enum SearchIndex { TAG = 'tag', CONTAINER = 'container', QUERY = 'query', + DATA_CONTRACT = 'dataContract', TEST_CASE = 'testCase', TEST_SUITE = 'testSuite', DATABASE_SCHEMA = 'databaseSchema', diff --git a/openmetadata-ui/src/main/resources/ui/src/interface/search.interface.ts b/openmetadata-ui/src/main/resources/ui/src/interface/search.interface.ts index 7abb32a31cae..d7785b50f162 100644 --- a/openmetadata-ui/src/main/resources/ui/src/interface/search.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/interface/search.interface.ts @@ -31,6 +31,7 @@ import { DatabaseServiceType, } from '../generated/entity/data/database'; import { DatabaseSchema } from '../generated/entity/data/databaseSchema'; +import { DataContract } from '../generated/entity/data/dataContract'; import { Directory } from '../generated/entity/data/directory'; import { File } from '../generated/entity/data/file'; import { Glossary } from '../generated/entity/data/glossary'; @@ -188,6 +189,9 @@ export interface GlossaryTermSearchSource GlossaryTerm {} // extends EntityInterface export interface QuerySearchSource extends SearchSourceBase, Query {} // extends EntityInterface +export interface DataContractSearchSource + extends SearchSourceBase, + DataContract {} // extends EntityInterface export interface TestCaseSearchSource extends SearchSourceBase, Exclude { @@ -333,6 +337,7 @@ export type SearchIndexSearchSourceMapping = { [SearchIndex.TAG]: TagClassSearchSource; [SearchIndex.CONTAINER]: ContainerSearchSource; [SearchIndex.QUERY]: QuerySearchSource; + [SearchIndex.DATA_CONTRACT]: DataContractSearchSource; [SearchIndex.TEST_CASE]: TestCaseSearchSource; [SearchIndex.DATABASE_SCHEMA]: DataBaseSchemaSearchSource; [SearchIndex.DATABASE]: DatabaseSearchSource; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.test.ts index de746fb25360..ccee5f737b0b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.test.ts @@ -86,6 +86,19 @@ describe('getEntitySpecificQueryBuilderFields', () => { advancedSearchClassBase = new AdvancedSearchClassBase(); }); + it('should return data contract specific fields', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + SearchIndex.DATA_CONTRACT, + ]); + + expect(Object.keys(result)).toEqual([ + EntityFields.DATA_CONTRACT_ENTITY_TYPE, + EntityFields.DATA_CONTRACT_ENTITY_FQN, + EntityFields.DATA_CONTRACT_LATEST_RESULT_STATUS, + EntityFields.REVIEWERS, + ]); + }); + it('should return table specific fields', () => { const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ SearchIndex.TABLE, @@ -1307,3 +1320,64 @@ describe('tag-like field autocomplete casing (#31999)', () => { ); }); }); + +describe('getQueryBuilderFields for data contracts', () => { + const advancedSearchClassBase = new AdvancedSearchClassBase(); + const fieldsUnpopulatedOnContracts = [ + EntityFields.SERVICE, + EntityFields.DOMAINS, + EntityFields.DATA_PRODUCT, + EntityFields.TAG, + EntityFields.GLOSSARY_TERMS, + EntityFields.TIER, + EntityFields.CERTIFICATION, + ]; + + it('should offer the contract own fields and none it never populates', () => { + const fields = Object.keys( + advancedSearchClassBase.getQueryBuilderFields({ + entitySearchIndex: [SearchIndex.DATA_CONTRACT], + }) + ); + + expect(fields).toEqual( + expect.arrayContaining([ + EntityFields.OWNERS, + EntityFields.NAME_KEYWORD, + EntityFields.DATA_CONTRACT_ENTITY_FQN, + EntityFields.DATA_CONTRACT_LATEST_RESULT_STATUS, + EntityFields.REVIEWERS, + ]) + ); + + fieldsUnpopulatedOnContracts.forEach((field) => + expect(fields).not.toContain(field) + ); + }); + + it('should drop those fields when contracts are selected alongside tables', () => { + const fields = Object.keys( + advancedSearchClassBase.getQueryBuilderFields({ + entitySearchIndex: [SearchIndex.TABLE, SearchIndex.DATA_CONTRACT], + }) + ); + + fieldsUnpopulatedOnContracts.forEach((field) => + expect(fields).not.toContain(field) + ); + + expect(fields).toContain(EntityFields.OWNERS); + }); + + it('should keep domains and tags for tables alone', () => { + const fields = Object.keys( + advancedSearchClassBase.getQueryBuilderFields({ + entitySearchIndex: [SearchIndex.TABLE], + }) + ); + + expect(fields).toEqual( + expect.arrayContaining([EntityFields.DOMAINS, EntityFields.TAG]) + ); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.ts index 6849d7981f29..bb8263fca358 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.ts @@ -21,7 +21,7 @@ import { type ListValues, type SelectFieldSettings, } from '@react-awesome-query-builder/antd'; -import { debounce, isEmpty, sortBy, toLower } from 'lodash'; +import { debounce, isEmpty, omit, sortBy, toLower } from 'lodash'; import { SearchOutputType, type CustomPropertyEnumConfig, @@ -44,6 +44,7 @@ import { import { SearchIndex } from '../enums/search.enum'; import type { Config } from '../generated/api/data/createCustomProperty'; import { EntityStatus } from '../generated/entity/data/searchIndex'; +import { ContractExecutionStatus } from '../generated/type/contractExecutionStatus'; import type { CustomPropertySummary } from '../rest/metadataTypeAPI.interface'; import { getAggregateFieldOptions } from '../rest/miscAPI'; import { renderAdvanceSearchButtons } from './AdvancedSearchUtils'; @@ -477,6 +478,79 @@ class AdvancedSearchClassBase { }, }; + // Common fields a search index never populates. A condition on one of them matches nothing in that index, so it is + // not offered while the index is selected — the same rule that keeps entity-specific fields to the common ones. + unsupportedCommonFields: Partial> = { + // A contract has no tags, tier, domains, data products, certification or service of its own. + [SearchIndex.DATA_CONTRACT]: [ + EntityFields.SERVICE, + EntityFields.SERVICE_TYPE, + EntityFields.DOMAINS, + EntityFields.DATA_PRODUCT, + EntityFields.TAG, + EntityFields.TAGS_LABEL_TYPE, + EntityFields.GLOSSARY_TERMS, + EntityFields.CERTIFICATION, + EntityFields.TIER, + EntityFields.TIER_LABEL_TYPE, + ], + }; + + // Fields specific to data contracts + dataContractQueryBuilderFields: Fields = { + [EntityFields.DATA_CONTRACT_ENTITY_TYPE]: { + label: t('label.asset-type'), + type: 'select', + mainWidgetProps: this.mainWidgetProps, + fieldSettings: { + asyncFetch: this.autocomplete({ + searchIndex: SearchIndex.DATA_CONTRACT, + entityField: EntityFields.DATA_CONTRACT_ENTITY_TYPE, + }), + useAsyncSearch: true, + }, + }, + [EntityFields.DATA_CONTRACT_ENTITY_FQN]: { + label: t('label.asset'), + type: 'select', + mainWidgetProps: this.mainWidgetProps, + fieldSettings: { + asyncFetch: this.autocomplete({ + searchIndex: SearchIndex.DATA_CONTRACT, + entityField: EntityFields.DATA_CONTRACT_ENTITY_FQN, + }), + useAsyncSearch: true, + }, + }, + [EntityFields.DATA_CONTRACT_LATEST_RESULT_STATUS]: { + label: t('label.contract-execution-status'), + type: 'select', + operators: LIST_VALUE_OPERATORS, + mainWidgetProps: this.mainWidgetProps, + valueSources: ['value'], + fieldSettings: { + listValues: Object.values(ContractExecutionStatus).map((status) => ({ + value: status, + title: status, + })), + showSearch: true, + useAsyncSearch: false, + }, + }, + [EntityFields.REVIEWERS]: { + label: t('label.reviewer-plural'), + type: 'select', + mainWidgetProps: this.mainWidgetProps, + fieldSettings: { + asyncFetch: this.autocomplete({ + searchIndex: [SearchIndex.USER, SearchIndex.TEAM], + entityField: EntityFields.DISPLAY_NAME_KEYWORD, + }), + useAsyncSearch: true, + }, + }, + }; + /** * Fields specific to dashboard */ @@ -1193,6 +1267,7 @@ class AdvancedSearchClassBase { [SearchIndex.FILE]: this.fileSearchQueryBuilderFields, [SearchIndex.SPREADSHEET]: this.spreadsheetSearchQueryBuilderFields, [SearchIndex.WORKSHEET]: this.worksheetSearchQueryBuilderFields, + [SearchIndex.DATA_CONTRACT]: this.dataContractQueryBuilderFields, [SearchIndex.ALL]: { ...this.tableQueryBuilderFields, ...this.pipelineQueryBuilderFields, @@ -1278,13 +1353,19 @@ class AdvancedSearchClassBase { }, }; - const fieldsConfig = { - ...this.getCommonConfig({ entitySearchIndex }), - ...(shouldAddServiceField ? serviceQueryBuilderFields : {}), - ...this.getEntitySpecificQueryBuilderFields(entitySearchIndex), - ...this.getColumnConfig(entitySearchIndex), - ...this.getColumnTagConfig(entitySearchIndex), - }; + const unsupportedFields = entitySearchIndex.flatMap( + (index) => this.unsupportedCommonFields[index] ?? [] + ); + const fieldsConfig = omit( + { + ...this.getCommonConfig({ entitySearchIndex }), + ...(shouldAddServiceField ? serviceQueryBuilderFields : {}), + ...this.getEntitySpecificQueryBuilderFields(entitySearchIndex), + ...this.getColumnConfig(entitySearchIndex), + ...this.getColumnTagConfig(entitySearchIndex), + }, + unsupportedFields + ); // Sort the fields according to the label const sortedFieldsConfig = sortBy(Object.entries(fieldsConfig), '1.label'); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchClassBase.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/SearchClassBase.test.ts index 527fa587008e..c10ddf290372 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchClassBase.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchClassBase.test.ts @@ -57,6 +57,19 @@ describe('SearchClassBase', () => { searchClassBase = new SearchClassBase(); }); + it('should map data contracts to their own search index both ways', () => { + expect( + searchClassBase.getEntityTypeSearchIndexMapping()[ + EntityType.DATA_CONTRACT + ] + ).toBe(SearchIndex.DATA_CONTRACT); + expect( + searchClassBase.getSearchIndexEntityTypeMapping()[ + SearchIndex.DATA_CONTRACT + ] + ).toBe(EntityType.DATA_CONTRACT); + }); + it('should return the correct search index for each entity type', () => { const searchIndexMapping = searchClassBase.getEntityTypeSearchIndexMapping(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/SearchClassBase.ts index 6db8c7e6e270..68f59da7304a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchClassBase.ts @@ -110,6 +110,7 @@ class SearchClassBase { [EntityType.TEST_SUITE]: SearchIndex.TEST_SUITE, [EntityType.GLOSSARY]: SearchIndex.GLOSSARY, [EntityType.INGESTION_PIPELINE]: SearchIndex.INGESTION_PIPELINE, + [EntityType.DATA_CONTRACT]: SearchIndex.DATA_CONTRACT, [EntityType.API_SERVICE]: SearchIndex.API_SERVICE, [EntityType.API_COLLECTION]: SearchIndex.API_COLLECTION, [EntityType.API_ENDPOINT]: SearchIndex.API_ENDPOINT, @@ -157,6 +158,7 @@ class SearchClassBase { [SearchIndex.TEST_SUITE]: EntityType.TEST_SUITE, [SearchIndex.GLOSSARY]: EntityType.GLOSSARY, [SearchIndex.INGESTION_PIPELINE]: EntityType.INGESTION_PIPELINE, + [SearchIndex.DATA_CONTRACT]: EntityType.DATA_CONTRACT, [SearchIndex.API_SERVICE]: EntityType.API_SERVICE, [SearchIndex.API_COLLECTION]: EntityType.API_COLLECTION, [SearchIndex.API_ENDPOINT]: EntityType.API_ENDPOINT,