/*
* Copyright (c) 2002-2009 "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 Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.index.lucene;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Index;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.IndexWriter.MaxFieldLength;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.neo4j.graphdb.Node;
import org.neo4j.kernel.impl.transaction.xaframework.XaLogicalLog;
class LuceneFulltextTransaction extends LuceneTransaction
{
private final Map<String, DirectoryAndWorkers> fulltextIndexed =
new HashMap<String, DirectoryAndWorkers>();
private final Map<String, DirectoryAndWorkers> fulltextRemoved =
new HashMap<String, DirectoryAndWorkers>();
private static final String DOC_ALWAYS_HIT = "always_hit";
private static final String ALWAYS_HIT_VALUE = "1";
LuceneFulltextTransaction( int identifier, XaLogicalLog xaLog,
LuceneDataSource luceneDs )
{
super( identifier, xaLog, luceneDs );
}
private DirectoryAndWorkers getDirectory(
Map<String, DirectoryAndWorkers> map, String key )
{
DirectoryAndWorkers result = map.get( key );
if ( result == null )
{
Directory directory = new RAMDirectory();
try
{
IndexWriter writer = new IndexWriter( directory,
getDataSource().getAnalyzer(), true,
MaxFieldLength.UNLIMITED );
writer.close();
result = new DirectoryAndWorkers( directory );
}
catch ( IOException e )
{
throw new RuntimeException( e );
}
map.put( key, result );
}
return result;
}
private IndexWriter newIndexWriter( Directory directory )
throws IOException
{
return new IndexWriter( directory,
getDataSource().getAnalyzer(),
MaxFieldLength.UNLIMITED );
}
private void insertAndRemove( DirectoryAndWorkers insertTo,
DirectoryAndWorkers removeFrom, Node node, String key, Object value )
{
try
{
if ( node == null && value == null )
{
removeFrom.writer.deleteAll();
}
else
{
BooleanQuery deletionQuery = new BooleanQuery();
if ( value != null )
{
deletionQuery.add( new TermQuery( new Term( getDataSource().
getDeleteDocumentsKey(), value.toString() ) ),
Occur.MUST );
}
deletionQuery.add( new TermQuery( new Term(
LuceneIndexService.DOC_ID_KEY, "" + node.getId() ) ),
Occur.MUST );
removeFrom.writer.deleteDocuments( deletionQuery );
}
removeFrom.invalidateSearcher();
if ( node == null && value == null )
{
insertTo.all = true;
}
else
{
Document document = new Document();
this.getDataSource().fillDocument( document, node.getId(), key,
value != null ? value : "" );
if ( value == null )
{
document.add( new Field( DOC_ALWAYS_HIT, ALWAYS_HIT_VALUE,
Store.NO, Index.NOT_ANALYZED ) );
}
insertTo.writer.addDocument( document );
}
insertTo.invalidateSearcher();
}
catch ( IOException e )
{
throw new RuntimeException( e );
}
}
@Override
void index( Node node, String key, Object value )
{
super.index( node, key, value );
insertAndRemove( getDirectory( fulltextIndexed, key ),
getDirectory( fulltextRemoved, key ), node, key, value );
}
@Override
boolean hasModifications( String key )
{
return fulltextIndexed.containsKey( key ) || fulltextRemoved.containsKey( key );
}
@Override
boolean getIndexDeleted( String key )
{
return getDirectory( fulltextRemoved, key ).all;
}
@Override
void removeIndex( Node node, String key, Object value )
{
super.removeIndex( node, key, value );
insertAndRemove( getDirectory( fulltextRemoved, key ),
getDirectory( fulltextIndexed, key ), node, key, value );
}
@Override
Set<Long> getDeletedNodesFor( String key, Object value, Object matching )
{
return getNodes( getDirectory( fulltextRemoved, key ), key, value, matching,
true );
}
@Override
Set<Long> getNodesFor( String key, Object value, Object matching )
{
return getNodes( getDirectory( fulltextIndexed, key ), key, value, matching,
false );
}
private Set<Long> getNodes( DirectoryAndWorkers directory, String key,
Object value, Object matching, boolean includeAlwaysHit )
{
try
{
IndexSearcher searcher = directory.getSearcher();
Query query =
getDataSource().getIndexService().formQuery( key, value, matching );
if ( includeAlwaysHit )
{
BooleanQuery booleanQuery = new BooleanQuery();
booleanQuery.add( query, Occur.SHOULD );
booleanQuery.add( new TermQuery(
new Term( DOC_ALWAYS_HIT, ALWAYS_HIT_VALUE ) ),
Occur.SHOULD );
query = booleanQuery;
}
Hits hits = searcher.search( query );
HashSet<Long> result = new HashSet<Long>();
for ( int i = 0; i < hits.length(); i++ )
{
result.add( Long.parseLong( hits.doc( i ).getField(
LuceneIndexService.DOC_ID_KEY ).stringValue() ) );
}
return result;
}
catch ( IOException e )
{
throw new RuntimeException( e );
}
}
@Override
protected void doCommit()
{
for ( DirectoryAndWorkers directory :
this.fulltextIndexed.values() )
{
directory.close();
}
for ( DirectoryAndWorkers directory :
this.fulltextRemoved.values() )
{
directory.close();
}
super.doCommit();
}
private class DirectoryAndWorkers
{
private final Directory directory;
private final IndexWriter writer;
private IndexSearcher searcher;
private boolean all;
private DirectoryAndWorkers( Directory directory )
throws IOException
{
this.directory = directory;
this.writer = newIndexWriter( directory );
}
private void safeClose( Object object )
{
if ( object == null )
{
return;
}
try
{
if ( object instanceof IndexWriter )
{
( ( IndexWriter ) object ).close();
}
else if ( object instanceof IndexSearcher )
{
( ( IndexSearcher ) object ).close();
}
}
catch ( IOException e )
{
// Ok
}
}
private void invalidateSearcher()
{
safeClose( this.searcher );
this.searcher = null;
}
private void close()
{
safeClose( this.writer );
invalidateSearcher();
}
private IndexSearcher getSearcher()
{
try
{
if ( this.searcher == null )
{
this.writer.commit();
this.searcher = new IndexSearcher( directory, true );
}
}
catch ( IOException e )
{
throw new RuntimeException( e );
}
return this.searcher;
}
}
}