Skip to content
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

[BUG] Unable to deserialize adjacency matrix aggregation buckets #1478

Open
CodeSlothTrent opened this issue Mar 6, 2025 · 4 comments
Open
Labels
bug Something isn't working

Comments

@CodeSlothTrent
Copy link

What is the bug?

A clear and concise description of the bug.

The OpenSearchClient is unable to deserialize Adjacency Matrix aggregation results.

How can one reproduce the bug?

Steps to reproduce the behavior.

A unit test can be found in the public repository at the following link:
https://github.com/CodeSlothTrent/codesloth-search-samples-java/blob/cab26928e8ccf367f9e8e10b01e1c4bc77ab2602/OpenSearchSamples/src/test/java/KeywordDemo/KeywordAggregationTests.java#L271

Code copied here for reference

@Test
    public void keywordMapping_CanBeUsedForAdjacencyMatrixAggregation() throws Exception {
        // Create a test index with keyword mapping for the ProductNames field
        try (OpenSearchTestIndex testIndex = fixture.createTestIndex(mapping ->
                mapping.properties("productNames", Property.of(p -> p.keyword(k -> k))))) {

            // Create and index user favourite products documents
            UserFavouriteProducts[] userFavouriteProducts = new UserFavouriteProducts[]{
                    new UserFavouriteProducts(1, new String[]{"mouse", "mouse pad"}),
                    new UserFavouriteProducts(2, new String[]{"mouse"}),
                    new UserFavouriteProducts(3, new String[]{"keyboard"}),
                    new UserFavouriteProducts(4, new String[]{"mouse pad", "keyboard"}),
                    new UserFavouriteProducts(5, new String[]{"mouse", "keyboard"}),
                    new UserFavouriteProducts(6, new String[]{"mouse", "mouse pad"})
            };
            testIndex.indexDocuments(userFavouriteProducts);

            // Create a map of the different filters
            var filterMap = Map.of(
                    "mouse", Query.of(q -> q.term(t -> t.field("productNames").value(FieldValue.of("mouse")))),
                    "mouse pad", Query.of(q -> q.term(t -> t.field("productNames").value(FieldValue.of("mouse pad")))),
                    "keyboard", Query.of(q -> q.term(t -> t.field("productNames").value(FieldValue.of("keyboard")))));

            // Create a search request with adjacency matrix aggregation
            SearchRequest searchRequest = new SearchRequest.Builder()
                    .index(testIndex.getName())
                    .query(builder -> builder.matchAll(new MatchAllQuery.Builder().build()))
                    .aggregations("product_matrix", a -> a
                            .adjacencyMatrix(am -> am
                                    .filters(filterMap)
                            )
                    )
                    .q("?typed_keys=true")
                    .build();

            OpenSearchRequestLogger.LogRequestJson(searchRequest);

            // Execute the search request
            SearchResponse<UserFavouriteProducts> response = openSearchClient.search(searchRequest, UserFavouriteProducts.class);

            // Verify the results
            assertThat(response.aggregations()).isNotNull();

            AdjacencyMatrixAggregate matrixAgg = response.aggregations().get("product_matrix").adjacencyMatrix();

            // Check the bucket counts
            Map<String, Long> bucketCounts = matrixAgg.buckets().array().stream()
                    .collect(Collectors.toMap(
                            AdjacencyMatrixBucket::toString,
                            AdjacencyMatrixBucket::docCount
                    ));

            // Format the results for verification
            String formattedResults = bucketCounts.entrySet().stream()
                    .map(entry -> entry.getKey() + ":" + entry.getValue())
                    .collect(Collectors.joining(", "));

            // Verify the expected results
            assertThat(bucketCounts).containsEntry("keyboard", 3L);
            assertThat(bucketCounts).containsEntry("keyboard&mouse", 1L);
            assertThat(bucketCounts).containsEntry("keyboard&mouse pad", 1L);
            assertThat(bucketCounts).containsEntry("mouse", 4L);
            assertThat(bucketCounts).containsEntry("mouse pad", 3L);
            assertThat(bucketCounts).containsEntry("mouse&mouse pad", 2L);
        }

Example query produced

{
  "aggregations" : {
    "product_matrix" : {
      "adjacency_matrix" : {
        "filters" : {
          "keyboard" : {
            "term" : {
              "productNames" : {
                "value" : "keyboard"
              }
            }
          },
          "mouse pad" : {
            "term" : {
              "productNames" : {
                "value" : "mouse pad"
              }
            }
          },
          "mouse" : {
            "term" : {
              "productNames" : {
                "value" : "mouse"
              }
            }
          }
        }
      }
    }
  },
  "query" : {
    "match_all" : { }
  }
}

What is the expected behavior?

A clear and concise description of what you expected to happen.

The above query when run in opensearch dashboards developer tools returns results

Image

However, looking at the results in code, the buckets are not populated

Image

What is your host/environment?

Operating system, version.

Windows 11 Home. opensearch-java 2.15.0.

Do you have any screenshots?

If applicable, add screenshots to help explain your problem.

Do you have any additional context?

Add any other context about the problem.

@CodeSlothTrent CodeSlothTrent added bug Something isn't working untriaged labels Mar 6, 2025
@Xtansia Xtansia removed the untriaged label Mar 6, 2025
@Xtansia
Copy link
Collaborator

Xtansia commented Mar 6, 2025

@CodeSlothTrent Could you please try removing .q("?typed_keys=true")? The q parameter is for passing a Lucene Query String, not for modifying the HTTP request's query parameters. So instead of setting typed_keys, you're instead searching for documents matching "?typed_keys=true". Additionally the client always sets typed_keys itself as it's required for unambiguous deserialization of the aggregations.

@CodeSlothTrent
Copy link
Author

Hi @Xtansia 👋

Without that parameter I receive the following exception:

java.lang.RuntimeException: Property name 'key' is not in the 'type#name' format. Make sure the request has 'typed_keys' set.

	at org.opensearch.client.transport.httpclient5.ApacheHttpClient5Transport.extractAndWrapCause(ApacheHttpClient5Transport.java:1163)
	at org.opensearch.client.transport.httpclient5.ApacheHttpClient5Transport.performRequest(ApacheHttpClient5Transport.java:158)
	at org.opensearch.client.opensearch.OpenSearchClient.search(OpenSearchClient.java:1386)
	at KeywordDemo.KeywordAggregationTests.keywordMapping_CanBeUsedForAdjacencyMatrixAggregation(KeywordAggregationTests.java:308)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1597)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1597)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
Caused by: jakarta.json.stream.JsonParsingException: Property name 'key' is not in the 'type#name' format. Make sure the request has 'typed_keys' set.
	at org.opensearch.client.json.ExternallyTaggedUnion$TypedKeysDeserializer.deserializeEntry(ExternallyTaggedUnion.java:129)
	at org.opensearch.client.opensearch._types.aggregations.MultiBucketBase.lambda$setupMultiBucketBaseDeserializer$0(MultiBucketBase.java:159)
	at org.opensearch.client.json.ObjectDeserializer.parseUnknownField(ObjectDeserializer.java:214)
	at org.opensearch.client.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:183)
	at org.opensearch.client.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:146)
	at org.opensearch.client.json.ObjectBuilderDeserializer.deserialize(ObjectBuilderDeserializer.java:97)
	at org.opensearch.client.json.DelegatingDeserializer$SameType.deserialize(DelegatingDeserializer.java:60)
	at org.opensearch.client.json.JsonpDeserializerBase$ArrayDeserializer.deserialize(JsonpDeserializerBase.java:343)
	at org.opensearch.client.json.JsonpDeserializerBase$ArrayDeserializer.deserialize(JsonpDeserializerBase.java:308)
	at org.opensearch.client.json.UnionDeserializer$SingleMemberHandler.deserialize(UnionDeserializer.java:91)
	at org.opensearch.client.json.UnionDeserializer.deserialize(UnionDeserializer.java:331)
	at org.opensearch.client.json.UnionDeserializer.deserialize(UnionDeserializer.java:285)
	at org.opensearch.client.json.ObjectDeserializer$FieldObjectDeserializer.deserialize(ObjectDeserializer.java:81)
	at org.opensearch.client.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:185)
	at org.opensearch.client.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:146)
	at org.opensearch.client.json.ObjectBuilderDeserializer.deserialize(ObjectBuilderDeserializer.java:97)
	at org.opensearch.client.json.DelegatingDeserializer$SameType.deserialize(DelegatingDeserializer.java:60)
	at org.opensearch.client.json.ExternallyTaggedUnion$Deserializer.deserialize(ExternallyTaggedUnion.java:94)
	at org.opensearch.client.json.ExternallyTaggedUnion$TypedKeysDeserializer.deserializeEntry(ExternallyTaggedUnion.java:136)
	at org.opensearch.client.json.ExternallyTaggedUnion$TypedKeysDeserializer.deserialize(ExternallyTaggedUnion.java:119)
	at org.opensearch.client.json.ExternallyTaggedUnion$TypedKeysDeserializer.deserialize(ExternallyTaggedUnion.java:106)
	at org.opensearch.client.json.JsonpDeserializer.deserialize(JsonpDeserializer.java:87)
	at org.opensearch.client.json.ObjectDeserializer$FieldObjectDeserializer.deserialize(ObjectDeserializer.java:81)
	at org.opensearch.client.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:185)
	at org.opensearch.client.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:146)
	at org.opensearch.client.json.JsonpDeserializer.deserialize(JsonpDeserializer.java:87)
	at org.opensearch.client.json.ObjectBuilderDeserializer.deserialize(ObjectBuilderDeserializer.java:91)
	at org.opensearch.client.json.DelegatingDeserializer$SameType.deserialize(DelegatingDeserializer.java:55)
	at org.opensearch.client.transport.endpoints.EndpointWithResponseMapperAttr$1.deserialize(EndpointWithResponseMapperAttr.java:68)
	at org.opensearch.client.transport.httpclient5.ApacheHttpClient5Transport.decodeResponse(ApacheHttpClient5Transport.java:676)
	at org.opensearch.client.transport.httpclient5.ApacheHttpClient5Transport.prepareResponse(ApacheHttpClient5Transport.java:561)
	at org.opensearch.client.transport.httpclient5.ApacheHttpClient5Transport.lambda$performRequestAsync$0(ApacheHttpClient5Transport.java:191)
	at java.base/java.util.concurrent.CompletableFuture$UniApply.tryFire(CompletableFuture.java:690)
	at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:554)
	at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2223)
	at org.opensearch.client.transport.httpclient5.ApacheHttpClient5Transport$1.completed(ApacheHttpClient5Transport.java:236)
	at org.opensearch.client.transport.httpclient5.ApacheHttpClient5Transport$1.completed(ApacheHttpClient5Transport.java:225)
	at org.apache.hc.core5.concurrent.BasicFuture.completed(BasicFuture.java:148)
	at org.apache.hc.core5.concurrent.ComplexFuture.completed(ComplexFuture.java:72)
	at org.apache.hc.client5.http.impl.async.InternalAbstractHttpAsyncClient$1$1.completed(InternalAbstractHttpAsyncClient.java:280)
	at org.apache.hc.core5.http.nio.support.AbstractAsyncResponseConsumer$1.completed(AbstractAsyncResponseConsumer.java:101)
	at org.apache.hc.core5.http.nio.entity.AbstractBinAsyncEntityConsumer.completed(AbstractBinAsyncEntityConsumer.java:87)
	at org.apache.hc.core5.http.nio.entity.AbstractBinDataConsumer.streamEnd(AbstractBinDataConsumer.java:83)
	at org.apache.hc.core5.http.nio.support.AbstractAsyncResponseConsumer.streamEnd(AbstractAsyncResponseConsumer.java:142)
	at org.apache.hc.client5.http.impl.async.HttpAsyncMainClientExec$1.streamEnd(HttpAsyncMainClientExec.java:251)
	at org.apache.hc.core5.http.impl.nio.ClientHttp1StreamHandler.dataEnd(ClientHttp1StreamHandler.java:276)
	at org.apache.hc.core5.http.impl.nio.ClientHttp1StreamDuplexer.dataEnd(ClientHttp1StreamDuplexer.java:366)
	at org.apache.hc.core5.http.impl.nio.AbstractHttp1StreamDuplexer.onInput(AbstractHttp1StreamDuplexer.java:338)
	at org.apache.hc.core5.http.impl.nio.AbstractHttp1IOEventHandler.inputReady(AbstractHttp1IOEventHandler.java:64)
	at org.apache.hc.core5.http.impl.nio.ClientHttp1IOEventHandler.inputReady(ClientHttp1IOEventHandler.java:41)
	at org.apache.hc.core5.reactor.InternalDataChannel.onIOEvent(InternalDataChannel.java:143)
	at org.apache.hc.core5.reactor.InternalChannel.handleIOEvent(InternalChannel.java:51)
	at org.apache.hc.core5.reactor.SingleCoreIOReactor.processEvents(SingleCoreIOReactor.java:176)
	at org.apache.hc.core5.reactor.SingleCoreIOReactor.doExecute(SingleCoreIOReactor.java:125)
	at org.apache.hc.core5.reactor.AbstractSingleCoreIOReactor.execute(AbstractSingleCoreIOReactor.java:92)
	at org.apache.hc.core5.reactor.IOReactorWorker.run(IOReactorWorker.java:44)
	at java.base/java.lang.Thread.run(Thread.java:1575)

Thanks!

@Xtansia
Copy link
Collaborator

Xtansia commented Mar 7, 2025

@CodeSlothTrent Ah that actually helps a lot! While not immediately obvious because of the error that it's hitting, this is due to AdjacencyMatrixBucket missing a definition for the key field and so it's trying to deserialize it as a nested aggregate.
Adding the .q("?typed_keys=true") wasn't actually resolving the error, it was just causing no results to be returned and therefore not triggering the error.

@CodeSlothTrent
Copy link
Author

Hi @Xtansia

Thanks for confirming 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants