mirror of https://github.com/apache/lucene.git
SOLR-12328: domain change using graph
This commit is contained in:
parent
e5998fcb5d
commit
f7500a6029
|
@ -134,6 +134,9 @@ New Features
|
||||||
Equivalent JSON Example : { "#colorfilt" : "color:blue" }
|
Equivalent JSON Example : { "#colorfilt" : "color:blue" }
|
||||||
(Dmitry Tikhonov, Mikhail Khludnev, yonik)
|
(Dmitry Tikhonov, Mikhail Khludnev, yonik)
|
||||||
|
|
||||||
|
* SOLR-12328: JSON Facet API: Domain change with graph query.
|
||||||
|
(Daniel Meehl, Kevin Watters, yonik)
|
||||||
|
|
||||||
Bug Fixes
|
Bug Fixes
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
|
|
@ -185,7 +185,8 @@ public abstract class FacetProcessor<FacetRequestT extends FacetRequest> {
|
||||||
evalFilters();
|
evalFilters();
|
||||||
|
|
||||||
handleJoinField();
|
handleJoinField();
|
||||||
|
handleGraphField();
|
||||||
|
|
||||||
boolean appliedFilters = handleBlockJoin();
|
boolean appliedFilters = handleBlockJoin();
|
||||||
|
|
||||||
if (this.filter != null && !appliedFilters) {
|
if (this.filter != null && !appliedFilters) {
|
||||||
|
@ -261,7 +262,15 @@ public abstract class FacetProcessor<FacetRequestT extends FacetRequest> {
|
||||||
final Query domainQuery = freq.domain.joinField.createDomainQuery(fcontext);
|
final Query domainQuery = freq.domain.joinField.createDomainQuery(fcontext);
|
||||||
fcontext.base = fcontext.searcher.getDocSet(domainQuery);
|
fcontext.base = fcontext.searcher.getDocSet(domainQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** modifies the context base if there is a graph field domain change */
|
||||||
|
private void handleGraphField() throws IOException {
|
||||||
|
if (null == freq.domain.graphField) return;
|
||||||
|
|
||||||
|
final Query domainQuery = freq.domain.graphField.createDomainQuery(fcontext);
|
||||||
|
fcontext.base = fcontext.searcher.getDocSet(domainQuery);
|
||||||
|
}
|
||||||
|
|
||||||
// returns "true" if filters were applied to fcontext.base already
|
// returns "true" if filters were applied to fcontext.base already
|
||||||
private boolean handleBlockJoin() throws IOException {
|
private boolean handleBlockJoin() throws IOException {
|
||||||
boolean appliedFilters = false;
|
boolean appliedFilters = false;
|
||||||
|
|
|
@ -16,28 +16,20 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.solr.search.facet;
|
package org.apache.solr.search.facet;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.EnumSet;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.apache.lucene.search.Query;
|
import org.apache.lucene.search.Query;
|
||||||
import org.apache.solr.common.SolrException;
|
import org.apache.solr.common.SolrException;
|
||||||
import org.apache.solr.common.params.FacetParams;
|
import org.apache.solr.common.params.FacetParams;
|
||||||
|
import org.apache.solr.common.params.SolrParams;
|
||||||
|
import org.apache.solr.common.util.NamedList;
|
||||||
import org.apache.solr.common.util.StrUtils;
|
import org.apache.solr.common.util.StrUtils;
|
||||||
import org.apache.solr.request.SolrQueryRequest;
|
import org.apache.solr.request.SolrQueryRequest;
|
||||||
import org.apache.solr.schema.IndexSchema;
|
import org.apache.solr.schema.IndexSchema;
|
||||||
import org.apache.solr.search.DocSet;
|
import org.apache.solr.search.*;
|
||||||
import org.apache.solr.search.JoinQParserPlugin;
|
import org.apache.solr.search.join.GraphQuery;
|
||||||
import org.apache.solr.search.FunctionQParser;
|
import org.apache.solr.search.join.GraphQueryParser;
|
||||||
import org.apache.solr.search.FunctionQParserPlugin;
|
|
||||||
import org.apache.solr.search.QParser;
|
import java.io.IOException;
|
||||||
import org.apache.solr.search.QueryContext;
|
import java.util.*;
|
||||||
import org.apache.solr.search.SolrConstantScoreQuery;
|
|
||||||
import org.apache.solr.search.SolrIndexSearcher;
|
|
||||||
import org.apache.solr.search.SyntaxError;
|
|
||||||
|
|
||||||
import static org.apache.solr.common.params.CommonParams.SORT;
|
import static org.apache.solr.common.params.CommonParams.SORT;
|
||||||
import static org.apache.solr.search.facet.FacetRequest.RefineMethod.NONE;
|
import static org.apache.solr.search.facet.FacetRequest.RefineMethod.NONE;
|
||||||
|
@ -98,6 +90,7 @@ public abstract class FacetRequest {
|
||||||
*/
|
*/
|
||||||
public List<String> excludeTags;
|
public List<String> excludeTags;
|
||||||
public JoinField joinField;
|
public JoinField joinField;
|
||||||
|
public GraphField graphField;
|
||||||
public boolean toParent;
|
public boolean toParent;
|
||||||
public boolean toChildren;
|
public boolean toChildren;
|
||||||
public String parents; // identifies the parent filter... the full set of parent documents for any block join operation
|
public String parents; // identifies the parent filter... the full set of parent documents for any block join operation
|
||||||
|
@ -118,18 +111,18 @@ public abstract class FacetRequest {
|
||||||
public static class JoinField {
|
public static class JoinField {
|
||||||
public final String from;
|
public final String from;
|
||||||
public final String to;
|
public final String to;
|
||||||
|
|
||||||
private JoinField(String from, String to) {
|
private JoinField(String from, String to) {
|
||||||
assert null != from;
|
assert null != from;
|
||||||
assert null != to;
|
assert null != to;
|
||||||
|
|
||||||
this.from = from;
|
this.from = from;
|
||||||
this.to = to;
|
this.to = to;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a <code>Domain</code>, and a (JSON) map specifying the configuration for that Domain,
|
* Given a <code>Domain</code>, and a (JSON) map specifying the configuration for that Domain,
|
||||||
* validates if a '<code>join</code>' is specified, and if so creates a <code>JoinField</code>
|
* validates if a '<code>join</code>' is specified, and if so creates a <code>JoinField</code>
|
||||||
* and sets it on the <code>Domain</code>.
|
* and sets it on the <code>Domain</code>.
|
||||||
*
|
*
|
||||||
* (params must not be null)
|
* (params must not be null)
|
||||||
|
@ -137,46 +130,109 @@ public abstract class FacetRequest {
|
||||||
public static void createJoinField(FacetRequest.Domain domain, Map<String,Object> domainMap) {
|
public static void createJoinField(FacetRequest.Domain domain, Map<String,Object> domainMap) {
|
||||||
assert null != domain;
|
assert null != domain;
|
||||||
assert null != domainMap;
|
assert null != domainMap;
|
||||||
|
|
||||||
final Object queryJoin = domainMap.get("join");
|
final Object queryJoin = domainMap.get("join");
|
||||||
if (null != queryJoin) {
|
if (null != queryJoin) {
|
||||||
// TODO: maybe allow simple string (instead of map) to mean "self join on this field name" ?
|
// TODO: maybe allow simple string (instead of map) to mean "self join on this field name" ?
|
||||||
if (! (queryJoin instanceof Map)) {
|
if (! (queryJoin instanceof Map)) {
|
||||||
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
|
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
|
||||||
"'join' domain change requires a map containing the 'from' and 'to' fields");
|
"'join' domain change requires a map containing the 'from' and 'to' fields");
|
||||||
}
|
}
|
||||||
final Map<String,String> join = (Map<String,String>) queryJoin;
|
final Map<String,String> join = (Map<String,String>) queryJoin;
|
||||||
if (! (join.containsKey("from") && join.containsKey("to") &&
|
if (! (join.containsKey("from") && join.containsKey("to") &&
|
||||||
null != join.get("from") && null != join.get("to")) ) {
|
null != join.get("from") && null != join.get("to")) ) {
|
||||||
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
|
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
|
||||||
"'join' domain change requires non-null 'from' and 'to' field names");
|
"'join' domain change requires non-null 'from' and 'to' field names");
|
||||||
}
|
}
|
||||||
if (2 != join.size()) {
|
if (2 != join.size()) {
|
||||||
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
|
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
|
||||||
"'join' domain change contains unexpected keys, only 'from' and 'to' supported: "
|
"'join' domain change contains unexpected keys, only 'from' and 'to' supported: "
|
||||||
+ join.toString());
|
+ join.toString());
|
||||||
}
|
}
|
||||||
domain.joinField = new JoinField(join.get("from"), join.get("to"));
|
domain.joinField = new JoinField(join.get("from"), join.get("to"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Query that can be used to recompute the new "base" for this domain, realtive to the
|
* Creates a Query that can be used to recompute the new "base" for this domain, relative to the
|
||||||
* current base of the FacetContext.
|
* current base of the FacetContext.
|
||||||
*/
|
*/
|
||||||
public Query createDomainQuery(FacetContext fcontext) throws IOException {
|
public Query createDomainQuery(FacetContext fcontext) throws IOException {
|
||||||
// NOTE: this code lives here, instead of in FacetProcessor.handleJoin, in order to minimize
|
// NOTE: this code lives here, instead of in FacetProcessor.handleJoin, in order to minimize
|
||||||
// the number of classes that have to know about the number of possible settings on the join
|
// the number of classes that have to know about the number of possible settings on the join
|
||||||
// (ie: if we add a score mode, or some other modifier to how the joins are done)
|
// (ie: if we add a score mode, or some other modifier to how the joins are done)
|
||||||
|
|
||||||
final SolrConstantScoreQuery fromQuery = new SolrConstantScoreQuery(fcontext.base.getTopFilter());
|
final SolrConstantScoreQuery fromQuery = new SolrConstantScoreQuery(fcontext.base.getTopFilter());
|
||||||
// this shouldn't matter once we're wrapped in a join query, but just in case it ever does...
|
// this shouldn't matter once we're wrapped in a join query, but just in case it ever does...
|
||||||
fromQuery.setCache(false);
|
fromQuery.setCache(false);
|
||||||
|
|
||||||
return JoinQParserPlugin.createJoinQuery(fromQuery, this.from, this.to);
|
return JoinQParserPlugin.createJoinQuery(fromQuery, this.from, this.to);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Are we doing a query time graph across other documents */
|
||||||
|
public static class GraphField {
|
||||||
|
public final SolrParams localParams;
|
||||||
|
|
||||||
|
private GraphField(SolrParams localParams) {
|
||||||
|
assert null != localParams;
|
||||||
|
|
||||||
|
this.localParams = localParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a <code>Domain</code>, and a (JSON) map specifying the configuration for that Domain,
|
||||||
|
* validates if a '<code>graph</code>' is specified, and if so creates a <code>GraphField</code>
|
||||||
|
* and sets it on the <code>Domain</code>.
|
||||||
|
*
|
||||||
|
* (params must not be null)
|
||||||
|
*/
|
||||||
|
public static void createGraphField(FacetRequest.Domain domain, Map<String,Object> domainMap) {
|
||||||
|
assert null != domain;
|
||||||
|
assert null != domainMap;
|
||||||
|
|
||||||
|
final Object queryGraph = domainMap.get("graph");
|
||||||
|
if (null != queryGraph) {
|
||||||
|
if (! (queryGraph instanceof Map)) {
|
||||||
|
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
|
||||||
|
"'graph' domain change requires a map containing the 'from' and 'to' fields");
|
||||||
|
}
|
||||||
|
final Map<String,String> graph = (Map<String,String>) queryGraph;
|
||||||
|
if (! (graph.containsKey("from") && graph.containsKey("to") &&
|
||||||
|
null != graph.get("from") && null != graph.get("to")) ) {
|
||||||
|
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
|
||||||
|
"'graph' domain change requires non-null 'from' and 'to' field names");
|
||||||
|
}
|
||||||
|
|
||||||
|
NamedList<String> graphParams = new NamedList<>();
|
||||||
|
graphParams.addAll(graph);
|
||||||
|
SolrParams localParams = SolrParams.toSolrParams(graphParams);
|
||||||
|
domain.graphField = new GraphField(localParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Query that can be used to recompute the new "base" for this domain, relative to the
|
||||||
|
* current base of the FacetContext.
|
||||||
|
*/
|
||||||
|
public Query createDomainQuery(FacetContext fcontext) throws IOException {
|
||||||
|
final SolrConstantScoreQuery fromQuery = new SolrConstantScoreQuery(fcontext.base.getTopFilter());
|
||||||
|
// this shouldn't matter once we're wrapped in a join query, but just in case it ever does...
|
||||||
|
fromQuery.setCache(false);
|
||||||
|
|
||||||
|
GraphQueryParser graphParser = new GraphQueryParser(null, localParams, null, fcontext.req);
|
||||||
|
try {
|
||||||
|
GraphQuery graphQuery = (GraphQuery)graphParser.parse();
|
||||||
|
graphQuery.setQ(fromQuery);
|
||||||
|
return graphQuery;
|
||||||
|
} catch (SyntaxError syntaxError) {
|
||||||
|
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, syntaxError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -494,6 +550,7 @@ abstract class FacetParser<FacetRequestT extends FacetRequest> {
|
||||||
}
|
}
|
||||||
|
|
||||||
FacetRequest.Domain.JoinField.createJoinField(domain, domainMap);
|
FacetRequest.Domain.JoinField.createJoinField(domain, domainMap);
|
||||||
|
FacetRequest.Domain.GraphField.createGraphField(domain, domainMap);
|
||||||
|
|
||||||
Object filterOrList = domainMap.get("filter");
|
Object filterOrList = domainMap.get("filter");
|
||||||
if (filterOrList != null) {
|
if (filterOrList != null) {
|
||||||
|
|
|
@ -496,6 +496,49 @@ public class TestJsonFacets extends SolrTestCaseHS {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testDomainGraph() throws Exception {
|
||||||
|
Client client = Client.localClient();
|
||||||
|
indexSimple(client);
|
||||||
|
|
||||||
|
// should be the same as join self
|
||||||
|
assertJQ(req("q", "*:*", "rows", "0",
|
||||||
|
"json.facet", ""
|
||||||
|
+ "{x: { type: terms, field: 'num_i', "
|
||||||
|
+ " facet: { y: { domain: { graph: { from: 'cat_s', to: 'cat_s' } }, "
|
||||||
|
+ " type: terms, field: 'where_s' "
|
||||||
|
+ " } } } }")
|
||||||
|
, "facets=={count:6, x:{ buckets:["
|
||||||
|
+ " { val:-5, count:2, "
|
||||||
|
+ " y : { buckets:[{ val:'NJ', count:2 }, { val:'NY', count:1 } ] } }, "
|
||||||
|
+ " { val:2, count:1, "
|
||||||
|
+ " y : { buckets:[{ val:'NJ', count:1 }, { val:'NY', count:1 } ] } }, "
|
||||||
|
+ " { val:3, count:1, "
|
||||||
|
+ " y : { buckets:[{ val:'NJ', count:1 }, { val:'NY', count:1 } ] } }, "
|
||||||
|
+ " { val:7, count:1, "
|
||||||
|
+ " y : { buckets:[{ val:'NJ', count:2 }, { val:'NY', count:1 } ] } } ] } }"
|
||||||
|
);
|
||||||
|
|
||||||
|
// This time, test with a traversalFilter
|
||||||
|
// should be the same as join self
|
||||||
|
assertJQ(req("q", "*:*", "rows", "0",
|
||||||
|
"json.facet", ""
|
||||||
|
+ "{x: { type: terms, field: 'num_i', "
|
||||||
|
+ " facet: { y: { domain: { graph: { from: 'cat_s', to: 'cat_s', traversalFilter: 'where_s:NY' } }, "
|
||||||
|
+ " type: terms, field: 'where_s' "
|
||||||
|
+ " } } } }")
|
||||||
|
, "facets=={count:6, x:{ buckets:["
|
||||||
|
+ " { val:-5, count:2, "
|
||||||
|
+ " y : { buckets:[{ val:'NJ', count:1 }, { val:'NY', count:1 } ] } }, "
|
||||||
|
+ " { val:2, count:1, "
|
||||||
|
+ " y : { buckets:[{ val:'NY', count:1 } ] } }, "
|
||||||
|
+ " { val:3, count:1, "
|
||||||
|
+ " y : { buckets:[{ val:'NJ', count:1 }, { val:'NY', count:1 } ] } }, "
|
||||||
|
+ " { val:7, count:1, "
|
||||||
|
+ " y : { buckets:[{ val:'NJ', count:1 }, { val:'NY', count:1 } ] } } ] } }"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void testNestedJoinDomain() throws Exception {
|
public void testNestedJoinDomain() throws Exception {
|
||||||
Client client = Client.localClient();
|
Client client = Client.localClient();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue