Skip to content

Commit

Permalink
Prefix index search via Core API
Browse files Browse the repository at this point in the history
  • Loading branch information
fickludd committed Mar 13, 2018
1 parent 6c20367 commit 422e2aa
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 70 deletions.
Expand Up @@ -134,6 +134,35 @@ public interface GraphDatabaseService
*/
ResourceIterator<Node> 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.
* <p>
* 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.
* <p>
* The search mode and value template are used to select nodes of interest. The search mode can
* be one of
* <ul>
* <li>EXACT: The value has to match the template exactly.</li>
* <li>PREFIX: The value must have a prefix matching the template.</li>
* <li>SUFFIX: The value must have a suffix matching the template.</li>
* <li>CONTAINS: The value must contain the template. Only exact matches are supported.</li>
* </ul>
* Note that in Neo4j the Character 'A' will be treated the same way as the String 'A'.
* <p>
* 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<Node> 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.
Expand Down
@@ -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 <http://www.gnu.org/licenses/>.
*/
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:
* <ul>
* <li>EXACT: The value has to match the template exactly.</li>
* <li>PREFIX: The value must have a prefix matching the template.</li>
* <li>SUFFIX: The value must have a suffix matching the template.</li>
* <li>CONTAINS: The value must contain the template. Only exact matches are supported.</li>
* </ul>
*/
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;
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -578,7 +578,41 @@ public <T> TransactionEventHandler<T> unregisterTransactionEventHandler(
@Override
public ResourceIterator<Node> 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<Node> 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
Expand Down Expand Up @@ -618,28 +652,23 @@ private InternalTransaction beginTransactionInternal( KernelTransaction.Type typ
return new TopLevelTransaction( spi.beginTransaction( type, loginContext, timeoutMillis ), statementContext );
}

private ResourceIterator<Node> nodesByLabelAndProperty( Label myLabel, String key, Value value )
private ResourceIterator<Node> 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 );
Expand All @@ -650,11 +679,11 @@ private ResourceIterator<Node> nodesByLabelAndProperty( Label myLabel, String ke
}
}

return getNodesByLabelAndPropertyWithoutIndex( propertyId, value, statement, labelId );
return getNodesByLabelAndPropertyWithoutIndex( query, statement, labelId );
}

private ResourceIterator<Node> getNodesByLabelAndPropertyWithoutIndex( int propertyId, Value value,
Statement statement, int labelId )
private ResourceIterator<Node> getNodesByLabelAndPropertyWithoutIndex(
IndexQuery query, Statement statement, int labelId )
{
KernelTransaction transaction = statementContext.getKernelTransactionBoundToThisThread( true );

Expand All @@ -670,8 +699,7 @@ private ResourceIterator<Node> getNodesByLabelAndPropertyWithoutIndex( int prope
propertyCursor,
statement,
this::newNodeProxy,
propertyId,
value );
query );
}

private ResourceIterator<Node> allNodesWithLabel( final Label myLabel )
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -550,25 +544,19 @@ 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 ) );
}

@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" );
Expand All @@ -583,25 +571,19 @@ 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 ) );
}

@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" );
Expand All @@ -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 ) );
Expand All @@ -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<Node> 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 )
Expand Down

0 comments on commit 422e2aa

Please sign in to comment.