From 0b817e6e495c40496b7cedc6f06060e43e5e2afc Mon Sep 17 00:00:00 2001 From: Christine Poerschke Date: Thu, 9 Feb 2017 14:55:06 +0000 Subject: [PATCH] SOLR-9912: Add facet.excludeTerms parameter support. (Jonny Marks, David Smiley, Christine Poerschke) --- solr/CHANGES.txt | 2 + .../org/apache/solr/request/SimpleFacets.java | 46 +++++- .../apache/solr/request/SimpleFacetsTest.java | 152 ++++++++++++++++++ .../solr/common/params/FacetParams.java | 5 + 4 files changed, 199 insertions(+), 6 deletions(-) diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 4d8aa0163e4..9453b5f7614 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -130,6 +130,8 @@ New Features * SOLR-9997: Enable configuring SolrHttpClientBuilder via java system property. (Hrishikesh Gadre via Mark Miller) +* SOLR-9912: Add facet.excludeTerms parameter support. (Jonny Marks, David Smiley, Christine Poerschke) + Bug Fixes ---------------------- diff --git a/solr/core/src/java/org/apache/solr/request/SimpleFacets.java b/solr/core/src/java/org/apache/solr/request/SimpleFacets.java index ac8e9109516..56c2643cb4d 100644 --- a/solr/core/src/java/org/apache/solr/request/SimpleFacets.java +++ b/solr/core/src/java/org/apache/solr/request/SimpleFacets.java @@ -21,9 +21,12 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.IdentityHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; @@ -343,15 +346,46 @@ public class SimpleFacets { ENUM, FC, FCS, UIF; } - protected Predicate newBytesRefFilter(String field, SolrParams params) { - final String contains = params.getFieldParam(field, FacetParams.FACET_CONTAINS); - final boolean ignoreCase = params.getFieldBool(field, FacetParams.FACET_CONTAINS_IGNORE_CASE, false); - - if (contains == null) { + protected Predicate newExcludeBytesRefFilter(String field, SolrParams params) { + final String exclude = params.getFieldParam(field, FacetParams.FACET_EXCLUDETERMS); + if (exclude == null) { return null; } - return new SubstringBytesRefFilter(contains, ignoreCase); + final Set excludeTerms = new HashSet<>(StrUtils.splitSmart(exclude, ",", true)); + + return new Predicate() { + @Override + public boolean test(BytesRef bytesRef) { + return !excludeTerms.contains(bytesRef.utf8ToString()); + } + }; + } + + protected Predicate newBytesRefFilter(String field, SolrParams params) { + final String contains = params.getFieldParam(field, FacetParams.FACET_CONTAINS); + + final Predicate containsFilter; + if (contains != null) { + final boolean containsIgnoreCase = params.getFieldBool(field, FacetParams.FACET_CONTAINS_IGNORE_CASE, false); + containsFilter = new SubstringBytesRefFilter(contains, containsIgnoreCase); + } else { + containsFilter = null; + } + + final Predicate excludeFilter = newExcludeBytesRefFilter(field, params); + + if (containsFilter == null && excludeFilter == null) { + return null; + } + + if (containsFilter != null && excludeFilter == null) { + return containsFilter; + } else if (containsFilter == null && excludeFilter != null) { + return excludeFilter; + } + + return containsFilter.and(excludeFilter); } /** diff --git a/solr/core/src/test/org/apache/solr/request/SimpleFacetsTest.java b/solr/core/src/test/org/apache/solr/request/SimpleFacetsTest.java index bec3aa0dfdc..fd8d6ecae5b 100644 --- a/solr/core/src/test/org/apache/solr/request/SimpleFacetsTest.java +++ b/solr/core/src/test/org/apache/solr/request/SimpleFacetsTest.java @@ -2081,6 +2081,158 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { doFacetPrefix("tt_s1", "{!threads=-1}", "", "facet.method","fcs"); // default / unlimited threads doFacetPrefix("tt_s1", "{!threads=2}", "", "facet.method","fcs"); // specific number of threads } + + @Test + public void testFacetExclude() { + for (String method : new String[] {"enum", "fcs", "fc", "uif"}) { + doFacetExclude("contains_s1", "contains_group_s1", "Astra", "facet.method", method); + } + } + + private void doFacetExclude(String f, String g, String termSuffix, String... params) { + String indent="on"; + String pre = "//lst[@name='"+f+"']"; + + final SolrQueryRequest req = req(params, "q", "id:[* TO *]" + ,"indent",indent + ,"facet","true" + ,"facet.field", f + ,"facet.mincount","0" + ,"facet.offset","0" + ,"facet.limit","100" + ,"facet.sort","count" + ,"facet.excludeTerms","B,BBB"+termSuffix + ); + + assertQ("test facet.exclude", + req + ,"*[count(//lst[@name='facet_fields']/lst/int)=10]" + ,pre+"/int[1][@name='BBB'][.='3']" + ,pre+"/int[2][@name='CCC'][.='3']" + ,pre+"/int[3][@name='CCC"+termSuffix+"'][.='3']" + ,pre+"/int[4][@name='BB'][.='2']" + ,pre+"/int[5][@name='BB"+termSuffix+"'][.='2']" + ,pre+"/int[6][@name='CC'][.='2']" + ,pre+"/int[7][@name='CC"+termSuffix+"'][.='2']" + ,pre+"/int[8][@name='AAA'][.='1']" + ,pre+"/int[9][@name='AAA"+termSuffix+"'][.='1']" + ,pre+"/int[10][@name='B"+termSuffix+"'][.='1']" + ); + + final SolrQueryRequest groupReq = req(params, "q", "id:[* TO *]" + ,"indent",indent + ,"facet","true" + ,"facet.field", f + ,"facet.mincount","0" + ,"facet.offset","0" + ,"facet.limit","100" + ,"facet.sort","count" + ,"facet.excludeTerms","B,BBB"+termSuffix + ,"group","true" + ,"group.field",g + ,"group.facet","true" + ); + + assertQ("test facet.exclude for grouped facets", + groupReq + ,"*[count(//lst[@name='facet_fields']/lst/int)=10]" + ,pre+"/int[1][@name='CCC'][.='3']" + ,pre+"/int[2][@name='CCC"+termSuffix+"'][.='3']" + ,pre+"/int[3][@name='BBB'][.='2']" + ,pre+"/int[4][@name='AAA'][.='1']" + ,pre+"/int[5][@name='AAA"+termSuffix+"'][.='1']" + ,pre+"/int[6][@name='B"+termSuffix+"'][.='1']" + ,pre+"/int[7][@name='BB'][.='1']" + ,pre+"/int[8][@name='BB"+termSuffix+"'][.='1']" + ,pre+"/int[9][@name='CC'][.='1']" + ,pre+"/int[10][@name='CC"+termSuffix+"'][.='1']" + ); + } + + @Test + public void testFacetContainsAndExclude() { + for (String method : new String[] {"enum", "fcs", "fc", "uif"}) { + String contains = "BAst"; + String groupContains = "Ast"; + final boolean ignoreCase = random().nextBoolean(); + if (ignoreCase) { + contains = randomizeStringCasing(contains); + groupContains = randomizeStringCasing(groupContains); + doFacetContainsAndExclude("contains_s1", "contains_group_s1", "Astra", contains, groupContains, "facet.method", method, "facet.contains.ignoreCase", "true"); + } else { + doFacetContainsAndExclude("contains_s1", "contains_group_s1", "Astra", contains, groupContains, "facet.method", method); + } + } + } + + private String randomizeStringCasing(String str) { + final char[] characters = str.toCharArray(); + + for (int i = 0; i != characters.length; ++i) { + final boolean switchCase = random().nextBoolean(); + if (!switchCase) { + continue; + } + + final char c = str.charAt(i); + if (Character.isUpperCase(c)) { + characters[i] = Character.toLowerCase(c); + } else { + characters[i] = Character.toUpperCase(c); + } + } + + return new String(characters); + } + + private void doFacetContainsAndExclude(String f, String g, String termSuffix, String contains, String groupContains, String... params) { + String indent="on"; + String pre = "//lst[@name='"+f+"']"; + + final SolrQueryRequest req = req(params, "q", "id:[* TO *]" + ,"indent",indent + ,"facet","true" + ,"facet.field", f + ,"facet.mincount","0" + ,"facet.offset","0" + ,"facet.limit","100" + ,"facet.sort","count" + ,"facet.contains",contains + ,"facet.excludeTerms","BBB"+termSuffix + ); + + assertQ("test facet.contains with facet.exclude", + req + ,"*[count(//lst[@name='facet_fields']/lst/int)=2]" + ,pre+"/int[1][@name='BB"+termSuffix+"'][.='2']" + ,pre+"/int[2][@name='B"+termSuffix+"'][.='1']" + ); + + final SolrQueryRequest groupReq = req(params, "q", "id:[* TO *]" + ,"indent",indent + ,"facet","true" + ,"facet.field", f + ,"facet.mincount","0" + ,"facet.offset","0" + ,"facet.limit","100" + ,"facet.sort","count" + ,"facet.contains",groupContains + ,"facet.excludeTerms","AAA"+termSuffix + ,"group","true" + ,"group.field",g + ,"group.facet","true" + ); + + assertQ("test facet.contains with facet.exclude for grouped facets", + groupReq + ,"*[count(//lst[@name='facet_fields']/lst/int)=5]" + ,pre+"/int[1][@name='CCC"+termSuffix+"'][.='3']" + ,pre+"/int[2][@name='BBB"+termSuffix+"'][.='2']" + ,pre+"/int[3][@name='B"+termSuffix+"'][.='1']" + ,pre+"/int[4][@name='BB"+termSuffix+"'][.='1']" + ,pre+"/int[5][@name='CC"+termSuffix+"'][.='1']" + ); + } @Test //@Ignore("SOLR-8466 - facet.method=uif ignores facet.contains") diff --git a/solr/solrj/src/java/org/apache/solr/common/params/FacetParams.java b/solr/solrj/src/java/org/apache/solr/common/params/FacetParams.java index 038fc6eb08e..e91b9af5a76 100644 --- a/solr/solrj/src/java/org/apache/solr/common/params/FacetParams.java +++ b/solr/solrj/src/java/org/apache/solr/common/params/FacetParams.java @@ -180,6 +180,11 @@ public interface FacetParams { */ public static final String FACET_CONTAINS_IGNORE_CASE = FACET_CONTAINS + ".ignoreCase"; + /** + * Only return constraints of a facet field excluding the given string. + */ + public static final String FACET_EXCLUDETERMS = FACET + ".excludeTerms"; + /** * When faceting by enumerating the terms in a field, * only use the filterCache for terms with a df >= to this parameter.