blob: 51f2dcc1f8107c3e070d2fa5d0b3ee3ad9b67a8d [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.solr.search.join;
import java.io.IOException;
import java.util.Objects;
import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryVisitor;
import org.apache.lucene.search.Weight;
import org.apache.lucene.search.join.JoinUtil;
import org.apache.lucene.search.join.ScoreMode;
import org.apache.solr.cloud.ZkController;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.Aliases;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.LocalSolrQueryRequest;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.search.JoinQParserPlugin;
import org.apache.solr.search.QParser;
import org.apache.solr.search.QParserPlugin;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.search.SyntaxError;
import org.apache.solr.uninverting.UninvertingReader;
import org.apache.solr.util.RefCounted;
/**
* Create a query-time join query with scoring.
* It just calls {@link JoinUtil#createJoinQuery(String, boolean, String, Query, org.apache.lucene.search.IndexSearcher, ScoreMode)}.
* It runs subordinate query and collects values of "from" field and scores, then it lookups these collected values in "to" field, and
* yields aggregated scores.
* Local parameters are similar to {@link JoinQParserPlugin} <a href="http://wiki.apache.org/solr/Join">{!join}</a>
* This plugin doesn't have own name, and is called by specifying local parameter <code>{!join score=...}...</code>.
* Note: this parser is invoked even if you specify <code>score=none</code>.
* <br>Example:<code>q={!join from=manu_id_s to=id score=total}foo</code>
* <ul>
* <li>from - "foreign key" field name to collect values while enumerating subordinate query (denoted as <code>foo</code> in example above).
* it's better to have this field declared as <code>type="string" docValues="true"</code>.
* note: if <a href="http://wiki.apache.org/solr/DocValues">docValues</a> are not enabled for this field, it will work anyway,
* but it costs some memory for {@link UninvertingReader}.
* Also, numeric doc values are not supported until <a href="https://issues.apache.org/jira/browse/LUCENE-5868">LUCENE-5868</a>.
* Thus, it only supports {@link DocValuesType#SORTED}, {@link DocValuesType#SORTED_SET}, {@link DocValuesType#BINARY}. </li>
* <li>fromIndex - optional parameter, a core name where subordinate query should run (and <code>from</code> values are collected) rather than current core.
* <br>Example:<code>q={!join from=manu_id_s to=id score=total fromIndex=products}foo</code>
* <li>to - "primary key" field name which is searched for values collected from subordinate query.
* it should be declared as <code>indexed="true"</code>. Now it's treated as a single value field.</li>
* <li>score - one of {@link ScoreMode}: <code>none,avg,total,max,min</code>. Capital case is also accepted.</li>
* </ul>
*/
public class ScoreJoinQParserPlugin extends QParserPlugin {
public static final String SCORE = "score";
static class OtherCoreJoinQuery extends SameCoreJoinQuery {
private final String fromIndex;
private final long fromCoreOpenTime;
public OtherCoreJoinQuery(Query fromQuery, String fromField,
String fromIndex, long fromCoreOpenTime, ScoreMode scoreMode,
String toField) {
super(fromQuery, fromField, toField, scoreMode);
this.fromIndex = fromIndex;
this.fromCoreOpenTime = fromCoreOpenTime;
}
@Override
public Weight createWeight(IndexSearcher searcher, org.apache.lucene.search.ScoreMode scoreMode, float boost) throws IOException {
SolrRequestInfo info = SolrRequestInfo.getRequestInfo();
CoreContainer container = info.getReq().getCore().getCoreContainer();
final SolrCore fromCore = container.getCore(fromIndex);
if (fromCore == null) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Cross-core join: no such core " + fromIndex);
}
RefCounted<SolrIndexSearcher> fromHolder = null;
fromHolder = fromCore.getRegisteredSearcher();
final Query joinQuery;
try {
joinQuery = JoinUtil.createJoinQuery(fromField, true,
toField, fromQuery, fromHolder.get(), this.scoreMode);
} finally {
fromCore.close();
fromHolder.decref();
}
return joinQuery.rewrite(searcher.getIndexReader()).createWeight(searcher, scoreMode, boost);
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result
+ (int) (fromCoreOpenTime ^ (fromCoreOpenTime >>> 32));
result = prime * result
+ ((fromIndex == null) ? 0 : fromIndex.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!super.equals(obj)) return false;
if (getClass() != obj.getClass()) return false;
OtherCoreJoinQuery other = (OtherCoreJoinQuery) obj;
if (fromCoreOpenTime != other.fromCoreOpenTime) return false;
if (fromIndex == null) {
if (other.fromIndex != null) return false;
} else if (!fromIndex.equals(other.fromIndex)) return false;
return true;
}
@Override
public String toString(String field) {
return "OtherCoreJoinQuery [fromIndex=" + fromIndex
+ ", fromCoreOpenTime=" + fromCoreOpenTime + " extends "
+ super.toString(field) + "]";
}
}
static class SameCoreJoinQuery extends Query {
protected final Query fromQuery;
protected final ScoreMode scoreMode;
protected final String fromField;
protected final String toField;
SameCoreJoinQuery(Query fromQuery, String fromField, String toField,
ScoreMode scoreMode) {
this.fromQuery = fromQuery;
this.scoreMode = scoreMode;
this.fromField = fromField;
this.toField = toField;
}
@Override
public Weight createWeight(IndexSearcher searcher, org.apache.lucene.search.ScoreMode scoreMode, float boost) throws IOException {
SolrRequestInfo info = SolrRequestInfo.getRequestInfo();
final Query jq = JoinUtil.createJoinQuery(fromField, true,
toField, fromQuery, info.getReq().getSearcher(), this.scoreMode);
return jq.rewrite(searcher.getIndexReader()).createWeight(searcher, scoreMode, boost);
}
@Override
public String toString(String field) {
return "SameCoreJoinQuery [fromQuery=" + fromQuery + ", fromField="
+ fromField + ", toField=" + toField + ", scoreMode=" + scoreMode
+ "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = classHash();
result = prime * result + Objects.hashCode(fromField);
result = prime * result + Objects.hashCode(fromQuery);
result = prime * result + Objects.hashCode(scoreMode);
result = prime * result + Objects.hashCode(toField);
return result;
}
@Override
public boolean equals(Object other) {
return sameClassAs(other) &&
equalsTo(getClass().cast(other));
}
private boolean equalsTo(SameCoreJoinQuery other) {
return Objects.equals(fromField, other.fromField) &&
Objects.equals(fromQuery, other.fromQuery) &&
Objects.equals(scoreMode, other.scoreMode) &&
Objects.equals(toField, other.toField);
}
@Override
public void visit(QueryVisitor visitor) {
visitor.visitLeaf(this);
}
}
@Override
public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
return new QParser(qstr, localParams, params, req) {
@Override
public Query parse() throws SyntaxError {
final String fromField = localParams.get("from");
final String fromIndex = localParams.get("fromIndex");
final String toField = localParams.get("to");
final ScoreMode scoreMode = ScoreModeParser.parse(getParam(SCORE));
final String v = localParams.get(CommonParams.VALUE);
final Query q = createQuery(fromField, v, fromIndex, toField, scoreMode,
CommonParams.TRUE.equals(localParams.get("TESTenforceSameCoreAsAnotherOne")));
return q;
}
private Query createQuery(final String fromField, final String fromQueryStr,
String fromIndex, final String toField, final ScoreMode scoreMode,
boolean byPassShortCircutCheck) throws SyntaxError {
final String myCore = req.getCore().getCoreDescriptor().getName();
if (fromIndex != null && (!fromIndex.equals(myCore) || byPassShortCircutCheck)) {
CoreContainer container = req.getCore().getCoreContainer();
final String coreName = getCoreName(fromIndex, container);
final SolrCore fromCore = container.getCore(coreName);
RefCounted<SolrIndexSearcher> fromHolder = null;
if (fromCore == null) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Cross-core join: no such core " + coreName);
}
long fromCoreOpenTime = 0;
LocalSolrQueryRequest otherReq = new LocalSolrQueryRequest(fromCore, params);
try {
QParser fromQueryParser = QParser.getParser(fromQueryStr, otherReq);
Query fromQuery = fromQueryParser.getQuery();
fromHolder = fromCore.getRegisteredSearcher();
if (fromHolder != null) {
fromCoreOpenTime = fromHolder.get().getOpenNanoTime();
}
return new OtherCoreJoinQuery(fromQuery, fromField, coreName, fromCoreOpenTime,
scoreMode, toField);
} finally {
otherReq.close();
fromCore.close();
if (fromHolder != null) fromHolder.decref();
}
} else {
QParser fromQueryParser = subQuery(fromQueryStr, null);
final Query fromQuery = fromQueryParser.getQuery();
return new SameCoreJoinQuery(fromQuery, fromField, toField, scoreMode);
}
}
};
}
/**
* Returns an String with the name of a core.
* <p>
* This method searches the core with fromIndex name in the core's container.
* If fromIndex isn't name of collection or alias it's returns fromIndex without changes.
* If fromIndex is name of alias but if the alias points to multiple collections it's throw
* SolrException.ErrorCode.BAD_REQUEST because multiple shards not yet supported.
*
* @param fromIndex name of the index
* @param container the core container for searching the core with fromIndex name or alias
* @return the string with name of core
*/
public static String getCoreName(final String fromIndex, CoreContainer container) {
if (container.isZooKeeperAware()) {
ZkController zkController = container.getZkController();
final String resolved = resolveAlias(fromIndex, zkController);
// TODO DWS: no need for this since later, clusterState.getCollection will throw a reasonable error
if (!zkController.getClusterState().hasCollection(resolved)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"SolrCloud join: Collection '" + fromIndex + "' not found!");
}
return findLocalReplicaForFromIndex(zkController, resolved);
}
return fromIndex;
}
/**
* A helper method for other plugins to create single-core JoinQueries
*
* @param subQuery the query to define the starting set of documents on the "left side" of the join
* @param fromField "left side" field name to use in the join
* @param toField "right side" field name to use in the join
* @param scoreMode the score statistic to produce while joining
*
* @see JoinQParserPlugin#createJoinQuery(Query, String, String, String)
*/
public static Query createJoinQuery(Query subQuery, String fromField, String toField, ScoreMode scoreMode) {
return new SameCoreJoinQuery(subQuery, fromField, toField, scoreMode);
}
private static String resolveAlias(String fromIndex, ZkController zkController) {
final Aliases aliases = zkController.getZkStateReader().getAliases();
try {
return aliases.resolveSimpleAlias(fromIndex); // if not an alias, returns input
} catch (IllegalArgumentException e) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"SolrCloud join: Collection alias '" + fromIndex +
"' maps to multiple collectiions, which is not currently supported for joins.", e);
}
}
private static String findLocalReplicaForFromIndex(ZkController zkController, String fromIndex) {
String fromReplica = null;
String nodeName = zkController.getNodeName();
for (Slice slice : zkController.getClusterState().getCollection(fromIndex).getActiveSlicesArr()) {
if (fromReplica != null)
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"SolrCloud join: To join with a sharded collection, use method=crossCollection.");
for (Replica replica : slice.getReplicas()) {
if (replica.getNodeName().equals(nodeName)) {
fromReplica = replica.getStr(ZkStateReader.CORE_NAME_PROP);
// found local replica, but is it Active?
if (replica.getState() != Replica.State.ACTIVE)
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"SolrCloud join: "+fromIndex+" has a local replica ("+fromReplica+
") on "+nodeName+", but it is "+replica.getState());
break;
}
}
}
if (fromReplica == null)
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"SolrCloud join: To join with a collection that might not be co-located, use method=crossCollection.");
return fromReplica;
}
}