/*
* Copyright (C) 2010 eXo Platform SAS.
*
* This is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This software 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 Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this software; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
* site: http://www.fsf.org.
*/
package org.xcmis.search.lucene;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.lang.Validate;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.FieldDoc;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TopFieldDocs;
import org.xcmis.search.VisitException;
import org.xcmis.search.Visitors;
import org.xcmis.search.config.IndexConfiguration;
import org.xcmis.search.config.IndexConfigurationException;
import org.xcmis.search.config.SearchServiceConfiguration;
import org.xcmis.search.content.ContentEntry;
import org.xcmis.search.content.command.InvocationContext;
import org.xcmis.search.content.command.index.ModifyIndexCommand;
import org.xcmis.search.content.command.query.ExecuteSelectorCommand;
import org.xcmis.search.content.interceptors.QueryableIndexStorage;
import org.xcmis.search.lucene.content.VirtualTableResolver;
import org.xcmis.search.lucene.index.FieldNames;
import org.xcmis.search.lucene.index.IndexException;
import org.xcmis.search.lucene.index.IndexTransactionException;
import org.xcmis.search.lucene.index.LuceneIndexTransaction;
import org.xcmis.search.lucene.index.LuceneIndexer;
import org.xcmis.search.lucene.search.UUIDFieldSelector;
import org.xcmis.search.model.Limit;
import org.xcmis.search.model.constraint.Constraint;
import org.xcmis.search.model.operand.FullTextSearchScore;
import org.xcmis.search.model.operand.Length;
import org.xcmis.search.model.operand.LowerCase;
import org.xcmis.search.model.operand.NodeDepth;
import org.xcmis.search.model.operand.NodeLocalName;
import org.xcmis.search.model.operand.NodeName;
import org.xcmis.search.model.operand.PropertyValue;
import org.xcmis.search.model.operand.UpperCase;
import org.xcmis.search.model.ordering.Order;
import org.xcmis.search.model.ordering.Ordering;
import org.xcmis.search.result.ScoredRow;
import org.xcmis.search.value.NameConverter;
import org.xcmis.search.value.PathSplitter;
import org.xcmis.spi.utils.Logger;
/**
* Base implementation of Lucene based {@link QueryableIndexStorage}.
*
*/
public abstract class AbstractLuceneQueryableIndexStorage extends QueryableIndexStorage
{
/**
* Class logger.
*/
private static final Logger LOG = Logger.getLogger(AbstractLuceneQueryableIndexStorage.class);
/**
* The upper limit for the initial fetch size.
*/
protected static final int MAX_FETCH_SIZE = 32 * 1024;
/**
* Convert one Sting name to other String name.
*/
protected final NameConverter nameConverter;
/**
* Split path string to names.
*/
protected final PathSplitter pathSplitter;
/**
* Reselve selector names to lucene querys.
*/
protected final VirtualTableResolver tableResolver;
/**
* Node indexer.
*/
protected LuceneIndexer nodeIndexer;
/**
* Index configuration.
*/
protected IndexConfiguration indexConfuguration;
/**
* @param serviceConfiguration
* @throws IndexException
* @throws IndexConfigurationException
* @throws org.xcmis.search.lucene.index.IndexException
*/
public AbstractLuceneQueryableIndexStorage(SearchServiceConfiguration serviceConfiguration) throws IndexException
{
super(serviceConfiguration);
Validate.notNull(serviceConfiguration.getTableResolver(),
"The TableResolver may not be null in SearchServiceConfiguration");
Validate.notNull(serviceConfiguration.getNameConverter(),
"The NameConverter may not be null in SearchServiceConfiguration");
Validate.notNull(serviceConfiguration.getPathSplitter(),
"The PathSplitter may not be null in SearchServiceConfiguration");
Validate.notNull(serviceConfiguration.getIndexConfuguration(),
"The TableResolver argument may not be null in SearchServiceConfiguration");
Validate.notNull(serviceConfiguration.getIndexConfuguration().getRootParentUuid(),
"The RootParentUuid argument may not be null in IndexConfiguration");
Validate.notNull(serviceConfiguration.getIndexConfuguration().getRootUuid(),
"The RootUuid may not be null in IndexConfiguration");
Validate.notNull(serviceConfiguration.getIndexConfuguration().getTikaConfiguration(),
"The TikaConfiguration may not be null in IndexConfiguration");
this.tableResolver = serviceConfiguration.getTableResolver();
this.nameConverter = serviceConfiguration.getNameConverter();
this.pathSplitter = serviceConfiguration.getPathSplitter();
this.indexConfuguration = serviceConfiguration.getIndexConfuguration();
this.nodeIndexer = new LuceneIndexer(indexConfuguration);
}
public Query getConstrainQuery(Constraint constraint, Map<String, Object> bindVariablesValues)
throws VisitException, IndexException
{
LuceneQueryBuilder luceneQueryBuilder =
new LuceneQueryBuilder(getIndexReader(), nameConverter, pathSplitter, bindVariablesValues, indexConfuguration);
Visitors.visit(constraint, luceneQueryBuilder);
return luceneQueryBuilder.getQuery();
}
/**
* @see org.xcmis.search.content.interceptors.QueryableIndexStorage#visitExecuteSelectorCommand(org.xcmis.search.content.command.InvocationContext,
* org.xcmis.search.content.command.query.ExecuteSelectorCommand)
*/
@Override
public Object visitExecuteSelectorCommand(InvocationContext ctx, ExecuteSelectorCommand command) throws Throwable
{
List<ScoredRow> resultNodes = new ArrayList<ScoredRow>();
Query query = (Query)ctx.getTableResolver().resolve(command.getSelector().getName(), true);
if (command.getConstrains().size() > 0)
{
BooleanQuery booleanQuery = new BooleanQuery();
for (Constraint constrain : command.getConstrains())
{
booleanQuery.add(getConstrainQuery(constrain, command.getBindVariablesValues()), Occur.MUST);
}
booleanQuery.add(query, Occur.MUST);
query = booleanQuery;
}
// Open writer
IndexSearcher searcher = null;
try
{
// get result
IndexReader indexReader = getIndexReader();
if (indexReader != null)
{
searcher = new Searcher(indexReader);
// query
Limit limit = command.getLimit();
int hits = Math.min(MAX_FETCH_SIZE, limit.getOffset() + limit.getRowLimit());
TopFieldDocs topDocs = searcher.search(query, null, hits, getSort(command.getOrderings()));
resultNodes = new LinkedList<ScoredRow>();
for (int i = limit.getOffset(); i < topDocs.scoreDocs.length; i++)
{
// get identifiers
final Document doc = searcher.doc(topDocs.scoreDocs[i].doc, new UUIDFieldSelector());
final String id = doc.get(FieldNames.UUID);
final FieldDoc fieldDoc = (FieldDoc) topDocs.scoreDocs[i];
Float score = Float.NaN;
if(fieldDoc.fields[0] instanceof Float)
{
score = (Float) fieldDoc.fields[0];
}
resultNodes.add(new ScoredRow(command.getAlias().getName(), id, score));
}
}
}
catch (final CorruptIndexException e)
{
throw new IndexException(e.getLocalizedMessage(), e);
}
catch (final IOException e)
{
throw new IndexException(e.getLocalizedMessage(), e);
}
finally
{
try
{
if (searcher != null)
{
searcher.close();
}
}
catch (final IOException e)
{
throw new IndexException(e.getLocalizedMessage(), e);
}
}
return resultNodes;
}
/**
* @see org.xcmis.search.content.interceptors.QueryableIndexStorage#visitModifyIndexCommand(org.xcmis.search.content.command.InvocationContext,
* org.xcmis.search.content.command.index.ModifyIndexCommand)
*/
@Override
public Object visitModifyIndexCommand(InvocationContext ctx, ModifyIndexCommand command) throws Throwable
{
Map<String, Document> addedDocuments = new HashMap<String, Document>();
// indexing content
for (ContentEntry entry : command.getAddedDocuments())
{
addedDocuments.put(entry.getIdentifier(), nodeIndexer.createDocument(entry));
}
LuceneIndexTransaction indexTransaction =
new LuceneIndexTransaction(addedDocuments, command.getDeletedDocuments());
return save(indexTransaction);
}
/**
* Different lucene storage's should override this method.
*/
protected abstract IndexReader getIndexReader() throws IndexException;
/**
* @param indexTransaction
* @return
* @throws IndexTransactionException
* @throws IndexException
*/
protected abstract Object save(LuceneIndexTransaction indexTransaction) throws IndexException,
IndexTransactionException;
/**
* Return lucene sorter by list of orderings.
*
* @param list
* @return
* @throws VisitException
*/
private Sort getSort(List<Ordering> list) throws VisitException
{
if (list.size() > 0)
{
SortField[] fields = new SortField[list.size()];
SortFieldVisitor sortVisitor = new SortFieldVisitor();
int i = 0;
for (Ordering ordering : list)
{
Visitors.visitAll(ordering, sortVisitor);
fields[i++] = sortVisitor.getSortField();
}
return new Sort(fields);
}
return new Sort();
}
private class SortFieldVisitor extends Visitors.AbstractModelVisitor
{
private Order order;
private SortField sortField;
public SortField getSortField()
{
return sortField;
}
/**
* @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.FullTextSearchScore)
*/
@Override
public void visit(FullTextSearchScore node) throws VisitException
{
//By default result sorted score descendant
sortField = new SortField(null, SortField.SCORE, order == Order.ASCENDING);
}
/**
* @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.Length)
*/
@Override
public void visit(Length node) throws VisitException
{
throw new NotImplementedException();
}
/**
* @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.LowerCase)
*/
@Override
public void visit(LowerCase node) throws VisitException
{
throw new NotImplementedException();
}
/**
* @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.NodeDepth)
*/
@Override
public void visit(NodeDepth depth) throws VisitException
{
throw new NotImplementedException();
}
/**
* @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.NodeLocalName)
*/
@Override
public void visit(NodeLocalName node) throws VisitException
{
super.visit(node);
}
/**
* @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.NodeName)
*/
@Override
public void visit(NodeName node) throws VisitException
{
throw new NotImplementedException();
}
/**
*
* @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.ordering.Ordering)
*/
public void visit(Ordering node) throws VisitException
{
order = node.getOrder();
}
/**
* @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.PropertyValue)
*/
@Override
public void visit(PropertyValue node) throws VisitException
{
//AUTO mode was removed since version 3.0. Therefore we will used STRING type as sort type.
sortField =
new SortField(FieldNames.createPropertyFieldName(node.getPropertyName()),
SortField.STRING, order == Order.DESCENDING);
}
/**
* @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.UpperCase)
*/
@Override
public void visit(UpperCase node) throws VisitException
{
throw new NotImplementedException();
}
}
}