You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ds...@apache.org on 2018/06/09 15:32:34 UTC
[2/2] lucene-solr:master: SOLR-11865: QueryElevationComponent changes
* new useConfiguredElevatedOrder setting * more extensible (customizable via
subclass) ** ElevationProvider ** handleInitializationException with cause
enum * use BytesRef for uniqueKe
SOLR-11865: QueryElevationComponent changes
* new useConfiguredElevatedOrder setting
* more extensible (customizable via subclass)
** ElevationProvider
** handleInitializationException with cause enum
* use BytesRef for uniqueKey ID pervasively instead of String.
* ElevatorComparatorSource now reuses getBoostedDocs logic
* setSort will short-circuit if there are no elevated Ids
* extensive refactoring and affects some interrelated components
Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/a06256cc
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/a06256cc
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/a06256cc
Branch: refs/heads/master
Commit: a06256ccee8742eaf47ac1f2c12ec8e68a70a163
Parents: 944b24f
Author: broustant <br...@salesforce.com>
Authored: Sat Jun 9 11:32:24 2018 -0400
Committer: David Smiley <ds...@apache.org>
Committed: Sat Jun 9 11:32:24 2018 -0400
----------------------------------------------------------------------
solr/CHANGES.txt | 5 +-
.../component/QueryElevationComponent.java | 1312 ++++++++++++------
.../transform/BaseEditorialTransformer.java | 17 +-
.../transform/ElevatedMarkerFactory.java | 6 +-
.../transform/ExcludedMarkerFactory.java | 6 +-
.../apache/solr/search/AbstractReRankQuery.java | 7 +-
.../solr/search/CollapsingQParserPlugin.java | 25 +-
.../org/apache/solr/search/ReRankCollector.java | 7 +-
.../component/QueryElevationComponentTest.java | 315 +++--
.../src/the-query-elevation-component.adoc | 11 +
.../common/params/QueryElevationParams.java | 9 +-
11 files changed, 1088 insertions(+), 632 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/a06256cc/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 4c0bb66..993edcb 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -67,7 +67,10 @@ Upgrade Notes
New Features
----------------------
-(no changes)
+* SOLR-11865: The QueryElevationComponent now has a useConfiguredElevatedOrder setting. When multiple docs are elevated,
+ this specifies whether their relative order should be the order in the configuration file or if not then should they
+ be subject to whatever the sort criteria is. Additionally, QEC was extensively refactored to be more extensible.
+ (Bruno Roustant, David Smiley)
Bug Fixes
----------------------
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/a06256cc/solr/core/src/java/org/apache/solr/handler/component/QueryElevationComponent.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/component/QueryElevationComponent.java b/solr/core/src/java/org/apache/solr/handler/component/QueryElevationComponent.java
index d7b8474..77d1e97 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/QueryElevationComponent.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/QueryElevationComponent.java
@@ -16,6 +16,7 @@
*/
package org.apache.solr.handler.component;
+import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
@@ -26,40 +27,42 @@ import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
+import java.util.Collection;
+import java.util.Collections;
import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import com.carrotsearch.hppc.IntIntHashMap;
+import com.carrotsearch.hppc.cursors.IntIntCursor;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Throwables;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.index.IndexReader;
-import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.IndexReaderContext;
import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.PostingsEnum;
import org.apache.lucene.index.Term;
-import org.apache.lucene.index.Terms;
-import org.apache.lucene.index.TermsEnum;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.BoostQuery;
-import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.search.FieldComparator;
import org.apache.lucene.search.FieldComparatorSource;
-import org.apache.lucene.search.Query;
import org.apache.lucene.search.SimpleFieldComparator;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermQuery;
-import org.apache.lucene.util.Bits;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.BytesRefBuilder;
-import org.apache.lucene.util.SentinelIntSet;
import org.apache.solr.cloud.ZkController;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.QueryElevationParams;
@@ -73,7 +76,6 @@ import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.transform.ElevatedMarkerFactory;
import org.apache.solr.response.transform.ExcludedMarkerFactory;
import org.apache.solr.schema.FieldType;
-import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.search.QueryParsing;
import org.apache.solr.search.SolrIndexSearcher;
@@ -88,82 +90,55 @@ import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
-
-import static org.apache.solr.common.params.CommonParams.ID;
+import org.xml.sax.SAXException;
/**
* A component to elevate some documents to the top of the result set.
*
* @since solr 1.3
*/
+@SuppressWarnings("WeakerAccess")
public class QueryElevationComponent extends SearchComponent implements SolrCoreAware {
+
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
// Constants used in solrconfig.xml
+ @VisibleForTesting
static final String FIELD_TYPE = "queryFieldType";
+ @VisibleForTesting
static final String CONFIG_FILE = "config-file";
- static final String EXCLUDE = "exclude";
- public static final String BOOSTED = "BOOSTED";
- public static final String BOOSTED_DOCIDS = "BOOSTED_DOCIDS";
- public static final String BOOSTED_PRIORITY = "BOOSTED_PRIORITY";
+ private static final String EXCLUDE = "exclude";
+ /** @see #getBoostDocs(SolrIndexSearcher, Set, Map) */
+ private static final String BOOSTED_DOCIDS = "BOOSTED_DOCIDS";
+ /** Key to {@link SolrQueryRequest#getContext()} for a {@code Set<BytesRef>} of included IDs in configured
+ * order (so-called priority). */
+ public static final String BOOSTED = "BOOSTED";
+ /** Key to {@link SolrQueryRequest#getContext()} for a {@code Set<BytesRef>} of excluded IDs. */
public static final String EXCLUDED = "EXCLUDED";
- // Runtime param -- should be in common?
-
- private SolrParams initArgs = null;
- private Analyzer analyzer = null;
- private String idField = null;
- private FieldType idSchemaFT;
-
- boolean forceElevation = false;
- // For each IndexReader, keep a query->elevation map
- // When the configuration is loaded from the data directory.
- // The key is null if loaded from the config directory, and
- // is never re-loaded.
- final Map<IndexReader, Map<String, ElevationObj>> elevationCache =
- new WeakHashMap<>();
-
- class ElevationObj {
- final String text;
- final String analyzed;
- final TermQuery [] exclude;//just keep the term query, b/c we will not always explicitly exclude the item based on markExcludes query time param
- final BooleanQuery include;
- final Map<BytesRef, Integer> priority;
- final Set<String> ids;
- final Set<String> excludeIds;
-
- ElevationObj(String qstr, List<String> elevate, List<String> exclude) throws IOException {
- this.text = qstr;
- this.analyzed = getAnalyzedQuery(this.text);
- this.ids = new HashSet<>();
- this.excludeIds = new HashSet<>();
-
- BooleanQuery.Builder include = new BooleanQuery.Builder();
- this.priority = new HashMap<>();
- int max = elevate.size() + 5;
- for (String id : elevate) {
- id = idSchemaFT.readableToIndexed(id);
- ids.add(id);
- TermQuery tq = new TermQuery(new Term(idField, id));
- include.add(tq, BooleanClause.Occur.SHOULD);
- this.priority.put(new BytesRef(id), max--);
- }
- this.include = include.build();
+ private static final boolean DEFAULT_FORCE_ELEVATION = false;
+ private static final boolean DEFAULT_USE_CONFIGURED_ELEVATED_ORDER = true;
+ private static final boolean DEFAULT_SUBSET_MATCH = false;
+ private static final String DEFAULT_EXCLUDE_MARKER_FIELD_NAME = "excluded";
+ private static final String DEFAULT_EDITORIAL_MARKER_FIELD_NAME = "elevated";
- if (exclude == null || exclude.isEmpty()) {
- this.exclude = null;
- } else {
- this.exclude = new TermQuery[exclude.size()];
- for (int i = 0; i < exclude.size(); i++) {
- String id = idSchemaFT.readableToIndexed(exclude.get(i));
- excludeIds.add(id);
- this.exclude[i] = new TermQuery(new Term(idField, id));
- }
- }
- }
- }
+ protected SolrParams initArgs;
+ protected Analyzer queryAnalyzer;
+ protected SchemaField uniqueKeyField;
+ /** @see QueryElevationParams#FORCE_ELEVATION */
+ protected boolean forceElevation;
+ /** @see QueryElevationParams#USE_CONFIGURED_ELEVATED_ORDER */
+ protected boolean useConfiguredElevatedOrder;
+
+ protected boolean initialized;
+
+ /**
+ * For each IndexReader, keep an ElevationProvider when the configuration is loaded from the data directory.
+ * The key is null if loaded from the config directory, and is never re-loaded.
+ */
+ private final Map<IndexReader, ElevationProvider> elevationProviderCache = new WeakHashMap<>();
@Override
public void init(NamedList args) {
@@ -172,128 +147,251 @@ public class QueryElevationComponent extends SearchComponent implements SolrCore
@Override
public void inform(SolrCore core) {
- IndexSchema schema = core.getLatestSchema();
+ initialized = false;
+ try {
+ parseFieldType(core);
+ setUniqueKeyField(core);
+ parseExcludedMarkerFieldName(core);
+ parseEditorialMarkerFieldName(core);
+ parseForceElevation();
+ parseUseConfiguredOrderForElevations();
+ loadElevationConfiguration(core);
+ initialized = true;
+ } catch (InitializationException e) {
+ assert !initialized;
+ handleInitializationException(e, e.exceptionCause);
+ } catch (Exception e) {
+ assert !initialized;
+ handleInitializationException(e, InitializationExceptionCause.OTHER);
+ }
+ }
+
+ private void parseFieldType(SolrCore core) throws InitializationException {
String a = initArgs.get(FIELD_TYPE);
if (a != null) {
- FieldType ft = schema.getFieldTypes().get(a);
+ FieldType ft = core.getLatestSchema().getFieldTypes().get(a);
if (ft == null) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
- "Unknown FieldType: '" + a + "' used in QueryElevationComponent");
+ throw new InitializationException("Parameter " + FIELD_TYPE + " defines an unknown field type \"" + a + "\"", InitializationExceptionCause.UNKNOWN_FIELD_TYPE);
}
- analyzer = ft.getQueryAnalyzer();
+ queryAnalyzer = ft.getQueryAnalyzer();
}
+ }
- SchemaField sf = schema.getUniqueKeyField();
- if( sf == null) {
- throw new SolrException( SolrException.ErrorCode.SERVER_ERROR,
- "QueryElevationComponent requires the schema to have a uniqueKeyField." );
- }
- idSchemaFT = sf.getType();
- idField = sf.getName();
- //register the EditorialMarkerFactory
- String excludeName = initArgs.get(QueryElevationParams.EXCLUDE_MARKER_FIELD_NAME, "excluded");
- if (excludeName == null || excludeName.equals("") == true){
- excludeName = "excluded";
- }
- ExcludedMarkerFactory excludedMarkerFactory = new ExcludedMarkerFactory();
- core.addTransformerFactory(excludeName, excludedMarkerFactory);
- ElevatedMarkerFactory elevatedMarkerFactory = new ElevatedMarkerFactory();
- String markerName = initArgs.get(QueryElevationParams.EDITORIAL_MARKER_FIELD_NAME, "elevated");
- if (markerName == null || markerName.equals("") == true) {
- markerName = "elevated";
+ private void setUniqueKeyField(SolrCore core) throws InitializationException {
+ uniqueKeyField = core.getLatestSchema().getUniqueKeyField();
+ if (uniqueKeyField == null) {
+ throw new InitializationException("This component requires the schema to have a uniqueKeyField", InitializationExceptionCause.MISSING_UNIQUE_KEY_FIELD);
}
- core.addTransformerFactory(markerName, elevatedMarkerFactory);
- forceElevation = initArgs.getBool(QueryElevationParams.FORCE_ELEVATION, forceElevation);
+ }
- String f = initArgs.get(CONFIG_FILE);
- if (f != null) {
- try {
- synchronized (elevationCache) {
- elevationCache.clear();
- boolean exists = false;
-
- // check if using ZooKeeper
- ZkController zkController = core.getCoreContainer().getZkController();
- if (zkController != null) {
- // TODO : shouldn't have to keep reading the config name when it has been read before
- exists = zkController.configFileExists(zkController.getZkStateReader().readConfigName(core.getCoreDescriptor().getCloudDescriptor().getCollectionName()), f);
+ private void parseExcludedMarkerFieldName(SolrCore core) {
+ String markerName = initArgs.get(QueryElevationParams.EXCLUDE_MARKER_FIELD_NAME, DEFAULT_EXCLUDE_MARKER_FIELD_NAME);
+ core.addTransformerFactory(markerName, new ExcludedMarkerFactory());
+ }
+
+ private void parseEditorialMarkerFieldName(SolrCore core) {
+ String markerName = initArgs.get(QueryElevationParams.EDITORIAL_MARKER_FIELD_NAME, DEFAULT_EDITORIAL_MARKER_FIELD_NAME);
+ core.addTransformerFactory(markerName, new ElevatedMarkerFactory());
+ }
+
+ private void parseForceElevation() {
+ forceElevation = initArgs.getBool(QueryElevationParams.FORCE_ELEVATION, DEFAULT_FORCE_ELEVATION);
+ }
+
+ private void parseUseConfiguredOrderForElevations() {
+ useConfiguredElevatedOrder = initArgs.getBool(QueryElevationParams.USE_CONFIGURED_ELEVATED_ORDER, DEFAULT_USE_CONFIGURED_ELEVATED_ORDER);
+ }
+
+ /**
+ * (Re)Loads elevation configuration.
+ *
+ * @param core The core holding this component.
+ * @return The number of elevation rules parsed.
+ */
+ @SuppressWarnings("WeakerAccess")
+ protected int loadElevationConfiguration(SolrCore core) throws Exception {
+ synchronized (elevationProviderCache) {
+ elevationProviderCache.clear();
+ String configFileName = initArgs.get(CONFIG_FILE);
+ if (configFileName == null) {
+ // Throw an exception which is handled by handleInitializationException().
+ // If not overridden handleInitializationException() simply skips this exception.
+ throw new InitializationException("Missing component parameter " + CONFIG_FILE + " - it has to define the path to the elevation configuration file", InitializationExceptionCause.NO_CONFIG_FILE_DEFINED);
+ }
+ boolean configFileExists = false;
+ ElevationProvider elevationProvider = NO_OP_ELEVATION_PROVIDER;
+
+ // check if using ZooKeeper
+ ZkController zkController = core.getCoreContainer().getZkController();
+ if (zkController != null) {
+ // TODO : shouldn't have to keep reading the config name when it has been read before
+ configFileExists = zkController.configFileExists(zkController.getZkStateReader().readConfigName(core.getCoreDescriptor().getCloudDescriptor().getCollectionName()), configFileName);
+ } else {
+ File fC = new File(core.getResourceLoader().getConfigDir(), configFileName);
+ File fD = new File(core.getDataDir(), configFileName);
+ if (fC.exists() == fD.exists()) {
+ InitializationException e = new InitializationException("Missing config file \"" + configFileName + "\" - either " + fC.getAbsolutePath() + " or " + fD.getAbsolutePath() + " must exist, but not both", InitializationExceptionCause.MISSING_CONFIG_FILE);
+ elevationProvider = handleConfigLoadingException(e, true);
+ elevationProviderCache.put(null, elevationProvider);
+ } else if (fC.exists()) {
+ if (fC.length() == 0) {
+ InitializationException e = new InitializationException("Empty config file \"" + configFileName + "\" - " + fC.getAbsolutePath(), InitializationExceptionCause.EMPTY_CONFIG_FILE);
+ elevationProvider = handleConfigLoadingException(e, true);
} else {
- File fC = new File(core.getResourceLoader().getConfigDir(), f);
- File fD = new File(core.getDataDir(), f);
- if (fC.exists() == fD.exists()) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
- "QueryElevationComponent missing config file: '" + f + "\n"
- + "either: " + fC.getAbsolutePath() + " or " + fD.getAbsolutePath() + " must exist, but not both.");
- }
- if (fC.exists()) {
- exists = true;
- log.info("Loading QueryElevation from: " + fC.getAbsolutePath());
- Config cfg = new Config(core.getResourceLoader(), f);
- elevationCache.put(null, loadElevationMap(cfg));
- }
+ configFileExists = true;
+ log.info("Loading QueryElevation from: " + fC.getAbsolutePath());
+ Config cfg = new Config(core.getResourceLoader(), configFileName);
+ elevationProvider = loadElevationProvider(cfg);
}
- //in other words, we think this is in the data dir, not the conf dir
- if (!exists) {
- // preload the first data
- RefCounted<SolrIndexSearcher> searchHolder = null;
- try {
- searchHolder = core.getNewestSearcher(false);
- IndexReader reader = searchHolder.get().getIndexReader();
- getElevationMap(reader, core);
- } finally {
- if (searchHolder != null) searchHolder.decref();
- }
+ elevationProviderCache.put(null, elevationProvider);
+ }
+ }
+ //in other words, we think this is in the data dir, not the conf dir
+ if (!configFileExists) {
+ // preload the first data
+ RefCounted<SolrIndexSearcher> searchHolder = null;
+ try {
+ searchHolder = core.getNewestSearcher(false);
+ if (searchHolder == null) {
+ elevationProvider = NO_OP_ELEVATION_PROVIDER;
+ } else {
+ IndexReader reader = searchHolder.get().getIndexReader();
+ elevationProvider = getElevationProvider(reader, core);
}
+ } finally {
+ if (searchHolder != null) searchHolder.decref();
}
- } catch (Exception ex) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
- "Error initializing QueryElevationComponent.", ex);
}
+ return elevationProvider.size();
+ }
+ }
+
+ /**
+ * Handles the exception that occurred while initializing this component.
+ * If this method does not throw an exception, this component silently fails to initialize
+ * and is muted with field {@link #initialized} which becomes {@code false}.
+ */
+ protected void handleInitializationException(Exception exception, InitializationExceptionCause cause) {
+ if (cause != InitializationExceptionCause.NO_CONFIG_FILE_DEFINED) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
+ "Error initializing " + QueryElevationComponent.class.getSimpleName(), exception);
}
}
- //get the elevation map from the data dir
- Map<String, ElevationObj> getElevationMap(IndexReader reader, SolrCore core) throws Exception {
- synchronized (elevationCache) {
- Map<String, ElevationObj> map = elevationCache.get(null);
- if (map != null) return map;
+ /**
+ * Handles an exception that occurred while loading the configuration resource.
+ *
+ * @param e The exception caught.
+ * @param resourceAccessIssue <code>true</code> if the exception has been thrown
+ * because the resource could not be accessed (missing or cannot be read)
+ * or the config file is empty; <code>false</code> if the resource has
+ * been found and accessed but the error occurred while loading the resource
+ * (invalid format, incomplete or corrupted).
+ * @return The {@link ElevationProvider} to use if the exception is absorbed. If {@code null}
+ * is returned, the {@link #NO_OP_ELEVATION_PROVIDER} is used but not cached in
+ * the {@link ElevationProvider} cache.
+ * @throws E If the exception is not absorbed.
+ */
+ protected <E extends Exception> ElevationProvider handleConfigLoadingException(E e, boolean resourceAccessIssue) throws E {
+ throw e;
+ }
- map = elevationCache.get(reader);
- if (map == null) {
- String f = initArgs.get(CONFIG_FILE);
- if (f == null) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
- "QueryElevationComponent must specify argument: " + CONFIG_FILE);
+ /**
+ * Gets the {@link ElevationProvider} from the data dir or from the cache.
+ *
+ * @return The cached or loaded {@link ElevationProvider}.
+ * @throws java.io.IOException If the configuration resource cannot be found, or if an I/O error occurs while analyzing the triggering queries.
+ * @throws org.xml.sax.SAXException If the configuration resource is not a valid XML content.
+ * @throws javax.xml.parsers.ParserConfigurationException If the configuration resource is not a valid XML configuration.
+ * @throws RuntimeException If the configuration resource is not an XML content of the expected format
+ * (either {@link RuntimeException} or {@link org.apache.solr.common.SolrException}).
+ */
+ @VisibleForTesting
+ ElevationProvider getElevationProvider(IndexReader reader, SolrCore core) throws Exception {
+ synchronized (elevationProviderCache) {
+ ElevationProvider elevationProvider;
+ elevationProvider = elevationProviderCache.get(null);
+ if (elevationProvider != null) return elevationProvider;
+
+ elevationProvider = elevationProviderCache.get(reader);
+ if (elevationProvider == null) {
+ Exception loadingException = null;
+ boolean resourceAccessIssue = false;
+ try {
+ elevationProvider = loadElevationProvider(core);
+ } catch (IOException e) {
+ loadingException = e;
+ resourceAccessIssue = true;
+ } catch (Exception e) {
+ loadingException = e;
}
- log.info("Loading QueryElevation from data dir: " + f);
-
- Config cfg;
-
- ZkController zkController = core.getCoreContainer().getZkController();
- if (zkController != null) {
- cfg = new Config(core.getResourceLoader(), f, null, null);
- } else {
- InputStream is = VersionedFile.getLatestFile(core.getDataDir(), f);
- cfg = new Config(core.getResourceLoader(), f, new InputSource(is), null);
+ boolean shouldCache = true;
+ if (loadingException != null) {
+ elevationProvider = handleConfigLoadingException(loadingException, resourceAccessIssue);
+ if (elevationProvider == null) {
+ elevationProvider = NO_OP_ELEVATION_PROVIDER;
+ shouldCache = false;
+ }
+ }
+ if (shouldCache) {
+ elevationProviderCache.put(reader, elevationProvider);
}
-
- map = loadElevationMap(cfg);
- elevationCache.put(reader, map);
}
- return map;
+ assert elevationProvider != null;
+ return elevationProvider;
+ }
+ }
+
+ /**
+ * Loads the {@link ElevationProvider} from the data dir.
+ *
+ * @return The loaded {@link ElevationProvider}.
+ * @throws java.io.IOException If the configuration resource cannot be found, or if an I/O error occurs while analyzing the triggering queries.
+ * @throws org.xml.sax.SAXException If the configuration resource is not a valid XML content.
+ * @throws javax.xml.parsers.ParserConfigurationException If the configuration resource is not a valid XML configuration.
+ * @throws RuntimeException If the configuration resource is not an XML content of the expected format
+ * (either {@link RuntimeException} or {@link org.apache.solr.common.SolrException}).
+ */
+ private ElevationProvider loadElevationProvider(SolrCore core) throws IOException, SAXException, ParserConfigurationException {
+ String configFileName = initArgs.get(CONFIG_FILE);
+ if (configFileName == null) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
+ "QueryElevationComponent must specify argument: " + CONFIG_FILE);
+ }
+ log.info("Loading QueryElevation from data dir: " + configFileName);
+
+ Config cfg;
+ ZkController zkController = core.getCoreContainer().getZkController();
+ if (zkController != null) {
+ cfg = new Config(core.getResourceLoader(), configFileName, null, null);
+ } else {
+ InputStream is = VersionedFile.getLatestFile(core.getDataDir(), configFileName);
+ cfg = new Config(core.getResourceLoader(), configFileName, new InputSource(is), null);
}
+ ElevationProvider elevationProvider = loadElevationProvider(cfg);
+ assert elevationProvider != null;
+ return elevationProvider;
}
- //load up the elevation map
- private Map<String, ElevationObj> loadElevationMap(Config cfg) throws IOException {
+ /**
+ * Loads the {@link ElevationProvider}.
+ *
+ * @throws java.io.IOException If an I/O error occurs while analyzing the triggering queries.
+ * @throws RuntimeException If the config does not provide an XML content of the expected format
+ * (either {@link RuntimeException} or {@link org.apache.solr.common.SolrException}).
+ */
+ @SuppressWarnings("WeakerAccess")
+ protected ElevationProvider loadElevationProvider(Config config) throws IOException {
+ Map<ElevatingQuery, ElevationBuilder> elevationBuilderMap = new LinkedHashMap<>();
XPath xpath = XPathFactory.newInstance().newXPath();
- Map<String, ElevationObj> map = new HashMap<>();
- NodeList nodes = (NodeList) cfg.evaluate("elevate/query", XPathConstants.NODESET);
+ NodeList nodes = (NodeList) config.evaluate("elevate/query", XPathConstants.NODESET);
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
- String qstr = DOMUtil.getAttr(node, "text", "missing query 'text'");
+ String queryString = DOMUtil.getAttr(node, "text", "missing query 'text'");
+ String matchString = DOMUtil.getAttr(node, "match");
+ ElevatingQuery elevatingQuery = new ElevatingQuery(queryString, parseMatchPolicy(matchString));
- NodeList children = null;
+ NodeList children;
try {
children = (NodeList) xpath.evaluate("doc", node, XPathConstants.NODESET);
} catch (XPathExpressionException e) {
@@ -301,67 +399,45 @@ public class QueryElevationComponent extends SearchComponent implements SolrCore
"query requires '<doc .../>' child");
}
- ArrayList<String> include = new ArrayList<>();
- ArrayList<String> exclude = new ArrayList<>();
+ if (children.getLength() == 0) { // weird
+ continue;
+ }
+ ElevationBuilder elevationBuilder = new ElevationBuilder();
for (int j = 0; j < children.getLength(); j++) {
Node child = children.item(j);
- String id = DOMUtil.getAttr(child, ID, "missing 'id'");
+ String id = DOMUtil.getAttr(child, "id", "missing 'id'");
String e = DOMUtil.getAttr(child, EXCLUDE, null);
if (e != null) {
if (Boolean.valueOf(e)) {
- exclude.add(id);
+ elevationBuilder.addExcludedIds(Collections.singleton(id));
continue;
}
}
- include.add(id);
+ elevationBuilder.addElevatedIds(Collections.singletonList(id));
}
- ElevationObj elev = new ElevationObj(qstr, include, exclude);
- if (map.containsKey(elev.analyzed)) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
- "Boosting query defined twice for query: '" + elev.text + "' (" + elev.analyzed + "')");
+ // It is allowed to define multiple times different elevations for the same query. In this case the elevations
+ // are merged in the ElevationBuilder (they will be triggered at the same time).
+ ElevationBuilder previousElevationBuilder = elevationBuilderMap.get(elevatingQuery);
+ if (previousElevationBuilder == null) {
+ elevationBuilderMap.put(elevatingQuery, elevationBuilder);
+ } else {
+ previousElevationBuilder.merge(elevationBuilder);
}
- map.put(elev.analyzed, elev);
}
- return map;
+ return createElevationProvider(queryAnalyzer, elevationBuilderMap);
}
- /**
- * Helpful for testing without loading config.xml
- *
- * @throws IOException If there is a low-level I/O error.
- */
- void setTopQueryResults(IndexReader reader, String query, String[] ids, String[] ex) throws IOException {
- if (ids == null) {
- ids = new String[0];
- }
- if (ex == null) {
- ex = new String[0];
- }
-
- Map<String, ElevationObj> elev = elevationCache.get(reader);
- if (elev == null) {
- elev = new HashMap<>();
- elevationCache.put(reader, elev);
- }
- ElevationObj obj = new ElevationObj(query, Arrays.asList(ids), Arrays.asList(ex));
- elev.put(obj.analyzed, obj);
- }
-
- String getAnalyzedQuery(String query) throws IOException {
- if (analyzer == null) {
- return query;
- }
- StringBuilder norm = new StringBuilder();
- try (TokenStream tokens = analyzer.tokenStream("", query)) {
- tokens.reset();
-
- CharTermAttribute termAtt = tokens.addAttribute(CharTermAttribute.class);
- while (tokens.incrementToken()) {
- norm.append(termAtt.buffer(), 0, termAtt.length());
- }
- tokens.end();
- return norm.toString();
+ private boolean parseMatchPolicy(String matchString) {
+ if (matchString == null) {
+ return DEFAULT_SUBSET_MATCH;
+ } else if (matchString.equalsIgnoreCase("exact")) {
+ return false;
+ } else if (matchString.equalsIgnoreCase("subset")) {
+ return true;
+ } else {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
+ "invalid value \"" + matchString + "\" for query match attribute");
}
}
@@ -371,125 +447,126 @@ public class QueryElevationComponent extends SearchComponent implements SolrCore
@Override
public void prepare(ResponseBuilder rb) throws IOException {
- SolrQueryRequest req = rb.req;
- SolrParams params = req.getParams();
- // A runtime param can skip
- if (!params.getBool(QueryElevationParams.ENABLE, true)) {
+ if (!initialized || !rb.req.getParams().getBool(QueryElevationParams.ENABLE, true)) {
return;
}
- boolean exclusive = params.getBool(QueryElevationParams.EXCLUSIVE, false);
- // A runtime parameter can alter the config value for forceElevation
- boolean force = params.getBool(QueryElevationParams.FORCE_ELEVATION, forceElevation);
- boolean markExcludes = params.getBool(QueryElevationParams.MARK_EXCLUDES, false);
- String boostStr = params.get(QueryElevationParams.IDS);
- String exStr = params.get(QueryElevationParams.EXCLUDE);
+ Elevation elevation = getElevation(rb);
+ if (elevation != null) {
+ setQuery(rb, elevation);
+ setSort(rb, elevation);
+ }
- Query query = rb.getQuery();
+ if (rb.isDebug() && rb.isDebugQuery()) {
+ addDebugInfo(rb, elevation);
+ }
+ }
+
+ @Override
+ public void process(ResponseBuilder rb) throws IOException {
+ // Do nothing -- the real work is modifying the input query
+ }
+
+ protected Elevation getElevation(ResponseBuilder rb) {
SolrParams localParams = rb.getQparser().getLocalParams();
- String qstr = localParams == null ? rb.getQueryString() : localParams.get(QueryParsing.V);
- if (query == null || qstr == null) {
- return;
+ String queryString = localParams == null ? rb.getQueryString() : localParams.get(QueryParsing.V);
+ if (queryString == null || rb.getQuery() == null) {
+ return null;
}
- ElevationObj booster = null;
+ SolrParams params = rb.req.getParams();
+ String paramElevatedIds = params.get(QueryElevationParams.IDS);
+ String paramExcludedIds = params.get(QueryElevationParams.EXCLUDE);
try {
- if(boostStr != null || exStr != null) {
- List<String> boosts = (boostStr != null) ? StrUtils.splitSmart(boostStr,",", true) : new ArrayList<String>(0);
- List<String> excludes = (exStr != null) ? StrUtils.splitSmart(exStr, ",", true) : new ArrayList<String>(0);
- booster = new ElevationObj(qstr, boosts, excludes);
+ if (paramElevatedIds != null || paramExcludedIds != null) {
+ List<String> elevatedIds = paramElevatedIds != null ? StrUtils.splitSmart(paramElevatedIds,",", true) : Collections.emptyList();
+ List<String> excludedIds = paramExcludedIds != null ? StrUtils.splitSmart(paramExcludedIds, ",", true) : Collections.emptyList();
+ return new ElevationBuilder().addElevatedIds(elevatedIds).addExcludedIds(excludedIds).build();
} else {
- IndexReader reader = req.getSearcher().getIndexReader();
- qstr = getAnalyzedQuery(qstr);
- booster = getElevationMap(reader, req.getCore()).get(qstr);
+ IndexReader reader = rb.req.getSearcher().getIndexReader();
+ return getElevationProvider(reader, rb.req.getCore()).getElevationForQuery(queryString);
}
- } catch (Exception ex) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
- "Error loading elevation", ex);
+ } catch (Exception e) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error loading elevation", e);
}
+ }
- if (booster != null) {
- rb.req.getContext().put(BOOSTED, booster.ids);
- rb.req.getContext().put(BOOSTED_PRIORITY, booster.priority);
-
- // Change the query to insert forced documents
- if (exclusive == true) {
- //we only want these results
- rb.setQuery(new BoostQuery(booster.include, 0f));
- } else {
- BooleanQuery.Builder newq = new BooleanQuery.Builder();
- newq.add(query, BooleanClause.Occur.SHOULD);
- newq.add(new BoostQuery(booster.include, 0f), BooleanClause.Occur.SHOULD);
- if (booster.exclude != null) {
- if (markExcludes == false) {
- for (TermQuery tq : booster.exclude) {
- newq.add(new BooleanClause(tq, BooleanClause.Occur.MUST_NOT));
- }
- } else {
- //we are only going to mark items as excluded, not actually exclude them. This works
- //with the EditorialMarkerFactory
- rb.req.getContext().put(EXCLUDED, booster.excludeIds);
+ private void setQuery(ResponseBuilder rb, Elevation elevation) {
+ rb.req.getContext().put(BOOSTED, elevation.elevatedIds);
+
+ // Change the query to insert forced documents
+ SolrParams params = rb.req.getParams();
+ if (params.getBool(QueryElevationParams.EXCLUSIVE, false)) {
+ // We only want these elevated results
+ rb.setQuery(new BoostQuery(elevation.includeQuery, 0f));
+ } else {
+ BooleanQuery.Builder queryBuilder = new BooleanQuery.Builder();
+ queryBuilder.add(rb.getQuery(), BooleanClause.Occur.SHOULD);
+ queryBuilder.add(new BoostQuery(elevation.includeQuery, 0f), BooleanClause.Occur.SHOULD);
+ if (elevation.excludeQueries != null) {
+ if (params.getBool(QueryElevationParams.MARK_EXCLUDES, false)) {
+ // We are only going to mark items as excluded, not actually exclude them.
+ // This works with the EditorialMarkerFactory.
+ rb.req.getContext().put(EXCLUDED, elevation.excludedIds);
+ } else {
+ for (TermQuery tq : elevation.excludeQueries) {
+ queryBuilder.add(tq, BooleanClause.Occur.MUST_NOT);
}
}
- rb.setQuery(newq.build());
}
+ rb.setQuery(queryBuilder.build());
+ }
+ }
- ElevationComparatorSource comparator = new ElevationComparatorSource(booster);
- // if the sort is 'score desc' use a custom sorting method to
- // insert documents in their proper place
- SortSpec sortSpec = rb.getSortSpec();
- if (sortSpec.getSort() == null) {
- sortSpec.setSortAndFields(new Sort(new SortField[]{
- new SortField("_elevate_", comparator, true),
- new SortField(null, SortField.Type.SCORE, false)
- }),
- Arrays.asList(new SchemaField[2]));
- } else {
- // Check if the sort is based on score
- SortSpec modSortSpec = this.modifySortSpec(sortSpec, force, comparator);
- if (null != modSortSpec) {
- rb.setSortSpec(modSortSpec);
- }
- }
+ private void setSort(ResponseBuilder rb, Elevation elevation) throws IOException {
+ if (elevation.elevatedIds.isEmpty()) {
+ return;
+ }
+ boolean forceElevation = rb.req.getParams().getBool(QueryElevationParams.FORCE_ELEVATION, this.forceElevation);
+ boolean useConfigured = rb.req.getParams().getBool(QueryElevationParams.USE_CONFIGURED_ELEVATED_ORDER, this.useConfiguredElevatedOrder);
+ final IntIntHashMap elevatedWithPriority = getBoostDocs(rb.req.getSearcher(), elevation.elevatedIds, rb.req.getContext());
+ ElevationComparatorSource comparator = new ElevationComparatorSource(elevatedWithPriority, useConfigured);
+ setSortSpec(rb, forceElevation, comparator);
+ setGroupingSpec(rb, forceElevation, comparator);
+ }
- // alter the sorting in the grouping specification if there is one
- GroupingSpecification groupingSpec = rb.getGroupingSpec();
- if(groupingSpec != null) {
- SortSpec groupSortSpec = groupingSpec.getGroupSortSpec();
- SortSpec modGroupSortSpec = this.modifySortSpec(groupSortSpec, force, comparator);
- if (modGroupSortSpec != null) {
- groupingSpec.setGroupSortSpec(modGroupSortSpec);
- }
- SortSpec withinGroupSortSpec = groupingSpec.getWithinGroupSortSpec();
- SortSpec modWithinGroupSortSpec = this.modifySortSpec(withinGroupSortSpec, force, comparator);
- if (modWithinGroupSortSpec != null) {
- groupingSpec.setWithinGroupSortSpec(modWithinGroupSortSpec);
- }
+ private void setSortSpec(ResponseBuilder rb, boolean forceElevation, ElevationComparatorSource comparator) {
+ // if the sort is 'score desc' use a custom sorting method to
+ // insert documents in their proper place
+ SortSpec sortSpec = rb.getSortSpec();
+ if (sortSpec.getSort() == null) {
+ sortSpec.setSortAndFields(
+ new Sort(
+ new SortField("_elevate_", comparator, true),
+ new SortField(null, SortField.Type.SCORE, false)),
+ Arrays.asList(new SchemaField[2]));
+ } else {
+ // Check if the sort is based on score
+ SortSpec modSortSpec = this.modifySortSpec(sortSpec, forceElevation, comparator);
+ if (null != modSortSpec) {
+ rb.setSortSpec(modSortSpec);
}
}
+ }
- // Add debugging information
- if (rb.isDebug()) {
- List<String> match = null;
- if (booster != null) {
- // Extract the elevated terms into a list
- match = new ArrayList<>(booster.priority.size());
- for (Object o : booster.include.clauses()) {
- TermQuery tq = (TermQuery) ((BooleanClause) o).getQuery();
- match.add(tq.getTerm().text());
- }
+ private void setGroupingSpec(ResponseBuilder rb, boolean forceElevation, ElevationComparatorSource comparator) {
+ // alter the sorting in the grouping specification if there is one
+ GroupingSpecification groupingSpec = rb.getGroupingSpec();
+ if(groupingSpec != null) {
+ SortSpec groupSortSpec = groupingSpec.getGroupSortSpec();
+ SortSpec modGroupSortSpec = this.modifySortSpec(groupSortSpec, forceElevation, comparator);
+ if (modGroupSortSpec != null) {
+ groupingSpec.setGroupSortSpec(modGroupSortSpec);
}
-
- SimpleOrderedMap<Object> dbg = new SimpleOrderedMap<>();
- dbg.add("q", qstr);
- dbg.add("match", match);
- if (rb.isDebugQuery()) {
- rb.addDebugInfo("queryBoosting", dbg);
+ SortSpec withinGroupSortSpec = groupingSpec.getWithinGroupSortSpec();
+ SortSpec modWithinGroupSortSpec = this.modifySortSpec(withinGroupSortSpec, forceElevation, comparator);
+ if (modWithinGroupSortSpec != null) {
+ groupingSpec.setWithinGroupSortSpec(modWithinGroupSortSpec);
}
}
}
- private SortSpec modifySortSpec(SortSpec current, boolean force, ElevationComparatorSource comparator) {
+ private SortSpec modifySortSpec(SortSpec current, boolean forceElevation, ElevationComparatorSource comparator) {
boolean modify = false;
SortField[] currentSorts = current.getSort().getSort();
List<SchemaField> currentFields = current.getSchemaFields();
@@ -498,7 +575,7 @@ public class QueryElevationComponent extends SearchComponent implements SolrCore
List<SchemaField> fields = new ArrayList<>(currentFields.size() + 1);
// Perhaps force it to always sort by score
- if (force && currentSorts[0].getType() != SortField.Type.SCORE) {
+ if (forceElevation && currentSorts[0].getType() != SortField.Type.SCORE) {
sorts.add(new SortField("_elevate_", comparator, true));
fields.add(null);
modify = true;
@@ -513,84 +590,81 @@ public class QueryElevationComponent extends SearchComponent implements SolrCore
sorts.add(sf);
fields.add(currentFields.get(i));
}
- if (modify) {
- SortSpec newSpec = new SortSpec(new Sort(sorts.toArray(new SortField[sorts.size()])),
- fields,
- current.getCount(),
- current.getOffset());
- return newSpec;
+ return modify ?
+ new SortSpec(new Sort(sorts.toArray(new SortField[sorts.size()])),
+ fields,
+ current.getCount(),
+ current.getOffset())
+ : null;
+ }
+
+ private void addDebugInfo(ResponseBuilder rb, Elevation elevation) {
+ List<String> match = null;
+ if (elevation != null) {
+ // Extract the elevated terms into a list
+ match = new ArrayList<>(elevation.includeQuery.clauses().size());
+ for (BooleanClause clause : elevation.includeQuery.clauses()) {
+ TermQuery tq = (TermQuery) clause.getQuery();
+ match.add(tq.getTerm().text());
+ }
}
- return null;
+ SimpleOrderedMap<Object> dbg = new SimpleOrderedMap<>();
+ dbg.add("q", rb.getQueryString());
+ dbg.add("match", match);
+ rb.addDebugInfo("queryBoosting", dbg);
}
+ //---------------------------------------------------------------------------------
+ // Boosted docs helper
+ //---------------------------------------------------------------------------------
- public static IntIntHashMap getBoostDocs(SolrIndexSearcher indexSearcher, Map<BytesRef, Integer>boosted, Map context) throws IOException {
+ /**
+ * Resolves a set of boosted docs by uniqueKey to a map of docIds mapped to a priority value > 0.
+ * @param indexSearcher the SolrIndexSearcher; required
+ * @param boosted are the set of uniqueKey values to be boosted in priority order. If null; returns null.
+ * @param context the {@link SolrQueryRequest#getContext()} or null if none. We'll cache our results here.
+ */
+ //TODO consider simplifying to remove "boosted" arg which can be looked up in context via BOOSTED key?
+ public static IntIntHashMap getBoostDocs(SolrIndexSearcher indexSearcher, Set<BytesRef> boosted, Map context) throws IOException {
IntIntHashMap boostDocs = null;
- if(boosted != null) {
+ if (boosted != null) {
- //First see if it's already in the request context. Could have been put there
- //by another caller.
- if(context != null) {
+ //First see if it's already in the request context. Could have been put there by another caller.
+ if (context != null) {
boostDocs = (IntIntHashMap) context.get(BOOSTED_DOCIDS);
+ if (boostDocs != null) {
+ return boostDocs;
+ }
}
- if(boostDocs != null) {
- return boostDocs;
- }
//Not in the context yet so load it.
-
- SchemaField idField = indexSearcher.getSchema().getUniqueKeyField();
- String fieldName = idField.getName();
- HashSet<BytesRef> localBoosts = new HashSet<>(boosted.size()*2);
- Iterator<BytesRef> boostedIt = boosted.keySet().iterator();
- while(boostedIt.hasNext()) {
- localBoosts.add(boostedIt.next());
- }
-
- boostDocs = new IntIntHashMap(boosted.size());
-
- List<LeafReaderContext>leaves = indexSearcher.getTopReaderContext().leaves();
- PostingsEnum postingsEnum = null;
- for(LeafReaderContext leaf : leaves) {
- LeafReader reader = leaf.reader();
- int docBase = leaf.docBase;
- Bits liveDocs = reader.getLiveDocs();
- Terms terms = reader.terms(fieldName);
- TermsEnum termsEnum = terms.iterator();
- Iterator<BytesRef> it = localBoosts.iterator();
- while(it.hasNext()) {
- BytesRef ref = it.next();
- if(termsEnum.seekExact(ref)) {
- postingsEnum = termsEnum.postings(postingsEnum);
- int doc = postingsEnum.nextDoc();
- while (doc != PostingsEnum.NO_MORE_DOCS && liveDocs != null && liveDocs.get(doc) == false) {
- doc = postingsEnum.nextDoc();
- }
- if(doc != PostingsEnum.NO_MORE_DOCS) {
- //Found the document.
- int p = boosted.get(ref);
- boostDocs.put(doc+docBase, p);
- it.remove();
- }
- }
+ boostDocs = new IntIntHashMap(boosted.size()); // docId to boost
+ int priority = boosted.size() + 1; // the corresponding priority for each boosted key (starts at this; decrements down)
+ for (BytesRef uniqueKey : boosted) {
+ priority--; // therefore first == bosted.size(); last will be 1
+ long segAndId = indexSearcher.lookupId(uniqueKey); // higher 32 bits == segment ID, low 32 bits == doc ID
+ if (segAndId == -1) { // not found
+ continue;
}
+ int seg = (int) (segAndId >> 32);
+ int localDocId = (int) segAndId;
+ final IndexReaderContext indexReaderContext = indexSearcher.getTopReaderContext().children().get(seg);
+ int docId = indexReaderContext.docBaseInParent + localDocId;
+ boostDocs.put(docId, priority);
}
+ assert priority == 1; // the last priority (lowest)
}
- if(context != null) {
+ if (context != null) {
+ //noinspection unchecked
context.put(BOOSTED_DOCIDS, boostDocs);
}
return boostDocs;
}
- @Override
- public void process(ResponseBuilder rb) throws IOException {
- // Do nothing -- the real work is modifying the input query
- }
-
//---------------------------------------------------------------------------------
// SolrInfoBean
//---------------------------------------------------------------------------------
@@ -600,101 +674,461 @@ public class QueryElevationComponent extends SearchComponent implements SolrCore
return "Query Boosting -- boost particular documents for a given query";
}
- class ElevationComparatorSource extends FieldComparatorSource {
- private QueryElevationComponent.ElevationObj elevations;
- private SentinelIntSet ordSet; //the key half of the map
- private BytesRef[] termValues;//the value half of the map
+ //---------------------------------------------------------------------------------
+ // Overrides
+ //---------------------------------------------------------------------------------
- public ElevationComparatorSource(final QueryElevationComponent.ElevationObj elevations) {
- this.elevations = elevations;
- int size = elevations.ids.size();
- ordSet = new SentinelIntSet(size, -1);
- termValues = new BytesRef[ordSet.keys.length];
+ /**
+ * Creates the {@link ElevationProvider} to set during configuration loading. The same instance will be used later
+ * when elevating results for queries.
+ *
+ * @param queryAnalyzer to analyze and tokenize the query.
+ * @param elevationBuilderMap map of all {@link ElevatingQuery} and their corresponding {@link ElevationBuilder}.
+ * @return The created {@link ElevationProvider}.
+ */
+ protected ElevationProvider createElevationProvider(Analyzer queryAnalyzer, Map<ElevatingQuery, ElevationBuilder> elevationBuilderMap) {
+ return new MapElevationProvider(elevationBuilderMap);
}
- @Override
- public FieldComparator<Integer> newComparator(String fieldname, final int numHits, int sortPos, boolean reversed) {
- return new SimpleFieldComparator<Integer>() {
- private final int[] values = new int[numHits];
- private int bottomVal;
- private int topVal;
- private PostingsEnum postingsEnum;
- Set<String> seen = new HashSet<>(elevations.ids.size());
-
- @Override
- public int compare(int slot1, int slot2) {
- return values[slot1] - values[slot2]; // values will be small enough that there is no overflow concern
+ //---------------------------------------------------------------------------------
+ // Query analysis and tokenization
+ //---------------------------------------------------------------------------------
+
+ /**
+ * Analyzes the provided query string and returns a space concatenation of the analyzed tokens.
+ */
+ public String analyzeQuery(String query) {
+ //split query terms with analyzer then join
+ StringBuilder norm = new StringBuilder();
+ try (TokenStream tokens = queryAnalyzer.tokenStream("", query)) {
+ tokens.reset();
+ CharTermAttribute termAtt = tokens.addAttribute(CharTermAttribute.class);
+ while (tokens.incrementToken()) {
+ norm.append(termAtt);
+ }
+ tokens.end();
+ } catch (IOException e) {
+ Throwables.propagate(e);
+ }
+ return norm.toString();
+ }
+
+ //---------------------------------------------------------------------------------
+ // Testing
+ //---------------------------------------------------------------------------------
+
+ /**
+ * Helpful for testing without loading config.xml.
+ *
+ * @param reader The {@link org.apache.lucene.index.IndexReader}.
+ * @param queryString The query for which to elevate some documents. If the query has already been defined an
+ * elevation, this method overwrites it.
+ * @param subsetMatch <code>true</code> for query subset match; <code>false</code> for query exact match.
+ * @param elevatedIds The readable ids of the documents to set as top results for the provided query.
+ * @param excludedIds The readable ids of the document to exclude from results for the provided query.
+ * @throws java.io.IOException If there is a low-level I/O error.
+ */
+ @VisibleForTesting
+ void setTopQueryResults(IndexReader reader, String queryString, boolean subsetMatch,
+ String[] elevatedIds, String[] excludedIds) throws IOException {
+ clearElevationProviderCache();
+ ElevatingQuery elevatingQuery = new ElevatingQuery(queryString, subsetMatch);
+ ElevationBuilder elevationBuilder = new ElevationBuilder();
+ elevationBuilder.addElevatedIds(elevatedIds == null ? Collections.emptyList() : Arrays.asList(elevatedIds));
+ elevationBuilder.addExcludedIds(excludedIds == null ? Collections.emptyList() : Arrays.asList(excludedIds));
+ Map<ElevatingQuery, ElevationBuilder> elevationBuilderMap = ImmutableMap.of(elevatingQuery, elevationBuilder);
+ synchronized (elevationProviderCache) {
+ elevationProviderCache.computeIfAbsent(reader, k -> createElevationProvider(queryAnalyzer, elevationBuilderMap));
+ }
+ }
+
+ @VisibleForTesting
+ void clearElevationProviderCache() {
+ synchronized (elevationProviderCache) {
+ elevationProviderCache.clear();
+ }
+ }
+
+ //---------------------------------------------------------------------------------
+ // Exception
+ //---------------------------------------------------------------------------------
+
+ private static class InitializationException extends Exception {
+
+ private final InitializationExceptionCause exceptionCause;
+
+ InitializationException(String message, InitializationExceptionCause exceptionCause) {
+ super(message);
+ this.exceptionCause = exceptionCause;
+ }
+ }
+
+ protected enum InitializationExceptionCause {
+ /**
+ * The component parameter {@link #FIELD_TYPE} defines an unknown field type.
+ */
+ UNKNOWN_FIELD_TYPE,
+ /**
+ * This component requires the schema to have a uniqueKeyField, which it does not have.
+ */
+ MISSING_UNIQUE_KEY_FIELD,
+ /**
+ * Missing component parameter {@link #CONFIG_FILE} - it has to define the path to the elevation configuration file (e.g. elevate.xml).
+ */
+ NO_CONFIG_FILE_DEFINED,
+ /**
+ * The elevation configuration file (e.g. elevate.xml) cannot be found, or is defined in both conf/ and data/ directories.
+ */
+ MISSING_CONFIG_FILE,
+ /**
+ * The elevation configuration file (e.g. elevate.xml) is empty.
+ */
+ EMPTY_CONFIG_FILE,
+ /**
+ * Unclassified exception cause.
+ */
+ OTHER,
+ }
+
+ //---------------------------------------------------------------------------------
+ // Elevation classes
+ //---------------------------------------------------------------------------------
+
+ /**
+ * Provides the elevations defined for queries.
+ */
+ protected interface ElevationProvider {
+ /**
+ * Gets the elevation associated to the provided query.
+ * <p>
+ * By contract and by design, only one elevation may be associated
+ * to a given query (this can be safely verified by an assertion).
+ *
+ * @param queryString The query string (not {@link #analyzeQuery(String) analyzed} yet,
+ * this {@link ElevationProvider} is in charge of analyzing it).
+ * @return The elevation associated with the query; or <code>null</code> if none.
+ */
+ Elevation getElevationForQuery(String queryString);
+
+ /**
+ * Gets the number of query elevations in this {@link ElevationProvider}.
+ */
+ @VisibleForTesting
+ int size();
+ }
+
+ /**
+ * {@link ElevationProvider} that returns no elevation.
+ */
+ @SuppressWarnings("WeakerAccess")
+ protected static final ElevationProvider NO_OP_ELEVATION_PROVIDER = new ElevationProvider() {
+ @Override
+ public Elevation getElevationForQuery(String queryString) {
+ return null;
+ }
+
+ @Override
+ public int size() {
+ return 0;
+ }
+ };
+
+ /**
+ * Simple query exact match {@link ElevationProvider}.
+ * <p>
+ * It does not support subset matching (see {@link #parseMatchPolicy(String)}).
+ * <p>
+ * Immutable.
+ */
+ @SuppressWarnings("WeakerAccess")
+ protected class MapElevationProvider implements ElevationProvider {
+
+ private final Map<String, Elevation> elevationMap;
+
+ public MapElevationProvider(Map<ElevatingQuery, ElevationBuilder> elevationBuilderMap) {
+ elevationMap = buildElevationMap(elevationBuilderMap);
+ }
+
+ private Map<String, Elevation> buildElevationMap(Map<ElevatingQuery, ElevationBuilder> elevationBuilderMap) {
+ Map<String, Elevation> elevationMap = Maps.newHashMapWithExpectedSize(elevationBuilderMap.size());
+ for (Map.Entry<ElevatingQuery, ElevationBuilder> entry : elevationBuilderMap.entrySet()) {
+ ElevatingQuery elevatingQuery = entry.getKey();
+ if (elevatingQuery.subsetMatch) {
+ throw new UnsupportedOperationException("Subset matching is not supported by " + getClass().getName());
+ }
+ String analyzedQuery = analyzeQuery(elevatingQuery.queryString);
+ Elevation elevation = entry.getValue().build();
+ Elevation duplicateElevation = elevationMap.put(analyzedQuery, elevation);
+ if (duplicateElevation != null) {
+ throw new IllegalArgumentException("Duplicate elevation for query. Analyzed: \"" + analyzedQuery + "\"" +
+ " Original: \"" + elevatingQuery.queryString + "\"");
+ }
+ }
+ return Collections.unmodifiableMap(elevationMap);
+ }
+
+ @Override
+ public Elevation getElevationForQuery(String queryString) {
+ String analyzedQuery = analyzeQuery(queryString);
+ return elevationMap.get(analyzedQuery);
+ }
+
+ @Override
+ public int size() {
+ return elevationMap.size();
+ }
+ }
+
+ /**
+ * Query triggering elevation.
+ */
+ @SuppressWarnings("WeakerAccess")
+ protected static class ElevatingQuery {
+
+ public final String queryString;
+ public final boolean subsetMatch;
+
+ /**
+ * @param queryString The query to elevate documents for (not the analyzed form).
+ * @param subsetMatch Whether to match a subset of query terms.
+ */
+ protected ElevatingQuery(String queryString, boolean subsetMatch) throws IOException {
+ this.queryString = queryString;
+ this.subsetMatch = subsetMatch;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ElevatingQuery)) {
+ return false;
+ }
+ ElevatingQuery eq = (ElevatingQuery) o;
+ return queryString.equals(eq.queryString) && subsetMatch == eq.subsetMatch;
+ }
+
+ @Override
+ public int hashCode() {
+ return queryString.hashCode() + (subsetMatch ? 1 : 0);
+ }
+ }
+
+ /**
+ * Builds an {@link Elevation}. This class is used to start defining query elevations, but allowing the merge of
+ * multiple elevations for the same query.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public class ElevationBuilder {
+
+ /**
+ * The ids of the elevated documents that should appear on top of search results; can be <code>null</code>.
+ * The order is retained.
+ */
+ private LinkedHashSet<BytesRef> elevatedIds;
+ /**
+ * The ids of the excluded documents that should not appear in search results; can be <code>null</code>.
+ */
+ private Set<BytesRef> excludedIds;
+
+ // for temporary/transient use when adding an elevated or excluded ID
+ private final BytesRefBuilder scratch = new BytesRefBuilder();
+
+ public ElevationBuilder addElevatedIds(List<String> ids) {
+ if (elevatedIds == null) {
+ elevatedIds = new LinkedHashSet<>(Math.max(10, ids.size()));
+ }
+ for (String id : ids) {
+ elevatedIds.add(toBytesRef(id));
}
+ return this;
+ }
- @Override
- public void setBottom(int slot) {
- bottomVal = values[slot];
+ public ElevationBuilder addExcludedIds(Collection<String> ids) {
+ if (excludedIds == null) {
+ excludedIds = new LinkedHashSet<>(Math.max(10, ids.size()));
+ }
+ for (String id : ids) {
+ excludedIds.add(toBytesRef(id));
}
+ return this;
+ }
- @Override
- public void setTopValue(Integer value) {
- topVal = value.intValue();
+ public BytesRef toBytesRef(String id) {
+ uniqueKeyField.getType().readableToIndexed(id, scratch);
+ return scratch.toBytesRef();
+ }
+
+ public ElevationBuilder merge(ElevationBuilder elevationBuilder) {
+ if (elevatedIds == null) {
+ elevatedIds = elevationBuilder.elevatedIds;
+ } else if (elevationBuilder.elevatedIds != null) {
+ elevatedIds.addAll(elevationBuilder.elevatedIds);
+ }
+ if (excludedIds == null) {
+ excludedIds = elevationBuilder.excludedIds;
+ } else if (elevationBuilder.excludedIds != null) {
+ excludedIds.addAll(elevationBuilder.excludedIds);
}
+ return this;
+ }
- private int docVal(int doc) {
- if (ordSet.size() > 0) {
- int slot = ordSet.find(doc);
- if (slot >= 0) {
- BytesRef id = termValues[slot];
- Integer prio = elevations.priority.get(id);
- return prio == null ? 0 : prio.intValue();
- }
+ public Elevation build() {
+ return new Elevation(elevatedIds, excludedIds, uniqueKeyField.getName());
+ }
+
+ }
+
+ /**
+ * Elevation of some documents in search results, with potential exclusion of others.
+ */
+ protected static class Elevation {
+
+ private static final BooleanQuery EMPTY_QUERY = new BooleanQuery.Builder().build();
+
+ public final Set<BytesRef> elevatedIds; // in configured order; not null
+ public final BooleanQuery includeQuery; // not null
+ public final Set<BytesRef> excludedIds; // not null
+ //just keep the term query, b/c we will not always explicitly exclude the item based on markExcludes query time param
+ public final TermQuery[] excludeQueries; //may be null
+
+ /**
+ * Constructs an elevation.
+ * @param elevatedIds The ids of the elevated documents that should appear on top of search results; can be <code>null</code>.
+ * In configured order.
+ * @param excludedIds The ids of the excluded documents that should not appear in search results; can be <code>null</code>.
+ * @param queryFieldName The field name to use to create query terms.
+ */
+ public Elevation(Set<BytesRef> elevatedIds, Set<BytesRef> excludedIds,
+ String queryFieldName) {
+ if (elevatedIds == null || elevatedIds.isEmpty()) {
+ includeQuery = EMPTY_QUERY;
+ this.elevatedIds = Collections.emptySet();
+ } else {
+ this.elevatedIds = new LinkedHashSet<>(elevatedIds);
+ BooleanQuery.Builder includeQueryBuilder = new BooleanQuery.Builder();
+ for (BytesRef elevatedId : elevatedIds) {
+ includeQueryBuilder.add(new TermQuery(new Term(queryFieldName, elevatedId)), BooleanClause.Occur.SHOULD);
}
- return 0;
+ includeQuery = includeQueryBuilder.build();
}
- @Override
- public int compareBottom(int doc) {
- return bottomVal - docVal(doc);
+ if (excludedIds == null || excludedIds.isEmpty()) {
+ this.excludedIds = Collections.emptySet();
+ excludeQueries = null;
+ } else {
+ this.excludedIds = ImmutableSet.copyOf(excludedIds);
+ List<TermQuery> excludeQueriesBuilder = new ArrayList<>(excludedIds.size());
+ for (BytesRef excludedId : excludedIds) {
+ excludeQueriesBuilder.add(new TermQuery(new Term(queryFieldName, excludedId)));
+ }
+ excludeQueries = excludeQueriesBuilder.toArray(new TermQuery[excludeQueriesBuilder.size()]);
}
+ }
+
+ @Override
+ public String toString() {
+ return "{elevatedIds=" + Collections2.transform(elevatedIds, BytesRef::utf8ToString) +
+ ", excludedIds=" + Collections2.transform(excludedIds, BytesRef::utf8ToString) + "}";
+ }
+ }
- @Override
- public void copy(int slot, int doc) {
- values[slot] = docVal(doc);
+ /** Elevates certain docs to the top. */
+ private class ElevationComparatorSource extends FieldComparatorSource {
+
+ private final IntIntHashMap elevatedWithPriority;
+ private final boolean useConfiguredElevatedOrder;
+ private final int[] sortedElevatedDocIds;
+
+ private ElevationComparatorSource(IntIntHashMap elevatedWithPriority, boolean useConfiguredElevatedOrder) {
+ this.elevatedWithPriority = elevatedWithPriority;
+ this.useConfiguredElevatedOrder = useConfiguredElevatedOrder;
+
+ // copy elevatedWithPriority keys (doc IDs) into sortedElevatedDocIds, sorted
+ sortedElevatedDocIds = new int[elevatedWithPriority.size()];
+ final Iterator<IntIntCursor> iterator = elevatedWithPriority.iterator();
+ for (int i = 0; i < sortedElevatedDocIds.length; i++) {
+ IntIntCursor next = iterator.next();
+ sortedElevatedDocIds[i] = next.key;
}
+ assert iterator.hasNext() == false;
+ Arrays.sort(sortedElevatedDocIds);
+ }
- @Override
- protected void doSetNextReader(LeafReaderContext context) throws IOException {
- //convert the ids to Lucene doc ids, the ordSet and termValues needs to be the same size as the number of elevation docs we have
- ordSet.clear();
- Terms terms = context.reader().terms(idField);
- if (terms == null) return;
- TermsEnum termsEnum = terms.iterator();
- BytesRefBuilder term = new BytesRefBuilder();
- Bits liveDocs = context.reader().getLiveDocs();
-
- for (String id : elevations.ids) {
- term.copyChars(id);
- if (seen.contains(id) == false && termsEnum.seekExact(term.get())) {
- postingsEnum = termsEnum.postings(postingsEnum, PostingsEnum.NONE);
- int docId = postingsEnum.nextDoc();
- while (docId != DocIdSetIterator.NO_MORE_DOCS && liveDocs != null && liveDocs.get(docId) == false) {
- docId = postingsEnum.nextDoc();
+ @Override
+ public FieldComparator<Integer> newComparator(String fieldName, final int numHits, int sortPos, boolean reversed) {
+ return new SimpleFieldComparator<Integer>() {
+ final int[] values = new int[numHits];
+ int bottomVal;
+ int topVal;
+
+ int docBase;
+ boolean hasElevatedDocsThisSegment;
+
+ @Override
+ protected void doSetNextReader(LeafReaderContext context) throws IOException {
+ docBase = context.docBase;
+ // ascertain if hasElevatedDocsThisSegment
+ final int idx = Arrays.binarySearch(sortedElevatedDocIds, docBase);
+ if (idx < 0) {
+ //first doc in segment isn't elevated (typical). Maybe another is?
+ int nextIdx = -idx - 1;
+ if (nextIdx < sortedElevatedDocIds.length) {
+ int nextElevatedDocId = sortedElevatedDocIds[nextIdx];
+ if (nextElevatedDocId > docBase + context.reader().maxDoc()) {
+ hasElevatedDocsThisSegment = false;
+ return;
+ }
}
- if (docId == DocIdSetIterator.NO_MORE_DOCS ) continue; // must have been deleted
- termValues[ordSet.put(docId)] = term.toBytesRef();
- seen.add(id);
- assert postingsEnum.nextDoc() == DocIdSetIterator.NO_MORE_DOCS;
}
+ hasElevatedDocsThisSegment = true;
}
- }
- @Override
- public Integer value(int slot) {
- return values[slot];
- }
+ @Override
+ public int compare(int slot1, int slot2) {
+ return values[slot1] - values[slot2]; // values will be small enough that there is no overflow concern
+ }
- @Override
- public int compareTop(int doc) {
- final int docValue = docVal(doc);
- return topVal - docValue; // values will be small enough that there is no overflow concern
- }
- };
- }
+ @Override
+ public void setBottom(int slot) {
+ bottomVal = values[slot];
+ }
+
+ @Override
+ public void setTopValue(Integer value) {
+ topVal = value;
+ }
+
+ private int docVal(int doc) {
+ if (!hasElevatedDocsThisSegment) {
+ assert elevatedWithPriority.containsKey(docBase + doc) == false;
+ return -1;
+ } else if (useConfiguredElevatedOrder) {
+ return elevatedWithPriority.getOrDefault(docBase + doc, -1);
+ } else {
+ return elevatedWithPriority.containsKey(docBase + doc) ? 1 : -1;
+ }
+ }
+
+ @Override
+ public int compareBottom(int doc) {
+ return bottomVal - docVal(doc);
+ }
+
+ @Override
+ public void copy(int slot, int doc) {
+ values[slot] = docVal(doc);
+ }
+
+ @Override
+ public Integer value(int slot) {
+ return values[slot];
+ }
+
+ @Override
+ public int compareTop(int doc) {
+ final int docValue = docVal(doc);
+ return topVal - docValue; // values will be small enough that there is no overflow concern
+ }
+ };
+ }
}
-}
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/a06256cc/solr/core/src/java/org/apache/solr/response/transform/BaseEditorialTransformer.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/transform/BaseEditorialTransformer.java b/solr/core/src/java/org/apache/solr/response/transform/BaseEditorialTransformer.java
index d646ee4..6324d8a 100644
--- a/solr/core/src/java/org/apache/solr/response/transform/BaseEditorialTransformer.java
+++ b/solr/core/src/java/org/apache/solr/response/transform/BaseEditorialTransformer.java
@@ -20,6 +20,8 @@ package org.apache.solr.response.transform;
import java.util.Set;
import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.BytesRefBuilder;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.schema.FieldType;
@@ -47,9 +49,9 @@ public abstract class BaseEditorialTransformer extends DocTransformer {
@Override
public void transform(SolrDocument doc, int docid) {
//this only gets added if QueryElevationParams.MARK_EXCLUDED is true
- Set<String> ids = getIdSet();
+ Set<BytesRef> ids = getIdSet();
if (ids != null && ids.isEmpty() == false) {
- String key = getKey(doc);
+ BytesRef key = getKey(doc);
doc.setField(name, ids.contains(key));
} else {
//if we have no ids, that means we weren't marking, but the user still asked for the field to be added, so just mark everything as false
@@ -57,17 +59,20 @@ public abstract class BaseEditorialTransformer extends DocTransformer {
}
}
- protected abstract Set<String> getIdSet();
+ protected abstract Set<BytesRef> getIdSet();
- protected String getKey(SolrDocument doc) {
+ protected BytesRef getKey(SolrDocument doc) {
Object obj = doc.get(idFieldName);
if (obj instanceof IndexableField) {
IndexableField f = (IndexableField) obj;
+ BytesRefBuilder bytesRefBuilder = new BytesRefBuilder();
Number n = f.numericValue();
if (n != null) {
- return ft.readableToIndexed(n.toString());
+ ft.readableToIndexed(n.toString(), bytesRefBuilder);
+ } else {
+ ft.readableToIndexed(f.stringValue(), bytesRefBuilder);
}
- return ft.readableToIndexed(f.stringValue());
+ return bytesRefBuilder.get();
}
throw new AssertionError("Expected an IndexableField but got: " + obj.getClass());
}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/a06256cc/solr/core/src/java/org/apache/solr/response/transform/ElevatedMarkerFactory.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/transform/ElevatedMarkerFactory.java b/solr/core/src/java/org/apache/solr/response/transform/ElevatedMarkerFactory.java
index 51f3cff..e5fb414 100644
--- a/solr/core/src/java/org/apache/solr/response/transform/ElevatedMarkerFactory.java
+++ b/solr/core/src/java/org/apache/solr/response/transform/ElevatedMarkerFactory.java
@@ -18,6 +18,7 @@ package org.apache.solr.response.transform;
import java.util.Set;
+import org.apache.lucene.util.BytesRef;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.handler.component.QueryElevationComponent;
import org.apache.solr.request.SolrQueryRequest;
@@ -44,9 +45,10 @@ class MarkTransformer extends BaseEditorialTransformer {
super(name, idFieldName, ft);
}
+ @SuppressWarnings("unchecked")
@Override
- protected Set<String> getIdSet() {
- return (Set<String>) context.getRequest().getContext().get(QueryElevationComponent.BOOSTED);
+ protected Set<BytesRef> getIdSet() {
+ return (Set<BytesRef>) context.getRequest().getContext().get(QueryElevationComponent.BOOSTED);
}
}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/a06256cc/solr/core/src/java/org/apache/solr/response/transform/ExcludedMarkerFactory.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/transform/ExcludedMarkerFactory.java b/solr/core/src/java/org/apache/solr/response/transform/ExcludedMarkerFactory.java
index 2d670d7..2036c48 100644
--- a/solr/core/src/java/org/apache/solr/response/transform/ExcludedMarkerFactory.java
+++ b/solr/core/src/java/org/apache/solr/response/transform/ExcludedMarkerFactory.java
@@ -18,6 +18,7 @@ package org.apache.solr.response.transform;
import java.util.Set;
+import org.apache.lucene.util.BytesRef;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.handler.component.QueryElevationComponent;
import org.apache.solr.request.SolrQueryRequest;
@@ -46,9 +47,10 @@ class ExcludedTransformer extends BaseEditorialTransformer {
super(name, idFieldName, ft);
}
+ @SuppressWarnings("unchecked")
@Override
- protected Set<String> getIdSet() {
- return (Set<String>)context.getRequest().getContext().get(QueryElevationComponent.EXCLUDED);
+ protected Set<BytesRef> getIdSet() {
+ return (Set<BytesRef>)context.getRequest().getContext().get(QueryElevationComponent.EXCLUDED);
}
}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/a06256cc/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java b/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
index f7679b0..0c2fb82 100644
--- a/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
+++ b/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
@@ -18,6 +18,7 @@ package org.apache.solr.search;
import java.io.IOException;
import java.util.Map;
+import java.util.Set;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.IndexSearcher;
@@ -35,7 +36,7 @@ public abstract class AbstractReRankQuery extends RankQuery {
protected Query mainQuery;
final protected int reRankDocs;
final protected Rescorer reRankQueryRescorer;
- protected Map<BytesRef, Integer> boostedPriority;
+ protected Set<BytesRef> boostedPriority;
public AbstractReRankQuery(Query mainQuery, int reRankDocs, Rescorer reRankQueryRescorer) {
this.mainQuery = mainQuery;
@@ -54,13 +55,13 @@ public abstract class AbstractReRankQuery extends RankQuery {
return null;
}
+ @SuppressWarnings("unchecked")
public TopDocsCollector getTopDocsCollector(int len, QueryCommand cmd, IndexSearcher searcher) throws IOException {
-
if(this.boostedPriority == null) {
SolrRequestInfo info = SolrRequestInfo.getRequestInfo();
if(info != null) {
Map context = info.getReq().getContext();
- this.boostedPriority = (Map<BytesRef, Integer>)context.get(QueryElevationComponent.BOOSTED_PRIORITY);
+ this.boostedPriority = (Set<BytesRef>)context.get(QueryElevationComponent.BOOSTED);
}
}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/a06256cc/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java b/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java
index d0f8cd4..5a6d48c 100644
--- a/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java
@@ -24,7 +24,14 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import com.carrotsearch.hppc.FloatArrayList;
+import com.carrotsearch.hppc.IntArrayList;
+import com.carrotsearch.hppc.IntIntHashMap;
+import com.carrotsearch.hppc.IntLongHashMap;
+import com.carrotsearch.hppc.cursors.IntIntCursor;
+import com.carrotsearch.hppc.cursors.IntLongCursor;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.codecs.DocValuesProducer;
import org.apache.lucene.index.DocValues;
@@ -69,13 +76,6 @@ import org.apache.solr.schema.NumberType;
import org.apache.solr.schema.StrField;
import org.apache.solr.uninverting.UninvertingReader;
-import com.carrotsearch.hppc.FloatArrayList;
-import com.carrotsearch.hppc.IntArrayList;
-import com.carrotsearch.hppc.IntIntHashMap;
-import com.carrotsearch.hppc.IntLongHashMap;
-import com.carrotsearch.hppc.cursors.IntIntCursor;
-import com.carrotsearch.hppc.cursors.IntLongCursor;
-
import static org.apache.solr.common.params.CommonParams.SORT;
/**
@@ -215,7 +215,7 @@ public class CollapsingQParserPlugin extends QParserPlugin {
public String hint;
private boolean needsScores = true;
private int nullPolicy;
- private Map<BytesRef, Integer> boosted;
+ private Set<BytesRef> boosted; // ordered by "priority"
public static final int NULL_POLICY_IGNORE = 0;
public static final int NULL_POLICY_COLLAPSE = 1;
public static final int NULL_POLICY_EXPAND = 2;
@@ -338,11 +338,6 @@ public class CollapsingQParserPlugin extends QParserPlugin {
}
}
- private IntIntHashMap getBoostDocs(SolrIndexSearcher indexSearcher, Map<BytesRef, Integer> boosted, Map context) throws IOException {
- IntIntHashMap boostDocs = QueryElevationComponent.getBoostDocs(indexSearcher, boosted, context);
- return boostDocs;
- }
-
public DelegatingCollector getFilterCollector(IndexSearcher indexSearcher) {
try {
@@ -360,10 +355,10 @@ public class CollapsingQParserPlugin extends QParserPlugin {
}
if(this.boosted == null && context != null) {
- this.boosted = (Map<BytesRef, Integer>)context.get(QueryElevationComponent.BOOSTED_PRIORITY);
+ this.boosted = (Set<BytesRef>)context.get(QueryElevationComponent.BOOSTED);
}
- boostDocsMap = getBoostDocs(searcher, this.boosted, context);
+ boostDocsMap = QueryElevationComponent.getBoostDocs(searcher, this.boosted, context);
return collectorFactory.getCollector(this.collapseField,
this.groupHeadSelector,
this.sortSpec,
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/a06256cc/solr/core/src/java/org/apache/solr/search/ReRankCollector.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/search/ReRankCollector.java b/solr/core/src/java/org/apache/solr/search/ReRankCollector.java
index 0447053..a1689dd 100644
--- a/solr/core/src/java/org/apache/solr/search/ReRankCollector.java
+++ b/solr/core/src/java/org/apache/solr/search/ReRankCollector.java
@@ -20,9 +20,10 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Map;
+import java.util.Set;
+
import com.carrotsearch.hppc.IntFloatHashMap;
import com.carrotsearch.hppc.IntIntHashMap;
-
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.LeafCollector;
@@ -46,7 +47,7 @@ public class ReRankCollector extends TopDocsCollector {
final private IndexSearcher searcher;
final private int reRankDocs;
final private int length;
- final private Map<BytesRef, Integer> boostedPriority;
+ final private Set<BytesRef> boostedPriority; // order is the "priority"
final private Rescorer reRankQueryRescorer;
@@ -55,7 +56,7 @@ public class ReRankCollector extends TopDocsCollector {
Rescorer reRankQueryRescorer,
QueryCommand cmd,
IndexSearcher searcher,
- Map<BytesRef, Integer> boostedPriority) throws IOException {
+ Set<BytesRef> boostedPriority) throws IOException {
super(null);
this.reRankDocs = reRankDocs;
this.length = length;