From 578fca2f998bd24d9042e870eb772a99594c916c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Finn=C3=A9?= Date: Mon, 17 Sep 2018 12:40:38 +0200 Subject: [PATCH] Points are indexed w/ coordinates in btree-1.0 index Allowing index to recreate actual PointValue instances and so will avoid round-tripping to the property store. --- .../impl/schema/GenericIndexValidationIT.java | 8 +- .../impl/index/schema/GenericKeyState.java | 19 +-- .../impl/index/schema/GenericLayout.java | 2 +- .../impl/index/schema/GeometryArrayType.java | 71 +++++++++-- .../impl/index/schema/GeometryType.java | 115 ++++++++++++++---- .../current-generic-key-state-format.zip | Bin 1730 -> 2145 bytes 6 files changed, 169 insertions(+), 46 deletions(-) diff --git a/community/community-it/index-it/src/test/java/org/neo4j/kernel/api/impl/schema/GenericIndexValidationIT.java b/community/community-it/index-it/src/test/java/org/neo4j/kernel/api/impl/schema/GenericIndexValidationIT.java index 03e99a58de53c..39eae57fdd1f8 100644 --- a/community/community-it/index-it/src/test/java/org/neo4j/kernel/api/impl/schema/GenericIndexValidationIT.java +++ b/community/community-it/index-it/src/test/java/org/neo4j/kernel/api/impl/schema/GenericIndexValidationIT.java @@ -360,10 +360,10 @@ private enum NamedDynamicValueGenerator localDateTimeArray( SIZE_LOCAL_DATE_TIME, 336, i -> random.randomValues().nextLocalDateTimeArrayRaw( i, i ) ), durationArray( SIZE_DURATION, 144, i -> random.randomValues().nextDurationArrayRaw( i, i ) ), periodArray( SIZE_DURATION, 144, i -> random.randomValues().nextPeriodArrayRaw( i, i ) ), - cartesianPointArray( SIZE_GEOMETRY, 504, i -> random.randomValues().nextCartesianPointArray( i, i ).asObjectCopy() ), - cartesian3DPointArray( SIZE_GEOMETRY, 504, i -> random.randomValues().nextCartesian3DPointArray( i, i ).asObjectCopy() ), - geographicPointArray( SIZE_GEOMETRY, 504, i -> random.randomValues().nextGeographicPointArray( i, i ).asObjectCopy() ), - geographic3DPointArray( SIZE_GEOMETRY, 504, i -> random.randomValues().nextGeographic3DPointArray( i, i ).asObjectCopy() ); + cartesianPointArray( SIZE_GEOMETRY, 168, i -> random.randomValues().nextCartesianPointArray( i, i ).asObjectCopy() ), + cartesian3DPointArray( SIZE_GEOMETRY, 126, i -> random.randomValues().nextCartesian3DPointArray( i, i ).asObjectCopy() ), + geographicPointArray( SIZE_GEOMETRY, 168, i -> random.randomValues().nextGeographicPointArray( i, i ).asObjectCopy() ), + geographic3DPointArray( SIZE_GEOMETRY, 126, i -> random.randomValues().nextGeographic3DPointArray( i, i ).asObjectCopy() ); private final int singleArrayEntrySize; private final DynamicValueGenerator generator; diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/GenericKeyState.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/GenericKeyState.java index 71cea62097c82..36111136d5fda 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/GenericKeyState.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/GenericKeyState.java @@ -52,6 +52,7 @@ public class GenericKeyState extends TemporalValueWriterAdapter GenericLayout( int numberOfSlots, IndexSpecificSpaceFillingCurveSettingsCache spatialSettings ) { - super( "NSIL", 0, 3 ); + super( "NSIL", 0, 4 ); this.numberOfSlots = numberOfSlots; this.spatialSettings = spatialSettings; } diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/GeometryArrayType.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/GeometryArrayType.java index e3a38d9775449..aff4ae14ce378 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/GeometryArrayType.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/GeometryArrayType.java @@ -21,18 +21,22 @@ import java.util.Arrays; +import org.neo4j.graphdb.spatial.Point; import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.values.storable.CoordinateReferenceSystem; import org.neo4j.values.storable.PointValue; import org.neo4j.values.storable.Value; import org.neo4j.values.storable.ValueGroup; import org.neo4j.values.storable.ValueWriter; +import org.neo4j.values.storable.Values; import static java.lang.String.format; -import static org.neo4j.kernel.impl.index.schema.GenericKeyState.toNonNegativeShortExact; +import static org.neo4j.collection.PrimitiveLongCollections.EMPTY_LONG_ARRAY; +import static org.neo4j.kernel.impl.index.schema.GeometryType.assertHasCoordinates; +import static org.neo4j.kernel.impl.index.schema.GeometryType.dimensions; import static org.neo4j.kernel.impl.index.schema.GeometryType.put; import static org.neo4j.kernel.impl.index.schema.GeometryType.putCrs; import static org.neo4j.kernel.impl.index.schema.GeometryType.readCrs; -import static org.neo4j.values.storable.Values.NO_VALUE; class GeometryArrayType extends AbstractArrayType { @@ -53,7 +57,8 @@ class GeometryArrayType extends AbstractArrayType @Override int valueSize( GenericKeyState state ) { - return GenericKeyState.SIZE_GEOMETRY_HEADER + arrayKeySize( state, GenericKeyState.SIZE_GEOMETRY ); + return GenericKeyState.SIZE_GEOMETRY_HEADER + + arrayKeySize( state, GenericKeyState.SIZE_GEOMETRY + dimensions( state ) * GenericKeyState.SIZE_GEOMETRY_COORDINATE ); } @Override @@ -63,27 +68,49 @@ void copyValue( GenericKeyState to, GenericKeyState from, int length ) System.arraycopy( from.long0Array, 0, to.long0Array, 0, length ); to.long1 = from.long1; to.long2 = from.long2; + to.long3 = from.long3; + int dimensions = dimensions( from ); + to.long1Array = ensureBigEnough( to.long1Array, dimensions * length ); + System.arraycopy( from.long1Array, 0, to.long1Array, 0, dimensions * length ); + to.spaceFillingCurve = from.spaceFillingCurve; } @Override void initializeArray( GenericKeyState key, int length, ValueWriter.ArrayType arrayType ) { key.long0Array = ensureBigEnough( key.long0Array, length ); - // plain long1 for tableId - // plain long2 for code + + // Since this method is called when serializing a PointValue into the key state, the CRS and number of dimensions + // are unknown at this point. Instead key.long1Array will be initialized lazily upon observing the first array item, + // because that's when we first will know that information. + if ( length == 0 && key.long1Array == null ) + { + // There's this special case where we're initializing an empty geometry array and so the long1Array + // won't be initialized at all. Therefore we're preemptively making sure it's at least not null. + key.long1Array = EMPTY_LONG_ARRAY; + } } @Override Value asValue( GenericKeyState state ) { - return NO_VALUE; + assertHasCoordinates( state.long3, state.long1Array ); + CoordinateReferenceSystem crs = CoordinateReferenceSystem.get( (int) state.long1, (int) state.long2 ); + Point[] points = new Point[state.arrayLength]; + int dimensions = dimensions( state ); + for ( int i = 0; i < points.length; i++ ) + { + points[i] = GeometryType.asValue( state, crs, dimensions * i ); + } + return Values.pointArray( points ); } @Override void putValue( PageCursor cursor, GenericKeyState state ) { - putCrs( cursor, state.long1, state.long2 ); - putArray( cursor, state, ( c, k, i ) -> put( c, state.long0Array[i] ) ); + putCrs( cursor, state.long1, state.long2, state.long3 ); + int dimensions = dimensions( state ); + putArray( cursor, state, ( c, k, i ) -> put( c, state.long0Array[i], state.long3, state.long1Array, i * dimensions ) ); } @Override @@ -96,18 +123,40 @@ boolean readValue( PageCursor cursor, int size, GenericKeyState into ) @Override String toString( GenericKeyState state ) { - return format( "Geometry[tableId:%d, code:%d, rawValues:%s]", + return format( "GeometryArray[tableId:%d, code:%d, rawValues:%s]", state.long1, state.long2, Arrays.toString( Arrays.copyOf( state.long0Array, state.arrayLength ) ) ); } private static boolean readGeometryArrayItem( PageCursor cursor, GenericKeyState into ) { - into.long0Array[into.currentArrayOffset++] = cursor.getLong(); + into.long0Array[into.currentArrayOffset] = cursor.getLong(); + int dimensions = dimensions( into ); + if ( into.currentArrayOffset == 0 ) + { + // Initialize the coordinates array lazily because we don't know the dimension count + // when initializeArray is called, only afterwards when the header have been read. + into.long1Array = ensureBigEnough( into.long1Array, dimensions * into.arrayLength ); + } + for ( int i = 0, offset = into.currentArrayOffset * dimensions; i < dimensions; i++ ) + { + into.long1Array[offset + i] = cursor.getLong(); + } + into.currentArrayOffset++; return true; } - void write( GenericKeyState state, int offset, long derivedSpaceFillingCurveValue ) + void write( GenericKeyState state, int offset, long derivedSpaceFillingCurveValue, double[] coordinates ) { state.long0Array[offset] = derivedSpaceFillingCurveValue; + if ( offset == 0 ) + { + int dimensions = coordinates.length; + state.long1Array = ensureBigEnough( state.long1Array, dimensions * state.arrayLength ); + state.long3 = dimensions; + } + for ( int i = 0, base = dimensions( state ) * offset; i < coordinates.length; i++ ) + { + state.long1Array[base + i] = Double.doubleToLongBits( coordinates[i] ); + } } } diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/GeometryType.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/GeometryType.java index 072ab0ae1b7ee..9602f6acaf18a 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/GeometryType.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/GeometryType.java @@ -20,12 +20,14 @@ package org.neo4j.kernel.impl.index.schema; import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.values.storable.CoordinateReferenceSystem; import org.neo4j.values.storable.PointValue; import org.neo4j.values.storable.Value; import org.neo4j.values.storable.ValueGroup; +import org.neo4j.values.storable.Values; +import static java.lang.Math.toIntExact; import static java.lang.String.format; -import static org.neo4j.values.storable.Values.NO_VALUE; class GeometryType extends Type { @@ -33,12 +35,18 @@ class GeometryType extends Type // long0 (rawValueBits) // long1 (coordinate reference system tableId) // long2 (coordinate reference system code) + // long3 (dimensions) + // long1Array (coordinates), use long1Array so that it doesn't clash mentally with long0Array in GeometryArrayType // code+table for points (geometry) is 3B in total - private static final int MASK_CODE = 0x3FFFFF; // 22b - private static final int SHIFT_TABLE = Integer.bitCount( MASK_CODE ); - private static final int MASK_TABLE_READ = 0xC00000; // 2b + private static final int MASK_CODE = 0b00000011_11111111_11111111; + private static final int MASK_DIMENSIONS_READ = 0b00011100_00000000_00000000; + // ^ this bit is reserved for future expansion of number of dimensions + private static final int MASK_TABLE_READ = 0b11000000_00000000_00000000; + private static final int SHIFT_DIMENSIONS = Integer.bitCount( MASK_CODE ); + private static final int SHIFT_TABLE = SHIFT_DIMENSIONS + 1/*the reserved dimension bit*/ + Integer.bitCount( MASK_DIMENSIONS_READ ); private static final int MASK_TABLE_PUT = MASK_TABLE_READ >>> SHIFT_TABLE; + private static final int MASK_DIMENSIONS_PUT = MASK_DIMENSIONS_READ >>> SHIFT_DIMENSIONS; GeometryType( byte typeId ) { @@ -48,7 +56,13 @@ class GeometryType extends Type @Override int valueSize( GenericKeyState state ) { - return GenericKeyState.SIZE_GEOMETRY_HEADER + GenericKeyState.SIZE_GEOMETRY; + int coordinatesSize = dimensions( state ) * GenericKeyState.SIZE_GEOMETRY_COORDINATE; + return GenericKeyState.SIZE_GEOMETRY_HEADER + GenericKeyState.SIZE_GEOMETRY + coordinatesSize; + } + + static int dimensions( GenericKeyState state ) + { + return toIntExact( state.long3 ); } @Override @@ -57,13 +71,29 @@ void copyValue( GenericKeyState to, GenericKeyState from ) to.long0 = from.long0; to.long1 = from.long1; to.long2 = from.long2; + to.long3 = from.long3; + int dimensions = dimensions( from ); + to.long1Array = ensureBigEnough( to.long1Array, dimensions ); + System.arraycopy( from.long1Array, 0, to.long1Array, 0, dimensions ); to.spaceFillingCurve = from.spaceFillingCurve; } @Override Value asValue( GenericKeyState state ) { - return NO_VALUE; + assertHasCoordinates( state.long3, state.long1Array ); + CoordinateReferenceSystem crs = CoordinateReferenceSystem.get( (int) state.long1, (int) state.long2 ); + return asValue( state, crs, 0 ); + } + + static PointValue asValue( GenericKeyState state, CoordinateReferenceSystem crs, int offset ) + { + double[] coordinates = new double[dimensions( state )]; + for ( int i = 0; i < coordinates.length; i++ ) + { + coordinates[i] = Double.longBitsToDouble( state.long1Array[offset + i] ); + } + return Values.pointValue( crs, coordinates ); } @Override @@ -77,8 +107,8 @@ int compareValue( GenericKeyState left, GenericKeyState right ) @Override void putValue( PageCursor cursor, GenericKeyState state ) { - putCrs( cursor, state.long1, state.long2 ); - put( cursor, state.long0 ); + putCrs( cursor, state.long1, state.long2, state.long3 ); + put( cursor, state.long0, state.long3, state.long1Array, 0 ); } @Override @@ -117,23 +147,49 @@ static int compare( return Long.compare( this_long0, that_long0 ); } - static void putCrs( PageCursor cursor, long long1, long long2 ) + static void putCrs( PageCursor cursor, long long1, long long2, long long3 ) + { + assertValueWithin( long1, MASK_TABLE_PUT, "tableId" ); + assertValueWithin( long2, MASK_CODE, "code" ); + assertValueWithin( long3, MASK_DIMENSIONS_PUT, "dimensions" ); + int header = (int) ((long1 << SHIFT_TABLE) | (long3 << SHIFT_DIMENSIONS) | long2); + put3BInt( cursor, header ); + } + + private static void assertValueWithin( long value, int maskAllowed, String name ) { - if ( (long1 & ~MASK_TABLE_PUT) != 0 ) + if ( (value & ~maskAllowed) != 0 ) { - throw new IllegalArgumentException( "Table id must be 0 < tableId <= " + MASK_TABLE_PUT + ", but was " + long1 ); + throw new IllegalArgumentException( "Expected 0 < " + name + " <= " + maskAllowed + ", but was " + value ); } - if ( (long2 & ~MASK_CODE) != 0 ) + } + + static void put( PageCursor cursor, long long0, long long3, long[] long1Array, int long1ArrayOffset ) + { + assertHasCoordinates( long3, long1Array ); + cursor.putLong( long0 ); + for ( int i = 0; i < long3; i++ ) { - throw new IllegalArgumentException( "Code must be 0 < code <= " + MASK_CODE + ", but was " + long1 ); + cursor.putLong( long1Array[long1ArrayOffset + i] ); } - int tableAndCode = (int) ((long1 << SHIFT_TABLE) | long2); - put3BInt( cursor, tableAndCode ); } - static void put( PageCursor cursor, long long0 ) + /** + * This check exists because of how range queries are performed, where one range gets broken down into multiple + * sub-ranges following a space filling curve. These sub-ranges doesn't have exact coordinates associated with them, + * only the derived 1D comparison value. The sub-range querying is only initialized into keys acting as from/to + * markers for a query and so should never be used for writing into the tree or generating values from, + * so practically it's not a problem, merely an inconvenience and slight inconsistency for this value type. + * + * @param long3 holds dimension count. + * @param long1Array holds the coordinates. + */ + static void assertHasCoordinates( long long3, long[] long1Array ) { - cursor.putLong( long0 ); + if ( long3 == 0 || long1Array == null ) + { + throw new IllegalStateException( "This geometry key doesn't have coordinates and can therefore neither be persisted nor generate point value." ); + } } private static void put3BInt( PageCursor cursor, int value ) @@ -144,15 +200,23 @@ private static void put3BInt( PageCursor cursor, int value ) static boolean readCrs( PageCursor cursor, GenericKeyState into ) { - int tableAndCode = read3BInt( cursor ); - into.long1 = (tableAndCode & MASK_TABLE_READ) >>> SHIFT_TABLE; - into.long2 = tableAndCode & MASK_CODE; + int header = read3BInt( cursor ); + into.long1 = (header & MASK_TABLE_READ) >>> SHIFT_TABLE; + into.long2 = header & MASK_CODE; + into.long3 = (header & MASK_DIMENSIONS_READ) >>> SHIFT_DIMENSIONS; return true; } static boolean read( PageCursor cursor, GenericKeyState into ) { into.long0 = cursor.getLong(); + // into.long3 have just been read by readCrs, before this method is called + int dimensions = dimensions( into ); + into.long1Array = ensureBigEnough( into.long1Array, dimensions ); + for ( int i = 0; i < dimensions; i++ ) + { + into.long1Array[i] = cursor.getLong(); + } return true; } @@ -160,12 +224,17 @@ private static int read3BInt( PageCursor cursor ) { int low = cursor.getShort() & 0xFFFF; int high = cursor.getByte() & 0xFF; - int i = high << Short.SIZE | low; - return i; + return high << Short.SIZE | low; } - void write( GenericKeyState state, long derivedSpaceFillingCurveValue ) + void write( GenericKeyState state, long derivedSpaceFillingCurveValue, double[] coordinate ) { state.long0 = derivedSpaceFillingCurveValue; + state.long1Array = ensureBigEnough( state.long1Array, coordinate.length ); + for ( int i = 0; i < coordinate.length; i++ ) + { + state.long1Array[i] = Double.doubleToLongBits( coordinate[i] ); + } + state.long3 = coordinate.length; } } diff --git a/community/kernel/src/test/resources/org/neo4j/kernel/impl/index/schema/current-generic-key-state-format.zip b/community/kernel/src/test/resources/org/neo4j/kernel/impl/index/schema/current-generic-key-state-format.zip index c00331fa5c66e9f75767988f24903e7d438de5bb..104a87f6a9dfcf7bf6f4ae0d7c3974ea6a65b7b6 100644 GIT binary patch delta 2080 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 EKhmHHivR!s