blob: 9754c7fb2e3950e706284069309f4a00e3533e08 [file] [log] [blame]
// Lucene version compatibility level 4.8.1
using Lucene.Net.Diagnostics;
using Lucene.Net.Support;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using JCG = J2N.Collections.Generic;
namespace Lucene.Net.Facet
{
/*
* 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.
*/
using BooleanClause = Lucene.Net.Search.BooleanClause;
using BooleanQuery = Lucene.Net.Search.BooleanQuery;
using ConstantScoreQuery = Lucene.Net.Search.ConstantScoreQuery;
using Filter = Lucene.Net.Search.Filter;
using FilteredQuery = Lucene.Net.Search.FilteredQuery;
using IndexReader = Lucene.Net.Index.IndexReader;
using MatchAllDocsQuery = Lucene.Net.Search.MatchAllDocsQuery;
using Occur = Lucene.Net.Search.Occur;
using Query = Lucene.Net.Search.Query;
using Term = Lucene.Net.Index.Term;
using TermQuery = Lucene.Net.Search.TermQuery;
/// <summary>
/// A <see cref="Query"/> for drill-down over facet categories. You
/// should call <see cref="Add(string, string[])"/> for every group of categories you
/// want to drill-down over.
/// <para>
/// <b>NOTE:</b> if you choose to create your own <see cref="Query"/> by calling
/// <see cref="Term"/>, it is recommended to wrap it with <see cref="ConstantScoreQuery"/>
/// and set the <see cref="Query.Boost">boost</see> to <c>0.0f</c>,
/// so that it does not affect the scores of the documents.
///
/// @lucene.experimental
/// </para>
/// </summary>
public sealed class DrillDownQuery : Query // LUCENENET TODO: Add collection initializer to make populating easier
{
/// <summary>
/// Creates a drill-down term.
/// </summary>
public static Term Term(string field, string dim, params string[] path)
{
return new Term(field, FacetsConfig.PathToString(dim, path));
}
private readonly FacetsConfig config;
private readonly BooleanQuery query;
private readonly IDictionary<string, int?> drillDownDims = new JCG.LinkedDictionary<string, int?>();
/// <summary>
/// Used by <see cref="Clone"/>
/// </summary>
internal DrillDownQuery(FacetsConfig config, BooleanQuery query, IDictionary<string, int?> drillDownDims)
{
this.query = (BooleanQuery)query.Clone();
this.drillDownDims.PutAll(drillDownDims);
this.config = config;
}
/// <summary>
/// Used by <see cref="DrillSideways"/>
/// </summary>
internal DrillDownQuery(FacetsConfig config, Filter filter, DrillDownQuery other)
{
query = new BooleanQuery(true); // disable coord
BooleanClause[] clauses = other.query.GetClauses();
if (clauses.Length == other.drillDownDims.Count)
{
throw new ArgumentException("cannot apply filter unless baseQuery isn't null; pass ConstantScoreQuery instead");
}
if (Debugging.AssertsEnabled) Debugging.Assert(clauses.Length == 1 + other.drillDownDims.Count, "{0} vs {1}", clauses.Length, (1 + other.drillDownDims.Count));
drillDownDims.PutAll(other.drillDownDims);
query.Add(new FilteredQuery(clauses[0].Query, filter), Occur.MUST);
for (int i = 1; i < clauses.Length; i++)
{
query.Add(clauses[i].Query, Occur.MUST);
}
this.config = config;
}
/// <summary>
/// Used by <see cref="DrillSideways"/>
/// </summary>
internal DrillDownQuery(FacetsConfig config, Query baseQuery, IList<Query> clauses, IDictionary<string, int?> drillDownDims)
{
query = new BooleanQuery(true);
if (baseQuery != null)
{
query.Add(baseQuery, Occur.MUST);
}
foreach (Query clause in clauses)
{
query.Add(clause, Occur.MUST);
}
this.drillDownDims.PutAll(drillDownDims);
this.config = config;
}
/// <summary>
/// Creates a new <see cref="DrillDownQuery"/> without a base query,
/// to perform a pure browsing query (equivalent to using
/// <see cref="MatchAllDocsQuery"/> as base).
/// </summary>
public DrillDownQuery(FacetsConfig config)
: this(config, null)
{
}
/// <summary>
/// Creates a new <see cref="DrillDownQuery"/> over the given base query. Can be
/// <c>null</c>, in which case the result <see cref="Query"/> from
/// <see cref="Rewrite(IndexReader)"/> will be a pure browsing query, filtering on
/// the added categories only.
/// </summary>
public DrillDownQuery(FacetsConfig config, Query baseQuery)
{
query = new BooleanQuery(true); // disable coord
if (baseQuery != null)
{
query.Add(baseQuery, Occur.MUST);
}
this.config = config;
}
/// <summary>
/// Merges (ORs) a new path into an existing AND'd
/// clause.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private void Merge(string dim, string[] path)
{
int index = 0;
if (drillDownDims.TryGetValue(dim, out int? idx) && idx.HasValue)
{
index = idx.Value;
}
if (query.GetClauses().Length == drillDownDims.Count + 1)
{
index++;
}
ConstantScoreQuery q = (ConstantScoreQuery)query.GetClauses()[index].Query;
if ((q.Query is BooleanQuery) == false)
{
// App called .add(dim, customQuery) and then tried to
// merge a facet label in:
throw new Exception("cannot merge with custom Query");
}
string indexedField = config.GetDimConfig(dim).IndexFieldName;
BooleanQuery bq = (BooleanQuery)q.Query;
bq.Add(new TermQuery(Term(indexedField, dim, path)), Occur.SHOULD);
}
/// <summary>
/// Adds one dimension of drill downs; if you pass the same
/// dimension more than once it is OR'd with the previous
/// cofnstraints on that dimension, and all dimensions are
/// AND'd against each other and the base query.
/// </summary>
public void Add(string dim, params string[] path)
{
if (drillDownDims.ContainsKey(dim))
{
Merge(dim, path);
return;
}
string indexedField = config.GetDimConfig(dim).IndexFieldName;
BooleanQuery bq = new BooleanQuery(true)
{
{ new TermQuery(Term(indexedField, dim, path)), Occur.SHOULD }
}; // disable coord
Add(dim, bq);
}
/// <summary>
/// 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.
/// </summary>
public void Add(string dim, Query subQuery)
{
if (drillDownDims.ContainsKey(dim))
{
throw new ArgumentException("dimension \"" + dim + "\" already has a drill-down");
}
// TODO: we should use FilteredQuery?
// So scores of the drill-down query don't have an
// effect:
ConstantScoreQuery drillDownQuery = new ConstantScoreQuery(subQuery);
drillDownQuery.Boost = 0.0f;
query.Add(drillDownQuery, Occur.MUST);
drillDownDims[dim] = drillDownDims.Count;
}
/// <summary>
/// Expert: add a custom drill-down Filter, e.g. when
/// drilling down after range faceting.
/// </summary>
public void Add(string dim, Filter subFilter)
{
if (drillDownDims.ContainsKey(dim))
{
throw new ArgumentException("dimension \"" + dim + "\" already has a drill-down");
}
// TODO: we should use FilteredQuery?
// So scores of the drill-down query don't have an
// effect:
ConstantScoreQuery drillDownQuery = new ConstantScoreQuery(subFilter);
drillDownQuery.Boost = 0.0f;
query.Add(drillDownQuery, Occur.MUST);
drillDownDims[dim] = drillDownDims.Count;
}
internal static Filter GetFilter(Query query)
{
if (query is ConstantScoreQuery scoreQuery)
{
ConstantScoreQuery csq = scoreQuery;
Filter filter = csq.Filter;
if (filter != null)
{
return filter;
}
else
{
return GetFilter(csq.Query);
}
}
else
{
return null;
}
}
public override object Clone()
{
return new DrillDownQuery(config, query, drillDownDims);
}
public override int GetHashCode()
{
const int prime = 31;
int result = base.GetHashCode();
return prime * result + query.GetHashCode();
}
public override bool Equals(object obj)
{
if (!(obj is DrillDownQuery))
{
return false;
}
DrillDownQuery other = (DrillDownQuery)obj;
return query.Equals(other.query) && base.Equals(other);
}
public override Query Rewrite(IndexReader r)
{
if (query.Clauses.Count == 0)
{
return new MatchAllDocsQuery();
}
IList<Filter> filters = new List<Filter>();
IList<Query> queries = new List<Query>();
IList<BooleanClause> clauses = query.Clauses;
Query baseQuery;
int startIndex;
if (drillDownDims.Count == query.Clauses.Count)
{
baseQuery = new MatchAllDocsQuery();
startIndex = 0;
}
else
{
baseQuery = clauses[0].Query;
startIndex = 1;
}
for (int i = startIndex; i < clauses.Count; i++)
{
BooleanClause clause = clauses[i];
Query queryClause = clause.Query;
Filter filter = GetFilter(queryClause);
if (filter != null)
{
filters.Add(filter);
}
else
{
queries.Add(queryClause);
}
}
if (filters.Count == 0)
{
return query;
}
else
{
// Wrap all filters using FilteredQuery
// TODO: this is hackish; we need to do it because
// BooleanQuery can't be trusted to handle the
// "expensive filter" case. Really, each Filter should
// know its cost and we should take that more
// carefully into account when picking the right
// strategy/optimization:
Query wrapped;
if (queries.Count == 0)
{
wrapped = baseQuery;
}
else
{
// disable coord
BooleanQuery wrappedBQ = new BooleanQuery(true);
if ((baseQuery is MatchAllDocsQuery) == false)
{
wrappedBQ.Add(baseQuery, Occur.MUST);
}
foreach (Query q in queries)
{
wrappedBQ.Add(q, Occur.MUST);
}
wrapped = wrappedBQ;
}
foreach (Filter filter in filters)
{
wrapped = new FilteredQuery(wrapped, filter, FilteredQuery.QUERY_FIRST_FILTER_STRATEGY);
}
return wrapped;
}
}
public override string ToString(string field)
{
return query.ToString(field);
}
internal BooleanQuery BooleanQuery => query;
internal IDictionary<string, int?> Dims => drillDownDims;
}
}