| 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; |
| } |
| } |