blob: badfe81f7dbf3d67a7bda8e53f65f181a96c55b9 [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.lucene.facet;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.BoostQuery;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryVisitor;
import org.apache.lucene.search.TermQuery;
/**
* A {@link Query} for drill-down over facet categories. You
* should call {@link #add(String, String...)} for every group of categories you
* want to drill-down over.
* <p>
* <b>NOTE:</b> if you choose to create your own {@link Query} by calling
* {@link #term}, it is recommended to wrap it in a {@link BoostQuery}
* with a boost of {@code 0.0f},
* so that it does not affect the scores of the documents.
*
* @lucene.experimental
*/
public final class DrillDownQuery extends Query {
/** Creates a drill-down term. */
public static Term term(String field, String dim, String... path) {
return new Term(field, FacetsConfig.pathToString(dim, path));
}
private final FacetsConfig config;
private final Query baseQuery;
private final List<BooleanQuery.Builder> dimQueries = new ArrayList<>();
private final Map<String,Integer> drillDownDims = new LinkedHashMap<>();
/** Used by clone() and DrillSideways */
DrillDownQuery(FacetsConfig config, Query baseQuery, List<BooleanQuery.Builder> dimQueries, Map<String,Integer> drillDownDims) {
this.baseQuery = baseQuery;
this.dimQueries.addAll(dimQueries);
this.drillDownDims.putAll(drillDownDims);
this.config = config;
}
/** Used by DrillSideways */
DrillDownQuery(FacetsConfig config, Query filter, DrillDownQuery other) {
this.baseQuery = new BooleanQuery.Builder()
.add(other.baseQuery == null ? new MatchAllDocsQuery() : other.baseQuery, Occur.MUST)
.add(filter, Occur.FILTER)
.build();
this.dimQueries.addAll(other.dimQueries);
this.drillDownDims.putAll(other.drillDownDims);
this.config = config;
}
/** Creates a new {@code DrillDownQuery} without a base query,
* to perform a pure browsing query (equivalent to using
* {@link MatchAllDocsQuery} as base). */
public DrillDownQuery(FacetsConfig config) {
this(config, null);
}
/** Creates a new {@code DrillDownQuery} over the given base query. Can be
* {@code null}, in which case the result {@link Query} from
* {@link #rewrite(IndexReader)} will be a pure browsing query, filtering on
* the added categories only. */
public DrillDownQuery(FacetsConfig config, Query baseQuery) {
this.baseQuery = baseQuery;
this.config = config;
}
/** Adds one dimension of drill downs; if you pass the same
* dimension more than once it is OR'd with the previous
* constraints on that dimension, and all dimensions are
* AND'd against each other and the base query. */
public void add(String dim, String... path) {
String indexedField = config.getDimConfig(dim).indexFieldName;
add(dim, new TermQuery(term(indexedField, dim, path)));
}
/** Expert: add a custom drill-down subQuery. Use this
* when you have a separate way to drill-down on the
* dimension than the indexed facet ordinals. */
public void add(String dim, Query subQuery) {
assert drillDownDims.size() == dimQueries.size();
if (drillDownDims.containsKey(dim) == false) {
drillDownDims.put(dim, drillDownDims.size());
BooleanQuery.Builder builder = new BooleanQuery.Builder();
dimQueries.add(builder);
}
final int index = drillDownDims.get(dim);
dimQueries.get(index).add(subQuery, Occur.SHOULD);
}
@Override
public DrillDownQuery clone() {
return new DrillDownQuery(config, baseQuery, dimQueries, drillDownDims);
}
@Override
public int hashCode() {
return classHash() + Objects.hash(baseQuery, dimQueries);
}
@Override
public boolean equals(Object other) {
return sameClassAs(other) &&
equalsTo(getClass().cast(other));
}
private boolean equalsTo(DrillDownQuery other) {
return Objects.equals(baseQuery, other.baseQuery) &&
dimQueries.equals(other.dimQueries);
}
@Override
public Query rewrite(IndexReader r) throws IOException {
BooleanQuery rewritten = getBooleanQuery();
if (rewritten.clauses().isEmpty()) {
return new MatchAllDocsQuery();
}
return rewritten;
}
@Override
public String toString(String field) {
return getBooleanQuery().toString(field);
}
@Override
public void visit(QueryVisitor visitor) {
visitor.visitLeaf(this);
}
private BooleanQuery getBooleanQuery() {
BooleanQuery.Builder bq = new BooleanQuery.Builder();
if (baseQuery != null) {
bq.add(baseQuery, Occur.MUST);
}
for (BooleanQuery.Builder builder : dimQueries) {
bq.add(builder.build(), Occur.FILTER);
}
return bq.build();
}
Query getBaseQuery() {
return baseQuery;
}
Query[] getDrillDownQueries() {
Query[] dimQueries = new Query[this.dimQueries.size()];
for (int i = 0; i < dimQueries.length; ++i) {
dimQueries[i] = this.dimQueries.get(i).build();
}
return dimQueries;
}
Map<String,Integer> getDims() {
return drillDownDims;
}
}