-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat(search): scope index resolution with fetchParentsAliases / fetchChildAliases flags #27762
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a07ad5d
1abfea9
e9155b5
f426da2
5062bc5
2ec748e
b08ef0f
96ff8b6
d3d3d8f
a48ac1e
6819193
fa8f8fe
1a4631d
cd49228
c56be4d
9038e90
299d3bf
a649f6d
bffc943
f062bfb
41d8421
601699f
2ff5718
971d678
b2ec7ab
b3def1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1926,4 +1926,261 @@ | |||||
| String[] lines = response.body().split("\n"); | ||||||
| assertEquals(1, lines.length, "Export beyond results should only contain header"); | ||||||
| } | ||||||
|
|
||||||
| // =================================================================== | ||||||
| // FETCH PARENT/CHILD ALIASES TESTS | ||||||
| // =================================================================== | ||||||
|
|
||||||
| private HttpResponse<String> httpGetJson(String path) throws Exception { | ||||||
| String baseUrl = SdkClients.getServerUrl(); | ||||||
| String token = SdkClients.getAdminToken(); | ||||||
|
|
||||||
| HttpRequest request = | ||||||
| HttpRequest.newBuilder() | ||||||
| .uri(URI.create(baseUrl + path)) | ||||||
| .header("Authorization", "Bearer " + token) | ||||||
| .header("Accept", "application/json") | ||||||
| .timeout(Duration.ofSeconds(30)) | ||||||
| .GET() | ||||||
| .build(); | ||||||
|
|
||||||
| return HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Wait until the column document is visible in {@code column_search_index}. The test fixtures | ||||||
| * deliberately put {@code unique} only into the column name (not the table name), which is | ||||||
| * exactly the shape the original bug report describes: a UI search for {@code index=table} | ||||||
| * accidentally returns column docs because ES alias expansion routes the {@code table} alias | ||||||
| * to {@code column_search_index} too. | ||||||
| */ | ||||||
| private void awaitColumnIndexed(String unique) { | ||||||
| Awaitility.await() | ||||||
| .atMost(90, TimeUnit.SECONDS) | ||||||
| .pollInterval(500, TimeUnit.MILLISECONDS) | ||||||
| .until( | ||||||
| () -> { | ||||||
| HttpResponse<String> r = | ||||||
| httpGetJson("/v1/search/query?q=" + unique + "&index=tableColumn&size=20"); | ||||||
| return r.statusCode() == 200 | ||||||
| && OBJECT_MAPPER.readTree(r.body()).path("hits").path("hits").size() > 0; | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| private static boolean anyHitOfType(JsonNode hits, String entityType) { | ||||||
| for (JsonNode hit : hits) { | ||||||
| if (entityType.equalsIgnoreCase(hit.path("_source").path("entityType").asText())) { | ||||||
| return true; | ||||||
| } | ||||||
| } | ||||||
| return false; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Bug regression: the UI now passes the alias {@code "table"} (instead of the legacy | ||||||
| * {@code table_search_index}). ES alias expansion bleeds {@code column_search_index} into the | ||||||
| * results because tableColumn declares {@code "table"} as a parent alias. Asserts the bleed | ||||||
| * reproduces when child expansion is explicitly enabled via {@code fetchChildAliases=*} and | ||||||
| * disappears under the new default ({@code fetchChildAliases=none}) — a comparison test, not | ||||||
| * a vacuous "no columns" check that would pass on an empty response. | ||||||
| */ | ||||||
| @Test | ||||||
| void testDefaultChildScopeExcludesColumns(TestNamespace ns) throws Exception { | ||||||
| String unique = "fetchchild_excl_" + ns.shortPrefix(); | ||||||
| Column uniqueColumn = | ||||||
| new Column() | ||||||
| .withName(unique + "_col") | ||||||
| .withDataType(ColumnDataType.VARCHAR) | ||||||
| .withDataLength(64); | ||||||
| createTestTableWithColumns(ns, ns.prefix("fetchchild_excl_t"), List.of(uniqueColumn)); | ||||||
|
|
||||||
| awaitColumnIndexed(unique); | ||||||
|
|
||||||
| // Opt-in legacy expansion (fetchChildAliases=*): tableColumn hits MUST appear so the rest | ||||||
| // of the assertion isn't vacuous. | ||||||
| HttpResponse<String> withChildren = | ||||||
| httpGetJson("/v1/search/query?q=" + unique + "&index=table&fetchChildAliases=*&size=20"); | ||||||
| assertEquals(200, withChildren.statusCode()); | ||||||
| JsonNode withChildrenHits = | ||||||
| OBJECT_MAPPER.readTree(withChildren.body()).path("hits").path("hits"); | ||||||
| assertTrue( | ||||||
| anyHitOfType(withChildrenHits, "tableColumn"), | ||||||
| "fetchChildAliases=* on index=table must surface tableColumn hits — otherwise the " | ||||||
| + "fixture is broken and the no-columns assertion would pass vacuously. body=" | ||||||
| + withChildren.body()); | ||||||
|
|
||||||
| // Default flags (fetchChildAliases=none) — the bug fix path. tableColumn hits must NOT leak. | ||||||
| HttpResponse<String> withoutChildren = | ||||||
| httpGetJson("/v1/search/query?q=" + unique + "&index=table&size=20"); | ||||||
| assertEquals(200, withoutChildren.statusCode()); | ||||||
| JsonNode withoutChildrenHits = | ||||||
| OBJECT_MAPPER.readTree(withoutChildren.body()).path("hits").path("hits"); | ||||||
| assertFalse( | ||||||
| anyHitOfType(withoutChildrenHits, "tableColumn"), | ||||||
| "Default flags on index=table must drop tableColumn hits, got: " + withoutChildren.body()); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Confirms the named-filter syntax: passing only a specific child entity type expands that one | ||||||
| * but no others. Uses {@code fetchChildAliases=tableColumn} so the response includes | ||||||
| * {@code tableColumn} hits but not, say, {@code testCase} hits (testCase isn't a child of | ||||||
| * 'table' in indexMapping.json so it's already implicitly excluded — this test pins the | ||||||
| * positive direction). | ||||||
| */ | ||||||
| @Test | ||||||
| void testFetchChildAliasesNamedFilterIncludesOnlySpecified(TestNamespace ns) throws Exception { | ||||||
| String unique = "fetchchild_named_" + ns.shortPrefix(); | ||||||
| Column uniqueColumn = | ||||||
| new Column() | ||||||
| .withName(unique + "_col") | ||||||
| .withDataType(ColumnDataType.VARCHAR) | ||||||
| .withDataLength(64); | ||||||
| createTestTableWithColumns(ns, ns.prefix("fetchchild_named_t"), List.of(uniqueColumn)); | ||||||
|
|
||||||
| awaitColumnIndexed(unique); | ||||||
|
|
||||||
| HttpResponse<String> response = | ||||||
| httpGetJson( | ||||||
| "/v1/search/query?q=" + unique + "&index=table&fetchChildAliases=tableColumn&size=20"); | ||||||
| assertEquals(200, response.statusCode()); | ||||||
| JsonNode hits = OBJECT_MAPPER.readTree(response.body()).path("hits").path("hits"); | ||||||
| assertTrue( | ||||||
| anyHitOfType(hits, "tableColumn"), | ||||||
| "fetchChildAliases=tableColumn must include tableColumn docs: " + response.body()); | ||||||
|
|
||||||
| // A filter that names a non-child must not introduce that index — passing 'topic' (not a | ||||||
| // child of 'table') should yield zero tableColumn hits. | ||||||
| HttpResponse<String> nonChild = | ||||||
| httpGetJson( | ||||||
| "/v1/search/query?q=" + unique + "&index=table&fetchChildAliases=topic&size=20"); | ||||||
| assertEquals(200, nonChild.statusCode()); | ||||||
| JsonNode nonChildHits = OBJECT_MAPPER.readTree(nonChild.body()).path("hits").path("hits"); | ||||||
| assertFalse( | ||||||
| anyHitOfType(nonChildHits, "tableColumn"), | ||||||
| "fetchChildAliases=topic must not introduce tableColumn hits: " + nonChild.body()); | ||||||
| } | ||||||
|
|
||||||
| @Test | ||||||
| void testFetchParentsAliasesWildcardIncludesParents(TestNamespace ns) throws Exception { | ||||||
| String unique = "fetchparents_" + ns.shortPrefix(); | ||||||
| Column uniqueColumn = | ||||||
| new Column() | ||||||
| .withName(unique + "_col") | ||||||
| .withDataType(ColumnDataType.VARCHAR) | ||||||
| .withDataLength(64); | ||||||
| Table parentTable = | ||||||
| createTestTableWithColumns(ns, ns.prefix("fetchparents_" + unique), List.of(uniqueColumn)); | ||||||
| String tableName = parentTable.getName(); | ||||||
|
|
||||||
| awaitColumnIndexed(unique); | ||||||
|
|
||||||
| Awaitility.await() | ||||||
| .atMost(90, TimeUnit.SECONDS) | ||||||
| .pollInterval(500, TimeUnit.MILLISECONDS) | ||||||
| .until( | ||||||
|
Check failure on line 2080 in openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/SearchResourceIT.java
|
||||||
| () -> { | ||||||
| HttpResponse<String> r = | ||||||
| httpGetJson( | ||||||
| "/v1/search/query?q=" | ||||||
| + tableName | ||||||
| + "&index=tableColumn&fetchParentsAliases=*&fetchChildAliases=none&size=20"); | ||||||
| return r.statusCode() == 200 | ||||||
| && anyHitOfType( | ||||||
| OBJECT_MAPPER.readTree(r.body()).path("hits").path("hits"), "table"); | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * The flag must also be honored on the streaming export endpoint. Comparison test: default | ||||||
| * export carries column rows; explicit fetchChildAliases=false drops them. | ||||||
| */ | ||||||
| @Test | ||||||
| void testDefaultChildScopeOnExportEndpoint(TestNamespace ns) throws Exception { | ||||||
| String unique = "fetchchild_exp_" + ns.shortPrefix(); | ||||||
| Column uniqueColumn = | ||||||
| new Column() | ||||||
| .withName(unique + "_col") | ||||||
| .withDataType(ColumnDataType.VARCHAR) | ||||||
| .withDataLength(64); | ||||||
| createTestTableWithColumns(ns, ns.prefix("fetchchild_exp_t"), List.of(uniqueColumn)); | ||||||
|
|
||||||
| awaitColumnIndexed(unique); | ||||||
|
|
||||||
| HttpResponse<String> wildcardExport = | ||||||
| httpGetExport("/v1/search/export?q=" + unique + "&index=table&fetchChildAliases=*&size=50"); | ||||||
| assertEquals(200, wildcardExport.statusCode()); | ||||||
| String wildcardBody = wildcardExport.body(); | ||||||
| boolean wildcardHasColumn = | ||||||
| java.util.Arrays.stream(wildcardBody.split("\n")) | ||||||
| .skip(1) | ||||||
| .anyMatch(row -> row.toLowerCase().startsWith("tablecolumn,")); | ||||||
| assertTrue( | ||||||
| wildcardHasColumn, | ||||||
| "fetchChildAliases=* export on index=table must include tableColumn rows; " | ||||||
| + "otherwise the no-columns assertion below is vacuous. body=" | ||||||
| + wildcardBody); | ||||||
|
|
||||||
| HttpResponse<String> defaultExport = | ||||||
| httpGetExport("/v1/search/export?q=" + unique + "&index=table&size=50"); | ||||||
| assertEquals(200, defaultExport.statusCode()); | ||||||
| String defaultBody = defaultExport.body(); | ||||||
| for (String row : defaultBody.split("\n")) { | ||||||
| assertFalse( | ||||||
| row.toLowerCase().startsWith("tablecolumn,"), | ||||||
| "Default export on /export must drop tableColumn rows, got: " + row); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * The flag must propagate to /aggregate. Comparison test: default aggregation has a column | ||||||
| * bucket; explicit fetchChildAliases=false drops it. | ||||||
|
||||||
| * bucket; explicit fetchChildAliases=false drops it. | |
| * bucket; explicit fetchChildAliases=none drops it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This Javadoc still refers to
fetchChildAliases=false, but the implementation and tests use the new string-based syntax (none/*/ comma-separated entity types). Update the comment to match the actual supported values to avoid confusing future maintainers and reviewers.