Skip to content

Commit 785554d

Browse files
committed
Use unique labels for indexing within multiple OSM layers
Essentially when adding nodes of a particular label, we also add a label with a unique name for indexing purposes. The unique name is made of the original name plus a hex suffix made of the MD5 hash of the layer name. For example, the layer `geom1` will have an MD5 hash of its name `9ECE5459EA0D46FC556E5E3F454A0795`. Then when adding an OSM node we label the node with both: * OSMNode * OSMNode_9ECE5459EA0D46FC556E5E3F454A0795 The second label is also used to create a label-property index to be used for looking up the node when building ways.
1 parent 5b96b36 commit 785554d

File tree

5 files changed

+92
-29
lines changed

5 files changed

+92
-29
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ This has meant that the spatial library needed a major refactoring to work with
7474
and simply add on the rights to create tokens and indexes. In 0.27.2 we instead use `RestrictedAccessMode`
7575
to restrict the users access right to the built in `AccessModel.Static.SCHEMA` and then boost to enable
7676
index and token writes. The difference is subtle and should only be possible to notice in Enterprise Edition.
77+
* 0.28.0 tackles the ability to import multiple OSM files. The initial solution for Neo4j 4.x made use
78+
of schema indexes keyed by the label and property. However, that means that all OSM imports would share
79+
the same index. If they are completely disjointed data sets, this would not matter. But if you import
80+
overlapping OSM files or different versions of the same file file, a mangled partial merger would result.
81+
0.28.0 solves this by using different indexes, and keeping all imports completely separate.
82+
The more complex problems of importing newer versions, and stitching together overlapping areas, are not
83+
yet solved.
7784

7885
Consequences of the port to Neo4j 4.x:
7986

@@ -347,6 +354,7 @@ The Neo4j Spatial Plugin is available for inclusion in the server version of Neo
347354
* [v0.27.0 for Neo4j 4.0.3](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.27.0-neo4j-4.0.3/neo4j-spatial-0.27.0-neo4j-4.0.3-server-plugin.jar?raw=true)
348355
* [v0.27.1 for Neo4j 4.1.7](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.27.1-neo4j-4.1.7/neo4j-spatial-0.27.1-neo4j-4.1.7-server-plugin.jar?raw=true)
349356
* [v0.27.2 for Neo4j 4.2.3](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.27.2-neo4j-4.2.3/neo4j-spatial-0.27.2-neo4j-4.2.3-server-plugin.jar?raw=true)
357+
* [v0.28.0 for Neo4j 4.2.3](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.28.0-neo4j-4.2.3/neo4j-spatial-0.28.0-neo4j-4.2.3-server-plugin.jar?raw=true)
350358

351359
For versions up to 0.15-neo4j-2.3.4:
352360

@@ -463,7 +471,7 @@ Add the following repositories and dependency to your project's pom.xml:
463471
<dependency>
464472
<groupId>org.neo4j</groupId>
465473
<artifactId>neo4j-spatial</artifactId>
466-
<version>0.27.2-neo4j-4.2.3</version>
474+
<version>0.28.0-neo4j-4.2.3</version>
467475
</dependency>
468476
~~~
469477

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<modelVersion>4.0.0</modelVersion>
2424
<artifactId>neo4j-spatial</artifactId>
2525
<groupId>org.neo4j</groupId>
26-
<version>0.27.2-neo4j-4.2.3</version>
26+
<version>0.28.0-neo4j-4.2.3</version>
2727
<name>Neo4j - Spatial Components</name>
2828
<description>Spatial utilities and components for Neo4j</description>
2929
<url>http://components.neo4j.org/${project.artifactId}/${project.version}</url>

src/main/java/org/neo4j/gis/spatial/osm/OSMImporter.java

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import org.neo4j.gis.spatial.rtree.Envelope;
2929
import org.neo4j.gis.spatial.rtree.Listener;
3030
import org.neo4j.gis.spatial.rtree.NullListener;
31-
import org.neo4j.gis.spatial.utilities.ReferenceNodes;
3231
import org.neo4j.graphdb.*;
3332
import org.neo4j.graphdb.schema.IndexDefinition;
3433
import org.neo4j.graphdb.traversal.Evaluators;
@@ -41,10 +40,13 @@
4140
import org.neo4j.kernel.impl.traversal.MonoDirectionalTraversalDescription;
4241
import org.neo4j.kernel.internal.GraphDatabaseAPI;
4342

43+
import javax.xml.bind.DatatypeConverter;
4444
import javax.xml.stream.XMLStreamException;
4545
import javax.xml.stream.XMLStreamReader;
4646
import java.io.*;
4747
import java.nio.charset.Charset;
48+
import java.security.MessageDigest;
49+
import java.security.NoSuchAlgorithmException;
4850
import java.text.DateFormat;
4951
import java.text.ParseException;
5052
import java.text.SimpleDateFormat;
@@ -392,7 +394,7 @@ private OSMWriter(StatsManager statsManager, OSMImporter osmImporter) {
392394
this.osmImporter = osmImporter;
393395
}
394396

395-
static OSMWriter<WrappedNode> fromGraphDatabase(GraphDatabaseService graphDb, SecurityContext securityContext, StatsManager stats, OSMImporter osmImporter, int txInterval) {
397+
static OSMWriter<WrappedNode> fromGraphDatabase(GraphDatabaseService graphDb, SecurityContext securityContext, StatsManager stats, OSMImporter osmImporter, int txInterval) throws NoSuchAlgorithmException {
396398
return new OSMGraphWriter(graphDb, securityContext, stats, osmImporter, txInterval);
397399
}
398400

@@ -867,18 +869,29 @@ private static class OSMGraphWriter extends OSMWriter<WrappedNode> {
867869
private IndexDefinition relationIndex;
868870
private IndexDefinition changesetIndex;
869871
private IndexDefinition userIndex;
872+
private final String layerHash;
873+
private final HashMap<Label, Label> hashedLabels = new HashMap<>();
870874

871-
private OSMGraphWriter(GraphDatabaseService graphDb, SecurityContext securityContext, StatsManager statsManager, OSMImporter osmImporter, int txInterval) {
875+
private OSMGraphWriter(GraphDatabaseService graphDb, SecurityContext securityContext, StatsManager statsManager, OSMImporter osmImporter, int txInterval) throws NoSuchAlgorithmException {
872876
super(statsManager, osmImporter);
873877
this.graphDb = graphDb;
874878
this.securityContext = securityContext;
875879
this.txInterval = txInterval;
876880
if (this.txInterval < 100) {
877881
System.err.println("Warning: Unusually short txInterval, expect bad insert performance");
878882
}
883+
this.layerHash = md5Hash(osmImporter.layerName);
879884
checkTx(null); // Opens transaction for future writes
880885
}
881886

887+
private static String md5Hash(String text) throws NoSuchAlgorithmException {
888+
MessageDigest md = MessageDigest.getInstance("MD5");
889+
md.update(text.getBytes());
890+
byte[] digest = md.digest();
891+
String hashed = DatatypeConverter.printHexBinary(digest).toUpperCase();
892+
return hashed;
893+
}
894+
882895
private void successTx() {
883896
if (tx != null) {
884897
tx.commit();
@@ -930,14 +943,19 @@ private void recoverNode(WrappedNode outOfTx) {
930943
}
931944
}
932945

933-
private WrappedNode findNode(Label label, String name) {
934-
Node node = tx.findNode(label, "name", name);
946+
private WrappedNode findNodeByName(Label label, String name) {
947+
Node node = findNodeByLabelProperty(tx, label, "name", name);
935948
if (node != null) {
936949
return WrappedNode.fromNode(node);
937950
}
938951
return null;
939952
}
940953

954+
private WrappedNode createNodeWithLabel(Transaction tx, Label label) {
955+
Label hashed = getLabelHashed(label);
956+
return WrappedNode.fromNode(tx.createNode(label, hashed));
957+
}
958+
941959
@Override
942960
protected void startWays() {
943961
System.out.println("About to create node index");
@@ -968,12 +986,29 @@ protected void optimize() {
968986
}
969987
}
970988

989+
private Label getLabelHashed(Label label) {
990+
if (hashedLabels.containsKey(label)) {
991+
return hashedLabels.get(label);
992+
} else {
993+
Label hashed = Label.label(label.name() + "_" + layerHash);
994+
hashedLabels.put(label, hashed);
995+
return hashed;
996+
}
997+
}
998+
999+
private Node findNodeByLabelProperty(Transaction tx, Label label, String propertyKey, Object value) {
1000+
Label hashed = getLabelHashed(label);
1001+
return tx.findNode(hashed, propertyKey, value);
1002+
}
1003+
9711004
private IndexDefinition createIndex(Label label, String propertyKey) {
972-
IndexDefinition index = findIndex(tx, label, propertyKey);
1005+
Label hashed = getLabelHashed(label);
1006+
String indexName = String.format("OSM-%s-%s-%s", osmImporter.layerName, hashed.name(), propertyKey);
1007+
IndexDefinition index = findIndex(tx, indexName, hashed, propertyKey);
9731008
if (index == null) {
9741009
successTx();
9751010
try (Transaction indexTx = beginIndexTx(graphDb)) {
976-
index = indexTx.schema().indexFor(label).on(propertyKey).create();
1011+
index = indexTx.schema().indexFor(hashed).on(propertyKey).withName(indexName).create();
9771012
indexTx.commit();
9781013
}
9791014
System.out.println("Created index " + index.getName());
@@ -990,24 +1025,28 @@ private IndexDefinition createIndexIfNotNull(IndexDefinition index, Label label,
9901025
return index;
9911026
}
9921027

993-
private IndexDefinition findIndex(Transaction tx, Label label, String propertyKey) {
1028+
private IndexDefinition findIndex(Transaction tx, String indexName, Label label, String propertyKey) {
9941029
for (IndexDefinition index : tx.schema().getIndexes(label)) {
9951030
for (String prop : index.getPropertyKeys()) {
9961031
if (prop.equals(propertyKey)) {
997-
return index;
1032+
if (index.getName().equals(indexName)) {
1033+
return index;
1034+
} else {
1035+
throw new IllegalStateException(String.format("Found pre-existing index '%s' for index '%s'", index.getName(), indexName));
1036+
}
9981037
}
9991038
}
10001039
}
10011040
return null;
10021041
}
10031042

10041043
private WrappedNode getOrCreateNode(Label label, String name, String type) {
1005-
WrappedNode node = findNode(label, name);
1044+
WrappedNode node = findNodeByName(label, name);
10061045
if (node == null) {
1007-
Node n = tx.createNode(label);
1046+
WrappedNode n = createNodeWithLabel(tx, label);
10081047
n.setProperty("name", name);
10091048
n.setProperty("type", type);
1010-
node = checkTx(WrappedNode.fromNode(n));
1049+
node = checkTx(n);
10111050
}
10121051
return node;
10131052
}
@@ -1038,9 +1077,9 @@ protected void addNodeTags(WrappedNode node, LinkedHashMap<String, Object> tags,
10381077
logNodeAddition(tags, type);
10391078
if (node != null && tags.size() > 0) {
10401079
statsManager.addToTagStats(type, tags.keySet());
1041-
Node tagsNode = tx.createNode(LABEL_TAGS);
1042-
addProperties(tagsNode, tags);
1043-
node.createRelationshipTo(new WrappedNode(tagsNode), OSMRelation.TAGS);
1080+
WrappedNode tagsNode = createNodeWithLabel(tx, LABEL_TAGS);
1081+
addProperties(tagsNode.inner, tags);
1082+
node.createRelationshipTo(tagsNode, OSMRelation.TAGS);
10441083
tags.clear();
10451084
}
10461085
}
@@ -1060,12 +1099,12 @@ protected void addNodeGeometry(WrappedNode node, int gtype, Envelope bbox, int v
10601099

10611100
@Override
10621101
protected WrappedNode addNode(Label label, Map<String, Object> properties, String indexKey) {
1063-
Node node = tx.createNode(label);
1102+
WrappedNode node = createNodeWithLabel(tx, label);
10641103
if (indexKey != null && properties.containsKey(indexKey)) {
10651104
properties.put(indexKey, Long.parseLong(properties.get(indexKey).toString()));
10661105
}
1067-
addProperties(node, properties);
1068-
return checkTx(WrappedNode.fromNode(node));
1106+
addProperties(node.inner, properties);
1107+
return checkTx(node);
10691108
}
10701109

10711110
@Override
@@ -1085,7 +1124,7 @@ protected long getDatasetId() {
10851124

10861125
@Override
10871126
protected WrappedNode getSingleNode(Label label, String property, Object value) {
1088-
Node node = tx.findNode(LABEL_NODE, property, value);
1127+
Node node = findNodeByLabelProperty(tx, LABEL_NODE, property, value);
10891128
return node == null ? null : WrappedNode.fromNode(node);
10901129
}
10911130

@@ -1116,7 +1155,7 @@ protected WrappedNode getOSMNode(long osmId, WrappedNode changesetNode) {
11161155
WrappedNode node = changesetNodes.get(osmId);
11171156
if (node == null) {
11181157
logNodeFoundFrom("node-index");
1119-
node = WrappedNode.fromNode(tx.findNode(LABEL_NODE, PROP_NODE_ID, osmId));
1158+
node = WrappedNode.fromNode(findNodeByLabelProperty(tx, LABEL_NODE, PROP_NODE_ID, osmId));
11201159
} else {
11211160
logNodeFoundFrom(PROP_CHANGESET);
11221161
}
@@ -1157,7 +1196,7 @@ protected WrappedNode getChangesetNode(Map<String, Object> nodeProps, WrappedNod
11571196
if (changeset != currentChangesetId) {
11581197
changesetIndex = createIndexIfNotNull(changesetIndex, LABEL_CHANGESET, PROP_CHANGESET);
11591198
currentChangesetId = changeset;
1160-
Node changesetNode = tx.findNode(LABEL_CHANGESET, PROP_CHANGESET, currentChangesetId);
1199+
Node changesetNode = findNodeByLabelProperty(tx, LABEL_CHANGESET, PROP_CHANGESET, currentChangesetId);
11611200
if (changesetNode != null) {
11621201
currentChangesetNode = WrappedNode.fromNode(changesetNode);
11631202
} else {
@@ -1187,7 +1226,7 @@ protected WrappedNode getUserNode(Map<String, Object> nodeProps) {
11871226
if (uid != currentUserId) {
11881227
currentUserId = uid;
11891228
userIndex = createIndexIfNotNull(userIndex, LABEL_USER, PROP_USER_ID);
1190-
Node userNode = tx.findNode(LABEL_USER, PROP_USER_ID, currentUserId);
1229+
Node userNode = findNodeByLabelProperty(tx, LABEL_USER, PROP_USER_ID, currentUserId);
11911230
if (userNode != null) {
11921231
currentUserNode = WrappedNode.fromNode(userNode);
11931232
} else {
@@ -1198,7 +1237,7 @@ protected WrappedNode getUserNode(Map<String, Object> nodeProps) {
11981237
currentUserNode = addNode(LABEL_USER, userProps, PROP_USER_ID);
11991238
userCount++;
12001239
if (usersNode == null) {
1201-
usersNode = WrappedNode.fromNode(tx.createNode(LABEL_USER));
1240+
usersNode = createNodeWithLabel(tx, LABEL_USER);
12021241
osm_dataset.createRelationshipTo(usersNode, OSMRelation.USERS);
12031242
}
12041243
usersNode.createRelationshipTo(currentUserNode, OSMRelation.OSM_USER);
@@ -1218,15 +1257,15 @@ public String toString() {
12181257

12191258
}
12201259

1221-
public void importFile(GraphDatabaseService database, String dataset) throws IOException, XMLStreamException {
1260+
public void importFile(GraphDatabaseService database, String dataset) throws Exception {
12221261
importFile(database, dataset, false, 5000);
12231262
}
12241263

1225-
public void importFile(GraphDatabaseService database, String dataset, int txInterval) throws IOException, XMLStreamException {
1264+
public void importFile(GraphDatabaseService database, String dataset, int txInterval) throws Exception {
12261265
importFile(database, dataset, false, txInterval);
12271266
}
12281267

1229-
public void importFile(GraphDatabaseService database, String dataset, boolean allPoints, int txInterval) throws IOException, XMLStreamException {
1268+
public void importFile(GraphDatabaseService database, String dataset, boolean allPoints, int txInterval) throws Exception {
12301269
importFile(OSMWriter.fromGraphDatabase(database, securityContext, stats, this, txInterval), dataset, allPoints, charset);
12311270
}
12321271

src/test/java/org/neo4j/gis/spatial/Neo4jSpatialDataStoreTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public class Neo4jSpatialDataStoreTest {
3535
public GraphDatabaseService graph;
3636

3737
@Before
38-
public void setup() throws IOException, XMLStreamException {
38+
public void setup() throws Exception {
3939
this.databases = new TestDatabaseManagementServiceBuilder(Path.of("target", "test")).impermanent().build();
4040
this.graph = databases.database(DEFAULT_DATABASE_NAME);
4141
OSMImporter importer = new OSMImporter("map", new ConsoleListener());

src/test/java/org/neo4j/gis/spatial/procedures/SpatialProceduresTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,22 @@ public void import_osm_to_layer() {
929929
testCallCount(db, "CALL spatial.layers()", null, 1);
930930
}
931931

932+
@Test
933+
public void import_osm_twice_should_pass_with_different_layers() {
934+
execute("CALL spatial.addLayer('geom1','OSM','')");
935+
execute("CALL spatial.addLayer('geom2','OSM','')");
936+
937+
testCountQuery("importOSM", "CALL spatial.importOSMToLayer('geom1','map.osm')", 55, "count", null);
938+
testCallCount(db, "CALL spatial.layers()", null, 2);
939+
testCallCount(db, "CALL spatial.withinDistance('geom1',{lon:6.3740429666,lat:50.93676351666},10000)", null, 217);
940+
testCallCount(db, "CALL spatial.withinDistance('geom2',{lon:6.3740429666,lat:50.93676351666},10000)", null, 0);
941+
942+
testCountQuery("importOSM", "CALL spatial.importOSMToLayer('geom2','map.osm')", 55, "count", null);
943+
testCallCount(db, "CALL spatial.layers()", null, 2);
944+
testCallCount(db, "CALL spatial.withinDistance('geom1',{lon:6.3740429666,lat:50.93676351666},10000)", null, 217);
945+
testCallCount(db, "CALL spatial.withinDistance('geom2',{lon:6.3740429666,lat:50.93676351666},10000)", null, 217);
946+
}
947+
932948
@Ignore
933949
public void import_cracow_to_layer() {
934950
execute("CALL spatial.addLayer('geom','OSM','')");

0 commit comments

Comments
 (0)