org.neo4j.index.impl.lucene.legacy.TestLuceneIndex.java Source code

Java tutorial

Introduction

Here is the source code for org.neo4j.index.impl.lucene.legacy.TestLuceneIndex.java

Source

/*
 * 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 <http://www.gnu.org/licenses/>.
 */
package org.neo4j.index.impl.lucene.legacy;

import org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.QueryParser.Operator;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.NumericRangeQuery;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.SortedNumericSortField;
import org.apache.lucene.search.SortedSetSortField;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.similarities.DefaultSimilarity;
import org.hamcrest.CoreMatchers;
import org.junit.Ignore;
import org.junit.Test;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;

import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.NotFoundException;
import org.neo4j.graphdb.PropertyContainer;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.index.Index;
import org.neo4j.graphdb.index.IndexHits;
import org.neo4j.graphdb.index.IndexManager;
import org.neo4j.graphdb.index.RelationshipIndex;
import org.neo4j.graphdb.index.UniqueFactory;
import org.neo4j.helpers.collection.Iterables;
import org.neo4j.helpers.collection.Iterators;
import org.neo4j.helpers.collection.MapUtil;
import org.neo4j.index.lucene.QueryContext;
import org.neo4j.index.lucene.ValueContext;
import org.neo4j.kernel.impl.MyRelTypes;
import org.neo4j.kernel.impl.index.IndexConfigStore;
import org.neo4j.kernel.internal.GraphDatabaseAPI;

import static org.apache.lucene.search.NumericRangeQuery.newIntRange;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.isOneOf;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.neo4j.graphdb.RelationshipType.withName;
import static org.neo4j.helpers.collection.Iterators.asSet;
import static org.neo4j.helpers.collection.Iterators.count;
import static org.neo4j.helpers.collection.MapUtil.stringMap;
import static org.neo4j.index.Neo4jTestCase.assertContains;
import static org.neo4j.index.Neo4jTestCase.assertContainsInOrder;
import static org.neo4j.index.impl.lucene.legacy.LuceneIndexImplementation.EXACT_CONFIG;
import static org.neo4j.index.lucene.QueryContext.numericRange;
import static org.neo4j.index.lucene.ValueContext.numeric;

public class TestLuceneIndex extends AbstractLuceneIndexTest {
    @SuppressWarnings("unchecked")
    private <T extends PropertyContainer> void makeSureAdditionsCanBeRead(Index<T> index,
            EntityCreator<T> entityCreator) {
        String key = "name";
        String value = "Mattias";
        assertThat(index.get(key, value).getSingle(), is(nullValue()));
        assertThat(index.get(key, value), emptyIterable());

        assertThat(index.query(key, "*"), emptyIterable());

        T entity1 = entityCreator.create();
        T entity2 = entityCreator.create();
        index.add(entity1, key, value);
        for (int i = 0; i < 2; i++) {
            assertThat(index.get(key, value), Contains.contains(entity1));
            assertThat(index.query(key, "*"), Contains.contains(entity1));
            assertThat(index.get(key, value), Contains.contains(entity1));

            restartTx();
        }

        index.add(entity2, key, value);
        assertThat(index.get(key, value), Contains.contains(entity1, entity2));

        restartTx();
        assertThat(index.get(key, value), Contains.contains(entity1, entity2));
        index.delete();
    }

    @Test
    public void queryIndexWithSortByNumeric() throws Exception {
        Index<Node> index = nodeIndex(stringMap());
        String numericProperty = "NODE_ID";

        try (Transaction transaction = graphDb.beginTx()) {
            for (int i = 0; i < 15; i++) {
                Node node = graphDb.createNode();
                node.setProperty(numericProperty, i);
                index.add(node, numericProperty, new ValueContext(i).indexNumeric());
            }
            transaction.success();
        }

        try (Transaction transaction = graphDb.beginTx()) {
            QueryContext queryContext = new QueryContext(numericProperty + ":**");
            queryContext.sort(new Sort(new SortedNumericSortField(numericProperty, SortField.Type.INT, false)));
            IndexHits<Node> nodes = index.query(queryContext);

            int expectedIndexId = 0;
            for (Node node : nodes) {
                assertEquals("Nodes should be sorted by numeric property", expectedIndexId++,
                        node.getProperty(numericProperty));
            }
            transaction.success();
        }
    }

    @Test
    public void queryIndexWithSortByString() throws Exception {
        Index<Node> index = nodeIndex(stringMap());
        String stringProperty = "NODE_NAME";

        String[] names = new String[] { "Fry", "Leela", "Bender", "Amy", "Hubert", "Calculon" };
        try (Transaction transaction = graphDb.beginTx()) {
            for (String name : names) {
                Node node = graphDb.createNode();
                node.setProperty(stringProperty, name);
                index.add(node, stringProperty, name);
            }
            transaction.success();
        }

        try (Transaction transaction = graphDb.beginTx()) {
            QueryContext queryContext = new QueryContext(stringProperty + ":**");
            queryContext.sort(new Sort(new SortedSetSortField(stringProperty, true)));
            IndexHits<Node> nodes = index.query(queryContext);

            int nameIndex = 0;
            String[] sortedNames = new String[] { "Leela", "Hubert", "Fry", "Calculon", "Bender", "Amy" };
            for (Node node : nodes) {
                assertEquals("Nodes should be sorted by string property", sortedNames[nameIndex++],
                        node.getProperty(stringProperty));
            }
            transaction.success();
        }
    }

    @Test
    public void makeSureYouGetLatestTxModificationsInQueryByDefault() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.FULLTEXT_CONFIG);
        Node node = graphDb.createNode();
        index.add(node, "key", "value");
        assertThat(index.query("key:value"), Contains.contains(node));
    }

    @Test
    public void makeSureLuceneIndexesReportAsWriteable() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.FULLTEXT_CONFIG);
        Node node = graphDb.createNode();
        index.add(node, "key", "value");
        assertTrue(index.isWriteable());
    }

    @Test
    public void makeSureAdditionsCanBeReadNodeExact() {
        makeSureAdditionsCanBeRead(nodeIndex(LuceneIndexImplementation.EXACT_CONFIG), NODE_CREATOR);
    }

    @Test
    public void makeSureAdditionsCanBeReadNodeFulltext() {
        makeSureAdditionsCanBeRead(nodeIndex(LuceneIndexImplementation.FULLTEXT_CONFIG), NODE_CREATOR);
    }

    @Test
    public void makeSureAdditionsCanBeReadRelationshipExact() {
        makeSureAdditionsCanBeRead(relationshipIndex(LuceneIndexImplementation.EXACT_CONFIG), RELATIONSHIP_CREATOR);
    }

    @Test
    public void makeSureAdditionsCanBeReadRelationshipFulltext() {
        makeSureAdditionsCanBeRead(relationshipIndex(LuceneIndexImplementation.FULLTEXT_CONFIG),
                RELATIONSHIP_CREATOR);
    }

    @Test
    public void makeSureAdditionsCanBeRemovedInSameTx() {
        makeSureAdditionsCanBeRemoved(false);
    }

    @Test
    public void removingAnIndexedNodeWillAlsoRemoveItFromTheIndex() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node = graphDb.createNode();
        node.setProperty("poke", 1);
        index.add(node, "key", "value");
        commitTx();

        beginTx();
        node.delete();
        commitTx();

        beginTx();
        IndexHits<Node> nodes = index.get("key", "value");
        // IndexHits.size is allowed to be inaccurate in this case:
        assertThat(nodes.size(), isOneOf(0, 1));
        for (Node n : nodes) {
            n.getProperty("poke");
            fail("Found node " + n);
        }
        commitTx();

        beginTx();
        IndexHits<Node> nodesAgain = index.get("key", "value");
        // After a read, the index should be repaired:
        assertThat(nodesAgain.size(), is(0));
        for (Node n : nodesAgain) {
            n.getProperty("poke");
            fail("Found node " + n);
        }
    }

    @Test
    public void removingAnIndexedRelationshipWillAlsoRemoveItFromTheIndex() {
        Index<Relationship> index = relationshipIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node a = graphDb.createNode();
        Node b = graphDb.createNode();
        Relationship rel = a.createRelationshipTo(b, withName("REL"));
        rel.setProperty("poke", 1);
        index.add(rel, "key", "value");
        commitTx();

        beginTx();
        rel.delete();
        commitTx();

        beginTx();
        IndexHits<Relationship> rels = index.get("key", "value");
        // IndexHits.size is allowed to be inaccurate in this case:
        assertThat(rels.size(), isOneOf(0, 1));
        for (Relationship r : rels) {
            r.getProperty("poke");
            fail("Found relationship " + r);
        }
        commitTx();

        beginTx();
        IndexHits<Relationship> relsAgain = index.get("key", "value");
        // After a read, the index should be repaired:
        assertThat(relsAgain.size(), is(0));
        for (Relationship r : relsAgain) {
            r.getProperty("poke");
            fail("Found relationship " + r);
        }
    }

    @Test
    public void makeSureYouCanAskIfAnIndexExistsOrNot() {
        String name = currentIndexName();
        assertFalse(graphDb.index().existsForNodes(name));
        graphDb.index().forNodes(name);
        assertTrue(graphDb.index().existsForNodes(name));

        assertFalse(graphDb.index().existsForRelationships(name));
        graphDb.index().forRelationships(name);
        assertTrue(graphDb.index().existsForRelationships(name));
    }

    private void makeSureAdditionsCanBeRemoved(boolean restartTx) {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        String key = "name";
        String value = "Mattias";
        assertNull(index.get(key, value).getSingle());
        Node node = graphDb.createNode();
        index.add(node, key, value);
        if (restartTx) {
            restartTx();
        }
        assertEquals(node, index.get(key, value).getSingle());
        index.remove(node, key, value);
        assertNull(index.get(key, value).getSingle());
        restartTx();
        assertNull(index.get(key, value).getSingle());
        node.delete();
        index.delete();
    }

    @Test
    public void makeSureAdditionsCanBeRemoved() {
        makeSureAdditionsCanBeRemoved(true);
    }

    private void makeSureSomeAdditionsCanBeRemoved(boolean restartTx) {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        String key1 = "name";
        String key2 = "title";
        String value1 = "Mattias";
        assertNull(index.get(key1, value1).getSingle());
        assertNull(index.get(key2, value1).getSingle());
        Node node = graphDb.createNode();
        Node node2 = graphDb.createNode();
        index.add(node, key1, value1);
        index.add(node, key2, value1);
        index.add(node2, key1, value1);
        if (restartTx) {
            restartTx();
        }
        index.remove(node, key1, value1);
        index.remove(node, key2, value1);
        assertEquals(node2, index.get(key1, value1).getSingle());
        assertNull(index.get(key2, value1).getSingle());
        assertEquals(node2, index.get(key1, value1).getSingle());
        assertNull(index.get(key2, value1).getSingle());
        node.delete();
        index.delete();
    }

    @Test
    public void makeSureSomeAdditionsCanBeRemovedInSameTx() {
        makeSureSomeAdditionsCanBeRemoved(false);
    }

    @Test
    public void makeSureSomeAdditionsCanBeRemoved() {
        makeSureSomeAdditionsCanBeRemoved(true);
    }

    @Test
    public void makeSureThereCanBeMoreThanOneValueForAKeyAndEntity() {
        makeSureThereCanBeMoreThanOneValueForAKeyAndEntity(false);
    }

    @Test
    public void makeSureThereCanBeMoreThanOneValueForAKeyAndEntitySameTx() {
        makeSureThereCanBeMoreThanOneValueForAKeyAndEntity(true);
    }

    private void makeSureThereCanBeMoreThanOneValueForAKeyAndEntity(boolean restartTx) {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        String key = "name";
        String value1 = "Lucene";
        String value2 = "Index";
        String value3 = "Rules";
        assertThat(index.query(key, "*"), emptyIterable());
        Node node = graphDb.createNode();
        index.add(node, key, value1);
        index.add(node, key, value2);
        if (restartTx) {
            restartTx();
        }
        index.add(node, key, value3);
        for (int i = 0; i < 2; i++) {
            assertThat(index.get(key, value1), Contains.contains(node));
            assertThat(index.get(key, value2), Contains.contains(node));
            assertThat(index.get(key, value3), Contains.contains(node));
            assertThat(index.get(key, "whatever"), emptyIterable());
            restartTx();
        }
        index.delete();
    }

    @Test
    public void indexHitsFromQueryingRemovedDoesNotReturnNegativeCount() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node theNode = graphDb.createNode();
        index.remove(theNode);
        IndexHits<Node> hits = index.query("someRandomKey", theNode.getId());
        assertTrue(hits.size() >= 0);
    }

    @Test
    public void shouldNotGetLatestTxModificationsWhenChoosingSpeedQueries() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node = graphDb.createNode();
        index.add(node, "key", "value");
        QueryContext queryContext = new QueryContext("value").tradeCorrectnessForSpeed();
        assertThat(index.query("key", queryContext), emptyIterable());
        assertThat(index.query("key", "value"), Contains.contains(node));
    }

    @Test
    public void makeSureArrayValuesAreSupported() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        String key = "name";
        String value1 = "Lucene";
        String value2 = "Index";
        String value3 = "Rules";
        assertThat(index.query(key, "*"), emptyIterable());
        Node node = graphDb.createNode();
        index.add(node, key, new String[] { value1, value2, value3 });
        for (int i = 0; i < 2; i++) {
            assertThat(index.get(key, value1), Contains.contains(node));
            assertThat(index.get(key, value2), Contains.contains(node));
            assertThat(index.get(key, value3), Contains.contains(node));
            assertThat(index.get(key, "whatever"), emptyIterable());
            restartTx();
        }

        index.remove(node, key, new String[] { value2, value3 });

        for (int i = 0; i < 2; i++) {
            assertThat(index.get(key, value1), Contains.contains(node));
            assertThat(index.get(key, value2), emptyIterable());
            assertThat(index.get(key, value3), emptyIterable());
            restartTx();
        }
        index.delete();
    }

    @Test
    public void makeSureWildcardQueriesCanBeAsked() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        String key = "name";
        String value1 = "neo4j";
        String value2 = "nescafe";
        Node node1 = graphDb.createNode();
        Node node2 = graphDb.createNode();
        index.add(node1, key, value1);
        index.add(node2, key, value2);

        for (int i = 0; i < 2; i++) {
            assertThat(index.query(key, "neo*"), Contains.contains(node1));
            assertThat(index.query(key, "n?o4j"), Contains.contains(node1));
            assertThat(index.query(key, "ne*"), Contains.contains(node1, node2));
            assertThat(index.query(key + ":neo4j"), Contains.contains(node1));
            assertThat(index.query(key + ":neo*"), Contains.contains(node1));
            assertThat(index.query(key + ":n?o4j"), Contains.contains(node1));
            assertThat(index.query(key + ":ne*"), Contains.contains(node1, node2));

            restartTx();
        }
        index.delete();
    }

    @Test
    public void makeSureCompositeQueriesCanBeAsked() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node neo = graphDb.createNode();
        Node trinity = graphDb.createNode();
        index.add(neo, "username", "neo@matrix");
        index.add(neo, "sex", "male");
        index.add(trinity, "username", "trinity@matrix");
        index.add(trinity, "sex", "female");

        for (int i = 0; i < 2; i++) {
            assertThat(index.query("username:*@matrix AND sex:male"), Contains.contains(neo));
            assertThat(index.query(new QueryContext("username:*@matrix sex:male").defaultOperator(Operator.AND)),
                    Contains

                            .contains(neo));
            assertThat(index.query("username:*@matrix OR sex:male"), Contains.contains(neo, trinity));
            assertThat(index.query(new QueryContext("username:*@matrix sex:male").defaultOperator(Operator.OR)),
                    Contains.contains(neo, trinity));

            restartTx();
        }
        index.delete();
    }

    @SuppressWarnings("unchecked")
    private <T extends PropertyContainer> void doSomeRandomUseCaseTestingWithExactIndex(Index<T> index,
            EntityCreator<T> creator) {
        String name = "name";
        String mattias = "Mattias Persson";
        String title = "title";
        String hacker = "Hacker";

        assertThat(index.get(name, mattias), emptyIterable());

        T entity1 = creator.create();
        T entity2 = creator.create();

        assertNull(index.get(name, mattias).getSingle());
        index.add(entity1, name, mattias);
        assertThat(index.get(name, mattias), Contains.contains(entity1));

        assertContains(index.query(name, "\"" + mattias + "\""), entity1);
        assertContains(index.query("name:\"" + mattias + "\""), entity1);

        assertEquals(entity1, index.get(name, mattias).getSingle());

        assertContains(index.query("name", "Mattias*"), entity1);

        commitTx();

        beginTx();
        assertThat(index.get(name, mattias), Contains.contains(entity1));
        assertThat(index.query(name, "\"" + mattias + "\""), Contains.contains(entity1));
        assertThat(index.query("name:\"" + mattias + "\""), Contains.contains(entity1));
        assertEquals(entity1, index.get(name, mattias).getSingle());
        assertThat(index.query("name", "Mattias*"), Contains.contains(entity1));
        commitTx();

        beginTx();
        index.add(entity2, title, hacker);
        index.add(entity1, title, hacker);
        assertThat(index.get(name, mattias), Contains.contains(entity1));
        assertThat(index.get(title, hacker), Contains.contains(entity1, entity2));

        assertContains(index.query("name:\"" + mattias + "\" OR title:\"" + hacker + "\""), entity1, entity2);

        commitTx();

        beginTx();
        assertThat(index.get(name, mattias), Contains.contains(entity1));
        assertThat(index.get(title, hacker), Contains.contains(entity1, entity2));
        assertThat(index.query("name:\"" + mattias + "\" OR title:\"" + hacker + "\""),
                Contains.contains(entity1, entity2));
        assertThat(index.query("name:\"" + mattias + "\" AND title:\"" + hacker + "\""),
                Contains.contains(entity1));
        commitTx();

        beginTx();
        index.remove(entity2, title, hacker);
        assertThat(index.get(name, mattias), Contains.contains(entity1));
        assertThat(index.get(title, hacker), Contains.contains(entity1));

        assertContains(index.query("name:\"" + mattias + "\" OR title:\"" + hacker + "\""), entity1);

        commitTx();

        beginTx();
        assertThat(index.get(name, mattias), Contains.contains(entity1));
        assertThat(index.get(title, hacker), Contains.contains(entity1));
        assertThat(index.query("name:\"" + mattias + "\" OR title:\"" + hacker + "\""), Contains.contains(entity1));
        commitTx();

        beginTx();
        index.remove(entity1, title, hacker);
        index.remove(entity1, name, mattias);
        index.delete();
        commitTx();
    }

    @Test
    public void doSomeRandomUseCaseTestingWithExactNodeIndex() {
        doSomeRandomUseCaseTestingWithExactIndex(nodeIndex(LuceneIndexImplementation.EXACT_CONFIG), NODE_CREATOR);
    }

    @Test
    public void doSomeRandomUseCaseTestingWithExactRelationshipIndex() {
        doSomeRandomUseCaseTestingWithExactIndex(relationshipIndex(LuceneIndexImplementation.EXACT_CONFIG),
                RELATIONSHIP_CREATOR);
    }

    @SuppressWarnings("unchecked")
    private <T extends PropertyContainer> void doSomeRandomTestingWithFulltextIndex(Index<T> index,
            EntityCreator<T> creator) {
        T entity1 = creator.create();
        T entity2 = creator.create();

        String key = "name";
        index.add(entity1, key, "The quick brown fox");
        index.add(entity2, key, "brown fox jumped over");

        for (int i = 0; i < 2; i++) {
            assertThat(index.get(key, "The quick brown fox"), Contains.contains(entity1));
            assertThat(index.get(key, "brown fox jumped over"), Contains.contains(entity2));
            assertThat(index.query(key, "quick"), Contains.contains(entity1));
            assertThat(index.query(key, "brown"), Contains.contains(entity1, entity2));
            assertThat(index.query(key, "quick OR jumped"), Contains.contains(entity1, entity2));
            assertThat(index.query(key, "brown AND fox"), Contains.contains(entity1, entity2));

            restartTx();
        }

        index.delete();
    }

    @Test
    public void doSomeRandomTestingWithNodeFulltextInde() {
        doSomeRandomTestingWithFulltextIndex(nodeIndex(LuceneIndexImplementation.FULLTEXT_CONFIG), NODE_CREATOR);
    }

    @Test
    public void doSomeRandomTestingWithRelationshipFulltextInde() {
        doSomeRandomTestingWithFulltextIndex(relationshipIndex(LuceneIndexImplementation.FULLTEXT_CONFIG),
                RELATIONSHIP_CREATOR);
    }

    @Test
    public void testNodeLocalRelationshipIndex() {
        RelationshipIndex index = relationshipIndex(LuceneIndexImplementation.EXACT_CONFIG);

        RelationshipType type = withName("YO");
        Node startNode = graphDb.createNode();
        Node endNode1 = graphDb.createNode();
        Node endNode2 = graphDb.createNode();
        Relationship rel1 = startNode.createRelationshipTo(endNode1, type);
        Relationship rel2 = startNode.createRelationshipTo(endNode2, type);
        index.add(rel1, "name", "something");
        index.add(rel2, "name", "something");

        for (int i = 0; i < 2; i++) {
            assertThat(index.query("name:something"), Contains.contains(rel1, rel2));
            assertThat(index.query("name:something", null, endNode1), Contains.contains(rel1));
            assertThat(index.query("name:something", startNode, endNode2), Contains.contains(rel2));
            assertThat(index.query(null, startNode, endNode1), Contains.contains(rel1));
            assertThat(index.get("name", "something", null, endNode1), Contains.contains(rel1));
            assertThat(index.get("name", "something", startNode, endNode2), Contains.contains(rel2));
            assertThat(index.get(null, null, startNode, endNode1), Contains.contains(rel1));

            restartTx();
        }

        rel2.delete();
        rel1.delete();
        startNode.delete();
        endNode1.delete();
        endNode2.delete();
        index.delete();
    }

    @Test
    public void testSortByRelevance() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);

        Node node1 = graphDb.createNode();
        Node node2 = graphDb.createNode();
        Node node3 = graphDb.createNode();
        index.add(node1, "name", "something");
        index.add(node2, "name", "something");
        index.add(node2, "foo", "yes");
        index.add(node3, "name", "something");
        index.add(node3, "foo", "yes");
        index.add(node3, "bar", "yes");
        restartTx();

        IndexHits<Node> hits = index
                .query(new QueryContext("+name:something foo:yes bar:yes").sort(Sort.RELEVANCE));
        assertEquals(node3, hits.next());
        assertEquals(node2, hits.next());
        assertEquals(node1, hits.next());
        assertFalse(hits.hasNext());
        index.delete();
        node1.delete();
        node2.delete();
        node3.delete();
    }

    @Test
    public void testSorting() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        String name = "name";
        String title = "title";
        String other = "other";
        String sex = "sex";
        Node adam = graphDb.createNode();
        Node adam2 = graphDb.createNode();
        Node jack = graphDb.createNode();
        Node eva = graphDb.createNode();

        index.add(adam, name, "Adam");
        index.add(adam, title, "Software developer");
        index.add(adam, sex, "male");
        index.add(adam, other, "aaa");
        index.add(adam2, name, "Adam");
        index.add(adam2, title, "Blabla");
        index.add(adam2, sex, "male");
        index.add(adam2, other, "bbb");
        index.add(jack, name, "Jack");
        index.add(jack, title, "Apple sales guy");
        index.add(jack, sex, "male");
        index.add(jack, other, "ccc");
        index.add(eva, name, "Eva");
        index.add(eva, title, "Secretary");
        index.add(eva, sex, "female");
        index.add(eva, other, "ddd");

        for (int i = 0; i < 2; i++) {
            assertContainsInOrder(index.query(new QueryContext("name:*").sort(name, title)), adam2, adam, eva,
                    jack);
            assertContainsInOrder(index.query(new QueryContext("name:*").sort(name, other)), adam, adam2, eva,
                    jack);
            assertContainsInOrder(index.query(new QueryContext("name:*").sort(sex, title)), eva, jack, adam2, adam);
            assertContainsInOrder(index.query(name, new QueryContext("*").sort(sex, title)), eva, jack, adam2,
                    adam);
            assertContainsInOrder(index.query(new QueryContext("name:*").sort(name, title).top(2)), adam2, adam);

            restartTx();
        }
    }

    @Test
    public void testNumericValuesExactIndex() throws Exception {
        testNumericValues(nodeIndex(LuceneIndexImplementation.EXACT_CONFIG));
    }

    @Test
    public void testNumericValuesFulltextIndex() throws Exception {
        testNumericValues(nodeIndex(LuceneIndexImplementation.FULLTEXT_CONFIG));
    }

    private void testNumericValues(Index<Node> index) {
        Node node10 = graphDb.createNode();
        Node node6 = graphDb.createNode();
        Node node31 = graphDb.createNode();

        String key = "key";
        index.add(node10, key, numeric(10));
        index.add(node6, key, numeric(6));
        index.add(node31, key, numeric(31));

        for (int i = 0; i < 2; i++) {
            assertThat(index.query(NumericRangeQuery.newIntRange(key, 4, 40, true, true)),
                    Contains.contains(node10, node6, node31));
            assertThat(index.query(NumericRangeQuery.newIntRange(key, 6, 15, true, true)),
                    Contains.contains(node10, node6));
            assertThat(index.query(NumericRangeQuery.newIntRange(key, 6, 15, false, true)),
                    Contains.contains(node10));
            restartTx();
        }

        index.remove(node6, key, numeric(6));
        assertThat(index.query(NumericRangeQuery.newIntRange(key, 4, 40, true, true)),
                Contains.contains(node10, node31));
        restartTx();
        assertThat(index.query(NumericRangeQuery.newIntRange(key, 4, 40, true, true)),
                Contains.contains(node10, node31));
    }

    @Test
    public void testNumericValueArrays() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);

        Node node1 = graphDb.createNode();
        index.add(node1, "number", new ValueContext[] { numeric(45), numeric(98) });
        Node node2 = graphDb.createNode();
        index.add(node2, "number", new ValueContext[] { numeric(47), numeric(100) });

        IndexHits<Node> indexResult1 = index.query("number", newIntRange("number", 47, 98, true, true));
        assertThat(indexResult1, Contains.contains(node1, node2));
        assertThat(indexResult1.size(), is(2));

        IndexHits<Node> indexResult2 = index.query("number", newIntRange("number", 44, 46, true, true));
        assertThat(indexResult2, Contains.contains(node1));
        assertThat(indexResult2.size(), is(1));

        IndexHits<Node> indexResult3 = index.query("number", newIntRange("number", 99, 101, true, true));
        assertThat(indexResult3, Contains.contains(node2));
        assertThat(indexResult3.size(), is(1));

        IndexHits<Node> indexResult4 = index.query("number", newIntRange("number", 47, 98, false, false));
        assertThat(indexResult4, emptyIterable());

        IndexHits<Node> indexResult5 = index.query("number", numericRange("number", null, 98, true, true));
        assertContains(indexResult5, node1, node2);

        IndexHits<Node> indexResult6 = index.query("number", numericRange("number", 47, null, true, true));
        assertContains(indexResult6, node1, node2);
    }

    @Test
    public void testRemoveNumericValues() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node1 = graphDb.createNode();
        Node node2 = graphDb.createNode();
        String key = "key";
        index.add(node1, key, new ValueContext(15).indexNumeric());
        index.add(node2, key, new ValueContext(5).indexNumeric());
        index.remove(node1, key, new ValueContext(15).indexNumeric());

        assertThat(index.query(NumericRangeQuery.newIntRange(key, 0, 20, false, false)), Contains.contains(node2));

        index.remove(node2, key, new ValueContext(5).indexNumeric());

        assertThat(index.query(NumericRangeQuery.newIntRange(key, 0, 20, false, false)), emptyIterable());

        restartTx();
        assertThat(index.query(NumericRangeQuery.newIntRange(key, 0, 20, false, false)), emptyIterable());

        index.add(node1, key, new ValueContext(15).indexNumeric());
        index.add(node2, key, new ValueContext(5).indexNumeric());
        restartTx();
        assertThat(index.query(NumericRangeQuery.newIntRange(key, 0, 20, false, false)),
                Contains.contains(node1, node2));
        index.remove(node1, key, new ValueContext(15).indexNumeric());

        assertThat(index.query(NumericRangeQuery.newIntRange(key, 0, 20, false, false)), Contains.contains(node2));

        restartTx();
        assertThat(index.query(NumericRangeQuery.newIntRange(key, 0, 20, false, false)), Contains.contains(node2));
    }

    @Test
    public void sortNumericValues() throws Exception {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node1 = graphDb.createNode();
        Node node2 = graphDb.createNode();
        Node node3 = graphDb.createNode();
        String key = "key";
        index.add(node1, key, numeric(5));
        index.add(node2, key, numeric(15));
        index.add(node3, key, numeric(10));
        restartTx();

        assertContainsInOrder(index.query(numericRange(key, 5, 15).sortNumeric(key, false)), node1, node3, node2);
    }

    @Test
    public void testIndexNumberAsString() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node1 = graphDb.createNode();
        index.add(node1, "key", 10);

        for (int i = 0; i < 2; i++) {
            assertEquals(node1, index.get("key", 10).getSingle());
            assertEquals(node1, index.get("key", "10").getSingle());
            assertEquals(node1, index.query("key", 10).getSingle());
            assertEquals(node1, index.query("key", "10").getSingle());
            restartTx();
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void makeSureIndexGetsCreatedImmediately() {
        // Since index creation is done outside of the normal transactions,
        // a rollback will not roll back index creation.

        nodeIndex(LuceneIndexImplementation.FULLTEXT_CONFIG);
        assertTrue(graphDb.index().existsForNodes(currentIndexName()));
        rollbackTx();
        beginTx();
        assertTrue(graphDb.index().existsForNodes(currentIndexName()));
        nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        rollbackTx();
    }

    @Test
    public void makeSureFulltextConfigIsCaseInsensitiveByDefault() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.FULLTEXT_CONFIG);
        Node node = graphDb.createNode();
        String key = "name";
        String value = "Mattias Persson";
        index.add(node, key, value);
        for (int i = 0; i < 2; i++) {
            assertThat(index.query("name", "[A TO Z]"), Contains.contains(node));
            assertThat(index.query("name", "[a TO z]"), Contains.contains(node));
            assertThat(index.query("name", "Mattias"), Contains.contains(node));
            assertThat(index.query("name", "mattias"), Contains.contains(node));
            assertThat(index.query("name", "Matt*"), Contains.contains(node));
            assertThat(index.query("name", "matt*"), Contains.contains(node));
            restartTx();
        }
    }

    @Test
    public void makeSureFulltextIndexCanBeCaseSensitive() {
        Index<Node> index = nodeIndex(MapUtil.stringMap(new HashMap<>(LuceneIndexImplementation.FULLTEXT_CONFIG),
                "to_lower_case", "false"));
        Node node = graphDb.createNode();
        String key = "name";
        String value = "Mattias Persson";
        index.add(node, key, value);
        for (int i = 0; i < 2; i++) {
            assertThat(index.query("name", "[A TO Z]"), Contains.contains(node));
            assertThat(index.query("name", "[a TO z]"), emptyIterable());
            assertThat(index.query("name", "Matt*"), Contains.contains(node));
            assertThat(index.query("name", "matt*"), emptyIterable());
            assertThat(index.query("name", "Persson"), Contains.contains(node));
            assertThat(index.query("name", "persson"), emptyIterable());
            restartTx();
        }
    }

    @Test
    public void makeSureCustomAnalyzerCanBeUsed() {
        CustomAnalyzer.called = false;
        Index<Node> index = nodeIndex(MapUtil.stringMap(IndexManager.PROVIDER, "lucene", "analyzer",
                CustomAnalyzer.class.getName(), "to_lower_case", "true"));
        Node node = graphDb.createNode();
        String key = "name";
        String value = "The value";
        index.add(node, key, value);
        restartTx();
        assertTrue(CustomAnalyzer.called);
        assertThat(index.query(key, "[A TO Z]"), Contains.contains(node));
    }

    @Test
    public void makeSureCustomAnalyzerCanBeUsed2() {
        CustomAnalyzer.called = false;
        Index<Node> index = nodeIndex("w-custom-analyzer-2", MapUtil.stringMap(IndexManager.PROVIDER, "lucene",
                "analyzer", CustomAnalyzer.class.getName(), "to_lower_case", "true", "type", "fulltext"));
        Node node = graphDb.createNode();
        String key = "name";
        String value = "The value";
        index.add(node, key, value);
        restartTx();
        assertTrue(CustomAnalyzer.called);
        assertThat(index.query(key, "[A TO Z]"), Contains.contains(node));
    }

    @Test
    public void makeSureIndexNameAndConfigCanBeReachedFromIndex() {
        String indexName = "my-index-1";
        Index<Node> nodeIndex = nodeIndex(indexName, LuceneIndexImplementation.EXACT_CONFIG);
        assertEquals(indexName, nodeIndex.getName());
        assertEquals(LuceneIndexImplementation.EXACT_CONFIG, graphDb.index().getConfiguration(nodeIndex));

        String indexName2 = "my-index-2";
        Index<Relationship> relIndex = relationshipIndex(indexName2, LuceneIndexImplementation.FULLTEXT_CONFIG);
        assertEquals(indexName2, relIndex.getName());
        assertEquals(LuceneIndexImplementation.FULLTEXT_CONFIG, graphDb.index().getConfiguration(relIndex));
    }

    @Test
    public void testStringQueryVsQueryObject() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.FULLTEXT_CONFIG);
        Node node = graphDb.createNode();
        index.add(node, "name", "Mattias Persson");
        for (int i = 0; i < 2; i++) {
            assertContains(index.query("name:Mattias AND name:Per*"), node);
            assertContains(index.query("name:mattias"), node);
            assertContains(index.query(new TermQuery(new Term("name", "mattias"))), node);
            restartTx();
        }
        assertNull(index.query(new TermQuery(new Term("name", "Mattias"))).getSingle());
    }

    @SuppressWarnings("unchecked")
    private <T extends PropertyContainer> void testAbandonedIds(EntityCreator<T> creator, Index<T> index) {
        // TODO This doesn't actually test that they are deleted, it just triggers it
        // so that you manually can inspect what's going on
        T a = creator.create();
        T b = creator.create();
        T c = creator.create();
        String key = "name";
        String value = "value";
        index.add(a, key, value);
        index.add(b, key, value);
        index.add(c, key, value);
        restartTx();

        creator.delete(b);
        restartTx();

        Iterators.count((Iterator<Node>) index.get(key, value));
        rollbackTx();
        beginTx();

        Iterators.count((Iterator<Node>) index.get(key, value));
        index.add(c, "something", "whatever");
        restartTx();

        Iterators.count((Iterator<Node>) index.get(key, value));
    }

    @Test
    public void testAbandonedNodeIds() {
        testAbandonedIds(NODE_CREATOR, nodeIndex(LuceneIndexImplementation.EXACT_CONFIG));
    }

    @Test
    public void testAbandonedNodeIdsFulltext() {
        testAbandonedIds(NODE_CREATOR, nodeIndex(LuceneIndexImplementation.FULLTEXT_CONFIG));
    }

    @Test
    public void testAbandonedRelIds() {
        testAbandonedIds(RELATIONSHIP_CREATOR, relationshipIndex(LuceneIndexImplementation.EXACT_CONFIG));
    }

    @Test
    public void testAbandonedRelIdsFulltext() {
        testAbandonedIds(RELATIONSHIP_CREATOR, relationshipIndex(LuceneIndexImplementation.FULLTEXT_CONFIG));
    }

    @Test
    public void makeSureYouCanRemoveFromRelationshipIndex() {
        Node n1 = graphDb.createNode();
        Node n2 = graphDb.createNode();
        Relationship r = n1.createRelationshipTo(n2, withName("foo"));
        RelationshipIndex index = graphDb.index().forRelationships("rel-index");
        String key = "bar";
        index.remove(r, key, "value");
        index.add(r, key, "otherValue");
        for (int i = 0; i < 2; i++) {
            assertThat(index.get(key, "value"), emptyIterable());
            assertThat(index.get(key, "otherValue"), Contains.contains(r));
            restartTx();
        }
    }

    @Test
    public void makeSureYouCanGetEntityTypeFromIndex() {
        Index<Node> nodeIndex = nodeIndex(MapUtil.stringMap(IndexManager.PROVIDER, "lucene", "type", "exact"));
        Index<Relationship> relIndex = relationshipIndex(
                MapUtil.stringMap(IndexManager.PROVIDER, "lucene", "type", "exact"));
        assertEquals(Node.class, nodeIndex.getEntityType());
        assertEquals(Relationship.class, relIndex.getEntityType());
    }

    @Test
    public void makeSureConfigurationCanBeModified() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        try {
            graphDb.index().setConfiguration(index, IndexManager.PROVIDER, "something");
            fail("Shouldn't be able to modify provider");
        } catch (IllegalArgumentException e) {
            /* Good*/ }
        try {
            graphDb.index().removeConfiguration(index, IndexManager.PROVIDER);
            fail("Shouldn't be able to modify provider");
        } catch (IllegalArgumentException e) {
            /* Good*/ }

        String key = "my-key";
        String value = "my-value";
        String newValue = "my-new-value";
        assertNull(graphDb.index().setConfiguration(index, key, value));
        assertEquals(value, graphDb.index().getConfiguration(index).get(key));
        assertEquals(value, graphDb.index().setConfiguration(index, key, newValue));
        assertEquals(newValue, graphDb.index().getConfiguration(index).get(key));
        assertEquals(newValue, graphDb.index().removeConfiguration(index, key));
        assertNull(graphDb.index().getConfiguration(index).get(key));
    }

    @Test
    public void makeSureSlightDifferencesInIndexConfigCanBeSupplied() {
        Map<String, String> config = MapUtil.stringMap(IndexManager.PROVIDER, "lucene", "type", "fulltext");
        String name = currentIndexName();
        nodeIndex(name, config);
        nodeIndex(name, MapUtil.stringMap(new HashMap<>(config), "to_lower_case", "true"));
        try {
            nodeIndex(name, MapUtil.stringMap(new HashMap<>(config), "to_lower_case", "false"));
            fail("Shouldn't be able to get index with these kinds of differences in config");
        } catch (IllegalArgumentException e) {
            /* */ }
        nodeIndex(name, MapUtil.stringMap(new HashMap<>(config), "whatever", "something"));
    }

    @Test
    public void testScoring() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.FULLTEXT_CONFIG);
        Node node1 = graphDb.createNode();
        Node node2 = graphDb.createNode();
        String key = "text";
        // Where the heck did I get this sentence from?
        index.add(node1, key, "a time where no one was really awake");
        index.add(node2, key, "once upon a time there was");
        restartTx();

        IndexHits<Node> hits = index.query(key, new QueryContext("once upon a time was").sort(Sort.RELEVANCE));
        Node hit1 = hits.next();
        float score1 = hits.currentScore();
        Node hit2 = hits.next();
        float score2 = hits.currentScore();
        assertEquals(node2, hit1);
        assertEquals(node1, hit2);
        assertTrue("Score 1 (" + score1 + ") should have been higher than score 2 (" + score2 + ")",
                score1 > score2);
    }

    @Test
    public void testTopHits() {
        Index<Relationship> index = relationshipIndex(LuceneIndexImplementation.FULLTEXT_CONFIG);
        EntityCreator<Relationship> creator = RELATIONSHIP_CREATOR;
        String key = "text";
        Relationship rel1 = creator.create(key, "one two three four five six seven eight nine ten");
        Relationship rel2 = creator.create(key, "one two three four five six seven eight other things");
        Relationship rel3 = creator.create(key, "one two three four five six some thing else");
        Relationship rel4 = creator.create(key, "one two three four five what ever");
        Relationship rel5 = creator.create(key, "one two three four all that is good and bad");
        Relationship rel6 = creator.create(key, "one two three hill or something");
        Relationship rel7 = creator.create(key, "one two other time than this");
        index.add(rel2, key, rel2.getProperty(key));
        index.add(rel1, key, rel1.getProperty(key));
        index.add(rel3, key, rel3.getProperty(key));
        index.add(rel7, key, rel7.getProperty(key));
        index.add(rel5, key, rel5.getProperty(key));
        index.add(rel4, key, rel4.getProperty(key));
        index.add(rel6, key, rel6.getProperty(key));
        String query = "one two three four five six seven";

        for (int i = 0; i < 2; i++) {
            assertContainsInOrder(index.query(key, new QueryContext(query).top(3).sort(Sort.RELEVANCE)), rel1, rel2,
                    rel3);
            restartTx();
        }
    }

    @Test
    public void testSimilarity() {
        Index<Node> index = nodeIndex(MapUtil.stringMap(IndexManager.PROVIDER, "lucene", "type", "fulltext",
                "similarity", DefaultSimilarity.class.getName()));
        Node node = graphDb.createNode();
        index.add(node, "key", "value");
        restartTx();
        assertContains(index.get("key", "value"), node);
    }

    @Test
    public void testCombinedHitsSizeProblem() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node1 = graphDb.createNode();
        Node node2 = graphDb.createNode();
        Node node3 = graphDb.createNode();
        String key = "key";
        String value = "value";
        index.add(node1, key, value);
        index.add(node2, key, value);
        restartTx();
        index.add(node3, key, value);
        IndexHits<Node> hits = index.get(key, value);
        assertEquals(3, hits.size());
    }

    @SuppressWarnings("unchecked")
    private <T extends PropertyContainer> void testRemoveWithoutKey(EntityCreator<T> creator, Index<T> index)
            throws Exception {
        String key1 = "key1";
        String key2 = "key2";
        String value = "value";

        T entity1 = creator.create();
        index.add(entity1, key1, value);
        index.add(entity1, key2, value);
        T entity2 = creator.create();
        index.add(entity2, key1, value);
        index.add(entity2, key2, value);
        restartTx();

        assertContains(index.get(key1, value), entity1, entity2);
        assertContains(index.get(key2, value), entity1, entity2);
        index.remove(entity1, key2);
        assertContains(index.get(key1, value), entity1, entity2);
        assertContains(index.get(key2, value), entity2);
        index.add(entity1, key2, value);
        for (int i = 0; i < 2; i++) {
            assertContains(index.get(key1, value), entity1, entity2);
            assertContains(index.get(key2, value), entity1, entity2);
            restartTx();
        }
    }

    @Test
    public void testRemoveWithoutKeyNodes() throws Exception {
        testRemoveWithoutKey(NODE_CREATOR, nodeIndex(LuceneIndexImplementation.EXACT_CONFIG));
    }

    @Test
    public void testRemoveWithoutKeyRelationships() throws Exception {
        testRemoveWithoutKey(RELATIONSHIP_CREATOR, relationshipIndex(LuceneIndexImplementation.EXACT_CONFIG));
    }

    @SuppressWarnings("unchecked")
    private <T extends PropertyContainer> void testRemoveWithoutKeyValue(EntityCreator<T> creator, Index<T> index)
            throws Exception {
        String key1 = "key1";
        String value1 = "value1";
        String key2 = "key2";
        String value2 = "value2";

        T entity1 = creator.create();
        index.add(entity1, key1, value1);
        index.add(entity1, key2, value2);
        T entity2 = creator.create();
        index.add(entity2, key1, value1);
        index.add(entity2, key2, value2);
        restartTx();

        assertContains(index.get(key1, value1), entity1, entity2);
        assertContains(index.get(key2, value2), entity1, entity2);
        index.remove(entity1);
        assertContains(index.get(key1, value1), entity2);
        assertContains(index.get(key2, value2), entity2);
        index.add(entity1, key1, value1);

        for (int i = 0; i < 2; i++) {
            assertContains(index.get(key1, value1), entity1, entity2);
            assertContains(index.get(key2, value2), entity2);
            restartTx();
        }
    }

    @Test
    public void testRemoveWithoutKeyValueNodes() throws Exception {
        testRemoveWithoutKeyValue(NODE_CREATOR, nodeIndex(LuceneIndexImplementation.EXACT_CONFIG));
    }

    @Test
    public void testRemoveWithoutKeyValueRelationships() throws Exception {
        testRemoveWithoutKeyValue(RELATIONSHIP_CREATOR, relationshipIndex(LuceneIndexImplementation.EXACT_CONFIG));
    }

    @SuppressWarnings("unchecked")
    private <T extends PropertyContainer> void testRemoveWithoutKeyFulltext(EntityCreator<T> creator,
            Index<T> index) throws Exception {
        String key1 = "key1";
        String key2 = "key2";
        String value1 = "value one";
        String value2 = "other value";
        String value = "value";

        T entity1 = creator.create();
        index.add(entity1, key1, value1);
        index.add(entity1, key2, value1);
        index.add(entity1, key2, value2);
        T entity2 = creator.create();
        index.add(entity2, key1, value1);
        index.add(entity2, key2, value1);
        index.add(entity2, key2, value2);
        restartTx();

        assertContains(index.query(key1, value), entity1, entity2);
        assertContains(index.query(key2, value), entity1, entity2);
        index.remove(entity1, key2);
        assertContains(index.query(key1, value), entity1, entity2);
        assertContains(index.query(key2, value), entity2);
        index.add(entity1, key2, value1);
        for (int i = 0; i < 2; i++) {
            assertContains(index.query(key1, value), entity1, entity2);
            assertContains(index.query(key2, value), entity1, entity2);
            restartTx();
        }
    }

    @Test
    public void testRemoveWithoutKeyFulltextNode() throws Exception {
        testRemoveWithoutKeyFulltext(NODE_CREATOR, nodeIndex(LuceneIndexImplementation.FULLTEXT_CONFIG));
    }

    @Test
    public void testRemoveWithoutKeyFulltextRelationship() throws Exception {
        testRemoveWithoutKeyFulltext(RELATIONSHIP_CREATOR,
                relationshipIndex(LuceneIndexImplementation.FULLTEXT_CONFIG));
    }

    @SuppressWarnings("unchecked")
    private <T extends PropertyContainer> void testRemoveWithoutKeyValueFulltext(EntityCreator<T> creator,
            Index<T> index) throws Exception {
        String value = "value";
        String key1 = "key1";
        String value1 = value + " one";
        String key2 = "key2";
        String value2 = value + " two";

        T entity1 = creator.create();
        index.add(entity1, key1, value1);
        index.add(entity1, key2, value2);
        T entity2 = creator.create();
        index.add(entity2, key1, value1);
        index.add(entity2, key2, value2);
        restartTx();

        assertContains(index.query(key1, value), entity1, entity2);
        assertContains(index.query(key2, value), entity1, entity2);
        index.remove(entity1);
        assertContains(index.query(key1, value), entity2);
        assertContains(index.query(key2, value), entity2);
        index.add(entity1, key1, value1);
        for (int i = 0; i < 2; i++) {
            assertContains(index.query(key1, value), entity1, entity2);
            assertContains(index.query(key2, value), entity2);
            restartTx();
        }
    }

    @Test
    public void testRemoveWithoutKeyValueFulltextNode() throws Exception {
        testRemoveWithoutKeyValueFulltext(NODE_CREATOR, nodeIndex(LuceneIndexImplementation.FULLTEXT_CONFIG));
    }

    @Test
    public void testRemoveWithoutKeyValueFulltextRelationship() throws Exception {
        testRemoveWithoutKeyValueFulltext(RELATIONSHIP_CREATOR,
                relationshipIndex(LuceneIndexImplementation.FULLTEXT_CONFIG));
    }

    @Test
    public void testSortingWithTopHitsInPartCommittedPartLocal() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.FULLTEXT_CONFIG);
        Node first = graphDb.createNode();
        Node second = graphDb.createNode();
        Node third = graphDb.createNode();
        Node fourth = graphDb.createNode();
        String key = "key";

        index.add(third, key, "ccc");
        index.add(second, key, "bbb");
        restartTx();
        index.add(fourth, key, "ddd");
        index.add(first, key, "aaa");

        assertContainsInOrder(index.query(key, new QueryContext("*").sort(key)), first, second, third, fourth);
        assertContainsInOrder(index.query(key, new QueryContext("*").sort(key).top(2)), first, second);
    }

    @Test
    public void shouldNotFindValueDeletedInSameTx() {
        Index<Node> nodeIndex = graphDb.index().forNodes("size-after-removal");
        Node node = graphDb.createNode();
        nodeIndex.add(node, "key", "value");
        restartTx();

        nodeIndex.remove(node);
        for (int i = 0; i < 2; i++) {
            IndexHits<Node> hits = nodeIndex.get("key", "value");
            assertEquals(0, hits.size());
            assertNull(hits.getSingle());
            hits.close();
            restartTx();
        }
    }

    @Test
    public void notAbleToIndexWithForbiddenKey() throws Exception {
        Index<Node> index = graphDb.index().forNodes("check-for-null");
        Node node = graphDb.createNode();
        try {
            index.add(node, null, "not allowed");
            fail("Shouldn't be able to index something with null key");
        } catch (IllegalArgumentException e) { // OK
        }

        try {
            index.add(node, "_id_", "not allowed");
            fail("Shouldn't be able to index something with null key");
        } catch (IllegalArgumentException e) { // OK
        }
    }

    private Node createAndIndexNode(Index<Node> index, String key, String value) {
        Node node = graphDb.createNode();
        node.setProperty(key, value);
        index.add(node, key, value);
        return node;
    }

    @Test
    public void testRemoveNodeFromIndex() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        String key = "key";
        String value = "MYID";
        Node node = createAndIndexNode(index, key, value);
        index.remove(node);
        node.delete();

        Node node2 = createAndIndexNode(index, key, value);
        assertEquals(node2, index.get(key, value).getSingle());
    }

    @Test
    public void canQueryWithWildcardEvenIfAlternativeRemovalMethodsUsedInSameTx1() throws Exception {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node = graphDb.createNode();
        index.add(node, "key", "value");
        restartTx();
        index.remove(node, "key");
        assertNull(index.query("key", "v*").getSingle());
        assertNull(index.query("key", "*").getSingle());
    }

    @Test
    public void canQueryWithWildcardEvenIfAlternativeRemovalMethodsUsedInSameTx2() throws Exception {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node = graphDb.createNode();
        index.add(node, "key", "value");
        restartTx();
        index.remove(node);
        assertNull(index.query("key", "v*").getSingle());
        assertNull(index.query("key", "*").getSingle());
    }

    @Test
    public void updateIndex() throws Exception {
        String TEXT = "text";
        String NUMERIC = "numeric";
        String TEXT_1 = "text_1";

        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node n = graphDb.createNode();
        index.add(n, NUMERIC, new ValueContext(5).indexNumeric());
        index.add(n, TEXT, "text");
        index.add(n, TEXT_1, "text");
        commitTx();

        beginTx();
        assertNotNull(index.query(QueryContext.numericRange(NUMERIC, 5, 5, true, true)).getSingle());
        assertNotNull(index.get(TEXT_1, "text").getSingle());
        index.remove(n, TEXT, "text");
        index.add(n, TEXT, "text 1");
        commitTx();

        beginTx();
        assertNotNull(index.get(TEXT_1, "text").getSingle());
        assertNotNull(index.query(QueryContext.numericRange(NUMERIC, 5, 5, true, true)).getSingle());
    }

    @Test
    public void exactIndexWithCaseInsensitive() throws Exception {
        Index<Node> index = nodeIndex(stringMap("analyzer", LowerCaseKeywordAnalyzer.class.getName()));
        Node node = graphDb.createNode();
        index.add(node, "name", "Thomas Anderson");
        assertContains(index.query("name", "\"Thomas Anderson\""), node);
        assertContains(index.query("name", "\"thoMas ANDerson\""), node);
        restartTx();
        assertContains(index.query("name", "\"Thomas Anderson\""), node);
        assertContains(index.query("name", "\"thoMas ANDerson\""), node);
    }

    @Test
    public void exactIndexWithCaseInsensitiveWithBetterConfig() throws Exception {
        // START SNIPPET: exact-case-insensitive
        Index<Node> index = graphDb.index().forNodes("exact-case-insensitive",
                stringMap("type", "exact", "to_lower_case", "true"));
        Node node = graphDb.createNode();
        index.add(node, "name", "Thomas Anderson");
        assertContains(index.query("name", "\"Thomas Anderson\""), node);
        assertContains(index.query("name", "\"thoMas ANDerson\""), node);
        // END SNIPPET: exact-case-insensitive
        restartTx();
        assertContains(index.query("name", "\"Thomas Anderson\""), node);
        assertContains(index.query("name", "\"thoMas ANDerson\""), node);
    }

    @Test
    public void notAbleToRemoveWithForbiddenKey() throws Exception {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node = graphDb.createNode();
        index.add(node, "name", "Mattias");
        restartTx();
        try {
            index.remove(node, null);
            fail("Shouldn't be able to");
        } catch (IllegalArgumentException e) { // OK
        }
        try {
            index.remove(node, "_id_");
            fail("Shouldn't be able to");
        } catch (IllegalArgumentException e) { // OK
        }
    }

    @Ignore("an issue that should be fixed at some point")
    @Test(expected = NotFoundException.class)
    public void shouldNotBeAbleToIndexNodeThatIsNotCommitted() throws Exception {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node = graphDb.createNode();
        String key = "noob";
        String value = "Johan";

        WorkThread thread = new WorkThread("other thread", index, graphDb, node);
        thread.beginTransaction();
        try {
            thread.add(node, key, value);
        } finally {
            thread.rollback();
        }
    }

    @Test
    public void putIfAbsentSingleThreaded() {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node = graphDb.createNode();
        String key = "name";
        String value = "Mattias";
        String value2 = "Persson";
        assertNull(index.putIfAbsent(node, key, value));
        assertEquals(node, index.get(key, value).getSingle());
        assertNotNull(index.putIfAbsent(node, key, value));
        assertNull(index.putIfAbsent(node, key, value2));
        assertNotNull(index.putIfAbsent(node, key, value2));
        restartTx();
        assertNotNull(index.putIfAbsent(node, key, value));
        assertNotNull(index.putIfAbsent(node, key, value2));
        assertEquals(node, index.get(key, value).getSingle());
    }

    @Test
    public void putIfAbsentMultiThreaded() throws Exception {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node = graphDb.createNode();
        commitTx();
        String key = "name";
        String value = "Mattias";

        WorkThread t1 = new WorkThread("t1", index, graphDb, node);
        WorkThread t2 = new WorkThread("t2", index, graphDb, node);
        t1.beginTransaction();
        t2.beginTransaction();
        assertNull(t2.putIfAbsent(node, key, value).get());
        Future<Node> futurePut = t1.putIfAbsent(node, key, value);
        t1.waitUntilWaiting();
        t2.commit();
        assertNotNull(futurePut.get());
        t1.commit();
        t1.close();
        t2.close();

        try (Transaction transaction = graphDb.beginTx()) {
            assertEquals(node, index.get(key, value).getSingle());
        }
    }

    @Test
    public void putIfAbsentOnOtherValueInOtherThread() throws Exception {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node = graphDb.createNode();
        commitTx();
        String key = "name";
        String value = "Mattias";
        String otherValue = "Tobias";

        WorkThread t1 = new WorkThread("t1", index, graphDb, node);
        WorkThread t2 = new WorkThread("t2", index, graphDb, node);
        t1.beginTransaction();
        t2.beginTransaction();
        assertNull(t2.putIfAbsent(node, key, value).get());
        Future<Node> futurePut = t1.putIfAbsent(node, key, otherValue);
        t2.commit();
        assertNull(futurePut.get());
        t1.commit();
        t1.close();
        t2.close();

        try (Transaction transaction = graphDb.beginTx()) {
            assertEquals(node, index.get(key, value).getSingle());
            assertEquals(node, index.get(key, otherValue).getSingle());
        }
    }

    @Test
    public void putIfAbsentOnOtherKeyInOtherThread() throws Exception {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node = graphDb.createNode();
        commitTx();
        String key = "name";
        String otherKey = "friend";
        String value = "Mattias";

        WorkThread t1 = new WorkThread("t1", index, graphDb, node);
        WorkThread t2 = new WorkThread("t2", index, graphDb, node);
        t1.beginTransaction();
        t2.beginTransaction();
        assertNull(t2.putIfAbsent(node, key, value).get());
        assertNull(t1.putIfAbsent(node, otherKey, value).get());
        t2.commit();
        t1.commit();
        t1.close();
        t2.close();

        try (Transaction transaction = graphDb.beginTx()) {
            assertEquals(node, index.get(key, value).getSingle());
            assertEquals(node, index.get(otherKey, value).getSingle());
        }
    }

    @Test
    public void putIfAbsentShouldntBlockIfNotAbsent() throws Exception {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        Node node = graphDb.createNode();
        String key = "key";
        String value = "value";
        index.add(node, key, value);
        restartTx();

        WorkThread otherThread = new WorkThread("other thread", index, graphDb, node);
        otherThread.beginTransaction();

        // Should not grab lock
        index.putIfAbsent(node, key, value);

        // Should be able to complete right away
        assertNotNull(otherThread.putIfAbsent(node, key, value).get());

        otherThread.commit();
        commitTx();

        otherThread.close();
    }

    @Test
    public void getOrCreateNodeWithUniqueFactory() throws Exception {
        final String key = "name";
        final String value = "Mattias";
        final String property = "counter";

        final Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        final AtomicInteger counter = new AtomicInteger();
        UniqueFactory<Node> factory = new UniqueFactory.UniqueNodeFactory(index) {
            @Override
            protected void initialize(Node node, Map<String, Object> properties) {
                assertEquals(value, properties.get(key));
                assertEquals(1, properties.size());
                node.setProperty(property, counter.getAndIncrement());
            }
        };
        Node unique = factory.getOrCreate(key, value);

        assertNotNull(unique);
        assertEquals("not initialized", 0, unique.getProperty(property, null));
        assertEquals(unique, index.get(key, value).getSingle());

        assertEquals(unique, factory.getOrCreate(key, value));
        assertEquals("initialized more than once", 0, unique.getProperty(property));
        assertEquals(unique, index.get(key, value).getSingle());
        finishTx(false);
    }

    @Test
    public void getOrCreateRelationshipWithUniqueFactory() throws Exception {
        final String key = "name";
        final String value = "Mattias";

        final Node root = graphDb.createNode();
        final Index<Relationship> index = relationshipIndex(LuceneIndexImplementation.EXACT_CONFIG);
        final RelationshipType type = withName("SINGLE");
        UniqueFactory<Relationship> factory = new UniqueFactory.UniqueRelationshipFactory(index) {
            @Override
            protected Relationship create(Map<String, Object> properties) {
                assertEquals(value, properties.get(key));
                assertEquals(1, properties.size());
                return root.createRelationshipTo(graphDatabase().createNode(), type);
            }
        };

        Relationship unique = factory.getOrCreate(key, value);
        assertEquals(unique, root.getSingleRelationship(type, Direction.BOTH));
        assertNotNull(unique);

        assertEquals(unique, index.get(key, value).getSingle());

        assertEquals(unique, factory.getOrCreate(key, value));
        assertEquals(unique, root.getSingleRelationship(type, Direction.BOTH));
        assertEquals(unique, index.get(key, value).getSingle());

        finishTx(false);
    }

    @Test
    public void getOrCreateMultiThreaded() throws Exception {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        String key = "name";
        String value = "Mattias";

        WorkThread t1 = new WorkThread("t1", index, graphDb, null);
        WorkThread t2 = new WorkThread("t2", index, graphDb, null);
        t1.beginTransaction();
        t2.beginTransaction();
        Node node = t2.getOrCreate(key, value, 0).get();
        assertNotNull(node);
        assertEquals(0, t2.getProperty(node, key));
        Future<Node> futurePut = t1.getOrCreate(key, value, 1);
        t1.waitUntilWaiting();
        t2.commit();
        assertEquals(node, futurePut.get());
        assertEquals(0, t1.getProperty(node, key));
        t1.commit();

        assertEquals(node, index.get(key, value).getSingle());

        t1.close();
        t2.close();
    }

    @Test
    public void useStandardAnalyzer() throws Exception {
        Index<Node> index = nodeIndex(stringMap("analyzer", MyStandardAnalyzer.class.getName()));
        Node node = graphDb.createNode();
        index.add(node, "name", "Mattias");
    }

    @Test
    public void numericValueForGetInExactIndex() throws Exception {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);
        numericValueForGet(index);
    }

    @Test
    public void numericValueForGetInFulltextIndex() throws Exception {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.FULLTEXT_CONFIG);
        numericValueForGet(index);
    }

    private void numericValueForGet(Index<Node> index) {
        Node node = graphDb.createNode();
        long id = 100L;
        index.add(node, "name", ValueContext.numeric(id));
        assertEquals(node, index.get("name", ValueContext.numeric(id)).getSingle());
        restartTx();
        assertEquals(node, index.get("name", ValueContext.numeric(id)).getSingle());
    }

    @Test
    public void combinedNumericalQuery() throws Exception {
        Index<Node> index = nodeIndex(LuceneIndexImplementation.EXACT_CONFIG);

        Node node = graphDb.createNode();
        index.add(node, "start", ValueContext.numeric(10));
        index.add(node, "end", ValueContext.numeric(20));
        restartTx();

        BooleanQuery q = new BooleanQuery();
        q.add(LuceneUtil.rangeQuery("start", 9, null, true, true), Occur.MUST);
        q.add(LuceneUtil.rangeQuery("end", null, 30, true, true), Occur.MUST);
        assertContains(index.query(q), node);
    }

    @Test
    public void failureToCreateAnIndexShouldNotLeaveConfigurationBehind() throws Exception {
        // WHEN
        try {
            // PerFieldAnalyzerWrapper is invalid since it has no public no-arg constructor
            nodeIndex(stringMap("analyzer", PerFieldAnalyzerWrapper.class.getName()));
            fail("Should have failed");
        } catch (RuntimeException e) {
            assertThat(e.getMessage(), CoreMatchers.containsString(PerFieldAnalyzerWrapper.class.getName()));
        }

        // THEN - assert that there's no index config about this index left behind
        assertFalse("There should be no index config for index '" + currentIndexName() + "' left behind",
                ((GraphDatabaseAPI) graphDb).getDependencyResolver().resolveDependency(IndexConfigStore.class)
                        .has(Node.class, currentIndexName()));
    }

    @Test
    public void shouldBeAbleToQueryAllMatchingDocsAfterRemovingWithWildcard() throws Exception {
        // GIVEN
        Index<Node> index = nodeIndex(EXACT_CONFIG);
        Node node1 = graphDb.createNode();
        index.add(node1, "name", "Mattias");
        finishTx(true);
        beginTx();

        // WHEN
        index.remove(node1, "name");
        Set<Node> nodes = Iterables.asSet(index.query("*:*"));

        // THEN
        assertEquals(asSet(), nodes);
    }

    @Test
    public void shouldNotSeeDeletedRelationshipWhenQueryingWithStartAndEndNode() {
        // GIVEN
        RelationshipIndex index = relationshipIndex(EXACT_CONFIG);
        Node start = graphDb.createNode();
        Node end = graphDb.createNode();
        RelationshipType type = withName("REL");
        Relationship rel = start.createRelationshipTo(end, type);
        index.add(rel, "Type", type.name());
        finishTx(true);
        beginTx();

        // WHEN
        IndexHits<Relationship> hits = index.get("Type", type.name(), start, end);
        assertEquals(1, count(hits));
        assertEquals(1, hits.size());
        index.remove(rel);

        // THEN
        hits = index.get("Type", type.name(), start, end);
        assertEquals(0, count(hits));
        assertEquals(0, hits.size());
    }

    @Test
    public void shouldNotBeAbleToAddNullValuesToNodeIndex() throws Exception {
        // GIVEN
        Index<Node> index = nodeIndex(EXACT_CONFIG);

        // WHEN single null
        try {
            index.add(graphDb.createNode(), "key", null);
            fail("Should have failed");
        } catch (IllegalArgumentException e) {
            // THEN Good
        }

        // WHEN null in array
        try {
            index.add(graphDb.createNode(), "key", new String[] { "a", null, "c" });
            fail("Should have failed");
        } catch (IllegalArgumentException e) {
            // THEN Good
        }
    }

    @Test
    public void shouldNotBeAbleToAddNullValuesToRelationshipIndex() throws Exception {
        // GIVEN
        RelationshipIndex index = relationshipIndex(EXACT_CONFIG);

        // WHEN single null
        try {
            index.add(graphDb.createNode().createRelationshipTo(graphDb.createNode(), MyRelTypes.TEST), "key",
                    null);
            fail("Should have failed");
        } catch (IllegalArgumentException e) {
            // THEN Good
        }

        // WHEN null in array
        try {
            index.add(graphDb.createNode().createRelationshipTo(graphDb.createNode(), MyRelTypes.TEST), "key",
                    new String[] { "a", null, "c" });
            fail("Should have failed");
        } catch (IllegalArgumentException e) {
            // THEN Good
        }
    }
}