-
Notifications
You must be signed in to change notification settings - Fork 25.6k
Optimize ST_EXTENT_AGG for geo_shape and cartesian_shape #119889
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 14 commits
ec4f4e7
daedc53
7fdef3e
5c79f04
f7f9403
898d1ba
2ade43c
a32e45c
26d3708
ad3f846
6a7e517
49e912a
2716c03
f877c9e
a503549
5284203
9c45f72
acd58ea
44d0392
7c3957c
a05c435
2295cd9
8adf8db
7b63b8a
b517801
eaa5b21
229aacb
7379c8f
9fb92b1
86f350f
e13bf12
736285d
3332224
394cc19
a66c67b
f0b7ab1
1100bf3
77d639b
ffb590f
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 |
---|---|---|
@@ -0,0 +1,5 @@ | ||
pr: 119889 | ||
summary: Optimize ST_EXTENT_AGG for `geo_shape` and `cartesian_shape` | ||
area: "ES|QL" | ||
type: enhancement | ||
issues: [] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -116,6 +116,8 @@ public interface PointVisitor { | |
boolean isValid(); | ||
|
||
Rectangle getResult(); | ||
|
||
void reset(); | ||
} | ||
|
||
/** | ||
|
@@ -124,18 +126,14 @@ public interface PointVisitor { | |
*/ | ||
public static class CartesianPointVisitor implements PointVisitor { | ||
private double minX = Double.POSITIVE_INFINITY; | ||
private double minY = Double.POSITIVE_INFINITY; | ||
private double maxX = Double.NEGATIVE_INFINITY; | ||
private double maxY = Double.NEGATIVE_INFINITY; | ||
private double minY = Double.POSITIVE_INFINITY; | ||
|
||
public double getMinX() { | ||
return minX; | ||
} | ||
|
||
public double getMinY() { | ||
return minY; | ||
} | ||
|
||
public double getMaxX() { | ||
return maxX; | ||
} | ||
|
@@ -144,12 +142,16 @@ public double getMaxY() { | |
return maxY; | ||
} | ||
|
||
public double getMinY() { | ||
return minY; | ||
} | ||
|
||
@Override | ||
public void visitPoint(double x, double y) { | ||
minX = Math.min(minX, x); | ||
minY = Math.min(minY, y); | ||
maxX = Math.max(maxX, x); | ||
maxY = Math.max(maxY, y); | ||
minY = Math.min(minY, y); | ||
} | ||
|
||
@Override | ||
|
@@ -160,9 +162,9 @@ public void visitRectangle(double minX, double maxX, double maxY, double minY) { | |
); | ||
} | ||
this.minX = Math.min(this.minX, minX); | ||
this.minY = Math.min(this.minY, minY); | ||
this.maxX = Math.max(this.maxX, maxX); | ||
this.maxY = Math.max(this.maxY, maxY); | ||
this.minY = Math.min(this.minY, minY); | ||
} | ||
|
||
@Override | ||
|
@@ -174,6 +176,14 @@ public boolean isValid() { | |
public Rectangle getResult() { | ||
return new Rectangle(minX, maxX, maxY, minY); | ||
} | ||
|
||
@Override | ||
public void reset() { | ||
minX = Double.POSITIVE_INFINITY; | ||
maxX = Double.NEGATIVE_INFINITY; | ||
maxY = Double.NEGATIVE_INFINITY; | ||
minY = Double.POSITIVE_INFINITY; | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -186,82 +196,118 @@ public Rectangle getResult() { | |
* </ul> | ||
*/ | ||
public static class GeoPointVisitor implements PointVisitor { | ||
protected double minY = Double.POSITIVE_INFINITY; | ||
protected double maxY = Double.NEGATIVE_INFINITY; | ||
protected double minNegX = Double.POSITIVE_INFINITY; | ||
protected double maxNegX = Double.NEGATIVE_INFINITY; | ||
protected double minPosX = Double.POSITIVE_INFINITY; | ||
protected double maxPosX = Double.NEGATIVE_INFINITY; | ||
protected double top = Double.NEGATIVE_INFINITY; | ||
protected double bottom = Double.POSITIVE_INFINITY; | ||
protected double negLeft = Double.POSITIVE_INFINITY; | ||
protected double negRight = Double.NEGATIVE_INFINITY; | ||
protected double posLeft = Double.POSITIVE_INFINITY; | ||
protected double posRight = Double.NEGATIVE_INFINITY; | ||
|
||
private final WrapLongitude wrapLongitude; | ||
|
||
public GeoPointVisitor(WrapLongitude wrapLongitude) { | ||
this.wrapLongitude = wrapLongitude; | ||
} | ||
|
||
public double getTop() { | ||
return top; | ||
} | ||
|
||
public double getBottom() { | ||
return bottom; | ||
} | ||
|
||
public double getNegLeft() { | ||
return negLeft; | ||
} | ||
|
||
public double getNegRight() { | ||
return negRight; | ||
} | ||
|
||
public double getPosLeft() { | ||
return posLeft; | ||
} | ||
|
||
public double getPosRight() { | ||
return posRight; | ||
} | ||
|
||
@Override | ||
public void visitPoint(double x, double y) { | ||
minY = Math.min(minY, y); | ||
maxY = Math.max(maxY, y); | ||
bottom = Math.min(bottom, y); | ||
top = Math.max(top, y); | ||
visitLongitude(x); | ||
} | ||
|
||
@Override | ||
public void visitRectangle(double minX, double maxX, double maxY, double minY) { | ||
this.minY = Math.min(this.minY, minY); | ||
this.maxY = Math.max(this.maxY, maxY); | ||
this.bottom = Math.min(this.bottom, minY); | ||
this.top = Math.max(this.top, maxY); | ||
visitLongitude(minX); | ||
visitLongitude(maxX); | ||
} | ||
|
||
private void visitLongitude(double x) { | ||
if (x >= 0) { | ||
minPosX = Math.min(minPosX, x); | ||
maxPosX = Math.max(maxPosX, x); | ||
posLeft = Math.min(posLeft, x); | ||
posRight = Math.max(posRight, x); | ||
} else { | ||
minNegX = Math.min(minNegX, x); | ||
maxNegX = Math.max(maxNegX, x); | ||
negLeft = Math.min(negLeft, x); | ||
negRight = Math.max(negRight, x); | ||
} | ||
} | ||
|
||
@Override | ||
public boolean isValid() { | ||
return minY != Double.POSITIVE_INFINITY; | ||
return bottom != Double.POSITIVE_INFINITY; | ||
} | ||
|
||
@Override | ||
public Rectangle getResult() { | ||
return getResult(minNegX, minPosX, maxNegX, maxPosX, maxY, minY, wrapLongitude); | ||
return getResult(top, bottom, negLeft, negRight, posLeft, posRight, wrapLongitude); | ||
} | ||
|
||
@Override | ||
public void reset() { | ||
bottom = Double.POSITIVE_INFINITY; | ||
top = Double.NEGATIVE_INFINITY; | ||
negLeft = Double.POSITIVE_INFINITY; | ||
negRight = Double.NEGATIVE_INFINITY; | ||
posLeft = Double.POSITIVE_INFINITY; | ||
posRight = Double.NEGATIVE_INFINITY; | ||
} | ||
|
||
protected static Rectangle getResult( | ||
double minNegX, | ||
double minPosX, | ||
double maxNegX, | ||
double maxPosX, | ||
double maxY, | ||
double minY, | ||
public static Rectangle getResult( | ||
double top, | ||
double bottom, | ||
double negLeft, | ||
double negRight, | ||
double posLeft, | ||
double posRight, | ||
WrapLongitude wrapLongitude | ||
) { | ||
assert Double.isFinite(maxY); | ||
if (Double.isInfinite(minPosX)) { | ||
return new Rectangle(minNegX, maxNegX, maxY, minY); | ||
} else if (Double.isInfinite(minNegX)) { | ||
return new Rectangle(minPosX, maxPosX, maxY, minY); | ||
assert Double.isFinite(top); | ||
// Due to this data coming through Extent (and aggs that use the same approach), which saves values as integers, | ||
|
||
// we must use posRight==-Inf for all-neg check, and negLeft==+Inf for all-pos check. | ||
if (Double.isInfinite(posRight)) { | ||
return new Rectangle(negLeft, negRight, top, bottom); | ||
} else if (Double.isInfinite(negLeft)) { | ||
return new Rectangle(posLeft, posRight, top, bottom); | ||
} else { | ||
return switch (wrapLongitude) { | ||
case NO_WRAP -> new Rectangle(minNegX, maxPosX, maxY, minY); | ||
case WRAP -> maybeWrap(minNegX, minPosX, maxNegX, maxPosX, maxY, minY); | ||
case NO_WRAP -> new Rectangle(negLeft, posRight, top, bottom); | ||
case WRAP -> maybeWrap(top, bottom, negLeft, negRight, posLeft, posRight); | ||
}; | ||
} | ||
} | ||
|
||
private static Rectangle maybeWrap(double minNegX, double minPosX, double maxNegX, double maxPosX, double maxY, double minY) { | ||
double unwrappedWidth = maxPosX - minNegX; | ||
double wrappedWidth = 360 + maxNegX - minPosX; | ||
private static Rectangle maybeWrap(double top, double bottom, double negLeft, double negRight, double posLeft, double posRight) { | ||
double unwrappedWidth = posRight - negLeft; | ||
double wrappedWidth = 360 + negRight - posLeft; | ||
return unwrappedWidth <= wrappedWidth | ||
? new Rectangle(minNegX, maxPosX, maxY, minY) | ||
: new Rectangle(minPosX, maxNegX, maxY, minY); | ||
? new Rectangle(negLeft, posRight, top, bottom) | ||
: new Rectangle(posLeft, negRight, top, bottom); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,16 +10,12 @@ | |
|
||
import org.apache.lucene.index.BinaryDocValues; | ||
import org.apache.lucene.index.LeafReaderContext; | ||
import org.apache.lucene.util.BytesRef; | ||
import org.elasticsearch.common.Explicit; | ||
import org.elasticsearch.common.geo.Orientation; | ||
import org.elasticsearch.geometry.Rectangle; | ||
import org.elasticsearch.geometry.utils.WellKnownBinary; | ||
import org.elasticsearch.lucene.spatial.CoordinateEncoder; | ||
import org.elasticsearch.lucene.spatial.GeometryDocValueReader; | ||
|
||
import java.io.IOException; | ||
import java.nio.ByteOrder; | ||
import java.util.Map; | ||
import java.util.function.Function; | ||
|
||
|
@@ -107,7 +103,7 @@ public BlockLoader.AllReader reader(LeafReaderContext context) throws IOExceptio | |
public BlockLoader.Block read(BlockLoader.BlockFactory factory, BlockLoader.Docs docs) throws IOException { | ||
var binaryDocValues = context.reader().getBinaryDocValues(fieldName); | ||
var reader = new GeometryDocValueReader(); | ||
try (var builder = factory.bytesRefs(docs.count())) { | ||
try (var builder = factory.ints(docs.count())) { | ||
for (int i = 0; i < docs.count(); i++) { | ||
read(binaryDocValues, docs.get(i), reader, builder); | ||
} | ||
|
@@ -119,27 +115,27 @@ public BlockLoader.Block read(BlockLoader.BlockFactory factory, BlockLoader.Docs | |
public void read(int docId, BlockLoader.StoredFields storedFields, BlockLoader.Builder builder) throws IOException { | ||
var binaryDocValues = context.reader().getBinaryDocValues(fieldName); | ||
var reader = new GeometryDocValueReader(); | ||
read(binaryDocValues, docId, reader, (BytesRefBuilder) builder); | ||
read(binaryDocValues, docId, reader, (IntBuilder) builder); | ||
} | ||
|
||
private void read(BinaryDocValues binaryDocValues, int doc, GeometryDocValueReader reader, BytesRefBuilder builder) | ||
private void read(BinaryDocValues binaryDocValues, int doc, GeometryDocValueReader reader, IntBuilder builder) | ||
throws IOException { | ||
if (binaryDocValues.advanceExact(doc) == false) { | ||
builder.appendNull(); | ||
return; | ||
} | ||
reader.reset(binaryDocValues.binaryValue()); | ||
var extent = reader.getExtent(); | ||
// This is rather silly: an extent is already encoded as ints, but we convert it to Rectangle to | ||
// preserve its properties as a WKB shape, only to convert it back to ints when we compute the | ||
// aggregation. An obvious optimization would be to avoid this back-and-forth conversion. | ||
var rectangle = new Rectangle( | ||
encoder.decodeX(extent.minX()), | ||
encoder.decodeX(extent.maxX()), | ||
encoder.decodeY(extent.maxY()), | ||
encoder.decodeY(extent.minY()) | ||
); | ||
builder.appendBytesRef(new BytesRef(WellKnownBinary.toWKB(rectangle, ByteOrder.LITTLE_ENDIAN))); | ||
// We store the 6 values as a single multi-valued field, in the same order as the fields in the Extent class | ||
// This requires that consumers also know the meaning of the values, which they can learn from the Extent class | ||
|
||
builder.beginPositionEntry(); | ||
builder.appendInt(extent.top); | ||
builder.appendInt(extent.bottom); | ||
builder.appendInt(extent.negLeft); | ||
builder.appendInt(extent.negRight); | ||
builder.appendInt(extent.posLeft); | ||
builder.appendInt(extent.posRight); | ||
builder.endPositionEntry(); | ||
} | ||
|
||
@Override | ||
|
@@ -151,7 +147,7 @@ public boolean canReuse(int startingDocID) { | |
|
||
@Override | ||
public BlockLoader.Builder builder(BlockLoader.BlockFactory factory, int expectedCount) { | ||
return factory.bytesRefs(expectedCount); | ||
return factory.ints(expectedCount); | ||
} | ||
} | ||
} | ||
|
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.
Consider adding a line of documentation on what this is supposed to do.