Skip to content

Commit b39e0c8

Browse files
committed
[feature] Also support passing multiple hierarchical facet values at query time
1 parent 8189035 commit b39e0c8

File tree

3 files changed

+207
-20
lines changed

3 files changed

+207
-20
lines changed

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
@@ -442,7 +442,7 @@ public NodeSet query(int contextId, DocumentSet docs, NodeSet contextSet,
442442
options.configureParser(parser.getConfiguration());
443443
query = parser.parse(queryStr);
444444
}
445-
Optional<Map<String, List<String>>> facets = options.getFacets();
445+
Optional<Map<String, QueryOptions.FacetQuery>> facets = options.getFacets();
446446
if (facets.isPresent() && config != null) {
447447
query = drilldown(facets.get(), query, config);
448448
}
@@ -485,7 +485,7 @@ public NodeSet query(int contextId, DocumentSet docs, NodeSet contextSet,
485485
LuceneConfig config = getLuceneConfig(broker, docs);
486486
analyzer = getAnalyzer(config, null, qname);
487487
Query query = queryRoot == null ? new ConstantScoreQuery(new FieldValueFilter(field)) : queryTranslator.parse(field, queryRoot, analyzer, options);
488-
Optional<Map<String, List<String>>> facets = options.getFacets();
488+
Optional<Map<String, QueryOptions.FacetQuery>> facets = options.getFacets();
489489
if (facets.isPresent() && config != null) {
490490
query = drilldown(facets.get(), query, config);
491491
}
@@ -515,19 +515,11 @@ public NodeSet queryField(int contextId, DocumentSet docs, NodeSet contextSet,
515515
});
516516
}
517517

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

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/xquery/lucene/facets.xql

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ declare variable $facet:XML :=
1515
<likes>9</likes>
1616
<score>6.0</score>
1717
<subject>art</subject>
18+
<subject>math</subject>
1819
</letter>
1920
<letter>
2021
<from>Rudi</from>
@@ -246,6 +247,90 @@ function facet:query-and-drill-down($from as xs:string+) {
246247
count(collection("/db/lucenetest")//letter[ft:query(., "Berlin", $options)])
247248
};
248249

250+
declare
251+
%test:assertEquals(5)
252+
function facet:query-and-drill-down-hierarchical-sequence() {
253+
let $options := map {
254+
"leading-wildcard": "yes",
255+
"filter-rewrite": "yes",
256+
"facets": map {
257+
"subject": ("humanities", "history")
258+
}
259+
}
260+
return
261+
count(collection("/db/lucenetest")//letter[ft:query(., (), $options)])
262+
};
263+
264+
declare
265+
%test:assertEquals(5)
266+
function facet:query-and-drill-down-hierarchical-array-single() {
267+
let $options := map {
268+
"leading-wildcard": "yes",
269+
"filter-rewrite": "yes",
270+
"facets": map {
271+
"subject": [("humanities", "history")]
272+
}
273+
}
274+
return
275+
count(collection("/db/lucenetest")//letter[ft:query(., (), $options)])
276+
};
277+
278+
declare
279+
%test:assertEquals(6)
280+
function facet:query-and-drill-down-hierarchical-array-multi() {
281+
let $options := map {
282+
"leading-wildcard": "yes",
283+
"filter-rewrite": "yes",
284+
"facets": map {
285+
"subject": [("humanities", "history"), ("humanities", "art")]
286+
}
287+
}
288+
return
289+
count(collection("/db/lucenetest")//letter[ft:query(., (), $options)])
290+
};
291+
292+
declare
293+
%test:assertEquals(2)
294+
function facet:query-and-drill-down-hierarchical-array-multi2() {
295+
let $options := map {
296+
"leading-wildcard": "yes",
297+
"filter-rewrite": "yes",
298+
"facets": map {
299+
"subject": [("humanities", "art"), ("science", "engineering")]
300+
}
301+
}
302+
return
303+
count(collection("/db/lucenetest")//letter[ft:query(., (), $options)])
304+
};
305+
306+
declare
307+
%test:assertEquals(5)
308+
function facet:query-and-drill-down-hierarchical-array-multi3() {
309+
let $options := map {
310+
"leading-wildcard": "yes",
311+
"filter-rewrite": "yes",
312+
"facets": map {
313+
"subject": [("humanities", "history"), ("science", "engineering")]
314+
}
315+
}
316+
return
317+
count(collection("/db/lucenetest")//letter[ft:query(., (), $options)])
318+
};
319+
320+
declare
321+
%test:assertEquals(1)
322+
function facet:query-and-drill-down-hierarchical-array-multi4() {
323+
let $options := map {
324+
"leading-wildcard": "yes",
325+
"filter-rewrite": "yes",
326+
"facets": map {
327+
"subject": [("humanities", "art"), ("science", "math")]
328+
}
329+
}
330+
return
331+
count(collection("/db/lucenetest")//letter[ft:query(., (), $options)])
332+
};
333+
249334
(:~
250335
: The facet 'cat' is defined on both: the index on 'document' and 'document/abstract'.
251336
: Querying 'document' should return 1 as facet count, while 'abstract' should return 2
@@ -299,6 +384,28 @@ function facet:hierarchical-facets-query($paths as xs:string+) {
299384
count($result)
300385
};
301386

387+
declare
388+
%test:arg("paths", "science")
389+
%test:assertEquals(2)
390+
%test:arg("paths", "science", "engineering")
391+
%test:assertEquals(1)
392+
%test:arg("paths", "humanities", "history")
393+
%test:assertEquals(5)
394+
%test:arg("paths", "humanities", "art")
395+
%test:assertEquals(1)
396+
%test:arg("paths", "science", "math")
397+
%test:assertEquals(1)
398+
function facet:hierarchical-facets-query-subjects($paths as xs:string+) {
399+
let $options := map {
400+
"facets": map {
401+
"subject": $paths
402+
}
403+
}
404+
let $result := collection("/db/lucenetest")//letter[ft:query(., (), $options)]
405+
return
406+
count($result)
407+
};
408+
302409
declare
303410
%test:arg("paths", "2017")
304411
%test:assertEquals('{"03":2}')
@@ -311,6 +418,16 @@ function facet:hierarchical-facets-retrieve($paths as xs:string*) {
311418
serialize($facets, map { "method": "json" })
312419
};
313420

421+
declare
422+
%test:arg("paths", "science")
423+
%test:assertEquals('{"math":1,"engineering":1}')
424+
function facet:hierarchical-facets-retrieve($paths as xs:string*) {
425+
let $result := collection("/db/lucenetest")//letter[ft:query(., ())]
426+
let $facets := ft:facets($result, "subject", (), $paths)
427+
return
428+
serialize($facets, map { "method": "json" })
429+
};
430+
314431
declare
315432
%test:assertEquals('{"Berlin":2,"Hamburg":1}','{"Wrocław":2}')
316433
function facet:hierarchical-place() {
@@ -326,7 +443,7 @@ function facet:hierarchical-place() {
326443
};
327444

328445
declare
329-
%test:assertEquals('{"art":1,"history":5}','{"engineering":1}')
446+
%test:assertEquals('{"art":1,"history":5}','{"math":1,"engineering":1}')
330447
function facet:hierarchical-subject() {
331448
let $result := collection("/db/lucenetest")//letter[ft:query(., ())]
332449
let $facets := ft:facets($result, "subject", 10) (: Returns facet counts for "science" and "humanities" :)

0 commit comments

Comments
 (0)