Skip to content

Commit 15663d8

Browse files
authored
Merge pull request #3182 from wolfgangmm/feature/multivalued-hierarchical-facets
Multi-valued hierarchical facets
2 parents 2909a75 + 4538da3 commit 15663d8

File tree

5 files changed

+294
-28
lines changed

5 files changed

+294
-28
lines changed

extensions/indexes/lucene/src/main/java/org/exist/indexing/lucene/LuceneFacetConfig.java

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,15 @@
2929
import org.exist.storage.DBBroker;
3030
import org.exist.util.DatabaseConfigurationException;
3131
import org.exist.xquery.XPathException;
32+
import org.exist.xquery.functions.array.ArrayType;
3233
import org.exist.xquery.value.Sequence;
3334
import org.exist.xquery.value.SequenceIterator;
35+
import org.exist.xquery.value.Type;
3436
import org.w3c.dom.Element;
3537

3638
import javax.annotation.Nonnull;
39+
import java.util.ArrayList;
40+
import java.util.List;
3741
import java.util.Map;
3842

3943
/**
@@ -62,7 +66,7 @@ public LuceneFacetConfig(LuceneConfig config, Element configElement, Map<String,
6266
(hierarchicalOpt.equalsIgnoreCase("true") || hierarchicalOpt.equalsIgnoreCase("yes"));
6367

6468
config.facetsConfig.setHierarchical(dimension, isHierarchical);
65-
config.facetsConfig.setMultiValued(dimension, !isHierarchical);
69+
config.facetsConfig.setMultiValued(dimension, true);
6670
}
6771

6872
@Nonnull
@@ -71,17 +75,19 @@ public String getDimension() {
7175
}
7276

7377
@Override
74-
protected void processResult(Sequence result, Document luceneDoc) throws XPathException {
78+
protected void processResult(final Sequence result, final Document luceneDoc) throws XPathException {
7579
if (isHierarchical) {
76-
String paths[] = new String[result.getItemCount()];
77-
int j = 0;
78-
for (SequenceIterator i = result.unorderedIterator(); i.hasNext(); j++) {
79-
final String value = i.nextItem().getStringValue();
80-
if (value.length() > 0) {
81-
paths[j] = value;
80+
// hierarchical facets may be multi-valued, so if we receive an array,
81+
// create one hierarchical facet for each member
82+
if (result.hasOne() && result.getItemType() == Type.ARRAY) {
83+
final ArrayType array = (ArrayType) result.itemAt(0);
84+
for (Sequence seq : array.toArray()) {
85+
createHierarchicalFacet(luceneDoc, seq);
8286
}
87+
} else {
88+
// otherwise create a single hierarchical facet
89+
createHierarchicalFacet(luceneDoc, result);
8390
}
84-
luceneDoc.add(new FacetField(dimension, paths));
8591
} else {
8692
for (SequenceIterator i = result.unorderedIterator(); i.hasNext(); ) {
8793
final String value = i.nextItem().getStringValue();
@@ -92,6 +98,19 @@ protected void processResult(Sequence result, Document luceneDoc) throws XPathEx
9298
}
9399
}
94100

101+
private void createHierarchicalFacet(Document luceneDoc, Sequence seq) throws XPathException {
102+
final List<String> paths = new ArrayList<>(seq.getItemCount());
103+
for (SequenceIterator i = seq.unorderedIterator(); i.hasNext(); ) {
104+
final String value = i.nextItem().getStringValue();
105+
if (value.length() > 0) {
106+
paths.add(value);
107+
}
108+
}
109+
if (!paths.isEmpty()) {
110+
luceneDoc.add(new FacetField(dimension, paths.toArray(new String[0])));
111+
}
112+
}
113+
95114
@Override
96115
protected void processText(CharSequence text, Document luceneDoc) {
97116
if (text.length() > 0) {

extensions/indexes/lucene/src/main/java/org/exist/indexing/lucene/LuceneIndexWorker.java

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ public NodeSet query(int contextId, DocumentSet docs, NodeSet contextSet,
444444
options.configureParser(parser.getConfiguration());
445445
query = parser.parse(queryStr);
446446
}
447-
Optional<Map<String, List<String>>> facets = options.getFacets();
447+
Optional<Map<String, QueryOptions.FacetQuery>> facets = options.getFacets();
448448
if (facets.isPresent() && config != null) {
449449
query = drilldown(facets.get(), query, config);
450450
}
@@ -487,7 +487,7 @@ public NodeSet query(int contextId, DocumentSet docs, NodeSet contextSet,
487487
LuceneConfig config = getLuceneConfig(broker, docs);
488488
analyzer = getAnalyzer(config, null, qname);
489489
Query query = queryRoot == null ? new ConstantScoreQuery(new FieldValueFilter(field)) : queryTranslator.parse(field, queryRoot, analyzer, options);
490-
Optional<Map<String, List<String>>> facets = options.getFacets();
490+
Optional<Map<String, QueryOptions.FacetQuery>> facets = options.getFacets();
491491
if (facets.isPresent() && config != null) {
492492
query = drilldown(facets.get(), query, config);
493493
}
@@ -517,19 +517,11 @@ public NodeSet queryField(int contextId, DocumentSet docs, NodeSet contextSet,
517517
});
518518
}
519519

520-
private Query drilldown(Map<String, List<String>> facets, Query baseQuery, LuceneConfig config) {
520+
private Query drilldown(Map<String, QueryOptions.FacetQuery> facets, Query baseQuery, LuceneConfig config) {
521521
final DrillDownQuery drillDownQuery = new DrillDownQuery(config.facetsConfig, baseQuery);
522-
for (final Map.Entry<String, List<String>> facet : facets.entrySet()) {
522+
for (final Map.Entry<String, QueryOptions.FacetQuery> facet : facets.entrySet()) {
523523
final FacetsConfig.DimConfig dimConfig = config.facetsConfig.getDimConfig(facet.getKey());
524-
if (dimConfig.hierarchical) {
525-
final String[] values = new String[facet.getValue().size()];
526-
facet.getValue().toArray(values);
527-
drillDownQuery.add(facet.getKey(), values);
528-
} else {
529-
for (String value : facet.getValue()) {
530-
drillDownQuery.add(facet.getKey(), value);
531-
}
532-
}
524+
facet.getValue().toQuery(facet.getKey(), drillDownQuery, dimConfig.hierarchical);
533525
}
534526
return drillDownQuery;
535527
}

extensions/indexes/lucene/src/main/java/org/exist/xquery/modules/lucene/QueryOptions.java

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,18 @@
2020

2121
package org.exist.xquery.modules.lucene;
2222

23+
import org.apache.lucene.facet.DrillDownQuery;
24+
import org.apache.lucene.facet.FacetsConfig;
2325
import org.apache.lucene.queryparser.classic.QueryParser;
2426
import org.apache.lucene.queryparser.classic.QueryParserBase;
2527
import org.apache.lucene.queryparser.flexible.standard.CommonQueryParserConfiguration;
2628
import org.apache.lucene.search.MultiTermQuery;
29+
import org.apache.lucene.search.Query;
2730
import org.exist.numbering.NodeId;
2831
import org.exist.stax.ExtendedXMLStreamReader;
2932
import org.exist.xquery.XPathException;
3033
import org.exist.xquery.XQueryContext;
34+
import org.exist.xquery.functions.array.ArrayType;
3135
import org.exist.xquery.functions.map.AbstractMapType;
3236
import org.exist.xquery.value.*;
3337

@@ -61,7 +65,7 @@ protected enum DefaultOperator {
6165

6266
protected boolean filterRewrite = false;
6367
protected boolean lowercaseExpandedTerms = false;
64-
protected Optional<Map<String, List<String>>> facets = Optional.empty();
68+
protected Optional<Map<String, FacetQuery>> facets = Optional.empty();
6569
protected Set<String> fields = null;
6670

6771
public QueryOptions() {
@@ -101,11 +105,16 @@ public QueryOptions(AbstractMapType map) throws XPathException {
101105
fields.add(i.nextItem().getStringValue());
102106
}
103107
} else if (key.equals(OPTION_FACETS) && entry.getValue().hasOne() && entry.getValue().getItemType() == Type.MAP) {
104-
final Map<String, List<String>> tf = new HashMap<>();
108+
// map to hold the facet values for each dimension
109+
final Map<String, FacetQuery> tf = new HashMap<>();
110+
// iterate over each dimension and collect its values into a FacetQuery
105111
for (Map.Entry<AtomicValue, Sequence> facet: (AbstractMapType) entry.getValue().itemAt(0)) {
106-
final List<String> values = new ArrayList<>(5);
107-
for (SequenceIterator si = facet.getValue().unorderedIterator(); si.hasNext(); ) {
108-
values.add(si.nextItem().getStringValue());
112+
final Sequence value = facet.getValue();
113+
FacetQuery values;
114+
if (value.hasOne() && value.getItemType() == Type.ARRAY) {
115+
values = new FacetQuery((ArrayType) facet.getValue().itemAt(0));
116+
} else {
117+
values = new FacetQuery(value);
109118
}
110119
tf.put(facet.getKey().getStringValue(), values);
111120
}
@@ -116,7 +125,76 @@ public QueryOptions(AbstractMapType map) throws XPathException {
116125
}
117126
}
118127

119-
public Optional<Map<String, List<String>>> getFacets() {
128+
/**
129+
* Holds the values of a facet for drill down. To support
130+
* multiple query values for a hierarchical facet, values are
131+
* kept in a two-dimensional list.
132+
*/
133+
public static class FacetQuery {
134+
final List<List<String>> values;
135+
136+
/**
137+
* Create a single query value from a flat sequence.
138+
*
139+
* @param input input sequence
140+
* @throws XPathException in case of conversion errors
141+
*/
142+
public FacetQuery(final Sequence input) throws XPathException {
143+
values = new ArrayList<>(1);
144+
List<String> subValues = new ArrayList<>(input.getItemCount());
145+
for (SequenceIterator si = input.unorderedIterator(); si.hasNext(); ) {
146+
final String value = si.nextItem().getStringValue();
147+
if (value.length() > 0) {
148+
subValues.add(value);
149+
}
150+
}
151+
values.add(subValues);
152+
}
153+
154+
/**
155+
* Create a multi-valued query from an XQuery array.
156+
*
157+
* @param input an XQuery array
158+
* @throws XPathException in case of conversion errors
159+
*/
160+
public FacetQuery(final ArrayType input) throws XPathException {
161+
final Sequence items[] = input.toArray();
162+
values = new ArrayList<>(items.length);
163+
for (Sequence seq : items) {
164+
final List<String> subValues = new ArrayList<>(seq.getItemCount());
165+
for (SequenceIterator si = seq.unorderedIterator(); si.hasNext(); ) {
166+
final String value = si.nextItem().getStringValue();
167+
if (value.length() > 0) {
168+
subValues.add(value);
169+
}
170+
}
171+
values.add(subValues);
172+
}
173+
}
174+
175+
/**
176+
* Add the values for the facet dimension to the drill down query.
177+
*
178+
* @param dimension the facet dimension
179+
* @param query the lucene drill down query
180+
* @param hierarchical true if the facet is hierarchical
181+
*/
182+
public void toQuery(final String dimension, final DrillDownQuery query, final boolean hierarchical) {
183+
for (List<String> subValues : values) {
184+
if (hierarchical) {
185+
final String[] result = new String[subValues.size()];
186+
subValues.toArray(result);
187+
query.add(dimension, result);
188+
} else {
189+
for (String value : subValues) {
190+
query.add(dimension, value);
191+
}
192+
}
193+
}
194+
}
195+
}
196+
197+
public Optional<Map<String, FacetQuery>> getFacets() {
120198
return facets;
121199
}
122200

extensions/indexes/lucene/src/test/resources-filtered/conf.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,7 @@
740740

741741
<!-- Needed for XQSuite! -->
742742
<module uri="http://www.w3.org/2005/xpath-functions/map" class="org.exist.xquery.functions.map.MapModule" />
743+
<module uri="http://www.w3.org/2005/xpath-functions/array" class="org.exist.xquery.functions.array.ArrayModule" />
743744
<module uri="http://exist-db.org/xquery/inspection" class="org.exist.xquery.functions.inspect.InspectionModule"/>
744745
<module uri="http://exist-db.org/xquery/response" class="org.exist.xquery.functions.response.ResponseModule" />
745746
<module uri="http://exist-db.org/xquery/system" class="org.exist.xquery.functions.system.SystemModule" />

0 commit comments

Comments
 (0)