From f7500a6029fc8284b932635c868e75b3b88c1720 Mon Sep 17 00:00:00 2001 From: yonik Date: Sun, 27 May 2018 21:02:15 -0400 Subject: [PATCH] SOLR-12328: domain change using graph --- solr/CHANGES.txt | 3 + .../solr/search/facet/FacetProcessor.java | 13 +- .../solr/search/facet/FacetRequest.java | 119 +++++++++++++----- .../solr/search/facet/TestJsonFacets.java | 43 +++++++ 4 files changed, 145 insertions(+), 33 deletions(-) diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index d1398bbb868..cbaf06c9967 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -134,6 +134,9 @@ New Features Equivalent JSON Example : { "#colorfilt" : "color:blue" } (Dmitry Tikhonov, Mikhail Khludnev, yonik) +* SOLR-12328: JSON Facet API: Domain change with graph query. + (Daniel Meehl, Kevin Watters, yonik) + Bug Fixes ---------------------- diff --git a/solr/core/src/java/org/apache/solr/search/facet/FacetProcessor.java b/solr/core/src/java/org/apache/solr/search/facet/FacetProcessor.java index c0625df70e8..6b66cfd4d38 100644 --- a/solr/core/src/java/org/apache/solr/search/facet/FacetProcessor.java +++ b/solr/core/src/java/org/apache/solr/search/facet/FacetProcessor.java @@ -185,7 +185,8 @@ public abstract class FacetProcessor { evalFilters(); handleJoinField(); - + handleGraphField(); + boolean appliedFilters = handleBlockJoin(); if (this.filter != null && !appliedFilters) { @@ -261,7 +262,15 @@ public abstract class FacetProcessor { final Query domainQuery = freq.domain.joinField.createDomainQuery(fcontext); 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 private boolean handleBlockJoin() throws IOException { boolean appliedFilters = false; diff --git a/solr/core/src/java/org/apache/solr/search/facet/FacetRequest.java b/solr/core/src/java/org/apache/solr/search/facet/FacetRequest.java index 6337bcbc3b3..ddf2e981985 100644 --- a/solr/core/src/java/org/apache/solr/search/facet/FacetRequest.java +++ b/solr/core/src/java/org/apache/solr/search/facet/FacetRequest.java @@ -16,28 +16,20 @@ */ 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.solr.common.SolrException; 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.request.SolrQueryRequest; import org.apache.solr.schema.IndexSchema; -import org.apache.solr.search.DocSet; -import org.apache.solr.search.JoinQParserPlugin; -import org.apache.solr.search.FunctionQParser; -import org.apache.solr.search.FunctionQParserPlugin; -import org.apache.solr.search.QParser; -import org.apache.solr.search.QueryContext; -import org.apache.solr.search.SolrConstantScoreQuery; -import org.apache.solr.search.SolrIndexSearcher; -import org.apache.solr.search.SyntaxError; +import org.apache.solr.search.*; +import org.apache.solr.search.join.GraphQuery; +import org.apache.solr.search.join.GraphQueryParser; + +import java.io.IOException; +import java.util.*; import static org.apache.solr.common.params.CommonParams.SORT; import static org.apache.solr.search.facet.FacetRequest.RefineMethod.NONE; @@ -98,6 +90,7 @@ public abstract class FacetRequest { */ public List excludeTags; public JoinField joinField; + public GraphField graphField; public boolean toParent; public boolean toChildren; 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 final String from; public final String to; - + private JoinField(String from, String to) { assert null != from; assert null != to; - + this.from = from; this.to = to; } /** * Given a Domain, and a (JSON) map specifying the configuration for that Domain, - * validates if a 'join' is specified, and if so creates a JoinField + * validates if a 'join' is specified, and if so creates a JoinField * and sets it on the Domain. * * (params must not be null) @@ -137,46 +130,109 @@ public abstract class FacetRequest { public static void createJoinField(FacetRequest.Domain domain, Map domainMap) { assert null != domain; assert null != domainMap; - + final Object queryJoin = domainMap.get("join"); if (null != queryJoin) { // TODO: maybe allow simple string (instead of map) to mean "self join on this field name" ? if (! (queryJoin instanceof Map)) { 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 join = (Map) queryJoin; - if (! (join.containsKey("from") && join.containsKey("to") && - null != join.get("from") && null != join.get("to")) ) { + if (! (join.containsKey("from") && join.containsKey("to") && + null != join.get("from") && null != join.get("to")) ) { 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()) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, - "'join' domain change contains unexpected keys, only 'from' and 'to' supported: " - + join.toString()); + "'join' domain change contains unexpected keys, only 'from' and 'to' supported: " + + join.toString()); } 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. */ public Query createDomainQuery(FacetContext fcontext) throws IOException { // 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 // (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()); // 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); } - - + + + } + + /** 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 Domain, and a (JSON) map specifying the configuration for that Domain, + * validates if a 'graph' is specified, and if so creates a GraphField + * and sets it on the Domain. + * + * (params must not be null) + */ + public static void createGraphField(FacetRequest.Domain domain, Map 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 graph = (Map) 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 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 { } FacetRequest.Domain.JoinField.createJoinField(domain, domainMap); + FacetRequest.Domain.GraphField.createGraphField(domain, domainMap); Object filterOrList = domainMap.get("filter"); if (filterOrList != null) { diff --git a/solr/core/src/test/org/apache/solr/search/facet/TestJsonFacets.java b/solr/core/src/test/org/apache/solr/search/facet/TestJsonFacets.java index 4402d78a50c..e19bb93b63f 100644 --- a/solr/core/src/test/org/apache/solr/search/facet/TestJsonFacets.java +++ b/solr/core/src/test/org/apache/solr/search/facet/TestJsonFacets.java @@ -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 { Client client = Client.localClient();