All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.neo4j.index.impl.lucene.legacy.LuceneLegacyIndex Maven / Gradle / Ivy

Go to download

Integration layer between Neo4j and Lucene, providing one possible implementation of the Index API.

There is a newer version: 5.26.1
Show newest version
/*
 * 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.index.impl.lucene.legacy;

import org.apache.lucene.document.Document;
import org.apache.lucene.index.MultiReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.TermQuery;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.neo4j.collection.primitive.PrimitiveLongCollections;
import org.neo4j.collection.primitive.PrimitiveLongIterator;
import org.neo4j.collection.primitive.PrimitiveLongSet;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.PropertyContainer;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.index.Index;
import org.neo4j.graphdb.index.IndexHits;
import org.neo4j.index.lucene.QueryContext;
import org.neo4j.index.lucene.ValueContext;
import org.neo4j.kernel.api.LegacyIndex;
import org.neo4j.kernel.api.LegacyIndexHits;
import org.neo4j.kernel.api.impl.index.collector.DocValuesCollector;
import org.neo4j.kernel.impl.util.IoPrimitiveUtils;
import org.neo4j.kernel.spi.legacyindex.IndexCommandFactory;

import static org.neo4j.collection.primitive.Primitive.longSet;
import static org.neo4j.index.impl.lucene.legacy.EntityId.IdData;
import static org.neo4j.index.impl.lucene.legacy.EntityId.LongCostume;
import static org.neo4j.index.impl.lucene.legacy.EntityId.RelationshipData;

public abstract class LuceneLegacyIndex implements LegacyIndex
{
    static final String KEY_DOC_ID = "_id_";
    static final String KEY_START_NODE_ID = "_start_node_id_";
    static final String KEY_END_NODE_ID = "_end_node_id_";

    private static Set FORBIDDEN_KEYS = new HashSet<>( Arrays.asList(
            null, KEY_DOC_ID, KEY_START_NODE_ID, KEY_END_NODE_ID ) );

    protected final IndexIdentifier identifier;
    final IndexType type;

    protected final LuceneTransactionState transaction;
    private final LuceneDataSource dataSource;
    protected final IndexCommandFactory commandFactory;

    LuceneLegacyIndex( LuceneDataSource dataSource, IndexIdentifier identifier, LuceneTransactionState transaction,
            IndexType type, IndexCommandFactory commandFactory )
    {
        this.dataSource = dataSource;
        this.identifier = identifier;
        this.transaction = transaction;
        this.type = type;
        this.commandFactory = commandFactory;
    }

    /**
     * See {@link Index#add(PropertyContainer, String, Object)} for more generic
     * documentation.
     *
     * Adds key/value to the {@code entity} in this index. Added values are
     * searchable within the transaction, but composite {@code AND}
     * queries aren't guaranteed to return added values correctly within that
     * transaction. When the transaction has been committed all such queries
     * are guaranteed to return correct results.
     *
     * @param key the key in the key/value pair to associate with the entity.
     * @param value the value in the key/value pair to associate with the
     * entity.
     */
    @Override
    public void addNode( long entityId, String key, Object value )
    {
        assertValidKey( key );
        assertValidValue( value );
        EntityId entity = new IdData( entityId );
        for ( Object oneValue : IoPrimitiveUtils.asArray( value ) )
        {
            oneValue = getCorrectValue( oneValue );
            transaction.add( this, entity, key, oneValue );
            commandFactory.addNode( identifier.indexName, entityId, key, oneValue );
        }
    }

    protected Object getCorrectValue( Object value )
    {
        assertValidValue( value );
        Object result = value instanceof ValueContext
                ? ((ValueContext) value).getCorrectValue()
                : value.toString();
        assertValidValue( value );
        return result;
    }

    private static void assertValidKey( String key )
    {
        if ( FORBIDDEN_KEYS.contains( key ) )
        {
            throw new IllegalArgumentException( "Key " + key + " forbidden" );
        }
    }

    private static void assertValidValue( Object singleValue )
    {
        if ( singleValue == null )
        {
            throw new IllegalArgumentException( "Null value" );
        }
        if ( !(singleValue instanceof Number) && singleValue.toString() == null )
        {
            throw new IllegalArgumentException( "Value of type " + singleValue.getClass() + " has null toString" );
        }
    }

    /**
     * See {@link Index#remove(PropertyContainer, String, Object)} for more
     * generic documentation.
     *
     * Removes key/value to the {@code entity} in this index. Removed values
     * are excluded within the transaction, but composite {@code AND}
     * queries aren't guaranteed to exclude removed values correctly within
     * that transaction. When the transaction has been committed all such
     * queries are guaranteed to return correct results.
     *
     * @param entityId the entity (i.e {@link Node} or {@link Relationship})
     * to dissociate the key/value pair from.
     * @param key the key in the key/value pair to dissociate from the entity.
     * @param value the value in the key/value pair to dissociate from the
     * entity.
     */
    @Override
    public void remove( long entityId, String key, Object value )
    {
        assertValidKey( key );
        EntityId entity = new IdData( entityId );
        for ( Object oneValue : IoPrimitiveUtils.asArray( value ) )
        {
            oneValue = getCorrectValue( oneValue );
            transaction.remove( this, entity, key, oneValue );
            addRemoveCommand( entityId, key, oneValue );
        }
    }

    @Override
    public void remove( long entityId, String key )
    {
        assertValidKey( key );
        EntityId entity = new IdData( entityId );
        transaction.remove( this, entity, key );
        addRemoveCommand( entityId, key, null );
    }

    @Override
    public void remove( long entityId )
    {
        EntityId entity = new IdData( entityId );
        transaction.remove( this, entity );
        addRemoveCommand( entityId, null, null );
    }

    @Override
    public void drop()
    {
        transaction.delete( this );
    }

    @Override
    public LegacyIndexHits get( String key, Object value )
    {
        return query( type.get( key, value ), key, value, null );
    }

    /**
     * {@inheritDoc}
     *
     * {@code queryOrQueryObject} can be a {@link String} containing the query
     * in Lucene syntax format, http://lucene.apache.org/java/3_0_2/queryparsersyntax.html.
     * Or it can be a {@link Query} object. If can even be a {@link QueryContext}
     * object which can contain a query ({@link String} or {@link Query}) and
     * additional parameters, such as {@link Sort}.
     *
     * Because of performance issues, including uncommitted transaction modifications
     * in the result is disabled by default, but can be enabled using
     * {@link QueryContext#tradeCorrectnessForSpeed()}.
     */
    @Override
    public LegacyIndexHits query( String key, Object queryOrQueryObject )
    {
        QueryContext context = queryOrQueryObject instanceof QueryContext ?
                (QueryContext) queryOrQueryObject : null;
        return query( type.query( key, context != null ?
                context.getQueryOrQueryObject() : queryOrQueryObject, context ), null, null, context );
    }

    /**
     * {@inheritDoc}
     *
     * @see #query(String, Object)
     */
    @Override
    public LegacyIndexHits query( Object queryOrQueryObject )
    {
        return query( null, queryOrQueryObject );
    }

    protected LegacyIndexHits query( Query query, String keyForDirectLookup,
            Object valueForDirectLookup, QueryContext additionalParametersOrNull )
    {
        List simpleTransactionStateIds = new ArrayList<>();
        Collection removedIdsFromTransactionState = Collections.emptySet();
        IndexSearcher fulltextTransactionStateSearcher = null;
        if ( transaction != null )
        {
            if ( keyForDirectLookup != null )
            {
                simpleTransactionStateIds.addAll( transaction.getAddedIds( this, keyForDirectLookup, valueForDirectLookup ) );
            }
            else
            {
                fulltextTransactionStateSearcher = transaction.getAdditionsAsSearcher( this, additionalParametersOrNull );
            }
            removedIdsFromTransactionState = keyForDirectLookup != null ?
                    transaction.getRemovedIds( this, keyForDirectLookup, valueForDirectLookup ) :
                    transaction.getRemovedIds( this, query );
        }
        LegacyIndexHits idIterator = null;
        IndexReference searcher = null;
        dataSource.getReadLock();
        try
        {
            searcher = dataSource.getIndexSearcher( identifier );
        }
        finally
        {
            dataSource.releaseReadLock();
        }

        if ( searcher != null )
        {
            try
            {
                // Gather all added ids from fulltextTransactionStateSearcher and simpleTransactionStateIds.
                PrimitiveLongSet idsModifiedInTransactionState = gatherIdsModifiedInTransactionState(
                        simpleTransactionStateIds, fulltextTransactionStateSearcher, query );

                // Do the combined search from store and fulltext tx state
                DocToIdIterator hits = new DocToIdIterator( search( searcher, fulltextTransactionStateSearcher,
                        query, additionalParametersOrNull, removedIdsFromTransactionState ),
                        removedIdsFromTransactionState, searcher, idsModifiedInTransactionState );

                idIterator = simpleTransactionStateIds.isEmpty() ? hits : new CombinedIndexHits(
                        Arrays.asList( hits,
                                new ConstantScoreIterator( simpleTransactionStateIds, Float.NaN ) ) );
            }
            catch ( IOException e )
            {
                throw new RuntimeException( "Unable to query " + this + " with " + query, e );
            }
        }

        // We've only got transaction state
        idIterator = idIterator == null ? new ConstantScoreIterator( simpleTransactionStateIds, 0 ) : idIterator;
        return idIterator;
    }

    private PrimitiveLongSet gatherIdsModifiedInTransactionState( List simpleTransactionStateIds,
            IndexSearcher fulltextTransactionStateSearcher, Query query ) throws IOException
    {
        // If there's no state them don't bother gathering it
        if ( simpleTransactionStateIds.isEmpty() && fulltextTransactionStateSearcher == null )
        {
            return PrimitiveLongCollections.emptySet();
        }
        // There's potentially some state
        DocValuesCollector docValuesCollector = null;
        int fulltextSize = 0;
        if ( fulltextTransactionStateSearcher != null )
        {
            docValuesCollector = new DocValuesCollector();
            fulltextTransactionStateSearcher.search( query, docValuesCollector );
            fulltextSize = docValuesCollector.getTotalHits();
            // Nah, no state
            if ( simpleTransactionStateIds.isEmpty() && fulltextSize == 0 )
            {
                return PrimitiveLongCollections.emptySet();
            }
        }

        PrimitiveLongSet set = longSet( simpleTransactionStateIds.size() + fulltextSize );

        // Add from simple tx state
        for ( EntityId id : simpleTransactionStateIds )
        {
            set.add( id.id() );
        }

        if ( docValuesCollector != null )
        {
            // Add from fulltext tx state
            PrimitiveLongIterator valuesIterator = docValuesCollector.getValuesIterator( LuceneLegacyIndex.KEY_DOC_ID );
            while (valuesIterator.hasNext()) {
                set.add( valuesIterator.next() );
            }
        }
        return set;
    }

    private IndexHits search( IndexReference searcherRef, IndexSearcher fulltextTransactionStateSearcher,
            Query query, QueryContext additionalParametersOrNull, Collection removed ) throws IOException
    {
        if ( fulltextTransactionStateSearcher != null && !removed.isEmpty() )
        {
            letThroughAdditions( fulltextTransactionStateSearcher, query, removed );
        }

        IndexSearcher searcher = fulltextTransactionStateSearcher == null ? searcherRef.getSearcher() :
                new IndexSearcher( new MultiReader( searcherRef.getSearcher().getIndexReader(),
                        fulltextTransactionStateSearcher.getIndexReader() ) );
        IndexHits result;
        if ( additionalParametersOrNull != null && additionalParametersOrNull.getTop() > 0 )
        {
            result = new TopDocsIterator( query, additionalParametersOrNull, searcher );
        }
        else
        {
            Sort sorting = additionalParametersOrNull != null ?
                    additionalParametersOrNull.getSorting() : null;
            boolean forceScore = additionalParametersOrNull == null ||
                    !additionalParametersOrNull.getTradeCorrectnessForSpeed();
            DocValuesCollector collector = new DocValuesCollector( forceScore );
            searcher.search( query, collector );
            return collector.getIndexHits( sorting );
        }
        return result;
    }

    private void letThroughAdditions( IndexSearcher additionsSearcher, Query query, Collection removed )
            throws IOException
    {
        // This could be improved further by doing a term-dict lookup for every term in removed
        // and retaining only those that did not match.
        // This is getting quite low-level though
        DocValuesCollector collector = new DocValuesCollector( false );
        additionsSearcher.search( query, collector );
        PrimitiveLongIterator valuesIterator = collector.getValuesIterator( KEY_DOC_ID );
        LongCostume id = new LongCostume();
        while ( valuesIterator.hasNext() )
        {
            long value = valuesIterator.next();
            removed.remove( id.setId( value ) );
        }
    }

    IndexIdentifier getIdentifier()
    {
        return this.identifier;
    }

    protected abstract void addRemoveCommand( long entity, String key, Object value );

    static class NodeLegacyIndex extends LuceneLegacyIndex
    {
        NodeLegacyIndex( LuceneDataSource dataSource, IndexIdentifier identifier,
                LuceneTransactionState transaction, IndexType type, IndexCommandFactory commandFactory )
        {
            super( dataSource, identifier, transaction, type, commandFactory );
        }

        @Override
        public LegacyIndexHits get( String key, Object value, long startNode, long endNode )
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public LegacyIndexHits query( String key, Object queryOrQueryObject, long startNode, long endNode )
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public LegacyIndexHits query( Object queryOrQueryObject, long startNode, long endNode )
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public void addRelationship( long entity, String key, Object value, long startNode, long endNode )
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public void removeRelationship( long entity, String key, Object value, long startNode, long endNode )
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public void removeRelationship( long entity, String key, long startNode, long endNode )
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public void removeRelationship( long entity, long startNode, long endNode )
        {
            throw new UnsupportedOperationException();
        }

        @Override
        protected void addRemoveCommand( long entity, String key, Object value )
        {
            commandFactory.removeNode( identifier.indexName, entity, key, value );
        }
    }

    static class RelationshipLegacyIndex extends LuceneLegacyIndex
    {
        RelationshipLegacyIndex( LuceneDataSource dataSource, IndexIdentifier identifier,
                LuceneTransactionState transaction, IndexType type, IndexCommandFactory commandFactory )
        {
            super( dataSource, identifier, transaction, type, commandFactory );
        }

        @Override
        public void addRelationship( long entityId, String key, Object value, long startNode, long endNode )
        {
            assertValidKey( key );
            assertValidValue( value );
            RelationshipData entity = new RelationshipData( entityId, startNode, endNode );
            for ( Object oneValue : IoPrimitiveUtils.asArray( value ) )
            {
                oneValue = getCorrectValue( oneValue );
                transaction.add( this, entity, key, oneValue );
                commandFactory.addRelationship( identifier.indexName, entityId, key, oneValue, startNode, endNode );
            }
        }

        @Override
        public LegacyIndexHits get( String key, Object valueOrNull, long startNode, long endNode )
        {
            BooleanQuery.Builder builder = new BooleanQuery.Builder();
            if ( key != null && valueOrNull != null )
            {
                builder.add( type.get( key, valueOrNull ), Occur.MUST );
            }
            addIfAssigned( builder, startNode, KEY_START_NODE_ID );
            addIfAssigned( builder, endNode, KEY_END_NODE_ID );
            return query( builder.build(), null, null, null );
        }

        @Override
        protected void addRemoveCommand( long entity, String key, Object value )
        {
            commandFactory.removeRelationship( identifier.indexName, entity, key, value );
        }

        @Override
        public LegacyIndexHits query( String key, Object queryOrQueryObjectOrNull, long startNode, long endNode )
        {
            QueryContext context = queryOrQueryObjectOrNull != null &&
                    queryOrQueryObjectOrNull instanceof QueryContext ?
                            (QueryContext) queryOrQueryObjectOrNull : null;

            BooleanQuery.Builder builder = new BooleanQuery.Builder();
            if ( (context != null && context.getQueryOrQueryObject() != null) ||
                    (context == null && queryOrQueryObjectOrNull != null ) )
            {
                builder.add( type.query( key, context != null ?
                                              context.getQueryOrQueryObject() : queryOrQueryObjectOrNull, context ),
                        Occur.MUST );
            }
            addIfAssigned( builder, startNode, KEY_START_NODE_ID );
            addIfAssigned( builder, endNode, KEY_END_NODE_ID );
            return query( builder.build(), null, null, context );
        }

        @Override
        public void removeRelationship( long entityId, String key, Object value, long startNode, long endNode )
        {
            assertValidKey( key );
            RelationshipData entity = new RelationshipData( entityId, startNode, endNode );
            for ( Object oneValue : IoPrimitiveUtils.asArray( value ) )
            {
                oneValue = getCorrectValue( oneValue );
                transaction.remove( this, entity, key, oneValue );
                addRemoveCommand( entityId, key, oneValue );
            }
        }

        @Override
        public void removeRelationship( long entityId, String key, long startNode, long endNode )
        {
            assertValidKey( key );
            RelationshipData entity = new RelationshipData( entityId, startNode, endNode );
            transaction.remove( this, entity, key );
            addRemoveCommand( entityId, key, null );
        }

        @Override
        public void removeRelationship( long entityId, long startNode, long endNode )
        {
            RelationshipData entity = new RelationshipData( entityId, startNode, endNode );
            transaction.remove( this, entity );
            addRemoveCommand( entityId, null, null );
        }

        private static void addIfAssigned( BooleanQuery.Builder builder, long node, String field )
        {
            if ( node != -1 )
            {
                builder.add( new TermQuery( new Term( field, "" + node ) ), Occur.MUST );
            }
        }

        @Override
        public LegacyIndexHits query( Object queryOrQueryObjectOrNull, long startNode, long endNode )
        {
            return query( null, queryOrQueryObjectOrNull, startNode, endNode );
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy