querqy.elasticsearch.DismaxSearchEngineRequestAdapter Maven / Gradle / Ivy
Show all versions of querqy-elasticsearch Show documentation
package querqy.elasticsearch;
import static querqy.lucene.PhraseBoosting.makePhraseFieldsBoostQuery;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.DelegatingAnalyzerWrapper;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.lucene.search.Queries;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.query.SearchExecutionContext;
import querqy.elasticsearch.infologging.ESInfoLoggingContext;
import querqy.elasticsearch.infologging.InfoLoggingSpecProvider;
import querqy.elasticsearch.query.BoostingQueries;
import querqy.elasticsearch.query.Generated;
import querqy.elasticsearch.query.InfoLoggingSpec;
import querqy.elasticsearch.query.PhraseBoosts;
import querqy.elasticsearch.query.QuerqyQueryBuilder;
import querqy.elasticsearch.query.QueryBuilderRawQuery;
import querqy.elasticsearch.query.Rewriter;
import querqy.elasticsearch.query.RewrittenQueries;
import querqy.infologging.InfoLogging;
import querqy.infologging.InfoLoggingContext;
import querqy.lucene.LuceneSearchEngineRequestAdapter;
import querqy.lucene.PhraseBoosting.PhraseBoostFieldParams;
import querqy.lucene.QuerySimilarityScoring;
import querqy.lucene.rewrite.SearchFieldsAndBoosting;
import querqy.lucene.rewrite.cache.TermQueryCache;
import querqy.model.QuerqyQuery;
import querqy.model.RawQuery;
import querqy.model.StringRawQuery;
import querqy.parser.QuerqyParser;
import querqy.rewrite.ContextAwareQueryRewriter;
import querqy.rewrite.QueryRewriter;
import querqy.rewrite.RewriteChain;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
/**
* Rewriters will access params using prefix 'querqy.<rewriter id>....
*/
public class DismaxSearchEngineRequestAdapter implements LuceneSearchEngineRequestAdapter, InfoLoggingSpecProvider {
private final RewriteChain rewriteChain;
private final SearchExecutionContext shardContext;
final ESInfoLoggingContext infoLoggingContext;
private final QuerqyQueryBuilder queryBuilder;
private final Map context = new HashMap<>();
public DismaxSearchEngineRequestAdapter(final QuerqyQueryBuilder queryBuilder,
final RewriteChain rewriteChain,
final SearchExecutionContext shardContext,
final InfoLogging infoLogging) {
this.shardContext = shardContext;
this.rewriteChain = rewriteChain;
this.queryBuilder = queryBuilder;
this.infoLoggingContext = (infoLogging != null) ? new ESInfoLoggingContext(infoLogging, this) : null;
}
/**
* Get the query string that should be parsed into the main query.
*
* Must be neither null or nor empty.
*
* @return The query string.
*/
@Override
public String getQueryString() {
return queryBuilder.getMatchingQuery().getQueryString();
}
/**
* Does this query string mean 'match all documents'?
*
* @param queryString The query string.
* @return true if the query string means 'match all documents' and false otherwise
*/
@Override
public boolean isMatchAllQuery(final String queryString) {
// TODO: do we want this?
return false;
}
/**
* Should the query results be scored?
*
* This should return false for filter queries. If this method returns false, no boost queries will be used
* (neither those from Querqy query rewriting nor those that were passed in request parameters).
*
* @return true if the query results should be scored and false otherwise
*/
@Override
public boolean needsScores() {
// TODO: should we allow to switch this off?
return true;
}
/**
* Get the analyzer for applying text analysis to query terms.
*
* This will normally be an {@link Analyzer} that delegates to other Analyzers based on the given query fields.
*
* @return The query analyzer.
*/
@Override
public Analyzer getQueryAnalyzer() {
return new MapperAnalyzerWrapper(mappedFieldType -> mappedFieldType.getTextSearchInfo().getSearchAnalyzer());
}
/**
* Get an optional {@link TermQueryCache}
*
* @return The optional TermQueryCache
*/
@Override
public Optional getTermQueryCache() {
// TODO
return Optional.empty();
}
/**
* Should Querqy boost queries be added to the main query? - yes, as we will not have control over
* re-rank queries from within the query builder.
*
* @return Always true
*/
@Override
public boolean addQuerqyBoostQueriesToMainQuery() {
return true;
}
@Override
public Optional getUserQuerySimilarityScoring() {
return queryBuilder.getMatchingQuery().getSimilarityScoring();
}
@Override
public Optional getBoostQuerySimilarityScoring() {
final BoostingQueries boostingQueries = queryBuilder.getBoostingQueries();
if (boostingQueries == null) {
return Optional.empty();
}
return boostingQueries.getRewrittenQueries().map(RewrittenQueries::getSimilarityScoring);
}
/**
* Get the query fields and their weights for the query entered by the user.
*
* @return A map of field names and field boost factors.
* @see #getGeneratedQueryFieldsAndBoostings()
*/
@Override
public Map getQueryFieldsAndBoostings() {
return queryBuilder.getQueryFieldsAndBoostings();
}
/**
* Get the query fields and their weights for queries that were generated during query rewriting.
*
* If this method returns an empty map, the map returned by {@link #getQueryFieldsAndBoostings()} will also be
* used for generated queries.
*
* @return A map of field names and field boost factors, or an empty map.
* @see #useFieldBoostingInQuerqyBoostQueries()
*/
@Override
public Map getGeneratedQueryFieldsAndBoostings() {
return queryBuilder.getGenerated().map(Generated::getQueryFieldsAndBoostings).orElse(Collections.emptyMap());
}
@Override
public Optional createQuerqyParser() {
// TODO
return Optional.empty();
}
@Override
public boolean useFieldBoostingInQuerqyBoostQueries() {
final BoostingQueries boostingQueries = queryBuilder.getBoostingQueries();
if (boostingQueries == null) {
return true;
}
return boostingQueries.getRewrittenQueries().map(RewrittenQueries::isUseFieldBoosts).orElse(true);
}
@Override
public Optional getTiebreaker() {
return queryBuilder.getTieBreaker();
}
/**
* Apply the 'minimum should match' setting of the request.
* It will be the responsibility of the LuceneSearchEngineRequestAdapter implementation to derive the
* 'minimum should match' setting from request parameters or other configuration.
* The query parameter is the rewritten user query. {@link QueryRewriter}s shall guarantee to
* preserve the number of top-level query clauses at query rewriting.
*
* @param query The parsed and rewritten user query.
* @return The query after application of 'minimum should match'
* @see BooleanQuery#getMinimumNumberShouldMatch()
*/
@Override
public Query applyMinimumShouldMatch(final BooleanQuery query) {
return Queries.applyMinimumShouldMatch(query, queryBuilder.getMinimumShouldMatch());
}
/**
* Get the weight to be multiplied with the main Querqy query (the query entered by the user).
*
* @return An optional weight for the main query
*/
@Override
public Optional getUserQueryWeight() {
return queryBuilder.getMatchingQuery().getWeight();
}
@Override
public Optional getGeneratedFieldBoost() {
final Optional generated = queryBuilder.getGenerated();
if (generated.isPresent()) {
return generated.get().getFieldBoostFactor();
} else {
return Optional.empty();
}
}
@Override
public Optional getPositiveQuerqyBoostWeight() {
final BoostingQueries boostingQueries = queryBuilder.getBoostingQueries();
if (boostingQueries == null) {
return Optional.empty();
}
return boostingQueries.getRewrittenQueries().map(RewrittenQueries::getPositiveWeight);
}
@Override
public Optional getNegativeQuerqyBoostWeight() {
final BoostingQueries boostingQueries = queryBuilder.getBoostingQueries();
if (boostingQueries == null) {
return Optional.empty();
}
return boostingQueries.getRewrittenQueries().map(RewrittenQueries::getNegativeWeight);
}
/**
* Get the list of boost queries whose scores should be added to the score of the main query.
* The queries are not a result of query rewriting but queries that may have been added as request parameters
* (like 'bq' in Solr's Dismax query parser).
*
* @param userQuery The user query parsed into a {@link QuerqyQuery}
* @return The list of additive boost queries or an empty list if no such query exists.
* @throws SyntaxException if a multiplicative boost query could not be parsed
*/
@Override
public List getAdditiveBoosts(final QuerqyQuery> userQuery) throws SyntaxException {
//final PhraseBoosts phraseBoosts = queryBuilder.getPhraseBoosts();
final BoostingQueries boostingQueries = queryBuilder.getBoostingQueries();
if (boostingQueries != null) {
final PhraseBoosts phraseBoosts = boostingQueries.getPhraseBoosts();
if (phraseBoosts != null) {
final List boosts = new ArrayList<>(1);
final List phraseBoostFieldParams = phraseBoosts.toPhraseBoostFieldParams();
if (phraseBoostFieldParams == null || phraseBoostFieldParams.isEmpty()) {
return boosts;
}
makePhraseFieldsBoostQuery(userQuery, phraseBoostFieldParams, phraseBoosts.getTieBreaker(),
getQueryAnalyzer()).ifPresent(boosts::add);
return boosts;
}
}
return null;
}
/**
* Get the list of boost queries whose scores should be multiplied to the score of the main query.
* The queries are
* not a result of query rewriting but queries that may have been added as request parameters (like 'boost'
* in Solr's Extended Dismax query parser).
*
* @param userQuery The user query parsed into a {@link QuerqyQuery}
* @return The list of multiplicative boost queries or an empty list if no such query exists.
* @throws SyntaxException if a multiplicative boost query could not be parsed
*/
@Override
public List getMultiplicativeBoosts(final QuerqyQuery> userQuery) throws SyntaxException {
return null;
}
@Override
public Optional parseRankQuery() throws SyntaxException {
return Optional.empty();
}
/**
* Parse a {@link RawQuery}. The RawQuery must be of type {@link QueryBuilderRawQuery} or {@link StringRawQuery}.
*
* @param rawQuery The raw query.
* @return The Query parsed from the RawQuery.
* @throws SyntaxException @throws SyntaxException if the raw query query could not be parsed
*/
@Override
public Query parseRawQuery(final RawQuery rawQuery) throws SyntaxException {
try {
if (rawQuery instanceof QueryBuilderRawQuery) {
return ((QueryBuilderRawQuery) rawQuery).getQueryBuilder().toQuery(shardContext);
}
if (rawQuery instanceof StringRawQuery) {
final XContentParser parser = XContentHelper.createParser(shardContext.getXContentRegistry(), null,
new BytesArray(((StringRawQuery) rawQuery).getQueryString()), XContentType.JSON);
return shardContext.parseInnerQueryBuilder(parser).toQuery(shardContext);
}
throw new IllegalArgumentException("Cannot handle RawQuery of type "+ rawQuery.getClass().getName());
} catch (final IOException e) {
throw new SyntaxException("Error parsing raw query", e);
}
}
@Override
public Optional getFieldBoostModel() {
return queryBuilder.getFieldBoostModel();
}
/**
* Get the rewrite chain to be applied to the user query.
*
* @return The rewrite chain.
*/
@Override
public RewriteChain getRewriteChain() {
return rewriteChain;
}
/**
* Get a map to hold context information while rewriting the query.
*
* @return A non-null context map.
* @see ContextAwareQueryRewriter
*/
@Override
public Map getContext() {
return context;
}
/**
* Get request parameter as String
*
* @param name the parameter name
* @return the optional parameter value
*/
@Override
public Optional getRequestParam(final String name) {
return getParam(name);
}
Optional getParam(final String name) {
final String[] parts = name.split("\\.");
if (parts.length < 3 || !"querqy".equals(parts[0])) {
return Optional.empty();
}
final String rewriterId = parts[1];
for (final Rewriter rewriter : queryBuilder.getRewriters()) {
if (rewriterId.equals(rewriter.getName())) {
return getRewriterParam(rewriter, Arrays.copyOfRange(parts, 2, parts.length));
}
}
return Optional.empty();
}
Optional getRewriterParam(final Rewriter rewriter, final String[] path) {
final Map params = rewriter.getParams();
if (params == null) {
return Optional.empty();
}
Map current = params;
final int len = path.length - 1;
for (int i = 0; i < len; i++) {
current = (Map) current.get(path[i]);
if (current == null) {
return Optional.empty();
}
}
return Optional.ofNullable((T) current.get(path[len]));
}
/**
* Get request parameter as an array of Strings
*
* @param name the parameter name
* @return the parameter value String array (String[0] if not set)
*/
@Override
public String[] getRequestParams(final String name) {
final String[] parts = name.split("\\.");
if (parts.length < 3 || !"querqy".equals(parts[0])) {
return new String[0];
}
final String rewriterId = parts[1];
for (final Rewriter rewriter : queryBuilder.getRewriters()) {
if (rewriterId.equals(rewriter.getName())) {
final Map params = rewriter.getParams();
if (params == null) {
return new String[0];
}
Map current = params;
final int len = parts.length - 1;
for (int i = 2; i < len; i++) {
current = ( Map) current.get(parts[i]);
if (current == null) {
return new String[0];
}
}
final Object obj = current.get(parts[len]);
if (obj == null) {
return new String[0];
}
if (obj instanceof String) {
return new String[] {obj.toString()};
} else {
return (String[]) obj;
}
}
}
return new String[0];
}
/**
* Get request parameter as Boolean
*
* @param name the parameter name
* @return the optional parameter value
*/
@Override
public Optional getBooleanRequestParam(final String name) {
return getParam(name);
}
/**
* Get request parameter as Integer
*
* @param name the parameter name
* @return the optional parameter value
*/
@Override
public Optional getIntegerRequestParam(final String name) {
return getParam(name);
}
/**
* Get request parameter as Float
*
* @param name the parameter name
* @return the optional parameter value
*/
@Override
public Optional getFloatRequestParam(final String name) {
return getParam(name);
}
/**
* Get request parameter as Double
*
* @param name the parameter name
* @return the optional parameter value
*/
@Override
public Optional getDoubleRequestParam(final String name) {
return getParam(name);
}
/**
* Get the per-request info logging. Return an empty option if logging hasn't been configured or was disabled
* for this request.
*
* @return the InfoLoggingContext object
*/
@Override
public Optional getInfoLoggingContext() {
return Optional.ofNullable(infoLoggingContext);
}
/**
* Should debug information be collected while rewriting the query?
* Debug information will be kept in the context map under the
* {@link querqy.rewrite.AbstractLoggingRewriter#CONTEXT_KEY_DEBUG_DATA} key.
*
* @return true if debug information shall be collected, false otherwise
* @see #getContext()
*/
@Override
public boolean isDebugQuery() {
return false;
}
public SearchExecutionContext getSearchExecutionContext() {
return shardContext;
}
@Override
public Optional getInfoLoggingSpec() {
return infoLoggingContext != null ? Optional.ofNullable(queryBuilder.getInfoLoggingSpec()) : Optional.empty();
}
class MapperAnalyzerWrapper extends DelegatingAnalyzerWrapper {
private final Function analyzerProvider;
MapperAnalyzerWrapper(final Function analyzerProvider) {
super(Analyzer.PER_FIELD_REUSE_STRATEGY);
this.analyzerProvider = analyzerProvider;
}
@Override
protected Analyzer getWrappedAnalyzer(final String fieldName) {
final MappedFieldType fieldType = shardContext.getFieldType(fieldName);
if (fieldType != null) {
final Analyzer analyzer = analyzerProvider.apply(fieldType);
if (analyzer != null) {
return analyzer;
}
}
return shardContext.getIndexAnalyzers().getDefaultSearchAnalyzer();
}
}
}