Skip to content

ES|QL: Fix BytesRef2BlockHash #130705

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

luigidellaquila
Copy link
Contributor

Fix retrieval of keys in BytesRef2BlockHash

Fixes: #130644

@elasticsearchmachine
Copy link
Collaborator

Pinging @elastic/es-analytical-engine (Team:Analytics)

@elasticsearchmachine elasticsearchmachine added the Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) label Jul 7, 2025
@elasticsearchmachine
Copy link
Collaborator

Hi @luigidellaquila, I've created a changelog YAML for you.

@luigidellaquila luigidellaquila added v8.19.0 v9.1.0 v8.16.7 v9.0.4 v8.18.4 v8.17.9 and removed Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) v9.2.0 labels Jul 7, 2025
@elasticsearchmachine elasticsearchmachine added the Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) label Jul 7, 2025
@luigidellaquila luigidellaquila added v9.2.0 and removed Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) labels Jul 7, 2025
@luigidellaquila luigidellaquila added Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) auto-backport Automatically create backport pull requests when merged labels Jul 7, 2025
}
}

public void test2BytesRefs() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same test we had for BytesRef3BlockHash

@@ -1326,7 +1513,7 @@ public void close() {
fail("hashes should not close AddInput");
}
});
assertThat(output1.size(), equalTo(output1.size()));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A typo probably

builder.append(BytesRefs.toString(block1.getBytesRef(i, scratch)));
distinctKeys.add(builder.toString());
}
assertThat(distinctKeys.size(), equalTo(totalPositions));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert the fix and you'll see a failure here

@@ -145,7 +145,7 @@ public Block[] getKeys() {
try {
try (BytesRefBlock.Builder b1 = blockFactory.newBytesRefBlockBuilder(positions)) {
for (int i = 0; i < positions; i++) {
int k1 = (int) (finalHash.get(i) & 0xffffL);
int k1 = (int) (finalHash.get(i) & 0xffffffffL);
if (k1 == 0) {
Copy link
Contributor

@bpintea bpintea Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this (and below) be if (k <= 0) { ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can't be 0 because we grab the low bits. Right? I haven't had enough coffee for bit math.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the finalHash.get(i) not return smth like 0xe0000000L (or some other value with the 32nd bit set)?
The hash1.hash.get() method seems to expect a non-negative argument. It might all be safe, it just stood out to me since we're truncating a long.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are always positive, they are generated by BytesRefBlockHash, see hashOrdToGroupNullReserved

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess one of the implicit assumptions is that a block hash will never have more than 2B positions. If it does, we'll need a larger data structure anyway I guess.

Copy link
Contributor

@bpintea bpintea Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are always positive

To make sure: 0xe0000000L is a positive long. But (int) (0xe0000000L & 0xffffffffL) is a negative int.

I guess one of the implicit assumptions is that a block hash will never have more than 2B positions.

Yeh, this is why I was thinking that this might still be safe. 👍 (At least if these are positions.)

Copy link
Contributor

@alex-spies alex-spies Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @nik9000 should probably comment here as well - and whatever we end up with should probably become a Java comment here, that's too many assumptions for my head to infer, at least when skimming the file :D

Copy link
Contributor

@alex-spies alex-spies left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, only a minor remark from me. Thanks @luigidellaquila !

It'd be good if @nik9000 could also give this a look as he's way more familiar with the block hashes than me.

@@ -1232,6 +1235,190 @@ public void testLongNull() {
}, blockFactory.newLongArrayVector(values, values.length).asBlock(), blockFactory.newConstantNullBlock(values.length));
}

public void test2BytesRefsKeys() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe a more descriptive name? Like test2BytesRefsHighCardinalityKey or so?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 on HighCardinality.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is java. Lean into the memes about long method names....

}
List<Output> output = new ArrayList<>();

try (BlockHash hash1 = new BytesRef2BlockHash(blockFactory, 0, 1, totalPositions);) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we randomize which key has high cardinality? It looks like this test only stuffs the high cardinality key into the first position, which repros the bug, but in principle the other position might have the same problem (if we implemented things differently).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -145,7 +145,7 @@ public Block[] getKeys() {
try {
try (BytesRefBlock.Builder b1 = blockFactory.newBytesRefBlockBuilder(positions)) {
for (int i = 0; i < positions; i++) {
int k1 = (int) (finalHash.get(i) & 0xffffL);
int k1 = (int) (finalHash.get(i) & 0xffffffffL);
if (k1 == 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can't be 0 because we grab the low bits. Right? I haven't had enough coffee for bit math.

@@ -1232,6 +1235,190 @@ public void testLongNull() {
}, blockFactory.newLongArrayVector(values, values.length).asBlock(), blockFactory.newConstantNullBlock(values.length));
}

public void test2BytesRefsKeys() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 on HighCardinality.

}
List<Output> output = new ArrayList<>();

try (BlockHash hash1 = new BytesRef2BlockHash(blockFactory, 0, 1, totalPositions);) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -1232,6 +1235,190 @@ public void testLongNull() {
}, blockFactory.newLongArrayVector(values, values.length).asBlock(), blockFactory.newConstantNullBlock(values.length));
}

public void test2BytesRefsKeys() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is java. Lean into the memes about long method names....

}
}
page = new Page(builder1.build(), builder2.build());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want this all in one page? I know the BlockHash needs it in memory. And we build that HashMap with it in memory below. But maybe we make Pages of a reasonable size here?

Meh. I guess it's only like 9mb. Probably not worth changing it unless you need to go bigger.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's small enough, and it reproduced the problem pretty consistently, so I guess it's not worth the effort and complication.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
:Analytics/ES|QL AKA ESQL auto-backport Automatically create backport pull requests when merged >bug Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) v8.16.7 v8.17.9 v8.18.4 v8.19.0 v9.0.4 v9.1.0 v9.2.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ESQL: Wrong STATS result in case of many distinct groups of 2 fields
6 participants