From 422e2aa990140ad3191829c610c1912f616b7f2f Mon Sep 17 00:00:00 2001 From: fickludd Date: Tue, 13 Mar 2018 10:14:20 +0100 Subject: [PATCH] Prefix index search via Core API --- .../neo4j/graphdb/GraphDatabaseService.java | 29 ++++++++ .../org/neo4j/graphdb/StringSearchMode.java | 50 +++++++++++++ .../impl/factory/GraphDatabaseFacade.java | 70 +++++++++++++------ .../neo4j/graphdb/IndexingAcceptanceTest.java | 65 +++++------------ .../org/neo4j/test/rule/DatabaseRule.java | 7 ++ .../kernel/ReadOnlyGraphDatabaseProxy.java | 7 ++ .../fulltext/StubGraphDatabaseService.java | 7 ++ 7 files changed, 165 insertions(+), 70 deletions(-) create mode 100644 community/graphdb-api/src/main/java/org/neo4j/graphdb/StringSearchMode.java diff --git a/community/graphdb-api/src/main/java/org/neo4j/graphdb/GraphDatabaseService.java b/community/graphdb-api/src/main/java/org/neo4j/graphdb/GraphDatabaseService.java index 9e108e34af186..e0828f9e59e66 100644 --- a/community/graphdb-api/src/main/java/org/neo4j/graphdb/GraphDatabaseService.java +++ b/community/graphdb-api/src/main/java/org/neo4j/graphdb/GraphDatabaseService.java @@ -134,6 +134,35 @@ public interface GraphDatabaseService */ ResourceIterator findNodes( Label label, String key, Object value ); + /** + * Returns all nodes having a given label, and a property value of type String or Character matching the + * given value template and search mode. + *

+ * If an online index is found, it will be used to look up the requested nodes. + * If no indexes exist for the label/property combination, the database will + * scan all labeled nodes looking for matching property values. + *

+ * The search mode and value template are used to select nodes of interest. The search mode can + * be one of + *

+ * Note that in Neo4j the Character 'A' will be treated the same way as the String 'A'. + *

+ * Please ensure that the returned {@link ResourceIterator} is closed correctly and as soon as possible + * inside your transaction to avoid potential blocking of write operations. + * + * @param label consider nodes with this label + * @param key required property key + * @param template required property value template + * @param searchMode required property value template + * @return an iterator containing all matching nodes. See {@link ResourceIterator} for responsibilities. + */ + ResourceIterator findNodes( Label label, String key, String template, StringSearchMode searchMode ); + /** * Equivalent to {@link #findNodes(Label, String, Object)}, however it must find no more than one * {@link Node node} or it will throw an exception. diff --git a/community/graphdb-api/src/main/java/org/neo4j/graphdb/StringSearchMode.java b/community/graphdb-api/src/main/java/org/neo4j/graphdb/StringSearchMode.java new file mode 100644 index 0000000000000..452f234e836a0 --- /dev/null +++ b/community/graphdb-api/src/main/java/org/neo4j/graphdb/StringSearchMode.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.graphdb; + +/** + * The string search mode is used together with a value template to find nodes of interest. + * The search mode can be one of: + *

+ */ +public enum StringSearchMode +{ + /** + * The value has to match the template exactly. + */ + EXACT, + /** + * The value must have a prefix matching the template. + */ + PREFIX, + /** + * The value must have a suffix matching the template. + */ + SUFFIX, + /** + * The value must contain the template. Only exact matches are supported. + */ + CONTAINS; +} diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/GraphDatabaseFacade.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/GraphDatabaseFacade.java index 04841e60154bc..3489ef34e97c6 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/GraphDatabaseFacade.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/GraphDatabaseFacade.java @@ -41,6 +41,7 @@ import org.neo4j.graphdb.ResourceIterable; import org.neo4j.graphdb.ResourceIterator; import org.neo4j.graphdb.Result; +import org.neo4j.graphdb.StringSearchMode; import org.neo4j.graphdb.Transaction; import org.neo4j.graphdb.TransactionTerminatedException; import org.neo4j.graphdb.event.KernelEventHandler; @@ -107,7 +108,6 @@ import org.neo4j.kernel.impl.util.ValueUtils; import org.neo4j.kernel.internal.GraphDatabaseAPI; import org.neo4j.storageengine.api.EntityType; -import org.neo4j.values.storable.Value; import org.neo4j.values.storable.Values; import org.neo4j.values.virtual.MapValue; @@ -578,7 +578,41 @@ public TransactionEventHandler unregisterTransactionEventHandler( @Override public ResourceIterator findNodes( final Label myLabel, final String key, final Object value ) { - return nodesByLabelAndProperty( myLabel, key, Values.of( value ) ); + KernelTransaction transaction = statementContext.getKernelTransactionBoundToThisThread( true ); + TokenRead tokenRead = transaction.tokenRead(); + int labelId = tokenRead.nodeLabel( myLabel.name() ); + int propertyId = tokenRead.propertyKey( key ); + return nodesByLabelAndProperty( transaction, labelId, IndexQuery.exact( propertyId, Values.of( value ) ) ); + } + + @Override + public ResourceIterator findNodes( + final Label myLabel, final String key, final String value, final StringSearchMode searchMode ) + { + KernelTransaction transaction = statementContext.getKernelTransactionBoundToThisThread( true ); + TokenRead tokenRead = transaction.tokenRead(); + int labelId = tokenRead.nodeLabel( myLabel.name() ); + int propertyId = tokenRead.propertyKey( key ); + IndexQuery query; + switch ( searchMode ) + { + case PREFIX: + query = IndexQuery.stringPrefix( propertyId, value ); + break; +// TODO: test the others +// case EXACT: +// query = IndexQuery.exact( propertyId, Values.stringValue( value ) ); +// break; +// case SUFFIX: +// query = IndexQuery.stringPrefix( propertyId, value ); +// break; +// case CONTAINS: +// query = IndexQuery.stringPrefix( propertyId, value ); +// break; + default: + throw new IllegalStateException( "Unknown string search mode: " + searchMode ); + } + return nodesByLabelAndProperty( transaction, labelId, query ); } @Override @@ -618,28 +652,23 @@ private InternalTransaction beginTransactionInternal( KernelTransaction.Type typ return new TopLevelTransaction( spi.beginTransaction( type, loginContext, timeoutMillis ), statementContext ); } - private ResourceIterator nodesByLabelAndProperty( Label myLabel, String key, Value value ) + private ResourceIterator nodesByLabelAndProperty( KernelTransaction transaction, int labelId, IndexQuery query ) { - KernelTransaction transaction = statementContext.getKernelTransactionBoundToThisThread( true ); Statement statement = transaction.acquireStatement(); Read read = transaction.dataRead(); - TokenRead tokenRead = transaction.tokenRead(); - int propertyId = tokenRead.propertyKey( key ); - int labelId = tokenRead.nodeLabel( myLabel.name() ); - if ( propertyId == NO_SUCH_PROPERTY_KEY || labelId == NO_SUCH_LABEL ) + if ( query.propertyKeyId() == NO_SUCH_PROPERTY_KEY || labelId == NO_SUCH_LABEL ) { statement.close(); return emptyResourceIterator(); } - CapableIndexReference index = transaction.schemaRead().index( labelId, propertyId ); + CapableIndexReference index = transaction.schemaRead().index( labelId, query.propertyKeyId() ); if ( index != CapableIndexReference.NO_INDEX ) { // Ha! We found an index - let's use it to find matching nodes try { NodeValueIndexCursor cursor = transaction.cursors().allocateNodeValueIndexCursor(); - IndexQuery.ExactPredicate query = IndexQuery.exact( propertyId, value ); read.nodeIndexSeek( index, cursor, IndexOrder.NONE, query ); return new NodeCursorResourceIterator<>( cursor, statement, this::newNodeProxy ); @@ -650,11 +679,11 @@ private ResourceIterator nodesByLabelAndProperty( Label myLabel, String ke } } - return getNodesByLabelAndPropertyWithoutIndex( propertyId, value, statement, labelId ); + return getNodesByLabelAndPropertyWithoutIndex( query, statement, labelId ); } - private ResourceIterator getNodesByLabelAndPropertyWithoutIndex( int propertyId, Value value, - Statement statement, int labelId ) + private ResourceIterator getNodesByLabelAndPropertyWithoutIndex( + IndexQuery query, Statement statement, int labelId ) { KernelTransaction transaction = statementContext.getKernelTransactionBoundToThisThread( true ); @@ -670,8 +699,7 @@ private ResourceIterator getNodesByLabelAndPropertyWithoutIndex( int prope propertyCursor, statement, this::newNodeProxy, - propertyId, - value ); + query ); } private ResourceIterator allNodesWithLabel( final Label myLabel ) @@ -813,8 +841,7 @@ private static class NodeLabelPropertyIterator extends PrefetchingNodeResourceIt private final NodeLabelIndexCursor nodeLabelCursor; private final NodeCursor nodeCursor; private final PropertyCursor propertyCursor; - private final int propertyKeyId; - private final Value value; + private final IndexQuery query; NodeLabelPropertyIterator( Read read, @@ -823,16 +850,14 @@ private static class NodeLabelPropertyIterator extends PrefetchingNodeResourceIt PropertyCursor propertyCursor, Statement statement, NodeFactory nodeFactory, - int propertyKeyId, - Value value ) + IndexQuery query ) { super( statement, nodeFactory ); this.read = read; this.nodeLabelCursor = nodeLabelCursor; this.nodeCursor = nodeCursor; this.propertyCursor = propertyCursor; - this.propertyKeyId = propertyKeyId; - this.value = value; + this.query = query; } @Override @@ -870,7 +895,8 @@ private boolean hasPropertyWithValue() nodeCursor.properties( propertyCursor ); while ( propertyCursor.next() ) { - if ( propertyCursor.propertyKey() == propertyKeyId && propertyCursor.propertyValue().equals( value ) ) + if ( propertyCursor.propertyKey() == query.propertyKeyId() && + query.acceptsValueAt( propertyCursor ) ) { return true; } diff --git a/community/kernel/src/test/java/org/neo4j/graphdb/IndexingAcceptanceTest.java b/community/kernel/src/test/java/org/neo4j/graphdb/IndexingAcceptanceTest.java index 711ebd8023432..d250042569e31 100644 --- a/community/kernel/src/test/java/org/neo4j/graphdb/IndexingAcceptanceTest.java +++ b/community/kernel/src/test/java/org/neo4j/graphdb/IndexingAcceptanceTest.java @@ -512,23 +512,18 @@ public void shouldAddIndexedPropertyToNodeWithDynamicLabels() @Test public void shouldSupportIndexSeekByPrefix() - throws SchemaRuleNotFoundException, IndexNotFoundKernelException, IndexNotApplicableKernelException { // GIVEN GraphDatabaseService db = dbRule.getGraphDatabaseAPI(); - IndexDefinition index = Neo4jMatchers.createIndex( db, LABEL1, "name" ); + Neo4jMatchers.createIndex( db, LABEL1, "name" ); createNodes( db, LABEL1, "name", "Mattias", "Mats", "Carla" ); PrimitiveLongSet expected = createNodes( db, LABEL1, "name", "Karl", "Karlsson" ); // WHEN PrimitiveLongSet found = Primitive.longSet(); - try ( Transaction tx = db.beginTx(); - Statement statement = getStatement( (GraphDatabaseAPI) db ) ) + try ( Transaction tx = db.beginTx() ) { - ReadOperations ops = statement.readOperations(); - SchemaIndexDescriptor descriptor = indexDescriptor( ops, index ); - int propertyKeyId = descriptor.schema().getPropertyId(); - found.addAll( ops.indexQuery( descriptor, stringPrefix( propertyKeyId, "Karl" ) ) ); + collectNodes( found, db.findNodes( LABEL1, "name", "Karl", StringSearchMode.PREFIX ) ); } // THEN @@ -537,11 +532,10 @@ public void shouldSupportIndexSeekByPrefix() @Test public void shouldIncludeNodesCreatedInSameTxInIndexSeekByPrefix() - throws SchemaRuleNotFoundException, IndexNotFoundKernelException, IndexNotApplicableKernelException { // GIVEN GraphDatabaseService db = dbRule.getGraphDatabaseAPI(); - IndexDefinition index = Neo4jMatchers.createIndex( db, LABEL1, "name" ); + Neo4jMatchers.createIndex( db, LABEL1, "name" ); createNodes( db, LABEL1, "name", "Mattias", "Mats" ); PrimitiveLongSet expected = createNodes( db, LABEL1, "name", "Carl", "Carlsson" ); // WHEN @@ -550,13 +544,8 @@ public void shouldIncludeNodesCreatedInSameTxInIndexSeekByPrefix() { expected.add( createNode( db, map( "name", "Carlchen" ), LABEL1 ).getId() ); createNode( db, map( "name", "Karla" ), LABEL1 ); - try ( Statement statement = getStatement( (GraphDatabaseAPI) db ) ) - { - ReadOperations readOperations = statement.readOperations(); - SchemaIndexDescriptor descriptor = indexDescriptor( readOperations, index ); - int propertyKeyId = descriptor.schema().getPropertyId(); - found.addAll( readOperations.indexQuery( descriptor, stringPrefix( propertyKeyId, "Carl" ) ) ); - } + + collectNodes( found, db.findNodes( LABEL1, "name", "Carl", StringSearchMode.PREFIX ) ); } // THEN assertThat( found, equalTo( expected ) ); @@ -564,11 +553,10 @@ public void shouldIncludeNodesCreatedInSameTxInIndexSeekByPrefix() @Test public void shouldNotIncludeNodesDeletedInSameTxInIndexSeekByPrefix() - throws SchemaRuleNotFoundException, IndexNotFoundKernelException, IndexNotApplicableKernelException { // GIVEN GraphDatabaseService db = dbRule.getGraphDatabaseAPI(); - IndexDefinition index = Neo4jMatchers.createIndex( db, LABEL1, "name" ); + Neo4jMatchers.createIndex( db, LABEL1, "name" ); createNodes( db, LABEL1, "name", "Mattias" ); PrimitiveLongSet toDelete = createNodes( db, LABEL1, "name", "Karlsson", "Mats" ); PrimitiveLongSet expected = createNodes( db, LABEL1, "name", "Karl" ); @@ -583,13 +571,8 @@ public void shouldNotIncludeNodesDeletedInSameTxInIndexSeekByPrefix() db.getNodeById( id ).delete(); expected.remove( id ); } - try ( Statement statement = getStatement( (GraphDatabaseAPI) db ) ) - { - ReadOperations readOperations = statement.readOperations(); - SchemaIndexDescriptor descriptor = indexDescriptor( readOperations, index ); - int propertyKeyId = descriptor.schema().getPropertyId(); - found.addAll( readOperations.indexQuery( descriptor, stringPrefix( propertyKeyId, "Karl" ) ) ); - } + + collectNodes( found, db.findNodes( LABEL1, "name", "Karl", StringSearchMode.PREFIX ) ); } // THEN assertThat( found, equalTo( expected ) ); @@ -597,11 +580,10 @@ public void shouldNotIncludeNodesDeletedInSameTxInIndexSeekByPrefix() @Test public void shouldConsiderNodesChangedInSameTxInIndexPrefixSearch() - throws SchemaRuleNotFoundException, IndexNotFoundKernelException, IndexNotApplicableKernelException { // GIVEN GraphDatabaseService db = dbRule.getGraphDatabaseAPI(); - IndexDefinition index = Neo4jMatchers.createIndex( db, LABEL1, "name" ); + Neo4jMatchers.createIndex( db, LABEL1, "name" ); createNodes( db, LABEL1, "name", "Mattias" ); PrimitiveLongSet toChangeToMatch = createNodes( db, LABEL1, "name", "Mats" ); PrimitiveLongSet toChangeToNotMatch = createNodes( db, LABEL1, "name", "Karlsson" ); @@ -625,13 +607,8 @@ public void shouldConsiderNodesChangedInSameTxInIndexPrefixSearch() db.getNodeById( id ).setProperty( "name", "X" + id ); expected.remove( id ); } - try ( Statement statement = getStatement( (GraphDatabaseAPI) db ) ) - { - ReadOperations readOperations = statement.readOperations(); - SchemaIndexDescriptor descriptor = indexDescriptor( readOperations, index ); - int propertyKeyId = descriptor.schema().getPropertyId(); - found.addAll( readOperations.indexQuery( descriptor, stringPrefix( propertyKeyId, prefix ) ) ); - } + + collectNodes( found, db.findNodes( LABEL1, "name", prefix, StringSearchMode.PREFIX ) ); } // THEN assertThat( found, equalTo( expected ) ); @@ -651,20 +628,12 @@ private PrimitiveLongSet createNodes( GraphDatabaseService db, Label label, Stri return expected; } - private SchemaIndexDescriptor indexDescriptor( ReadOperations readOperations, IndexDefinition index ) - throws SchemaRuleNotFoundException - { - int labelId = readOperations.labelGetForName( index.getLabel().name() ); - int[] propertyKeyIds = getPropertyIds( readOperations, index.getPropertyKeys() ); - - LabelSchemaDescriptor descriptor = SchemaDescriptorFactory.forLabel( labelId, propertyKeyIds ); - return readOperations.indexGetForSchema( descriptor ); - } - - private Statement getStatement( GraphDatabaseAPI db ) + private void collectNodes( PrimitiveLongSet bucket, ResourceIterator toCollect ) { - return db.getDependencyResolver() - .resolveDependency( ThreadToStatementContextBridge.class ).get(); + while ( toCollect.hasNext() ) + { + bucket.add( toCollect.next().getId() ); + } } private void assertCanCreateAndFind( GraphDatabaseService db, Label label, String propertyKey, Object value ) diff --git a/community/kernel/src/test/java/org/neo4j/test/rule/DatabaseRule.java b/community/kernel/src/test/java/org/neo4j/test/rule/DatabaseRule.java index dc357719ef5d6..a119839ae8c24 100644 --- a/community/kernel/src/test/java/org/neo4j/test/rule/DatabaseRule.java +++ b/community/kernel/src/test/java/org/neo4j/test/rule/DatabaseRule.java @@ -39,6 +39,7 @@ import org.neo4j.graphdb.ResourceIterable; import org.neo4j.graphdb.ResourceIterator; import org.neo4j.graphdb.Result; +import org.neo4j.graphdb.StringSearchMode; import org.neo4j.graphdb.Transaction; import org.neo4j.graphdb.config.Setting; import org.neo4j.graphdb.event.KernelEventHandler; @@ -534,6 +535,12 @@ public ResourceIterator findNodes( Label label, String key, Object value ) return database.findNodes( label, key, value ); } + @Override + public ResourceIterator findNodes( Label label, String key, String template, StringSearchMode searchMode ) + { + return database.findNodes( label, key, template, searchMode ); + } + @Override public Node findNode( Label label, String key, Object value ) { diff --git a/community/shell/src/main/java/org/neo4j/shell/kernel/ReadOnlyGraphDatabaseProxy.java b/community/shell/src/main/java/org/neo4j/shell/kernel/ReadOnlyGraphDatabaseProxy.java index c88fd769a2852..5b66ccebdb569 100644 --- a/community/shell/src/main/java/org/neo4j/shell/kernel/ReadOnlyGraphDatabaseProxy.java +++ b/community/shell/src/main/java/org/neo4j/shell/kernel/ReadOnlyGraphDatabaseProxy.java @@ -38,6 +38,7 @@ import org.neo4j.graphdb.ResourceIterable; import org.neo4j.graphdb.ResourceIterator; import org.neo4j.graphdb.Result; +import org.neo4j.graphdb.StringSearchMode; import org.neo4j.graphdb.Transaction; import org.neo4j.graphdb.event.KernelEventHandler; import org.neo4j.graphdb.event.TransactionEventHandler; @@ -1084,6 +1085,12 @@ public ResourceIterator findNodes( Label label, String key, Object value ) return actual.findNodes( label, key, value ); } + @Override + public ResourceIterator findNodes( Label label, String key, String template, StringSearchMode searchMode ) + { + return actual.findNodes( label, key, template, searchMode ); + } + @Override public Node findNode( Label label, String key, Object value ) { diff --git a/enterprise/fulltext-addon/src/test/java/org/neo4j/kernel/api/impl/fulltext/StubGraphDatabaseService.java b/enterprise/fulltext-addon/src/test/java/org/neo4j/kernel/api/impl/fulltext/StubGraphDatabaseService.java index 21c77c77098f3..44e5e4bea79f1 100644 --- a/enterprise/fulltext-addon/src/test/java/org/neo4j/kernel/api/impl/fulltext/StubGraphDatabaseService.java +++ b/enterprise/fulltext-addon/src/test/java/org/neo4j/kernel/api/impl/fulltext/StubGraphDatabaseService.java @@ -31,6 +31,7 @@ import org.neo4j.graphdb.ResourceIterable; import org.neo4j.graphdb.ResourceIterator; import org.neo4j.graphdb.Result; +import org.neo4j.graphdb.StringSearchMode; import org.neo4j.graphdb.Transaction; import org.neo4j.graphdb.event.KernelEventHandler; import org.neo4j.graphdb.event.TransactionEventHandler; @@ -89,6 +90,12 @@ public ResourceIterator findNodes( Label label, String key, Object value ) return null; } + @Override + public ResourceIterator findNodes( Label label, String key, String template, StringSearchMode searchMode ) + { + return null; + } + @Override public Node findNode( Label label, String key, Object value ) {