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

ESQL Support loading points from source into WKB blocks #103698

Merged
merged 50 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
261bf04
ESQL reading points from source
craigtaverner Dec 1, 2023
eb47224
Initial support for BytesRefBlock to support WKB for Geometries
craigtaverner Dec 22, 2023
081468e
Update docs/changelog/103698.yaml
craigtaverner Dec 22, 2023
17ae512
Fixed changelog files
craigtaverner Dec 22, 2023
4cbac42
Remove support for PointBlock
craigtaverner Dec 28, 2023
3b0c89d
Merge remote-tracking branch 'origin/main' into esql_points_from_sour…
craigtaverner Dec 28, 2023
895cad1
One change from code review
craigtaverner Dec 28, 2023
60985fe
Removed some leftovers from the PointBlock removal
craigtaverner Dec 29, 2023
35c5df0
Some code-review and TODO checks
craigtaverner Dec 29, 2023
eeeffda
Some code-review and TODO checks, and deal with Plan serialization
craigtaverner Dec 29, 2023
0487203
Fixed failing tests with point WKT rendering
craigtaverner Dec 29, 2023
cfc4341
Revert stab at implementing forStats for doc-values vs source
craigtaverner Dec 29, 2023
1e6e744
Disabled failing test
craigtaverner Dec 29, 2023
8c5638e
Use max precision when serializing points to XContent
craigtaverner Jan 2, 2024
b96933b
Removed unused class from PointBlock code
craigtaverner Jan 2, 2024
db2094b
Merge remote-tracking branch 'origin/main' into esql_points_from_sour…
craigtaverner Jan 2, 2024
22f631f
Simplifications from code review
craigtaverner Jan 2, 2024
1b41302
Remove intermediate WKT from GeoPointFieldMapper
craigtaverner Jan 3, 2024
4d9eed2
Refactor WKBTopNEncoder to use length prefix encoding
craigtaverner Jan 3, 2024
e0c869d
Fix failing test
craigtaverner Jan 3, 2024
1ead4ce
Remove WKBTopNEncoder since DefaultUnsortableTopNEncoder can do the job
craigtaverner Jan 3, 2024
d699483
Cleanup from code review
craigtaverner Jan 3, 2024
bf1ab97
Remove concrete SpatialPoint class
craigtaverner Jan 4, 2024
c62e5aa
Bring back SpatialPointTests
craigtaverner Jan 4, 2024
935bfd6
Reduce object creation in some tests
craigtaverner Jan 4, 2024
5bcbaca
Removed unused code
craigtaverner Jan 4, 2024
3e89588
Removed unused code from earlier versions
craigtaverner Jan 4, 2024
07e162a
Tests were testing cases that no longer apply
craigtaverner Jan 4, 2024
cb436f5
Tests were testing cases that no longer apply
craigtaverner Jan 4, 2024
378614f
We need to consider WKB in PlanNamedTypes mixed-cluster
craigtaverner Jan 4, 2024
a72d0c2
Removed SpatialPoint from PlanNamedTypes
craigtaverner Jan 4, 2024
572e60f
Removed one more artifact of using SpatialPoint in tests
craigtaverner Jan 4, 2024
f1a3600
Some work towards removing creating SpatialPoints
craigtaverner Jan 4, 2024
0f1a58b
Merge remote-tracking branch 'origin/main' into esql_points_from_sour…
craigtaverner Jan 5, 2024
d0f6a12
Updated GeoPointFieldMapperTests after merge from main
craigtaverner Jan 5, 2024
8dffba7
Get row-stride-reader test working
craigtaverner Jan 5, 2024
d1d4ce6
Geo flaky tests to work more reliably
craigtaverner Jan 5, 2024
084d08e
Removed defensive coding in plan serialization
craigtaverner Jan 5, 2024
c176db1
Simplify and ensure error is thrown on wrong type
craigtaverner Jan 5, 2024
4509eab
Do version checks on reading plan from PlanStreamInput
craigtaverner Jan 5, 2024
97f5589
Merge remote-tracking branch 'origin/main' into esql_points_from_sour…
craigtaverner Jan 5, 2024
d18e3a9
Mute failing test
craigtaverner Jan 5, 2024
a830c79
Use specific TransportVersions for point literal in query plans
craigtaverner Jan 5, 2024
a11ebe0
Fixed failing test after merge with main
craigtaverner Jan 5, 2024
b69889e
Fixed failing test after merge with main, with nullValue from source
craigtaverner Jan 8, 2024
1927ec6
Merge remote-tracking branch 'origin/main' into esql_points_from_sour…
craigtaverner Jan 8, 2024
bb74171
Fixed compile error from previous fix with nullValues
craigtaverner Jan 8, 2024
6500d83
It seems mixed cluster yaml tests don't work
craigtaverner Jan 8, 2024
82d95b6
Merge branch 'main' into esql_points_from_source_wkb
elasticmachine Jan 8, 2024
3fd2b18
Merge remote-tracking branch 'origin/main' into esql_points_from_sour…
craigtaverner Jan 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/103698.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 103698
summary: Reading points from source to reduce precision loss
area: ES|QL
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ static TransportVersion def(int id) {
public static final TransportVersion SMALLER_RELOAD_SECURE_SETTINGS_REQUEST = def(8_567_00_0);
public static final TransportVersion UPDATE_API_KEY_EXPIRATION_TIME_ADDED = def(8_568_00_0);
public static final TransportVersion LAZY_ROLLOVER_ADDED = def(8_569_00_0);
public static final TransportVersion ESQL_PLAN_POINT_LITERAL_WKB = def(8_570_00_0);

/*
* STOP! READ THIS FIRST! No, really,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,30 @@
* To facilitate maximizing the use of common code between GeoPoint and projected CRS
* we introduced this ElasticPoint as an interface of commonality.
*/
public interface SpatialPoint {
public interface SpatialPoint extends Comparable<SpatialPoint> {
double getX();

double getY();

default String toWKT() {
// Code designed to mimic WellKnownText.toWKT, with much less stack depth and object creation
return "POINT (" + getX() + " " + getY() + ")";
}

@Override
default int compareTo(SpatialPoint other) {
if (this.getClass().equals(other.getClass())) {
double xd = this.getX() - other.getX();
double yd = this.getY() - other.getY();
return (xd == 0) ? comparison(yd) : comparison(xd);
} else {
// TODO: Rather separate based on CRS, but since we don't have that yet, we use class name
// The sort order here is unimportant and does not (yet) introduce BWC issues, so we are free to change it later with CRS
return this.getClass().getSimpleName().compareTo(other.getClass().getSimpleName());
}
}

private int comparison(double delta) {
return delta == 0 ? 0 : delta < 0 ? -1 : 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,19 @@ private static XContentParser wrapObject(Object sourceMap) throws IOException {
public abstract static class AbstractGeometryFieldType<T> extends MappedFieldType {

protected final Parser<T> geometryParser;
protected final T nullValue;

protected AbstractGeometryFieldType(
String name,
boolean indexed,
boolean stored,
boolean hasDocValues,
Parser<T> geometryParser,
T nullValue,
Map<String, String> meta
) {
super(name, indexed, stored, hasDocValues, TextSearchInfo.NONE, meta);
this.nullValue = nullValue;
this.geometryParser = geometryParser;
}

Expand Down Expand Up @@ -127,7 +130,7 @@ protected Object parseSourceValue(Object value) {

public ValueFetcher valueFetcher(Set<String> sourcePaths, Object nullValue, String format) {
Function<List<T>, List<Object>> formatter = getFormatter(format != null ? format : GeometryFormatterFactory.GEOJSON);
return new ArraySourceValueFetcher(sourcePaths, nullValue) {
return new ArraySourceValueFetcher(sourcePaths, nullValueAsSource(nullValue)) {
@Override
protected Object parseSourceValue(Object value) {
final List<T> values = new ArrayList<>();
Expand All @@ -136,6 +139,8 @@ protected Object parseSourceValue(Object value) {
}
};
}

protected abstract Object nullValueAsSource(Object nullValue);
}

private final Explicit<Boolean> ignoreMalformed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.Explicit;
import org.elasticsearch.common.TriFunction;
import org.elasticsearch.common.geo.GeometryFormatterFactory;
import org.elasticsearch.common.geo.SpatialPoint;
import org.elasticsearch.core.CheckedConsumer;
import org.elasticsearch.core.CheckedFunction;
import org.elasticsearch.xcontent.XContentParser;

import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
Expand Down Expand Up @@ -155,4 +158,36 @@ private void parseAndConsumeFromObject(
}
}
}

public abstract static class AbstractPointFieldType<T extends SpatialPoint> extends AbstractGeometryFieldType<T> {

protected AbstractPointFieldType(
String name,
boolean indexed,
boolean stored,
boolean hasDocValues,
Parser<T> geometryParser,
T nullValue,
Map<String, String> meta
) {
super(name, indexed, stored, hasDocValues, geometryParser, nullValue, meta);
}

@Override
protected Object nullValueAsSource(Object nullValue) {
if (nullValue == null) {
return null;
}
SpatialPoint point = (SpatialPoint) nullValue;
return point.toWKT();
}

@Override
public BlockLoader blockLoader(BlockLoaderContext blContext) {
// Currently we can only load from source in ESQL
ValueFetcher fetcher = valueFetcher(blContext.sourcePaths(name()), nullValue, GeometryFormatterFactory.WKB);
// TODO consider optimization using BlockSourceReader.lookupFromFieldNames(blContext.fieldNames(), name())
return new BlockSourceReader.GeometriesBlockLoader(fetcher, BlockSourceReader.lookupMatchingAll());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,25 @@ protected AbstractShapeGeometryFieldType(
Orientation orientation,
Map<String, String> meta
) {
super(name, isSearchable, isStored, hasDocValues, parser, meta);
super(name, isSearchable, isStored, hasDocValues, parser, null, meta);
this.orientation = orientation;
}

public Orientation orientation() {
return this.orientation;
}

@Override
public BlockLoader blockLoader(BlockLoaderContext blContext) {
// TODO: Support shapes in ESQL
return null;
}

@Override
protected Object nullValueAsSource(Object nullValue) {
// TODO: When we support shapes in ESQL; we need to return a shape in source format here
return nullValue;
}
}

protected Explicit<Boolean> coerce;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,27 @@ protected String name() {
}
}

public static class GeometriesBlockLoader extends SourceBlockLoader {
Copy link
Contributor

Choose a reason for hiding this comment

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

It makes me feel uneasy, cannot we reuse the BytesRef reader as we are reading an array of bytes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now we need this to get to the code in the 'Geometries' BlockSourceReader, but as that can get simplified, perhaps this can too.

public GeometriesBlockLoader(ValueFetcher fetcher, LeafIteratorLookup lookup) {
super(fetcher, lookup);
}

@Override
public final Builder builder(BlockFactory factory, int expectedCount) {
return factory.bytesRefs(expectedCount);
}

@Override
protected RowStrideReader rowStrideReader(LeafReaderContext context, DocIdSetIterator iter) {
return new Geometries(fetcher, iter);
}

@Override
protected String name() {
return "Geometries";
}
}

private static class BytesRefs extends BlockSourceReader {
private final BytesRef scratch = new BytesRef();

Expand All @@ -217,6 +238,27 @@ public String toString() {
}
}

private static class Geometries extends BlockSourceReader {

Geometries(ValueFetcher fetcher, DocIdSetIterator iter) {
super(fetcher, iter);
}

@Override
protected void append(BlockLoader.Builder builder, Object v) {
if (v instanceof byte[] wkb) {
((BlockLoader.BytesRefBuilder) builder).appendBytesRef(new BytesRef(wkb));
} else {
throw new IllegalArgumentException("Unsupported source type for spatial geometry: " + v.getClass().getSimpleName());
}
}

@Override
public String toString() {
return "BlockSourceReader.Geometries";
}
}

/**
* Load {@code double}s from {@code _source}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,14 +357,13 @@ protected String contentType() {
return CONTENT_TYPE;
}

public static class GeoPointFieldType extends AbstractGeometryFieldType<GeoPoint> implements GeoShapeQueryable {
public static class GeoPointFieldType extends AbstractPointFieldType<GeoPoint> implements GeoShapeQueryable {
private final TimeSeriesParams.MetricType metricType;

public static final GeoFormatterFactory<GeoPoint> GEO_FORMATTER_FACTORY = new GeoFormatterFactory<>(
List.of(new SimpleVectorTileFormatter())
);

private final GeoPoint nullValue;
private final FieldValues<GeoPoint> scriptValues;
private final IndexMode indexMode;

Expand All @@ -380,8 +379,7 @@ private GeoPointFieldType(
TimeSeriesParams.MetricType metricType,
IndexMode indexMode
) {
super(name, indexed, stored, hasDocValues, parser, meta);
this.nullValue = nullValue;
super(name, indexed, stored, hasDocValues, parser, nullValue, meta);
this.scriptValues = scriptValues;
this.metricType = metricType;
this.indexMode = indexMode;
Expand Down Expand Up @@ -482,19 +480,6 @@ public IndexFieldData.Builder fielddataBuilder(FieldDataContext fieldDataContext
throw new IllegalStateException("unknown field data type [" + operation.name() + "]");
}

@Override
public BlockLoader blockLoader(BlockLoaderContext blContext) {
if (hasDocValues()) {
return new BlockDocValuesReader.LongsBlockLoader(name());
}
// TODO: Currently we use longs in the compute engine and render to WKT in ESQL
ValueFetcher fetcher = valueFetcher(blContext.sourcePaths(name()), nullValue, GeometryFormatterFactory.WKT);
BlockSourceReader.LeafIteratorLookup lookup = isStored() || isIndexed()
? BlockSourceReader.lookupFromFieldNames(blContext.fieldNames(), name())
: BlockSourceReader.lookupMatchingAll();
return new BlockSourceReader.LongsBlockLoader(fetcher, lookup);
}

@Override
public Query distanceFeatureQuery(Object origin, String pivot, SearchExecutionContext context) {
failIfNotIndexedNorDocValuesFallback(context);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.common.geo;

import org.elasticsearch.test.ESTestCase;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.not;

public class SpatialPointTests extends ESTestCase {

public void testEqualsAndHashcode() {
iverase marked this conversation as resolved.
Show resolved Hide resolved
for (int i = 0; i < 100; i++) {
SpatialPoint point = randomGeoPoint();
GeoPoint geoPoint = new GeoPoint(point);
TestPoint testPoint = new TestPoint(point);
TestPoint testPoint2 = new TestPoint(point);
assertEqualsAndHashcode("Same point", point, point);
assertEqualsAndHashcode("Same geo-point", point, geoPoint);
assertNotEqualsAndHashcode("Same location, but different class", point, testPoint);
assertEqualsAndHashcode("Same location, same class", testPoint, testPoint2);
}
}

public void testCompareTo() {
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe we should add a test where two different subclasses compare to false?

for (int i = 0; i < 100; i++) {
SpatialPoint point = randomValueOtherThanMany(p -> p.getX() < -170 || p.getX() > 170, ESTestCase::randomGeoPoint);
GeoPoint smaller = new GeoPoint(point.getY(), point.getX() - 1);
GeoPoint bigger = new GeoPoint(point.getY(), point.getX() + 1);
TestPoint testSmaller = new TestPoint(smaller);
TestPoint testBigger = new TestPoint(bigger);
assertThat(smaller + " smaller than " + point, smaller.compareTo(point), lessThan(0));
assertThat(bigger + " bigger than " + point, bigger.compareTo(point), greaterThan(0));
assertThat(testSmaller + " smaller than " + testBigger, testSmaller.compareTo(testBigger), lessThan(0));
// TestPoint always greater than GeoPoint
assertThat(testSmaller + " bigger than " + point, testSmaller.compareTo(point), greaterThan(0));
assertThat(testBigger + " bigger than " + point, testBigger.compareTo(point), greaterThan(0));
}
}

private void assertEqualsAndHashcode(String message, SpatialPoint a, SpatialPoint b) {
assertThat("Equals: " + message, a, equalTo(b));
assertThat("Hashcode: " + message, a.hashCode(), equalTo(b.hashCode()));
assertThat("Compare: " + message, a.compareTo(b), equalTo(0));
}

private void assertNotEqualsAndHashcode(String message, SpatialPoint a, SpatialPoint b) {
assertThat("Equals: " + message, a, not(equalTo(b)));
assertThat("Hashcode: " + message, a.hashCode(), not(equalTo(b.hashCode())));
assertThat("Compare: " + message, a.compareTo(b), not(equalTo(0)));
}

/**
* This test class used to be trivial, when SpatialPoint was a concrete class.
* If we ever revert back to a concrete class, we can simplify this test class.
* The only requirement is that it extends SpatialPoint, but have a different class name.
*/
private static class TestPoint implements SpatialPoint {
double x;
double y;

private TestPoint(SpatialPoint template) {
this.x = template.getX();
this.y = template.getY();
}

@Override
public double getX() {
return x;
}

@Override
public double getY() {
return y;
}

@Override
public int hashCode() {
return 31 * 31 * getClass().getSimpleName().hashCode() + 31 * Double.hashCode(x) + Double.hashCode(y);
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
SpatialPoint point = (SpatialPoint) obj;
return (Double.compare(point.getX(), x) == 0) && Double.compare(point.getY(), y) == 0;
}

@Override
public String toString() {
return toWKT();
}
}
}
Loading