blob: 0276022f72895baf386cbc5b9423bc2719fdc963 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.oak.plugins.index.solr.query;
import java.io.IOException;
import java.util.*;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Queues;
import com.google.common.collect.Sets;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.oak.api.PropertyValue;
import org.apache.jackrabbit.oak.api.Result.SizePrecision;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
import org.apache.jackrabbit.oak.commons.json.JsopWriter;
import org.apache.jackrabbit.oak.plugins.index.search.FieldNames;
import org.apache.jackrabbit.oak.plugins.index.search.util.LMSEstimator;
import org.apache.jackrabbit.oak.plugins.index.solr.configuration.OakSolrConfiguration;
import org.apache.jackrabbit.oak.plugins.index.solr.configuration.OakSolrConfigurationProvider;
import org.apache.jackrabbit.oak.plugins.index.solr.configuration.SolrServerConfigurationProvider;
import org.apache.jackrabbit.oak.plugins.index.solr.configuration.nodestate.NodeStateSolrServerConfigurationProvider;
import org.apache.jackrabbit.oak.plugins.index.solr.configuration.nodestate.OakSolrNodeStateConfiguration;
import org.apache.jackrabbit.oak.plugins.index.solr.server.OakSolrServer;
import org.apache.jackrabbit.oak.plugins.index.solr.server.SolrServerProvider;
import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextExpression;
import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextTerm;
import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextVisitor;
import org.apache.jackrabbit.oak.spi.query.Cursor;
import org.apache.jackrabbit.oak.plugins.index.Cursors;
import org.apache.jackrabbit.oak.spi.query.Filter;
import org.apache.jackrabbit.oak.spi.query.IndexRow;
import org.apache.jackrabbit.oak.plugins.memory.PropertyValues;
import org.apache.jackrabbit.oak.spi.query.QueryConstants;
import org.apache.jackrabbit.oak.spi.query.QueryIndex;
import org.apache.jackrabbit.oak.spi.query.QueryIndex.FulltextQueryIndex;
import org.apache.jackrabbit.oak.spi.query.QueryLimits;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.SpellCheckResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.jackrabbit.oak.commons.PathUtils.*;
import static org.apache.jackrabbit.oak.plugins.index.solr.util.SolrIndexInitializer.isSolrIndexNode;
/**
* A Solr based {@link QueryIndex}
*/
public class SolrQueryIndex implements FulltextQueryIndex, QueryIndex.AdvanceFulltextQueryIndex {
public static final String TYPE = "solr";
static final String NATIVE_SOLR_QUERY = "native*solr";
static final String NATIVE_LUCENE_QUERY = "native*lucene";
private static double MIN_COST = 2.3;
private final Logger log = LoggerFactory.getLogger(SolrQueryIndex.class);
private final NodeAggregator aggregator;
private final OakSolrConfigurationProvider fallbackOakSolrConfigurationProvider;
private final SolrServerProvider fallbackSolrServerProvider;
private static final Map<String, LMSEstimator> estimators = new WeakHashMap<String, LMSEstimator>();
public SolrQueryIndex(NodeAggregator aggregator, OakSolrConfigurationProvider oakSolrConfigurationProvider, SolrServerProvider solrServerProvider) {
this.aggregator = aggregator;
this.fallbackOakSolrConfigurationProvider = oakSolrConfigurationProvider;
this.fallbackSolrServerProvider = solrServerProvider;
}
@Override
public double getMinimumCost() {
return MIN_COST;
}
@Override
public String getIndexName() {
return "solr";
}
@Override
public double getCost(Filter filter, NodeState root) {
throw new UnsupportedOperationException("Not supported as implementing AdvancedQueryIndex");
}
int getMatchingFilterRestrictions(Filter filter, OakSolrConfiguration configuration) {
int match = 0;
// full text expressions OR full text conditions defined
if (filter.getFullTextConstraint() != null || (filter.getFulltextConditions() != null
&& filter.getFulltextConditions().size() > 0)) {
match++; // full text queries have usually a significant recall
}
// property restriction OR native language property restriction defined AND property restriction handled
if (filter.getPropertyRestrictions() != null
&& filter.getPropertyRestrictions().size() > 0
&& (filter.getPropertyRestriction(NATIVE_SOLR_QUERY) != null
|| filter.getPropertyRestriction(NATIVE_LUCENE_QUERY) != null
|| configuration.useForPropertyRestrictions())
&& !hasIgnoredProperties(filter.getPropertyRestrictions(), configuration)) {
match++;
}
// path restriction defined AND path restrictions handled
if (filter.getPathRestriction() != null &&
!Filter.PathRestriction.NO_RESTRICTION.equals(filter.getPathRestriction())
&& configuration.useForPathRestrictions()) {
if (match > 0) {
match++;
}
}
// primary type restriction defined AND primary type restriction handled
if (filter.getPrimaryTypes().size() > 0 && configuration.useForPrimaryTypes()) {
if (match > 0) {
match++;
}
}
log.debug("{} matched restrictions for filter {} and configuration {}", match, filter, configuration);
return match;
}
private static boolean hasIgnoredProperties(Collection<Filter.PropertyRestriction> propertyRestrictions, OakSolrConfiguration configuration) {
for (Filter.PropertyRestriction pr : propertyRestrictions) {
if (isIgnoredProperty(pr, configuration)) {
return true;
}
}
return false;
}
@Override
public String getPlan(Filter filter, NodeState nodeState) {
throw new UnsupportedOperationException("Not supported as implementing AdvancedQueryIndex");
}
/**
* Get the set of relative paths of a full-text condition. For example, for
* the condition "contains(a/b, 'hello') and contains(c/d, 'world'), the set
* { "a", "c" } is returned. If there are no relative properties, then one
* entry is returned (the empty string). If there is no expression, then an
* empty set is returned.
*
* @param ft the full-text expression
* @return the set of relative paths (possibly empty)
*/
private static Set<String> getRelativePaths(FullTextExpression ft) {
final HashSet<String> relPaths = new HashSet<String>();
ft.accept(new FullTextVisitor.FullTextVisitorBase() {
@Override
public boolean visit(FullTextTerm term) {
String p = term.getPropertyName();
if (p == null) {
relPaths.add("");
} else if (p.startsWith("../") || p.startsWith("./")) {
throw new IllegalArgumentException("Relative parent is not supported:" + p);
} else if (getDepth(p) > 1) {
String parent = getParentPath(p);
relPaths.add(parent);
} else {
relPaths.add("");
}
return true;
}
});
return relPaths;
}
@Override
public Cursor query(final IndexPlan plan, final NodeState root) {
Cursor cursor;
try {
Filter filter = plan.getFilter();
final Set<String> relPaths = filter.getFullTextConstraint() != null ? getRelativePaths(filter.getFullTextConstraint())
: Collections.<String>emptySet();
final String parent = relPaths.size() == 0 ? "" : relPaths.iterator().next();
final int parentDepth = getDepth(parent);
String path = plan.getPlanName();
OakSolrConfiguration configuration = getConfiguration(path, root);
SolrClient solrServer = getServer(path, root);
LMSEstimator estimator = getEstimator(path);
AbstractIterator<SolrResultRow> iterator = getIterator(filter, plan, parent, parentDepth, configuration,
solrServer, estimator);
cursor = new SolrRowCursor(iterator, plan, filter.getQueryLimits(), estimator, solrServer, configuration);
} catch (Exception e) {
throw new RuntimeException(e);
}
return cursor;
}
private synchronized LMSEstimator getEstimator(String path) {
estimators.putIfAbsent(path, new LMSEstimator());
return estimators.get(path);
}
private SolrClient getServer(String path, NodeState root) {
NodeState node = root;
for (String name : PathUtils.elements(path)) {
node = node.getChildNode(name);
}
try {
if (isSolrIndexNode(node)) {
if (node.hasChildNode("server")) {
SolrServerConfigurationProvider solrServerConfigurationProvider = new NodeStateSolrServerConfigurationProvider(
node.getChildNode("server"));
return new OakSolrServer(solrServerConfigurationProvider);
} else {
return fallbackSolrServerProvider.getSearchingSolrServer();
}
} else if (node.exists()) {
log.warn("Cannot open Solr Index at path {} as the index is not of type 'solr'", path);
}
} catch (Exception e) {
log.error("Could not access the Solr index at " + path, e);
}
return null;
}
private AbstractIterator<SolrResultRow> getIterator(final Filter filter, final IndexPlan plan,
final String parent, final int parentDepth,
final OakSolrConfiguration configuration, final SolrClient solrServer,
final LMSEstimator estimator) {
return new AbstractIterator<SolrResultRow>() {
public Collection<FacetField> facetFields = new LinkedList<FacetField>();
private final Set<String> seenPaths = Sets.newHashSet();
private final Deque<SolrResultRow> queue = Queues.newArrayDeque();
private int offset = 0;
private boolean noDocs = false;
private long numFound = 0;
@Override
protected SolrResultRow computeNext() {
if (!queue.isEmpty() || loadDocs()) {
return queue.remove();
}
return endOfData();
}
private SolrResultRow convertToRow(SolrDocument doc) {
String path = String.valueOf(doc.getFieldValue(configuration.getPathField()));
if ("".equals(path)) {
path = "/";
}
if (!parent.isEmpty()) {
path = getAncestorPath(path, parentDepth);
// avoid duplicate entries
if (seenPaths.contains(path)) {
return null;
}
seenPaths.add(path);
}
float score = 0f;
Object scoreObj = doc.get("score");
if (scoreObj != null) {
score = (Float) scoreObj;
}
return new SolrResultRow(path, score, doc, facetFields);
}
/**
* Loads the Solr documents in batches
* @return true if any document is loaded
*/
private boolean loadDocs() {
if (noDocs) {
return false;
}
try {
if (log.isDebugEnabled()) {
log.debug("converting filter {}", filter);
}
SolrQuery query = FilterQueryParser.getQuery(filter, plan, configuration);
if (numFound > 0) {
long rows = configuration.getRows();
long maxQueries = numFound / 2;
if (maxQueries > configuration.getRows()) {
// adjust the rows to avoid making more than 3 Solr requests for this particular query
rows = maxQueries;
query.setParam("rows", String.valueOf(rows));
}
long newOffset = configuration.getRows() + offset * rows;
if (newOffset >= numFound) {
return false;
}
query.setParam("start", String.valueOf(newOffset));
offset++;
}
if (log.isDebugEnabled()) {
log.debug("sending query {}", query);
}
QueryResponse queryResponse = solrServer.query(query);
if (log.isDebugEnabled()) {
log.debug("getting response {}", queryResponse.getHeader());
}
SolrDocumentList docs = queryResponse.getResults();
if (docs != null) {
numFound = docs.getNumFound();
estimator.update(filter, numFound);
Map<String, Map<String, List<String>>> highlighting = queryResponse.getHighlighting();
for (SolrDocument doc : docs) {
// handle highlight
if (highlighting != null) {
Object pathObject = doc.getFieldValue(configuration.getPathField());
if (pathObject != null && highlighting.get(String.valueOf(pathObject)) != null) {
Map<String, List<String>> value = highlighting.get(String.valueOf(pathObject));
for (Map.Entry<String, List<String>> entry : value.entrySet()) {
// all highlighted values end up in 'rep:excerpt', regardless of field match
for (String v : entry.getValue()) {
doc.addField(QueryConstants.REP_EXCERPT, v);
}
}
}
}
SolrResultRow row = convertToRow(doc);
if (row != null) {
queue.add(row);
}
}
}
// get facets
List<FacetField> returnedFieldFacet = queryResponse.getFacetFields();
if (returnedFieldFacet != null) {
facetFields.addAll(returnedFieldFacet);
}
// filter facets on doc paths
if (!facetFields.isEmpty() && docs != null) {
for (SolrDocument doc : docs) {
String path = String.valueOf(doc.getFieldValue(configuration.getPathField()));
// if facet path doesn't exist for the calling user, filter the facet for this doc
for (FacetField ff : facetFields) {
if (!filter.isAccessible(path + "/" + ff.getName())) {
filterFacet(doc, ff);
}
}
}
}
// handle spellcheck
SpellCheckResponse spellCheckResponse = queryResponse.getSpellCheckResponse();
if (spellCheckResponse != null && spellCheckResponse.getSuggestions() != null &&
spellCheckResponse.getSuggestions().size() > 0) {
putSpellChecks(spellCheckResponse, queue, filter, configuration, solrServer);
noDocs = true;
}
// handle suggest
NamedList<Object> response = queryResponse.getResponse();
Map suggest = (Map) response.get("suggest");
if (suggest != null) {
Set<Map.Entry<String, Object>> suggestEntries = suggest.entrySet();
if (!suggestEntries.isEmpty()) {
putSuggestions(suggestEntries, queue, filter, configuration, solrServer);
noDocs = true;
}
}
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("query via {} failed.", solrServer, e);
}
}
return !queue.isEmpty();
}
};
}
private void filterFacet(SolrDocument doc, FacetField facetField) {
// facet filtering by value requires that the facet values match the stored values
// a *_facet field must exist, stored (or /w docValues) to be used for faceting and at filtering time
if (doc.getFieldNames().contains(facetField.getName())) {
// decrease facet value
Collection<Object> docFieldValues = doc.getFieldValues(facetField.getName());
if (docFieldValues != null) {
for (Object docFieldValue : docFieldValues) {
String valueString = String.valueOf(docFieldValue);
List<FacetField.Count> toRemove = new LinkedList<FacetField.Count>();
for (FacetField.Count count : facetField.getValues()) {
long existingCount = count.getCount();
if (valueString.equals(count.getName())) {
if (existingCount > 1) {
// decrease the count
count.setCount(existingCount - 1);
} else {
// remove the entire entry
toRemove.add(count);
}
}
}
for (FacetField.Count f : toRemove) {
assert facetField.getValues().remove(f);
}
}
}
}
}
private void putSpellChecks(SpellCheckResponse spellCheckResponse,
final Deque<SolrResultRow> queue,
Filter filter, OakSolrConfiguration configuration, SolrClient solrServer) throws IOException, SolrServerException {
List<SpellCheckResponse.Suggestion> suggestions = spellCheckResponse.getSuggestions();
Collection<String> alternatives = new ArrayList<String>(suggestions.size());
for (SpellCheckResponse.Suggestion suggestion : suggestions) {
alternatives.addAll(suggestion.getAlternatives());
}
// ACL filter spellcheck results
for (String alternative : alternatives) {
SolrQuery solrQuery = new SolrQuery();
solrQuery.setParam("q", alternative);
solrQuery.setParam("df", configuration.getCatchAllField());
solrQuery.setParam("q.op", "AND");
solrQuery.setParam("rows", "100");
QueryResponse suggestQueryResponse = solrServer.query(solrQuery);
SolrDocumentList results = suggestQueryResponse.getResults();
if (results != null && results.getNumFound() > 0) {
for (SolrDocument doc : results) {
if (filter.isAccessible(String.valueOf(doc.getFieldValue(configuration.getPathField())))) {
queue.add(new SolrResultRow(alternative));
break;
}
}
}
}
}
private void putSuggestions(Set<Map.Entry<String, Object>> suggestEntries, final Deque<SolrResultRow> queue,
Filter filter, OakSolrConfiguration configuration, SolrClient solrServer) throws IOException, SolrServerException {
Collection<SimpleOrderedMap<Object>> retrievedSuggestions = new HashSet<SimpleOrderedMap<Object>>();
for (Map.Entry<String, Object> suggester : suggestEntries) {
SimpleOrderedMap<Object> suggestionResponses = ((SimpleOrderedMap) suggester.getValue());
for (Map.Entry<String, Object> suggestionResponse : suggestionResponses) {
SimpleOrderedMap<Object> suggestionResults = ((SimpleOrderedMap) suggestionResponse.getValue());
for (Map.Entry<String, Object> suggestionResult : suggestionResults) {
if ("suggestions".equals(suggestionResult.getKey())) {
ArrayList<SimpleOrderedMap<Object>> suggestions = ((ArrayList<SimpleOrderedMap<Object>>) suggestionResult.getValue());
if (!suggestions.isEmpty()) {
for (SimpleOrderedMap<Object> suggestion : suggestions) {
retrievedSuggestions.add(suggestion);
}
}
}
}
}
}
// ACL filter suggestions
for (SimpleOrderedMap<Object> suggestion : retrievedSuggestions) {
SolrQuery solrQuery = new SolrQuery();
solrQuery.setParam("q", String.valueOf(suggestion.get("term")));
solrQuery.setParam("df", configuration.getCatchAllField());
solrQuery.setParam("q.op", "AND");
solrQuery.setParam("rows", "100");
QueryResponse suggestQueryResponse = solrServer.query(solrQuery);
SolrDocumentList results = suggestQueryResponse.getResults();
if (results != null && results.getNumFound() > 0) {
for (SolrDocument doc : results) {
if (filter.isAccessible(String.valueOf(doc.getFieldValue(configuration.getPathField())))) {
queue.add(new SolrResultRow(suggestion.get("term").toString(),
Double.parseDouble(suggestion.get("weight").toString())));
break;
}
}
}
}
}
static boolean isIgnoredProperty(Filter.PropertyRestriction property, OakSolrConfiguration configuration) {
if (NATIVE_LUCENE_QUERY.equals(property.propertyName) || NATIVE_SOLR_QUERY.equals(property.propertyName)) {
return false;
} else return (!configuration.useForPropertyRestrictions() // Solr index not used for properties
|| (configuration.getUsedProperties().size() > 0 && !configuration.getUsedProperties().contains(property.propertyName)) // not explicitly contained in the used properties
|| property.propertyName.contains("/") // no child-level property restrictions
|| QueryConstants.REP_EXCERPT.equals(property.propertyName) // rep:excerpt is not handled at the property level
|| QueryConstants.OAK_SCORE_EXPLANATION.equals(property.propertyName) // score explain is not handled at the property level
|| QueryConstants.REP_FACET.equals(property.propertyName) // rep:facet is not handled at the property level
|| QueryConstants.RESTRICTION_LOCAL_NAME.equals(property.propertyName)
|| property.propertyName.startsWith(QueryConstants.FUNCTION_RESTRICTION_PREFIX)
|| configuration.getIgnoredProperties().contains(property.propertyName));
}
@Override
public List<IndexPlan> getPlans(Filter filter, List<OrderEntry> sortOrder, NodeState rootState) {
Collection<String> indexPaths = new SolrIndexLookup(rootState).collectIndexNodePaths(filter);
List<IndexPlan> plans = Lists.newArrayListWithCapacity(indexPaths.size());
log.debug("looking for plans for paths : {}", indexPaths);
for (String path : indexPaths) {
OakSolrConfiguration configuration = getConfiguration(path, rootState);
SolrClient solrServer = getServer(path, rootState);
log.debug("building plan for server {} and configuration {}", solrServer, configuration);
// only provide the plan if both valid configuration and server exist
if (configuration != null && solrServer != null) {
LMSEstimator estimator = getEstimator(path);
IndexPlan plan = getIndexPlan(filter, configuration, estimator, sortOrder, path);
if (plan != null) {
plans.add(plan);
}
}
}
return plans;
}
private OakSolrConfiguration getConfiguration(String path, NodeState rootState) {
NodeState node = rootState;
for (String name : PathUtils.elements(path)) {
node = node.getChildNode(name);
}
try {
if (isSolrIndexNode(node)) {
if (node.hasChildNode("server")) {
return new OakSolrNodeStateConfiguration(node);
} else {
return fallbackOakSolrConfigurationProvider.getConfiguration();
}
} else if (node.exists()) {
log.warn("Cannot open Solr Index at path {} as the index is not of type 'solr'", path);
}
} catch (Exception e) {
log.error("Could not access the Solr index at " + path, e);
}
return null;
}
private IndexPlan getIndexPlan(Filter filter, OakSolrConfiguration configuration, LMSEstimator estimator,
List<OrderEntry> sortOrder, String path) {
if (getMatchingFilterRestrictions(filter, configuration) > 0) {
// we can't order by functions
// so remove those entries from the plan's sort order
ArrayList<OrderEntry> sortOrder2 = new ArrayList<>();
if (sortOrder != null) {
for (OrderEntry e : sortOrder) {
if (!e.getPropertyName().startsWith(FieldNames.FUNCTION_PREFIX)) {
sortOrder2.add(e);
}
}
}
IndexPlan indexPlan = planBuilder(filter)
.setEstimatedEntryCount(estimator.estimate(filter))
.setSortOrder(sortOrder2)
.setPlanName(path)
.setPathPrefix(getPathPrefix(path))
.build();
log.debug("index plan {}", indexPlan);
return indexPlan;
} else {
return null;
}
}
private String getPathPrefix(String indexPath) {
// 2 = /oak:index/<index name>
String parentPath = PathUtils.getAncestorPath(indexPath, 2);
return PathUtils.denotesRoot(parentPath) ? "" : parentPath;
}
private IndexPlan.Builder planBuilder(Filter filter) {
return new IndexPlan.Builder()
.setCostPerExecution(1.5) // disk I/O + network I/O
.setCostPerEntry(0.3) // with properly configured SolrCaches ~70% of the doc fetches should hit them
.setFilter(filter)
.setFulltextIndex(true)
.setIncludesNodeData(true) // we currently include node data
.setDelayed(true); //Solr is most usually async
}
@Override
public String getPlanDescription(IndexPlan plan, NodeState root) {
return plan.toString();
}
@Override
public Cursor query(Filter filter, NodeState rootState) {
throw new UnsupportedOperationException("Not supported as implementing AdvancedQueryIndex");
}
static class SolrResultRow {
final String path;
final double score;
final SolrDocument doc;
final Collection<FacetField> facetFields;
final String suggestion;
private SolrResultRow(String path, double score, SolrDocument doc, String suggestion, Collection<FacetField> facetFields) {
this.path = path;
this.score = score;
this.doc = doc;
this.suggestion = suggestion;
this.facetFields = facetFields;
}
SolrResultRow(String suggestion, double score) {
this("/", score, null, suggestion, null);
}
SolrResultRow(String suggestion) {
this("/", 1.0, null, suggestion, null);
}
SolrResultRow(String path, float score, SolrDocument doc, Collection<FacetField> facetFields) {
this(path, score, doc, null, facetFields);
}
@Override
public String toString() {
return String.format("%s (%1.2f)", path, score);
}
}
/**
* A cursor over Solr results. The result includes the path and the jcr:score pseudo-property as returned by Solr,
* plus, eventually, the returned stored values if {@link org.apache.solr.common.SolrDocument} is included in the
* {@link org.apache.jackrabbit.oak.plugins.index.solr.query.SolrQueryIndex.SolrResultRow}.
*/
private class SolrRowCursor implements Cursor {
private final Cursor pathCursor;
private final IndexPlan plan;
private final LMSEstimator estimator;
private final SolrClient solrServer;
private final OakSolrConfiguration configuration;
SolrResultRow currentRow;
SolrRowCursor(final Iterator<SolrResultRow> it, IndexPlan plan, QueryLimits settings,
LMSEstimator estimator, SolrClient solrServer, OakSolrConfiguration configuration) {
this.estimator = estimator;
this.solrServer = solrServer;
this.configuration = configuration;
Iterator<String> pathIterator = new Iterator<String>() {
@Override
public boolean hasNext() {
return it.hasNext();
}
@Override
public String next() {
currentRow = it.next();
String path = currentRow.path;
if (configuration.collapseJcrContentParents() && path.endsWith(JcrConstants.JCR_CONTENT)) {
return PathUtils.getParentPath(path);
} else {
return path;
}
}
@Override
public void remove() {
it.remove();
}
};
this.plan = plan;
this.pathCursor = new Cursors.PathCursor(pathIterator, false, settings);
}
@Override
public boolean hasNext() {
return pathCursor.hasNext();
}
@Override
public void remove() {
pathCursor.remove();
}
@Override
public IndexRow next() {
final IndexRow pathRow = pathCursor.next();
return new IndexRow() {
@Override
public boolean isVirtualRow() {
return currentRow.doc == null;
}
@Override
public String getPath() {
String sub = pathRow.getPath();
if (isVirtualRow()) {
return sub;
} else if (PathUtils.isAbsolute(sub)) {
return plan.getPathPrefix() + sub;
} else {
return PathUtils.concat(plan.getPathPrefix(), sub);
}
}
@Override
public PropertyValue getValue(String columnName) {
// overlay the score
if (QueryConstants.JCR_SCORE.equals(columnName)) {
return PropertyValues.newDouble(currentRow.score);
}
if (columnName.startsWith(QueryConstants.REP_FACET)) {
String facetFieldName = columnName.substring(QueryConstants.REP_FACET.length() + 1, columnName.length() - 1);
FacetField facetField = null;
for (FacetField ff : currentRow.facetFields) {
if (ff.getName().equals(facetFieldName + "_facet")) {
facetField = ff;
break;
}
}
if (facetField != null) {
JsopWriter writer = new JsopBuilder();
writer.object();
for (FacetField.Count count : facetField.getValues()) {
writer.key(count.getName()).value(count.getCount());
}
writer.endObject();
return PropertyValues.newString(writer.toString());
} else {
return null;
}
}
if (QueryConstants.REP_SPELLCHECK.equals(columnName) || QueryConstants.REP_SUGGEST.equals(columnName)) {
return PropertyValues.newString(currentRow.suggestion);
}
Collection<Object> fieldValues = currentRow.doc.getFieldValues(columnName);
String value;
if (fieldValues != null && fieldValues.size() > 0) {
if (fieldValues.size() > 1) {
value = Iterables.toString(fieldValues);
} else {
Object fieldValue = currentRow.doc.getFieldValue(columnName);
if (fieldValue != null) {
value = fieldValue.toString();
} else {
return null;
}
}
} else {
value = Iterables.toString(Collections.emptyList());
}
return PropertyValues.newString(value);
}
};
}
@Override
public long getSize(SizePrecision precision, long max) {
long estimate = -1;
switch (precision) {
case EXACT:
// query solr
SolrQuery countQuery = FilterQueryParser.getQuery(plan.getFilter(), plan, this.configuration);
countQuery.setRows(0);
try {
estimate = this.solrServer.query(countQuery).getResults().getNumFound();
} catch (IOException | SolrServerException e) {
log.warn("could not perform count query {}", countQuery);
}
break;
case APPROXIMATION:
// estimate result size
estimate = this.estimator.estimate(plan.getFilter());
break;
case FAST_APPROXIMATION:
// use already computed index plan's estimate
estimate = plan.getEstimatedEntryCount();
break;
}
return Math.min(estimate, max);
}
}
@Override
@Nullable
public NodeAggregator getNodeAggregator() {
return aggregator;
}
}