diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java index d530cefd57fe..442ec0f2d99e 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java @@ -641,13 +641,59 @@ private boolean isKnownCanonicalIndex(String name) { return false; } + /** + * Resolve the supplied index alias into the actual Elasticsearch / OpenSearch index name to + * query. Handles four shapes: + * + * + * + * Comma-separated tokens are resolved independently. Empty tokens (from {@code "table,"} or + * {@code ","}) are dropped instead of materializing as a bare cluster prefix; if every token + * is empty the original input is returned unchanged so downstream ES surfaces a normal + * "unknown index" error instead of an empty-target failure. + */ public String getIndexOrAliasName(String name) { - if (clusterAlias == null || clusterAlias.isEmpty()) { + if (nullOrEmpty(name)) { return name; } - return Arrays.stream(name.split(",")) - .map(index -> clusterAlias + INDEX_NAME_SEPARATOR + index.trim()) - .collect(Collectors.joining(",")); + String prefix = + clusterAlias == null || clusterAlias.isEmpty() ? null : clusterAlias + INDEX_NAME_SEPARATOR; + String resolved = + Arrays.stream(name.split(",")) + .map(String::trim) + .filter(t -> !t.isEmpty()) + .map(t -> resolveSingleAliasToken(t, prefix)) + .collect(Collectors.joining(",")); + return resolved.isEmpty() ? name : resolved; + } + + private String resolveSingleAliasToken(String token, String clusterPrefix) { + if (clusterPrefix != null && token.startsWith(clusterPrefix)) { + return token; + } + IndexMapping mapping = entityIndexMap == null ? null : entityIndexMap.get(token); + if (mapping != null) { + return mapping.getIndexName(clusterAlias); + } + return clusterPrefix == null ? token : clusterPrefix + token; } private static final Map> RBAC_CHILD_TYPES = diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/search/SearchRepositoryBehaviorTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/search/SearchRepositoryBehaviorTest.java index e68df716ded1..0bc8e1bc61ba 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/search/SearchRepositoryBehaviorTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/search/SearchRepositoryBehaviorTest.java @@ -275,6 +275,68 @@ void indexNameHelpersRespectClusterAlias() { "table_search_index", repository.getIndexNameWithoutAlias("cluster_table_search_index")); } + /** + * Bug regression for issue #27761: passing the entity-specific alias {@code "table"} used to + * leak into ES alias expansion and surface tableColumn docs (because column_search_index is + * registered with {@code "table"} as one of its aliases). Resolving the alias to its canonical + * index name here bypasses ES's alias resolution, so the search hits exactly the table index. + */ + @Test + void getIndexOrAliasNameResolvesEntitySpecificAliasToCanonicalIndex() { + assertEquals("cluster_table_search_index", repository.getIndexOrAliasName("table")); + assertEquals("cluster_domain_search_index", repository.getIndexOrAliasName("domain")); + } + + /** + * Compound aliases like {@code "all"} and {@code "dataAsset"} have no entry in + * {@code entityIndexMap} (they're meta-aliases registered against many entities at index + * creation time). The resolver passes them through with the cluster prefix so ES expands them + * natively — searching {@code dataAsset} should still surface every data-asset entity. + */ + @Test + void getIndexOrAliasNamePassesCompoundAliasesThroughForNativeESExpansion() { + assertEquals("cluster_dataAsset", repository.getIndexOrAliasName("dataAsset")); + assertEquals("cluster_all", repository.getIndexOrAliasName("all")); + } + + /** + * Defense-in-depth: a token that already carries the cluster prefix must not get prefixed + * again. Otherwise multi-tenant deployments would 404 on + * {@code cluster_cluster_table_search_index} if any internal code accidentally hands a + * resolved value back to this method. + */ + @Test + void getIndexOrAliasNameIsIdempotentForAlreadyPrefixedTokens() { + assertEquals( + "cluster_table_search_index", repository.getIndexOrAliasName("cluster_table_search_index")); + } + + /** + * Mixed input: each comma-separated token is resolved independently. Entity-specific aliases + * resolve to canonical names; compound aliases pass through. + */ + @Test + void getIndexOrAliasNameResolvesEachCommaSeparatedTokenIndependently() { + assertEquals( + "cluster_table_search_index,cluster_dataAsset", + repository.getIndexOrAliasName("table,dataAsset")); + } + + /** + * Stray-comma / empty-token input must not produce bare cluster prefixes such as + * {@code "cluster_"}. Empty tokens are dropped; if every token is empty the original string + * is returned unchanged so downstream ES surfaces a normal "unknown index" error instead of + * a confusing empty-target failure. + */ + @Test + void getIndexOrAliasNameDropsEmptyTokensAndPreservesAllEmptyInput() { + assertEquals("cluster_table_search_index", repository.getIndexOrAliasName("table,")); + assertEquals( + "cluster_table_search_index,cluster_domain_search_index", + repository.getIndexOrAliasName("table, ,domain")); + assertEquals(", ,", repository.getIndexOrAliasName(", ,")); + } + @Test void indexExistsFallsBackToAliasLookup() { when(searchClient.indexExists("cluster_table_search_index")).thenReturn(false);