/*
* Copyright (c) 2002-2016 "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.unsafe.batchinsert;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.io.File;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.config.Setting;
import org.neo4j.graphdb.factory.GraphDatabaseBuilder;
import org.neo4j.graphdb.factory.GraphDatabaseSettings;
import org.neo4j.graphdb.index.Index;
import org.neo4j.graphdb.index.IndexHits;
import org.neo4j.graphdb.index.IndexManager;
import org.neo4j.helpers.Service;
import org.neo4j.helpers.collection.LruCache;
import org.neo4j.index.impl.lucene.legacy.LuceneBatchInserterIndexProviderNewImpl;
import org.neo4j.index.impl.lucene.legacy.LuceneIndexImplementation;
import org.neo4j.index.impl.lucene.legacy.MyStandardAnalyzer;
import org.neo4j.index.lucene.ValueContext;
import org.neo4j.index.lucene.unsafe.batchinsert.LuceneBatchInserterIndexProvider;
import org.neo4j.kernel.extension.KernelExtensionFactory;
import org.neo4j.kernel.impl.api.index.inmemory.InMemoryIndexProviderFactory;
import org.neo4j.kernel.impl.api.scan.InMemoryLabelScanStoreExtension;
import org.neo4j.test.TestGraphDatabaseFactory;
import org.neo4j.test.rule.TestDirectory;
import static org.apache.lucene.search.NumericRangeQuery.newIntRange;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.neo4j.helpers.Exceptions.launderedException;
import static org.neo4j.helpers.collection.Iterables.filter;
import static org.neo4j.helpers.collection.MapUtil.map;
import static org.neo4j.helpers.collection.MapUtil.stringMap;
import static org.neo4j.index.Neo4jTestCase.assertContains;
import static org.neo4j.index.impl.lucene.legacy.Contains.contains;
import static org.neo4j.index.impl.lucene.legacy.LuceneIndexImplementation.EXACT_CONFIG;
import static org.neo4j.index.lucene.ValueContext.numeric;
public class TestLuceneBatchInsert
{
@Rule
public final TestDirectory testDirectory = TestDirectory.testDirectory();
private File storeDir;
private BatchInserter inserter;
private GraphDatabaseService db;
@SuppressWarnings( { "rawtypes", "unchecked" } )
@Before
public void startInserter() throws Exception
{
storeDir = testDirectory.graphDbDir();
Iterable filteredKernelExtensions = filter( onlyRealLuceneExtensions(),
Service.load( KernelExtensionFactory.class ) );
inserter = BatchInserters.inserter( storeDir, stringMap(), filteredKernelExtensions );
}
@After
public void shutdown()
{
shutdownInserter();
if ( db != null )
{
db.shutdown();
db = null;
}
}
@Test
public void testSome() throws Exception
{
BatchInserterIndexProvider provider = new LuceneBatchInserterIndexProviderNewImpl( inserter );
String indexName = "users";
BatchInserterIndex index = provider.nodeIndex( indexName, EXACT_CONFIG );
Map ids = new HashMap<>();
int count = 5;
for ( int i = 0; i < count; i++ )
{
long id = inserter.createNode( null );
index.add( id, map( "name", "Joe" + i, "other", "Schmoe" ) );
ids.put( i, id );
}
index.flush();
for ( int i = 0; i < count; i++ )
{
assertContains( index.get( "name", "Joe" + i ), ids.get( i ) );
}
assertContains( index.query( "name:Joe0 AND other:Schmoe" ), ids.get( 0 ) );
assertContains( index.query( "name", "Joe*" ), ids.values().toArray( new Long[ids.size()] ) );
provider.shutdown();
switchToGraphDatabaseService();
try ( Transaction transaction = db.beginTx() )
{
IndexManager indexManager = db.index();
assertFalse( indexManager.existsForRelationships( indexName ) );
assertTrue( indexManager.existsForNodes( indexName ) );
assertNotNull( indexManager.forNodes( indexName ) );
Index dbIndex = db.index().forNodes( "users" );
for ( int i = 0; i < count; i++ )
{
assertContains( dbIndex.get( "name", "Joe" + i ), db.getNodeById( ids.get( i ) ) );
}
Collection nodes = new ArrayList<>();
for ( long id : ids.values() )
{
nodes.add( db.getNodeById( id ) );
}
assertContains( dbIndex.query( "name", "Joe*" ), nodes.toArray( new Node[nodes.size()] ) );
assertContains( dbIndex.query( "name:Joe0 AND other:Schmoe" ), db.getNodeById( ids.get( 0 ) ) );
transaction.success();
}
}
@Test
public void testFulltext()
{
BatchInserterIndexProvider provider = new LuceneBatchInserterIndexProviderNewImpl( inserter );
String name = "users";
BatchInserterIndex index = provider.nodeIndex( name, stringMap( "type", "fulltext" ) );
long id1 = inserter.createNode( null );
index.add( id1, map( "name", "Mattias Persson", "email", "something@somewhere", "something", "bad" ) );
long id2 = inserter.createNode( null );
index.add( id2, map( "name", "Lars PerssoN" ) );
index.flush();
assertContains( index.get( "name", "Mattias Persson" ), id1 );
assertContains( index.query( "name", "mattias" ), id1 );
assertContains( index.query( "name", "bla" ) );
assertContains( index.query( "name", "persson" ), id1, id2 );
assertContains( index.query( "email", "*@*" ), id1 );
assertContains( index.get( "something", "bad" ), id1 );
long id3 = inserter.createNode( null );
index.add( id3, map( "name", new String[] { "What Ever", "Anything" } ) );
index.flush();
assertContains( index.get( "name", "What Ever" ), id3 );
assertContains( index.get( "name", "Anything" ), id3 );
provider.shutdown();
switchToGraphDatabaseService();
try ( Transaction transaction = db.beginTx() )
{
Index dbIndex = db.index().forNodes( name );
Node node1 = db.getNodeById( id1 );
Node node2 = db.getNodeById( id2 );
assertContains( dbIndex.query( "name", "persson" ), node1, node2 );
transaction.success();
}
}
@Test
public void testCanIndexRelationships()
{
BatchInserterIndexProvider indexProvider = new LuceneBatchInserterIndexProviderNewImpl( inserter );
BatchInserterIndex edgesIndex = indexProvider.relationshipIndex(
"edgeIndex", stringMap( IndexManager.PROVIDER, "lucene", "type", "exact" ) );
long nodeId1 = inserter.createNode( map( "ID", "1" ) );
long nodeId2 = inserter.createNode( map( "ID", "2" ) );
long relationshipId = inserter.createRelationship( nodeId1, nodeId2,
EdgeType.KNOWS, null );
edgesIndex.add( relationshipId, map( "EDGE_TYPE", EdgeType.KNOWS.name() ) );
edgesIndex.flush();
assertEquals(
String.format( "Should return relationship id" ),
new Long( relationshipId ),
edgesIndex.query( "EDGE_TYPE", EdgeType.KNOWS.name() ).getSingle() );
indexProvider.shutdown();
}
@Test
public void triggerNPEAfterFlush()
{
BatchInserterIndexProvider provider = new LuceneBatchInserterIndexProviderNewImpl( inserter );
BatchInserterIndex index = provider.nodeIndex( "Node-exact", EXACT_CONFIG );
Map map = map( "name", "Something" );
long node = inserter.createNode( map );
index.add( node, map );
index.flush();
assertContains( index.get( "name", "Something" ), node );
provider.shutdown();
}
@Test
public void testNumericValues()
{
BatchInserterIndexProvider provider = new LuceneBatchInserterIndexProviderNewImpl( inserter );
BatchInserterIndex index = provider.nodeIndex( "mine", EXACT_CONFIG );
long node1 = inserter.createNode( null );
index.add( node1, map( "number", numeric( 45 ) ) );
long node2 = inserter.createNode( null );
index.add( node2, map( "number", numeric( 21 ) ) );
index.flush();
assertContains( index.query( "number",
newIntRange( "number", 21, 50, true, true ) ), node1, node2 );
provider.shutdown();
switchToGraphDatabaseService();
try ( Transaction transaction = db.beginTx() )
{
Node n1 = db.getNodeById( node1 );
db.getNodeById( node2 );
Index idx = db.index().forNodes( "mine" );
assertContains( idx.query( "number", newIntRange( "number", 21, 45, false, true ) ), n1 );
transaction.success();
}
}
@Test
public void testNumericValueArrays()
{
BatchInserterIndexProvider provider = new LuceneBatchInserterIndexProviderNewImpl( inserter );
BatchInserterIndex batchIndex = provider.nodeIndex( "mine", EXACT_CONFIG );
long nodeId1 = inserter.createNode( null );
batchIndex.add( nodeId1, map( "number", new ValueContext[]{ numeric( 45 ), numeric( 98 ) } ) );
long nodeId2 = inserter.createNode( null );
batchIndex.add( nodeId2, map( "number", new ValueContext[]{numeric( 47 ), numeric( 100 )} ) );
batchIndex.flush();
IndexHits batchIndexResult1 = batchIndex.query( "number", newIntRange( "number", 47, 98, true, true ) );
assertThat( batchIndexResult1, contains(nodeId1, nodeId2));
assertThat( batchIndexResult1.size(), is( 2 ));
IndexHits batchIndexResult2 = batchIndex.query( "number", newIntRange( "number", 44, 46, true, true ) );
assertThat( batchIndexResult2, contains(nodeId1));
assertThat( batchIndexResult2.size(), is( 1 ) );
IndexHits batchIndexResult3 = batchIndex.query( "number", newIntRange( "number", 99, 101, true, true ) );
assertThat( batchIndexResult3, contains( nodeId2 ) );
assertThat( batchIndexResult3.size(), is( 1 ) );
IndexHits batchIndexResult4 = batchIndex.query( "number", newIntRange( "number", 47, 98, false, false ) );
assertThat( batchIndexResult4, Matchers.emptyIterable() );
provider.shutdown();
switchToGraphDatabaseService();
try ( Transaction transaction = db.beginTx() )
{
Node node1 = db.getNodeById( nodeId1 );
Node node2 = db.getNodeById( nodeId2 );
Index index = db.index().forNodes( "mine" );
IndexHits indexResult1 = index.query( "number", newIntRange( "number", 47, 98, true, true ) );
assertThat(indexResult1, contains(node1, node2));
assertThat( indexResult1.size(), is( 2 ));
IndexHits indexResult2 = index.query( "number", newIntRange( "number", 44, 46, true, true ) );
assertThat(indexResult2, contains(node1));
assertThat( indexResult2.size(), is( 1 ) );
IndexHits indexResult3 = index.query( "number", newIntRange( "number", 99, 101, true, true ) );
assertThat( indexResult3, contains( node2 ) );
assertThat( indexResult3.size(), is( 1 ) );
IndexHits indexResult4 = index.query( "number", newIntRange( "number", 47, 98, false, false ) );
assertThat( indexResult4, Matchers.emptyIterable() );
transaction.success();
}
}
@Test
public void indexNumbers() throws Exception
{
BatchInserterIndexProvider provider = new LuceneBatchInserterIndexProviderNewImpl( inserter );
BatchInserterIndex index = provider.nodeIndex( "mine", EXACT_CONFIG );
long id = inserter.createNode( null );
Map props = new HashMap<>();
props.put( "key", 123L );
index.add( id, props );
index.flush();
assertEquals( 1, index.get( "key", 123L ).size() );
assertEquals( 1, index.get( "key", "123" ).size() );
provider.shutdown();
}
@SuppressWarnings("deprecation")
@Test
public void shouldCreateAutoIndexThatIsUsableInEmbedded() throws Exception
{
BatchInserterIndexProvider provider = new LuceneBatchInserterIndexProviderNewImpl( inserter );
BatchInserterIndex index = provider.nodeIndex( "node_auto_index", EXACT_CONFIG );
long id = inserter.createNode( null );
Map props = new HashMap<>();
props.put( "name", "peter" );
index.add( id, props );
index.flush();
provider.shutdown();
shutdownInserter();
switchToGraphDatabaseService( configure( GraphDatabaseSettings.node_keys_indexable, "name" ),
configure( GraphDatabaseSettings.relationship_keys_indexable, "relProp1,relProp2" ),
configure( GraphDatabaseSettings.node_auto_indexing, "true" ),
configure( GraphDatabaseSettings.relationship_auto_indexing, "true" ) );
try ( Transaction tx = db.beginTx() )
{
// Create the primitives
Node node1 = db.createNode();
// Add indexable and non-indexable properties
node1.setProperty( "name", "bob" );
// Make things persistent
tx.success();
}
try ( Transaction tx = db.beginTx() )
{
assertTrue( db.index().getNodeAutoIndexer().getAutoIndex().get( "name", "peter" ).hasNext() );
assertTrue( db.index().getNodeAutoIndexer().getAutoIndex().get( "name", "bob" ).hasNext() );
assertFalse( db.index().getNodeAutoIndexer().getAutoIndex().get( "name", "joe" ).hasNext() );
tx.success();
}
}
@Test
public void addOrUpdateFlushBehaviour() throws Exception
{
BatchInserterIndexProvider provider = new LuceneBatchInserterIndexProviderNewImpl( inserter );
BatchInserterIndex index = provider.nodeIndex( "update", EXACT_CONFIG );
long id = inserter.createNode( null );
Map props = new HashMap<>();
props.put( "key", "value" );
index.add( id, props );
index.updateOrAdd( id, props );
index.flush();
assertEquals( 1, index.get( "key", "value" ).size() );
index.flush();
props.put( "key", "value2" );
index.updateOrAdd( id, props );
index.flush();
assertEquals( 1, index.get( "key", "value2" ).size() );
assertEquals( 0, index.get( "key", "value" ).size() );
props.put( "key2", "value2" );
props.put( "key", "value" );
index.updateOrAdd( id, props );
assertEquals( 0, index.get( "key2", "value2" ).size() );
index.flush();
assertEquals( 1, index.get( "key2", "value2" ).size() );
assertEquals( 1, index.get( "key", "value" ).size() );
long id2 = inserter.createNode( null );
props = new HashMap<>();
props.put("2key","value");
index.updateOrAdd( id2, props );
props.put("2key","value2");
props.put("2key2","value3");
index.updateOrAdd( id2, props );
index.flush();
assertEquals( 1, index.get( "2key", "value2" ).size() );
provider.shutdown();
}
@Test
public void useStandardAnalyzer() throws Exception
{
BatchInserterIndexProvider provider = new LuceneBatchInserterIndexProviderNewImpl( inserter );
BatchInserterIndex index = provider.nodeIndex( "myindex",
stringMap( "analyzer", MyStandardAnalyzer.class.getName() ) );
index.add( 0, map( "name", "Mattias" ) );
provider.shutdown();
}
@Test
public void cachesShouldBeFilledWhenAddToMultipleIndexesCreatedNow() throws Exception
{
BatchInserterIndexProvider provider = new LuceneBatchInserterIndexProvider( inserter );
BatchInserterIndex index = provider.nodeIndex( "index1", LuceneIndexImplementation.EXACT_CONFIG );
index.setCacheCapacity( "name", 100000 );
String nameKey = "name";
String titleKey = "title";
assertCacheIsEmpty( index, nameKey, titleKey );
index.add( 0, map( "name", "Neo", "title", "Matrix" ) );
assertCacheContainsSomething( index, nameKey );
assertCacheIsEmpty( index, titleKey );
BatchInserterIndex index2 = provider.nodeIndex( "index2", LuceneIndexImplementation.EXACT_CONFIG );
index2.setCacheCapacity( "title", 100000 );
assertCacheIsEmpty( index2, nameKey, titleKey );
index2.add( 0, map( "name", "Neo", "title", "Matrix" ) );
assertCacheContainsSomething( index2, titleKey );
assertCacheIsEmpty( index2, nameKey );
provider.shutdown();
}
@Test
public void cachesDoesntGetFilledWhenAddingForAnExistingIndex() throws Exception
{
// Prepare the test case, i.e. create a store with a populated index.
BatchInserterIndexProvider provider = new LuceneBatchInserterIndexProvider( inserter );
String indexName = "index";
BatchInserterIndex index = provider.nodeIndex( indexName, LuceneIndexImplementation.EXACT_CONFIG );
String key = "name";
index.add( 0, map( key, "Mattias" ) );
provider.shutdown();
shutdownInserter();
// Test so that the next run doesn't start caching inserted stuff right away,
// because that would lead to invalid results being returned.
startInserter();
provider = new LuceneBatchInserterIndexProvider( inserter );
index = provider.nodeIndex( indexName, LuceneIndexImplementation.EXACT_CONFIG );
index.setCacheCapacity( key, 100000 );
assertCacheIsEmpty( index, key );
index.add( 1, map( key, "Persson" ) );
index.flush();
assertCacheIsEmpty( index, key );
assertEquals( 1, index.get( key, "Persson" ).getSingle().intValue() );
provider.shutdown();
}
@Test
public void shouldKeepAroundUnusedIndexesAfterConsecutiveInsertion() throws Exception
{
// GIVEN -- a batch insertion creating two indexes
String indexName1 = "first", indexName2 = "second", key = "name";
{
BatchInserterIndexProvider provider = new LuceneBatchInserterIndexProvider( inserter );
BatchInserterIndex index1 = provider.nodeIndex( indexName1, LuceneIndexImplementation.EXACT_CONFIG );
index1.add( 0, map( key, "Mattias" ) );
BatchInserterIndex index2 = provider.nodeIndex( indexName1, LuceneIndexImplementation.EXACT_CONFIG );
index2.add( 0, map( key, "Mattias" ) );
provider.shutdown();
shutdownInserter();
}
// WHEN -- doing a second insertion, only adding to the second index
{
startInserter();
BatchInserterIndexProvider provider = new LuceneBatchInserterIndexProvider( inserter );
BatchInserterIndex index2 = provider.nodeIndex( indexName2, LuceneIndexImplementation.EXACT_CONFIG );
index2.add( 1, map( key, "Mattias" ) );
provider.shutdown();
shutdownInserter();
}
// THEN -- both indexes should exist when starting up in "graph mode"
{
switchToGraphDatabaseService();
try ( Transaction transaction = db.beginTx() )
{
assertTrue( indexName1 + " should exist", db.index().existsForNodes( indexName1 ) );
assertTrue( indexName2 + " should exist", db.index().existsForNodes( indexName2 ) );
transaction.success();
}
}
}
private enum EdgeType implements RelationshipType
{
KNOWS
}
private void assertCacheContainsSomething( BatchInserterIndex index, String... keys )
{
Map>> cache = getIndexCache( index );
for ( String key : keys )
{
assertTrue( cache.get( key ).size() > 0 );
}
}
private void assertCacheIsEmpty( BatchInserterIndex index, String... keys )
{
Map>> cache = getIndexCache( index );
for ( String key : keys )
{
LruCache> keyCache = cache.get( key );
assertTrue( keyCache == null || keyCache.size() == 0 );
}
}
@SuppressWarnings( "unchecked" )
private Map>> getIndexCache( BatchInserterIndex index )
{
try
{
Field field = index.getClass().getDeclaredField( "cache" );
field.setAccessible( true );
return (Map>>) field.get( index );
}
catch ( Exception e )
{
throw launderedException( e );
}
}
@SuppressWarnings( "rawtypes" )
private Predicate super KernelExtensionFactory> onlyRealLuceneExtensions()
{
return extension -> !(extension instanceof InMemoryLabelScanStoreExtension ||
extension instanceof InMemoryIndexProviderFactory);
}
private void switchToGraphDatabaseService( ConfigurationParameter... config )
{
shutdownInserter();
GraphDatabaseBuilder builder = new TestGraphDatabaseFactory().newEmbeddedDatabaseBuilder( storeDir );
for ( ConfigurationParameter configurationParameter : config )
{
builder = builder.setConfig( configurationParameter.key, configurationParameter.value );
}
db = builder.newGraphDatabase();
}
private static ConfigurationParameter configure( Setting> key, String value )
{
return new ConfigurationParameter( key, value );
}
private static class ConfigurationParameter
{
private final Setting> key;
private final String value;
public ConfigurationParameter( Setting> key, String value )
{
this.key = key;
this.value = value;
}
}
private void shutdownInserter()
{
if ( inserter != null )
{
inserter.shutdown();
inserter = null;
}
}
}