diff --git a/src/main/java/org/springframework/data/solr/core/DefaultQueryParser.java b/src/main/java/org/springframework/data/solr/core/DefaultQueryParser.java index 219d5bbef..2419c8238 100644 --- a/src/main/java/org/springframework/data/solr/core/DefaultQueryParser.java +++ b/src/main/java/org/springframework/data/solr/core/DefaultQueryParser.java @@ -15,11 +15,19 @@ */ package org.springframework.data.solr.core; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map.Entry; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; import org.apache.commons.lang3.StringUtils; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrQuery.ORDER; @@ -36,7 +44,6 @@ import org.springframework.data.domain.Sort.Order; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.solr.core.query.*; -import org.springframework.data.solr.core.query.Criteria.Predicate; import org.springframework.data.solr.core.query.FacetOptions.FacetParameter; import org.springframework.data.solr.core.query.FacetOptions.FieldWithDateRangeParameters; import org.springframework.data.solr.core.query.FacetOptions.FieldWithFacetParameters; @@ -65,11 +72,14 @@ * @author Joachim Uhrlaß * @author Petar Tahchiev * @author Juan Manuel de Blas + * @author Joe Linn */ public class DefaultQueryParser extends QueryParserBase { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultQueryParser.class); + private final ObjectMapper objectMapper; + /** * Create a new {@link DefaultQueryParser} using the provided {@link MappingContext} to map {@link Field fields} to * domain domain type {@link org.springframework.data.mapping.PersistentProperty properties}. @@ -79,6 +89,11 @@ public class DefaultQueryParser extends QueryParserBase { */ public DefaultQueryParser(@Nullable MappingContext mappingContext) { super(mappingContext); + this.objectMapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL) + .registerModule(new SimpleModule("solr") + .addSerializer(StatFacetFunction.class, new StatFacetFunctionSerializer(StatFacetFunction.class)) + .addSerializer(Criteria.class, new CriteriaSerializer(Criteria.class)) + .addSerializer(Enum.class, new LowerCaseEnumSerializer<>(Enum.class))); } /** @@ -139,6 +154,7 @@ private void processFacetOptions(SolrQuery solrQuery, FacetQuery query, @Nullabl appendFacetingQueries(solrQuery, query, domainType); appendFacetingOnPivot(solrQuery, query, domainType); appendRangeFacetingOnFields(solrQuery, query, domainType); + appendJsonFaceting(solrQuery, query); } } @@ -441,6 +457,21 @@ private void appendRangeFacetingOnFields(SolrQuery solrQuery, FacetQuery query, } } + /** + * Serializes any configured JSON facets and adds them to the given {@link SolrQuery}. + * + * @param solrQuery the Solr query to which JSON facets should be added + * @param query query abstraction which may contain JSON facet configurations + */ + private void appendJsonFaceting(SolrQuery solrQuery, FacetQuery query) { + FacetOptions facetOptions = query.getFacetOptions(); + try { + solrQuery.add("json.facet", objectMapper.writeValueAsString(facetOptions.getJsonFacets())); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Unable to serialize JSON facet(s).", e); + } + } + private void appendFieldFacetingByNumberRange(SolrQuery solrQuery, FieldWithNumericRangeParameters field, @Nullable Class domainType) { @@ -566,4 +597,47 @@ private List getFilterQueryStrings(List filterQueries, @Nul } return filterQueryStrings; } + + /** + * Used when serializing JSON facets for transmission to Solr. Lowercases Enum names. + */ + private static class LowerCaseEnumSerializer extends StdSerializer { + public LowerCaseEnumSerializer(Class t) { + super(t); + } + + @Override + public void serialize(Enum value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(value.name().toLowerCase()); + } + } + + /** + * Used when serializing JSON facets for transmission to Solr. Calls + * {@link #createQueryStringFromCriteria(Criteria, Class)} to serialize queries which are used within JSON facets. + */ + private class CriteriaSerializer extends StdSerializer { + protected CriteriaSerializer(Class t) { + super(t); + } + + @Override + public void serialize(Criteria value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(createQueryStringFromCriteria(value, null)); + } + } + + /** + * Used when serializing JSON facets for transmission to Solr. Serializes functions used in stats facets. + */ + private class StatFacetFunctionSerializer extends StdSerializer { + protected StatFacetFunctionSerializer(Class t) { + super(t); + } + + @Override + public void serialize(StatFacetFunction value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(createFunctionFragment(value, 0, null)); + } + } } diff --git a/src/main/java/org/springframework/data/solr/core/ResultHelper.java b/src/main/java/org/springframework/data/solr/core/ResultHelper.java index 419915793..e828a996e 100644 --- a/src/main/java/org/springframework/data/solr/core/ResultHelper.java +++ b/src/main/java/org/springframework/data/solr/core/ResultHelper.java @@ -18,24 +18,19 @@ import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.lang3.reflect.FieldUtils; -import org.apache.solr.client.solrj.response.FacetField; +import org.apache.solr.client.solrj.response.*; import org.apache.solr.client.solrj.response.FacetField.Count; -import org.apache.solr.client.solrj.response.FieldStatsInfo; -import org.apache.solr.client.solrj.response.Group; -import org.apache.solr.client.solrj.response.GroupCommand; -import org.apache.solr.client.solrj.response.GroupResponse; -import org.apache.solr.client.solrj.response.PivotField; -import org.apache.solr.client.solrj.response.QueryResponse; -import org.apache.solr.client.solrj.response.RangeFacet; -import org.apache.solr.client.solrj.response.SpellCheckResponse; -import org.apache.solr.client.solrj.response.TermsResponse; import org.apache.solr.client.solrj.response.TermsResponse.Term; +import org.apache.solr.client.solrj.response.json.BucketBasedJsonFacet; +import org.apache.solr.client.solrj.response.json.BucketJsonFacet; +import org.apache.solr.client.solrj.response.json.NestableJsonFacet; import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.util.NamedList; import org.springframework.beans.DirectFieldAccessor; @@ -66,6 +61,7 @@ * @author Francisco Spaeth * @author Venil Noronha * @author Vitezslav Zak + * @author Joe Linn */ final class ResultHelper { @@ -226,6 +222,79 @@ static List convertFacetQueryResponseToFacetQueryResult(FacetQu return facetResult; } + /** + * Parses any JSON facet results present in the given {@link QueryResponse}. + * + * @param query the query which produced the given response + * @param response the query responce received from Solr + * @return a Map of JSON facet names to their parsed results + */ + static Map convertJsonFacetQueryResponseToFacetResultMap(FacetQuery query, + QueryResponse response) { + Assert.notNull(query, "Cannot convert response for 'null', query"); + + if (!hasFacets(query, response)) { + return new HashMap<>(); + } + + NestableJsonFacet jsonFacetingResponse = response.getJsonFacetingResponse(); + return convertNestableFacetsToFacetResultMap(jsonFacetingResponse); + } + + /** + * Parses the given {@link NestableJsonFacet}, including any nested facets it contains. + * + * @param facet a JSON facet result received from Solr + * @return a Map of facet names to their results + */ + private static Map convertNestableFacetsToFacetResultMap(NestableJsonFacet facet) { + Map facetResults = new HashMap<>(); + + for (String statName : facet.getStatNames()) { + Object value = facet.getStatValue(statName); + if (value instanceof Number) { + facetResults.put(statName, new SingleStatJsonFacetResult(facet.getCount(), (Number) value)); + } + } + + for (String facetName : facet.getBucketBasedFacetNames()) { + BucketBasedJsonFacet childFacet = facet.getBucketBasedFacets(facetName); + List resultBuckets = new ArrayList<>(childFacet.getBuckets().size()); + for (BucketJsonFacet bucket : childFacet.getBuckets()) { + if (!hasChildFacets(bucket)) { + resultBuckets.add(new BucketFacetEntry(bucket.getVal(), bucket.getCount())); + } else { + resultBuckets.add( + new BucketFacetEntry(bucket.getVal(), bucket.getCount(), convertNestableFacetsToFacetResultMap(bucket))); + } + } + facetResults.put(facetName, new MultiBucketJsonFacetResult(facet.getCount(), resultBuckets)); + } + + for (String facetName : facet.getQueryFacetNames()) { + NestableJsonFacet childFacet = facet.getQueryFacet(facetName); + if (!hasChildFacets(childFacet)) { + facetResults.put(facetName, new SingleBucketJsonFacetResult(childFacet.getCount())); + } else { + facetResults.put(facetName, + new SingleBucketJsonFacetResult(childFacet.getCount(), convertNestableFacetsToFacetResultMap(childFacet))); + } + } + + return facetResults; + } + + /** + * Determines whether or not the given facet result contains child facets + * + * @param facet a {@link NestableJsonFacet} received from Solr + * @return true if the given facet has child (nested) facets + */ + private static boolean hasChildFacets(NestableJsonFacet facet) { + return !facet.getBucketBasedFacetNames().isEmpty() || !facet.getStatNames().isEmpty() + || !facet.getHeatmapFacetNames().isEmpty() || !facet.getQueryFacetNames().isEmpty(); + } + static List> convertAndAddHighlightQueryResponseToResultPage(@Nullable QueryResponse response, @Nullable SolrResultPage page) { if (response == null || CollectionUtils.isEmpty(response.getHighlighting()) || page == null) { diff --git a/src/main/java/org/springframework/data/solr/core/SolrTemplate.java b/src/main/java/org/springframework/data/solr/core/SolrTemplate.java index 536b954e8..1ee2921be 100644 --- a/src/main/java/org/springframework/data/solr/core/SolrTemplate.java +++ b/src/main/java/org/springframework/data/solr/core/SolrTemplate.java @@ -91,6 +91,7 @@ * @author Petar Tahchiev * @author Mark Paluch * @author Juan Manuel de Blas + * @author Joe Linn */ public class SolrTemplate implements SolrOperations, InitializingBean, ApplicationContextAware { @@ -461,6 +462,8 @@ private SolrResultPage createSolrResultPage(Query query, Class clazz, ResultHelper.convertFacetQueryResponseToFacetPivotMap((FacetQuery) query, response)); page.addAllRangeFacetFieldResultPages( ResultHelper.convertFacetQueryResponseToRangeFacetPageMap((FacetQuery) query, response)); + page.addAllJsonFacetResults( + ResultHelper.convertJsonFacetQueryResponseToFacetResultMap((FacetQuery) query, response)); } if (query.getSpellcheckOptions() != null) { diff --git a/src/main/java/org/springframework/data/solr/core/query/AbstractJsonFacet.java b/src/main/java/org/springframework/data/solr/core/query/AbstractJsonFacet.java new file mode 100644 index 000000000..add922c11 --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/AbstractJsonFacet.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query; + +/** + * @author Joe Linn + */ +public abstract class AbstractJsonFacet implements JsonFacet { + private String name; + + public AbstractJsonFacet() {} + + public AbstractJsonFacet(String name) { + this.name = name; + } + + /** + * @return the type of JSON facet represented by this class. This string will be passed to Solr at request time. + */ + public abstract String getType(); + + @Override + public String getName() { + return name; + } + + /** + * Sets the name of this JSON facet. + * + * @param name facet name. Should be unique within the current request and nesting level. + * @return + */ + public T setName(String name) { + this.name = name; + return (T) this; + } +} diff --git a/src/main/java/org/springframework/data/solr/core/query/FacetOptions.java b/src/main/java/org/springframework/data/solr/core/query/FacetOptions.java index ed7d86e01..e0fa143c5 100644 --- a/src/main/java/org/springframework/data/solr/core/query/FacetOptions.java +++ b/src/main/java/org/springframework/data/solr/core/query/FacetOptions.java @@ -20,7 +20,9 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.solr.common.params.FacetParams; import org.apache.solr.common.params.FacetParams.FacetRangeInclude; @@ -35,6 +37,7 @@ * * @author Christoph Strobl * @author Francisco Spaeth + * @author Joe Linn */ public class FacetOptions { @@ -50,6 +53,7 @@ public enum FacetSort { private List facetOnPivotFields = new ArrayList<>(0); private List> facetRangeOnFields = new ArrayList<>(1); private List facetQueries = new ArrayList<>(0); + private Map jsonFacets = new HashMap<>(); private int facetMinCount = DEFAULT_FACET_MIN_COUNT; private int facetLimit = DEFAULT_FACET_LIMIT; @@ -353,10 +357,10 @@ private boolean hasFacetRages() { } /** - * @return true if any {@code facet.field} or {@code facet.query} set + * @return true if any {@code facet.field} or {@code facet.query} or {@code json.facet} set */ public boolean hasFacets() { - return hasFields() || hasFacetQueries() || hasPivotFields() || hasFacetRages(); + return hasFields() || hasFacetQueries() || hasPivotFields() || hasFacetRages() || hasJsonFacets(); } /** @@ -380,6 +384,31 @@ public Collection getFieldsWithParameters() { return result; } + /** + * @return any configured JSON facets + */ + public Map getJsonFacets() { + return jsonFacets; + } + + /** + * @return true if any JSON facets have been configured + */ + public boolean hasJsonFacets() { + return !jsonFacets.isEmpty(); + } + + /** + * Adds a JSON facet ({@code json.facet}) + * + * @param facet the facet to be added + * @return + */ + public FacetOptions addJsonFacet(JsonFacet facet) { + jsonFacets.put(facet.getName(), facet); + return this; + } + public static class FacetParameter extends QueryParameterImpl { public FacetParameter(String parameter, @Nullable Object value) { diff --git a/src/main/java/org/springframework/data/solr/core/query/JsonFacet.java b/src/main/java/org/springframework/data/solr/core/query/JsonFacet.java new file mode 100644 index 000000000..2f5bcae51 --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/JsonFacet.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query; + +/** + * @author Joe Linn + */ +public interface JsonFacet { + /** + * @return the name assigned to this facet + */ + String getName(); +} diff --git a/src/main/java/org/springframework/data/solr/core/query/JsonFieldFacet.java b/src/main/java/org/springframework/data/solr/core/query/JsonFieldFacet.java new file mode 100644 index 000000000..1e3d58be4 --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/JsonFieldFacet.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query; + +/** + * Represents a JSON facet that operates on a single field + * + * @author Joe Linn + */ +public abstract class JsonFieldFacet extends NestedJsonFacet { + private String field; + + public JsonFieldFacet() {} + + public JsonFieldFacet(String name, String field) { + super(name); + this.field = field; + } + + /** + * @return the name of the field on which this JSON facet will operate + */ + public String getField() { + return field; + } + + public T setField(String field) { + this.field = field; + return (T) this; + } +} diff --git a/src/main/java/org/springframework/data/solr/core/query/JsonQueryFacet.java b/src/main/java/org/springframework/data/solr/core/query/JsonQueryFacet.java new file mode 100644 index 000000000..3cff9f7b2 --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/JsonQueryFacet.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query; + +/** + * Represents a JSON query facet. + * + * @author Joe Linn + */ +public class JsonQueryFacet extends NestedJsonFacet { + private Criteria query; + + public JsonQueryFacet() {} + + public JsonQueryFacet(String name, Criteria query) { + super(name); + this.query = query; + } + + public Criteria getQuery() { + return query; + } + + public JsonQueryFacet setQuery(Criteria query) { + this.query = query; + return this; + } + + @Override + public String getType() { + return "query"; + } +} diff --git a/src/main/java/org/springframework/data/solr/core/query/JsonRangeFacet.java b/src/main/java/org/springframework/data/solr/core/query/JsonRangeFacet.java new file mode 100644 index 000000000..526bd120c --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/JsonRangeFacet.java @@ -0,0 +1,160 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query; + +import java.util.LinkedList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a JSON range facet. + * + * @author Joe Linn + */ +public class JsonRangeFacet extends JsonFieldFacet { + private Number start; + private Number end; + private Number gap; + @JsonProperty("hardend") private Boolean hardEnd; + private Other other; + private Include include = Include.LOWER; + private List ranges = new LinkedList<>(); + + @Override + public String getType() { + return "range"; + } + + public Number getStart() { + return start; + } + + public JsonRangeFacet setStart(Number start) { + this.start = start; + return this; + } + + public Number getEnd() { + return end; + } + + public JsonRangeFacet setEnd(Number end) { + this.end = end; + return this; + } + + public Number getGap() { + return gap; + } + + public JsonRangeFacet setGap(Number gap) { + this.gap = gap; + return this; + } + + public Boolean isHardEnd() { + return hardEnd; + } + + public JsonRangeFacet setHardEnd(Boolean hardEnd) { + this.hardEnd = hardEnd; + return this; + } + + public Other getOther() { + return other; + } + + public JsonRangeFacet setOther(Other other) { + this.other = other; + return this; + } + + public Include getInclude() { + return include; + } + + public JsonRangeFacet setInclude(Include include) { + this.include = include; + return this; + } + + public List getRanges() { + return ranges; + } + + public JsonRangeFacet setRanges(List ranges) { + this.ranges = ranges; + return this; + } + + public JsonRangeFacet addRange(Range range) { + ranges.add(range); + return this; + } + + public static enum Other { + BEFORE, AFTER, BETWEEN, NONE, ALL; + } + + public static enum Include { + LOWER, UPPER, EDGE, OUTER, ALL; + } + + public static class Range { + private Number from; + private Number to; + @JsonProperty("inclusive_from") private boolean inclusiveFrom = true; + @JsonProperty("inclusive_to") private boolean inclusiveTo = false; + + public Number getFrom() { + return from; + } + + public Range setFrom(Number from) { + this.from = from; + return this; + } + + public Number getTo() { + return to; + } + + public Range setTo(Number to) { + this.to = to; + return this; + } + + public boolean isInclusiveFrom() { + return inclusiveFrom; + } + + public Range setInclusiveFrom(boolean inclusiveFrom) { + this.inclusiveFrom = inclusiveFrom; + return this; + } + + public boolean isInclusiveTo() { + return inclusiveTo; + } + + public Range setInclusiveTo(boolean inclusiveTo) { + this.inclusiveTo = inclusiveTo; + return this; + } + } +} diff --git a/src/main/java/org/springframework/data/solr/core/query/JsonStatFacet.java b/src/main/java/org/springframework/data/solr/core/query/JsonStatFacet.java new file mode 100644 index 000000000..935dc8a7c --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/JsonStatFacet.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a JSON stats + * facet. + * + * @author Joe Linn + */ +public class JsonStatFacet extends AbstractJsonFacet { + @JsonProperty("func") private StatFacetFunction function; + private Map params = new HashMap<>(); + + public JsonStatFacet() {} + + public JsonStatFacet(String name, StatFacetFunction function) { + super(name); + this.function = function; + } + + @Override + public String getType() { + return "func"; + } + + public StatFacetFunction getFunction() { + return function; + } + + public JsonStatFacet setFunction(StatFacetFunction function) { + this.function = function; + return this; + } + + @JsonAnyGetter + public Map getParams() { + return params; + } + + public JsonStatFacet setParams(Map params) { + this.params = params; + return this; + } + + public JsonStatFacet addParam(String name, Object value) { + this.params.put(name, value); + return this; + } +} diff --git a/src/main/java/org/springframework/data/solr/core/query/JsonTermsFacet.java b/src/main/java/org/springframework/data/solr/core/query/JsonTermsFacet.java new file mode 100644 index 000000000..e1d56f8ad --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/JsonTermsFacet.java @@ -0,0 +1,159 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query; + +/** + * Represents a JSON terms facet. + * + * @author Joe Linn + */ +public class JsonTermsFacet extends JsonFieldFacet { + private int offset; + private int limit = 10; + private TermsOptions.Sort sort = TermsOptions.DEFAULT_SORT; + private int overRequest = -1; + private boolean refine; + private int overRefine = -1; + private int minCount = 1; + private boolean missing; + private boolean numBuckets; + private boolean allBuckets; + private String prefix; + private Method method = Method.SMART; + + public JsonTermsFacet() {} + + public JsonTermsFacet(String name, String field) { + super(name, field); + } + + public int getOffset() { + return offset; + } + + public JsonTermsFacet setOffset(int offset) { + this.offset = offset; + return this; + } + + public int getLimit() { + return limit; + } + + public JsonTermsFacet setLimit(int limit) { + this.limit = limit; + return this; + } + + public TermsOptions.Sort getSort() { + return sort; + } + + public JsonTermsFacet setSort(TermsOptions.Sort sort) { + this.sort = sort; + return this; + } + + public int getOverRequest() { + return overRequest; + } + + public JsonTermsFacet setOverRequest(int overRequest) { + this.overRequest = overRequest; + return this; + } + + public boolean isRefine() { + return refine; + } + + public JsonTermsFacet setRefine(boolean refine) { + this.refine = refine; + return this; + } + + public int getOverRefine() { + return overRefine; + } + + public JsonTermsFacet setOverRefine(int overRefine) { + this.overRefine = overRefine; + return this; + } + + public int getMinCount() { + return minCount; + } + + public JsonTermsFacet setMinCount(int minCount) { + this.minCount = minCount; + return this; + } + + public boolean isMissing() { + return missing; + } + + public JsonTermsFacet setMissing(boolean missing) { + this.missing = missing; + return this; + } + + public boolean isNumBuckets() { + return numBuckets; + } + + public JsonTermsFacet setNumBuckets(boolean numBuckets) { + this.numBuckets = numBuckets; + return this; + } + + public boolean isAllBuckets() { + return allBuckets; + } + + public JsonTermsFacet setAllBuckets(boolean allBuckets) { + this.allBuckets = allBuckets; + return this; + } + + public String getPrefix() { + return prefix; + } + + public JsonTermsFacet setPrefix(String prefix) { + this.prefix = prefix; + return this; + } + + public Method getMethod() { + return method; + } + + public JsonTermsFacet setMethod(Method method) { + this.method = method; + return this; + } + + @Override + public String getType() { + return "terms"; + } + + public enum Method { + DV, UIF, DVHASH, ENUM, STREAM, SMART; + } +} diff --git a/src/main/java/org/springframework/data/solr/core/query/NestedJsonFacet.java b/src/main/java/org/springframework/data/solr/core/query/NestedJsonFacet.java new file mode 100644 index 000000000..2b70d9d87 --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/NestedJsonFacet.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a JSON facet which can contain nested facets. + * + * @author Joe Linn + */ +public abstract class NestedJsonFacet extends AbstractJsonFacet { + @JsonProperty("facet") private Map facets = new HashMap<>(); + + public NestedJsonFacet() {} + + public NestedJsonFacet(String name) { + super(name); + } + + public Map getFacets() { + return facets; + } + + public void setFacets(Map facets) { + this.facets = facets; + } + + /** + * Adds a {@link JsonFacet} as a nested facet of this facet. + * + * @param facet the facet to be added + * @return + */ + public T addFacet(JsonFacet facet) { + this.facets.put(facet.getName(), facet); + return (T) this; + } +} diff --git a/src/main/java/org/springframework/data/solr/core/query/StatFacetFunction.java b/src/main/java/org/springframework/data/solr/core/query/StatFacetFunction.java new file mode 100644 index 000000000..65714193b --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/StatFacetFunction.java @@ -0,0 +1,169 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query; + +import java.util.Arrays; + +import com.google.common.base.CaseFormat; + +/** + * @author Joe Linn + */ +public class StatFacetFunction extends AbstractFunction { + private final Func func; + + public StatFacetFunction(Func func, Object... arguments) { + super(Arrays.asList(arguments)); + this.func = func; + } + + @Override + public String getOperation() { + return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, func.name()); + } + + @Override + public String toSolrFunction(Context context) { + StringBuilder str = new StringBuilder(getOperation()).append("("); + String comma = ""; + for (Object parameter : getArguments()) { + str.append(comma); + if (parameter instanceof StatFacetFunction) { + str.append(((StatFacetFunction) parameter).toSolrFunction(context)); + } else { + str.append(context.convert(parameter)); + } + comma = ","; + } + str.append(")"); + return str.toString(); + } + + public static StatFacetFunction sum(String field) { + return new StatFacetFunction(Func.SUM, field); + } + + public static StatFacetFunction sum(StatFacetFunction function) { + return new StatFacetFunction(Func.SUM, function); + } + + public static StatFacetFunction avg(String field) { + return new StatFacetFunction(Func.AVG, field); + } + + public static StatFacetFunction avg(StatFacetFunction function) { + return new StatFacetFunction(Func.AVG, function); + } + + public static StatFacetFunction min(String field) { + return new StatFacetFunction(Func.MIN, field); + } + + public static StatFacetFunction min(StatFacetFunction function) { + return new StatFacetFunction(Func.MIN, function); + } + + public static StatFacetFunction max(String field) { + return new StatFacetFunction(Func.MAX, field); + } + + public static StatFacetFunction max(StatFacetFunction function) { + return new StatFacetFunction(Func.MAX, function); + } + + public static StatFacetFunction missing(String field) { + return new StatFacetFunction(Func.MISSING, field); + } + + public static StatFacetFunction missing(StatFacetFunction function) { + return new StatFacetFunction(Func.MISSING, function); + } + + public static StatFacetFunction countvals(String field) { + return new StatFacetFunction(Func.COUNTVALS, field); + } + + public static StatFacetFunction countvals(StatFacetFunction function) { + return new StatFacetFunction(Func.COUNTVALS, function); + } + + public static StatFacetFunction unique(String field) { + return new StatFacetFunction(Func.UNIQUE, field); + } + + public static StatFacetFunction uniqueBlock(String field) { + return new StatFacetFunction(Func.UNIQUE_BLOCK, field); + } + + public static StatFacetFunction uniqueBlock(Criteria criteria) { + return new StatFacetFunction(Func.UNIQUE_BLOCK, criteria); + } + + public static StatFacetFunction hll(String field) { + return new StatFacetFunction(Func.HLL, field); + } + + // bug in solrj causes results of percentile ƒacet to not be parsed properly: + // https://issues.apache.org/jira/browse/SOLR-14006 + /*public static StatFacetFunction percentile(String field, float... quantiles) { + return percentile((Object) field, quantiles); + } + + public static StatFacetFunction percentile(StatFacetFunction function, float... quantiles) { + return percentile((Object) function, quantiles); + }*/ + + private static StatFacetFunction percentile(Object fieldOrFunction, float... quantiles) { + Object[] params = new Object[quantiles.length + 1]; + params[0] = fieldOrFunction; + for (int i = 0; i < quantiles.length; i++) { + params[i + 1] = quantiles[i]; + } + return new StatFacetFunction(Func.PERCENTILE, params); + } + + public static StatFacetFunction sumsq(String field) { + return new StatFacetFunction(Func.SUMSQ, field); + } + + public static StatFacetFunction sumsq(StatFacetFunction function) { + return new StatFacetFunction(Func.SUMSQ, function); + } + + public static StatFacetFunction variance(String field) { + return new StatFacetFunction(Func.VARIANCE, field); + } + + public static StatFacetFunction variance(StatFacetFunction function) { + return new StatFacetFunction(Func.VARIANCE, function); + } + + public static StatFacetFunction stddev(String field) { + return new StatFacetFunction(Func.STDDEV, field); + } + + public static StatFacetFunction stddev(StatFacetFunction function) { + return new StatFacetFunction(Func.STDDEV, function); + } + + public static StatFacetFunction relatedness(String foreground, String background) { + return new StatFacetFunction(Func.RELATEDNESS, foreground, background); + } + + public enum Func { + SUM, AVG, MIN, MAX, MISSING, COUNTVALS, UNIQUE, UNIQUE_BLOCK, HLL, PERCENTILE, SUMSQ, VARIANCE, STDDEV, RELATEDNESS + } +} diff --git a/src/main/java/org/springframework/data/solr/core/query/result/AbstractJsonFacetResult.java b/src/main/java/org/springframework/data/solr/core/query/result/AbstractJsonFacetResult.java new file mode 100644 index 000000000..1e38fffde --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/result/AbstractJsonFacetResult.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query.result; + +/** + * @author Joe Linn + */ +public abstract class AbstractJsonFacetResult implements JsonFacetResult { + private long count; + + protected AbstractJsonFacetResult(long count) { + this.count = count; + } + + @Override + public long getCount() { + return count; + } +} diff --git a/src/main/java/org/springframework/data/solr/core/query/result/BucketFacetEntry.java b/src/main/java/org/springframework/data/solr/core/query/result/BucketFacetEntry.java new file mode 100644 index 000000000..d8ff5b5d6 --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/result/BucketFacetEntry.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query.result; + +import java.util.Map; + +/** + * Represents a single bucket in a multi-bucketed (terms, range) JSON facet result. + * + * @author Joe Linn + */ +public class BucketFacetEntry extends ValueCountEntry implements NestableFacetEntry { + private Map facets; + private Object value; + + public BucketFacetEntry(Object value, long valueCount) { + super(value.toString(), valueCount); + this.value = value; + } + + public BucketFacetEntry(Object value, long valueCount, Map facets) { + this(value, valueCount); + this.facets = facets; + } + + @Override + public Map getFacets() { + return facets; + } + + @Override + public Object getKey() { + return value; + } + + /** + * Attempts to return the {@link #value} of this bucket as a number if possible. + * + * @return this bucket's value in number form + */ + public Number getValueAsNumber() { + if (value instanceof Number) { + return (Number) value; + } else { + return Double.parseDouble((String) value); + } + } +} diff --git a/src/main/java/org/springframework/data/solr/core/query/result/BucketFacetResult.java b/src/main/java/org/springframework/data/solr/core/query/result/BucketFacetResult.java new file mode 100644 index 000000000..3b348be0e --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/result/BucketFacetResult.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query.result; + +import java.util.List; + +/** + * @author Joe Linn + */ +public interface BucketFacetResult extends JsonFacetResult { + /** + * @return a List of all buckets in this facet result + */ + List getBuckets(); +} diff --git a/src/main/java/org/springframework/data/solr/core/query/result/FacetQueryResult.java b/src/main/java/org/springframework/data/solr/core/query/result/FacetQueryResult.java index c99cd550a..6ada0e857 100644 --- a/src/main/java/org/springframework/data/solr/core/query/result/FacetQueryResult.java +++ b/src/main/java/org/springframework/data/solr/core/query/result/FacetQueryResult.java @@ -17,6 +17,7 @@ import java.util.Collection; import java.util.List; +import java.util.Map; import org.springframework.data.domain.Page; import org.springframework.data.solr.core.query.Field; @@ -27,6 +28,7 @@ * * @param * @author David Webb + * @author Joe Linn * @since 2.1.0 */ public interface FacetQueryResult { @@ -84,6 +86,11 @@ public interface FacetQueryResult { */ Collection> getFacetResultPages(); + /** + * @return Map of JSON facet names to their results + */ + Map getJsonFacetResults(); + /** * @return empty collection if not set */ diff --git a/src/main/java/org/springframework/data/solr/core/query/result/JsonFacetResult.java b/src/main/java/org/springframework/data/solr/core/query/result/JsonFacetResult.java new file mode 100644 index 000000000..ce0e90e61 --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/result/JsonFacetResult.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query.result; + +/** + * @author Joe Linn + */ +public interface JsonFacetResult { + /** + * @return the total number of documents which match this facet result + */ + long getCount(); +} diff --git a/src/main/java/org/springframework/data/solr/core/query/result/MultiBucketJsonFacetResult.java b/src/main/java/org/springframework/data/solr/core/query/result/MultiBucketJsonFacetResult.java new file mode 100644 index 000000000..850fa83c5 --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/result/MultiBucketJsonFacetResult.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query.result; + +import java.util.List; + +/** + * @author Joe Linn + */ +public class MultiBucketJsonFacetResult extends AbstractJsonFacetResult implements BucketFacetResult { + private List buckets; + + public MultiBucketJsonFacetResult(long count, List buckets) { + super(count); + this.buckets = buckets; + } + + @Override + public List getBuckets() { + return buckets; + } +} diff --git a/src/main/java/org/springframework/data/solr/core/query/result/NestableFacetEntry.java b/src/main/java/org/springframework/data/solr/core/query/result/NestableFacetEntry.java new file mode 100644 index 000000000..64e771809 --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/result/NestableFacetEntry.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query.result; + +import java.util.Map; + +/** + * @author Joe Linn + */ +public interface NestableFacetEntry extends FacetEntry { + /** + * @return a Map of nested facet names to their results + */ + Map getFacets(); +} diff --git a/src/main/java/org/springframework/data/solr/core/query/result/SingleBucketJsonFacetResult.java b/src/main/java/org/springframework/data/solr/core/query/result/SingleBucketJsonFacetResult.java new file mode 100644 index 000000000..7d7deced9 --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/result/SingleBucketJsonFacetResult.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query.result; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Joe Linn + */ +public class SingleBucketJsonFacetResult extends AbstractJsonFacetResult { + private Map facets; + + public SingleBucketJsonFacetResult(long count) { + super(count); + facets = new HashMap<>(); + } + + public SingleBucketJsonFacetResult(long count, Map facets) { + super(count); + this.facets = facets; + } + + public Map getFacets() { + return facets; + } +} diff --git a/src/main/java/org/springframework/data/solr/core/query/result/SingleStatJsonFacetResult.java b/src/main/java/org/springframework/data/solr/core/query/result/SingleStatJsonFacetResult.java new file mode 100644 index 000000000..1335658b2 --- /dev/null +++ b/src/main/java/org/springframework/data/solr/core/query/result/SingleStatJsonFacetResult.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012 - 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.solr.core.query.result; + +/** + * @author Joe Linn + */ +public class SingleStatJsonFacetResult extends AbstractJsonFacetResult { + private Number value; + + public SingleStatJsonFacetResult(long count, Number value) { + super(count); + this.value = value; + } + + public Number getValue() { + return value; + } + + @Override + public String toString() { + return "SingleStatJsonFacetResult{" + "value=" + value + '}'; + } +} diff --git a/src/main/java/org/springframework/data/solr/core/query/result/SolrResultPage.java b/src/main/java/org/springframework/data/solr/core/query/result/SolrResultPage.java index 70d4a09e7..1b09c4006 100644 --- a/src/main/java/org/springframework/data/solr/core/query/result/SolrResultPage.java +++ b/src/main/java/org/springframework/data/solr/core/query/result/SolrResultPage.java @@ -43,6 +43,7 @@ * @author Francisco Spaeth * @author David Webb * @author Petar Tahchiev + * @author Joe Linn */ public class SolrResultPage extends PageImpl implements FacetPage, HighlightPage, FacetAndHighlightPage, ScoredPage, GroupPage, StatsPage, SpellcheckedPage { @@ -52,6 +53,7 @@ public class SolrResultPage extends PageImpl implements FacetPage, High private Map> facetResultPages = new LinkedHashMap<>(1); private Map> facetPivotResultPages = new LinkedHashMap<>(); private Map> facetRangeResultPages = new LinkedHashMap<>(1); + private Map jsonFacetResults = new LinkedHashMap<>(); private @Nullable Page facetQueryResult; private List> highlighted = Collections.emptyList(); private @Nullable Float maxScore; @@ -150,6 +152,15 @@ public void addAllFacetPivotFieldResult(Map resultMap) { + this.jsonFacetResults.putAll(resultMap); + } + + @Override + public Map getJsonFacetResults() { + return jsonFacetResults; + } + @Override public Collection> getFacetResultPages() { return Collections.unmodifiableCollection(this.facetResultPages.values()); diff --git a/src/test/java/org/springframework/data/solr/core/DefaultQueryParserTests.java b/src/test/java/org/springframework/data/solr/core/DefaultQueryParserTests.java index f19938d26..7bb2aa7e0 100644 --- a/src/test/java/org/springframework/data/solr/core/DefaultQueryParserTests.java +++ b/src/test/java/org/springframework/data/solr/core/DefaultQueryParserTests.java @@ -21,9 +21,13 @@ import java.util.Calendar; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.TimeZone; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.MapType; import org.apache.commons.lang3.StringUtils; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.beans.Field; @@ -1823,6 +1827,65 @@ public String getAbbreviation() { .isEqualTo("{!geofilt pt=48.303056,14.290556 sfield=field_1 d=1.0 score=distance}"); } + @Test // DATASOLR-564 + public void testJsonTermsFacet() throws Exception { + JsonTermsFacet termsFacet = new JsonTermsFacet(). setName("categories") + . setField("cat").setMethod(JsonTermsFacet.Method.DVHASH); + FacetOptions facetOptions = new FacetOptions().addJsonFacet(termsFacet); + SimpleFacetQuery query = new SimpleFacetQuery((AnyCriteria.any())).setFacetOptions(facetOptions); + + SolrQuery solrQuery = queryParser.constructSolrQuery(query, null); + + assertThat(solrQuery).isNotNull(); + String jsonFacetString = solrQuery.get("json.facet"); + assertThat(jsonFacetString).isNotNull(); + + ObjectMapper objectMapper = new ObjectMapper(); + MapType mapType = objectMapper.getTypeFactory().constructMapType(HashMap.class, String.class, Object.class); + Map facetMap = objectMapper.readValue(jsonFacetString, mapType); + + assertThat(facetMap).hasSize(1).containsKey("categories"); + Map categories = (Map) facetMap.get("categories"); + assertThat(categories) + .containsEntry("limit", 10) + .containsEntry("field", "cat") + .containsEntry("method", "dvhash") + .containsEntry("type", "terms"); + } + + @Test // DATASOLR-564 + public void testJsonQueryFacet() throws Exception { + JsonQueryFacet jsonQueryFacet = new JsonQueryFacet("catFoo", Criteria.where("cat").is("foo")) + .addFacet(new JsonTermsFacet("pop", "popularity")); + + FacetOptions facetOptions = new FacetOptions().addJsonFacet(jsonQueryFacet); + SimpleFacetQuery query = new SimpleFacetQuery((AnyCriteria.any())).setFacetOptions(facetOptions); + + SolrQuery solrQuery = queryParser.constructSolrQuery(query, null); + + assertThat(solrQuery).isNotNull(); + String facetString = solrQuery.get("json.facet"); + assertThat(facetString).isNotNull(); + + ObjectMapper objectMapper = new ObjectMapper(); + MapType mapType = objectMapper.getTypeFactory().constructMapType(HashMap.class, String.class, Object.class); + Map facetMap = objectMapper.readValue(facetString, mapType); + + assertThat(facetMap).hasSize(1).containsKey(jsonQueryFacet.getName()); + Map queryFacetMap = (Map) facetMap.get(jsonQueryFacet.getName()); + assertThat(queryFacetMap) + .containsEntry("query", "cat:foo") + .containsEntry("type", "query") + .containsKey("facet"); + Map nestedFacetsMap = (Map) queryFacetMap.get("facet"); + assertThat(nestedFacetsMap).hasSize(1).containsKey("pop"); + Map popularityFacetMap = (Map) nestedFacetsMap.get("pop"); + assertThat(popularityFacetMap) + .containsEntry("field", "popularity") + .containsEntry("method", "smart") + .containsEntry("type", "terms"); + } + private void assertPivotFactingPresent(SolrQuery solrQuery, String... expected) { assertThat(solrQuery.getParams(FacetParams.FACET_PIVOT)).isEqualTo(expected); } diff --git a/src/test/java/org/springframework/data/solr/core/ITestSolrTemplate.java b/src/test/java/org/springframework/data/solr/core/ITestSolrTemplate.java index 82e49e28e..b50ca603f 100644 --- a/src/test/java/org/springframework/data/solr/core/ITestSolrTemplate.java +++ b/src/test/java/org/springframework/data/solr/core/ITestSolrTemplate.java @@ -20,8 +20,6 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.solr.core.query.Field.*; -import lombok.Data; - import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -33,11 +31,14 @@ import java.util.Map; import java.util.Optional; +import com.google.common.collect.Lists; +import lombok.Data; import org.apache.solr.client.solrj.beans.Field; import org.apache.solr.client.solrj.impl.HttpSolrClient; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.params.FacetParams; import org.apache.solr.common.params.FacetParams.*; +import org.assertj.core.api.Condition; import org.junit.After; import org.junit.Before; import org.junit.Ignore; @@ -61,13 +62,12 @@ import org.springframework.data.solr.core.query.result.*; import org.springframework.data.solr.server.support.HttpSolrClientFactory; -import com.google.common.collect.Lists; - /** * @author Christoph Strobl * @author Andrey Paramonov * @author Francisco Spaeth * @author Radek Mensik + * @author Joe Linn */ public class ITestSolrTemplate extends AbstractITestWithEmbeddedSolrServer { @@ -1336,6 +1336,176 @@ public void deleteShouldUseMappedFieldName() { assertThat(solrTemplate.count(COLLECTION_NAME, new SimpleQuery(AnyCriteria.any()))).isEqualTo(1L); } + @Test // DATASOLR-564 + public void testNestedJsonFacets() { + ExampleSolrBean bean1 = new ExampleSolrBean("id-1", "name1", "foo"); + bean1.setPopularity(4); + ExampleSolrBean bean2 = new ExampleSolrBean("id-2", "name2", "foo"); + bean2.setPopularity(2); + ExampleSolrBean bean3 = new ExampleSolrBean("id-3", "name3", "bar"); + bean3.setPopularity(4); + ExampleSolrBean bean4 = new ExampleSolrBean("id-4", "name4", "baz"); + bean4.setPopularity(2); + ExampleSolrBean bean5 = new ExampleSolrBean("id-5", "name5", "baz"); + bean5.setPopularity(10); + + solrTemplate.saveBeans(COLLECTION_NAME, Arrays.asList(bean1, bean2, bean3, bean4, bean5)); + solrTemplate.commit(COLLECTION_NAME); + + JsonTermsFacet categoriesFacet = new JsonTermsFacet(). setName("categories").setField("cat"); + + JsonTermsFacet popularityTermsFacet = new JsonTermsFacet(); + popularityTermsFacet.setName("popularityValues"); + popularityTermsFacet.setField("popularity"); + popularityTermsFacet.addFacet(categoriesFacet); + + JsonRangeFacet popularityRangeFacet = new JsonRangeFacet(). setName("popularityRanges") + . setField("popularity").addRange(new JsonRangeFacet.Range().setTo(5)) + .addRange(new JsonRangeFacet.Range().setInclusiveFrom(false).setFrom(5).setTo(15)); + categoriesFacet.addFacet(popularityRangeFacet); + + JsonQueryFacet jsonQueryFacet = new JsonQueryFacet("catFoo", Criteria.where("cat").is("foo")); + + FacetOptions facetOptions = new FacetOptions().addJsonFacet(categoriesFacet).addJsonFacet(popularityTermsFacet) + .addJsonFacet(jsonQueryFacet); + SimpleFacetQuery query = new SimpleFacetQuery((AnyCriteria.any())).setFacetOptions(facetOptions); + FacetPage page = solrTemplate.queryForFacetPage(COLLECTION_NAME, query, ExampleSolrBean.class); + + Map jsonFacetResults = page.getJsonFacetResults(); + assertThat(jsonFacetResults).isNotNull().hasSize(facetOptions.getJsonFacets().size()) + .containsKeys(popularityTermsFacet.getName(), jsonQueryFacet.getName(), categoriesFacet.getName()); + + MultiBucketJsonFacetResult popularityTermsResult = (MultiBucketJsonFacetResult) jsonFacetResults + .get(popularityTermsFacet.getName()); + assertThat(popularityTermsResult.getBuckets()).hasSize(3); + for (BucketFacetEntry bucket : popularityTermsResult.getBuckets()) { + assertThat(bucket.getValueAsNumber().intValue()).isGreaterThan(0); + assertThat(bucket.getValueCount()).isGreaterThan(0); + } + + SingleBucketJsonFacetResult queryResult = (SingleBucketJsonFacetResult) jsonFacetResults + .get(jsonQueryFacet.getName()); + assertThat(queryResult.getCount()).isEqualTo(2); + + MultiBucketJsonFacetResult categoriesResult = (MultiBucketJsonFacetResult) jsonFacetResults + .get(categoriesFacet.getName()); + assertThat(categoriesResult.getBuckets()).hasSize(3); + for (BucketFacetEntry bucket : categoriesResult.getBuckets()) { + assertThat(bucket.getFacets()).hasSize(1).containsKey(popularityRangeFacet.getName()); + MultiBucketJsonFacetResult popularityRangeResult = (MultiBucketJsonFacetResult) bucket.getFacets() + .get(popularityRangeFacet.getName()); + assertThat(popularityRangeResult.getBuckets()).hasSize(2); + } + } + + @Test // DATASOLR-564 + public void testJsonTermsFacet() { + ExampleSolrBean bean1 = new ExampleSolrBean("id-1", "name1", "foo"); + bean1.setPopularity(4); + ExampleSolrBean bean2 = new ExampleSolrBean("id-2", "name2", "foo"); + bean2.setPopularity(2); + ExampleSolrBean bean3 = new ExampleSolrBean("id-3", "name3", "bar"); + bean3.setPopularity(4); + ExampleSolrBean bean4 = new ExampleSolrBean("id-4", "name4", "baz"); + bean4.setPopularity(2); + ExampleSolrBean bean5 = new ExampleSolrBean("id-5", "name5", "baz"); + bean5.setPopularity(10); + + solrTemplate.saveBeans(COLLECTION_NAME, Arrays.asList(bean1, bean2, bean3, bean4, bean5)); + solrTemplate.commit(COLLECTION_NAME); + + JsonTermsFacet termsFacet = new JsonTermsFacet(). setName("categories") + . setField("cat").setMethod(JsonTermsFacet.Method.DVHASH); + FacetOptions facetOptions = new FacetOptions().addJsonFacet(termsFacet); + SimpleFacetQuery query = new SimpleFacetQuery((AnyCriteria.any())).setFacetOptions(facetOptions); + FacetPage page = solrTemplate.queryForFacetPage(COLLECTION_NAME, query, ExampleSolrBean.class); + + Map jsonFacetResults = page.getJsonFacetResults(); + assertThat(jsonFacetResults).isNotNull().hasSize(facetOptions.getJsonFacets().size()) + .containsKeys(termsFacet.getName()); + + MultiBucketJsonFacetResult termsResults = (MultiBucketJsonFacetResult) jsonFacetResults.get(termsFacet.getName()); + List buckets = termsResults.getBuckets(); + assertThat(buckets).hasSize(3); + for (BucketFacetEntry bucket : buckets) { + assertThat(bucket.getValueCount()).isGreaterThan(0).isLessThanOrEqualTo(2); + } + } + + @Test // DATASOLR-564 + public void testJsonQueryFacet() { + ExampleSolrBean bean1 = new ExampleSolrBean("id-1", "name1", "foo"); + bean1.setPopularity(4); + ExampleSolrBean bean2 = new ExampleSolrBean("id-2", "name2", "foo"); + bean2.setPopularity(2); + ExampleSolrBean bean3 = new ExampleSolrBean("id-3", "name3", "bar"); + bean3.setPopularity(4); + ExampleSolrBean bean4 = new ExampleSolrBean("id-4", "name4", "baz"); + bean4.setPopularity(2); + ExampleSolrBean bean5 = new ExampleSolrBean("id-5", "name5", "baz"); + bean5.setPopularity(10); + + solrTemplate.saveBeans(COLLECTION_NAME, Arrays.asList(bean1, bean2, bean3, bean4, bean5)); + solrTemplate.commit(COLLECTION_NAME); + + JsonQueryFacet jsonQueryFacet = new JsonQueryFacet("catFoo", Criteria.where("cat").is("foo")) + .addFacet(new JsonTermsFacet("pop", "popularity")); + + FacetOptions facetOptions = new FacetOptions().addJsonFacet(jsonQueryFacet); + SimpleFacetQuery query = new SimpleFacetQuery((AnyCriteria.any())).setFacetOptions(facetOptions); + FacetPage page = solrTemplate.queryForFacetPage(COLLECTION_NAME, query, ExampleSolrBean.class); + + Map facetResults = page.getJsonFacetResults(); + assertThat(facetResults).isNotNull().hasSize(1).containsKey("catFoo"); + SingleBucketJsonFacetResult queryFacetResult = (SingleBucketJsonFacetResult) facetResults.get("catFoo"); + assertThat(queryFacetResult.getCount()).isEqualTo(2); + assertThat(queryFacetResult.getFacets()).isNotNull().containsKey("pop").hasSize(1); + JsonFacetResult popularityFacet = queryFacetResult.getFacets().get("pop"); + assertThat(popularityFacet.getCount()).isEqualTo(2); + } + + @Test // DATASOLR-564 + public void testJsonStatsFacet() { + ExampleSolrBean bean1 = new ExampleSolrBean("id-1", "name1", "foo"); + bean1.setPopularity(4); + ExampleSolrBean bean2 = new ExampleSolrBean("id-2", "name2", "foo"); + bean2.setPopularity(2); + ExampleSolrBean bean3 = new ExampleSolrBean("id-3", "name3", "bar"); + bean3.setPopularity(4); + ExampleSolrBean bean4 = new ExampleSolrBean("id-4", "name4", "baz"); + bean4.setPopularity(2); + ExampleSolrBean bean5 = new ExampleSolrBean("id-5", "name5", "baz"); + bean5.setPopularity(10); + + solrTemplate.saveBeans(COLLECTION_NAME, Arrays.asList(bean1, bean2, bean3, bean4, bean5)); + solrTemplate.commit(COLLECTION_NAME); + + FacetOptions facetOptions = new FacetOptions() + .addJsonFacet(new JsonStatFacet("sum", StatFacetFunction.sum("popularity"))) + .addJsonFacet(new JsonStatFacet("avgSum", StatFacetFunction.avg(StatFacetFunction.sum("popularity")))) + .addJsonFacet(new JsonStatFacet("unique", StatFacetFunction.unique("popularity"))) + .addJsonFacet(new JsonStatFacet("relatedness", StatFacetFunction.relatedness("$fore", "$back")) + .addParam("fore", Criteria.where("popularity").greaterThan(3)) + .addParam("back", Criteria.where("cat").is("foo"))); + + // bug in solrj causes results of percentile ƒacet to not be parsed properly: + // https://issues.apache.org/jira/browse/SOLR-14006 + // facetOptions.addJsonFacet(new JsonStatFacet("percentile", JsonStatFacet.percentile("popularity", 50, 90))); + + SimpleFacetQuery query = new SimpleFacetQuery((AnyCriteria.any())).setFacetOptions(facetOptions); + FacetPage page = solrTemplate.queryForFacetPage(COLLECTION_NAME, query, ExampleSolrBean.class); + Map facetResults = page.getJsonFacetResults(); + assertThat(facetResults).hasSize(facetOptions.getJsonFacets().size()) + .hasKeySatisfying(new Condition<>(s -> s.equals("sum"), null)); + SingleStatJsonFacetResult uniqueResult = (SingleStatJsonFacetResult) facetResults.get("unique"); + assertThat(uniqueResult.getValue()).isEqualTo(3); + + // relatedness results are parsed as child stat facets by solrj + SingleBucketJsonFacetResult relatednessResult = (SingleBucketJsonFacetResult) facetResults.get("relatedness"); + assertThat(relatednessResult).isNotNull(); + assertThat(relatednessResult.getFacets()).hasSize(3); + } + private void executeAndCheckStatsRequest(StatsOptions statsOptions) { ExampleSolrBean bean1 = new ExampleSolrBean("id-1", "one", null); diff --git a/src/test/java/org/springframework/data/solr/core/ResultHelperTests.java b/src/test/java/org/springframework/data/solr/core/ResultHelperTests.java index 3b0dc7289..ff3f4a76b 100644 --- a/src/test/java/org/springframework/data/solr/core/ResultHelperTests.java +++ b/src/test/java/org/springframework/data/solr/core/ResultHelperTests.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.Map.Entry; +import com.google.common.collect.ImmutableList; import org.apache.solr.client.solrj.response.FacetField; import org.apache.solr.client.solrj.response.FieldStatsInfo; import org.apache.solr.client.solrj.response.Group; @@ -35,8 +36,10 @@ import org.apache.solr.client.solrj.response.RangeFacet; import org.apache.solr.client.solrj.response.TermsResponse; import org.apache.solr.client.solrj.response.TermsResponse.Term; +import org.apache.solr.client.solrj.response.json.NestableJsonFacet; import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.util.NamedList; +import org.apache.solr.common.util.SimpleOrderedMap; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -46,22 +49,14 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.solr.core.query.*; -import org.springframework.data.solr.core.query.result.FacetFieldEntry; -import org.springframework.data.solr.core.query.result.FacetPivotFieldEntry; -import org.springframework.data.solr.core.query.result.FacetQueryEntry; -import org.springframework.data.solr.core.query.result.FieldStatsResult; -import org.springframework.data.solr.core.query.result.GroupEntry; -import org.springframework.data.solr.core.query.result.GroupResult; -import org.springframework.data.solr.core.query.result.HighlightEntry; +import org.springframework.data.solr.core.query.result.*; import org.springframework.data.solr.core.query.result.HighlightEntry.Highlight; -import org.springframework.data.solr.core.query.result.SolrResultPage; -import org.springframework.data.solr.core.query.result.StatsResult; -import org.springframework.data.solr.core.query.result.TermsFieldEntry; /** * @author Christoph Strobl * @author Francisco Spaeth * @author Vitezslav Zak + * @author Joe Linn */ @RunWith(MockitoJUnitRunner.Silent.class) public class ResultHelperTests { @@ -773,6 +768,74 @@ public void testconvertFacetQueryResponseToRangeFacetPageMapForNegativeFacetLimi assertThat(result.size()).isEqualTo(1); } + @Test // DATASOLR-564 + public void testConvertJsonTermsFacetResult() { + NamedList terms = new NamedList<>(); + terms.add("buckets", ImmutableList.of(createTermsFacetBucket("foo", 5), createTermsFacetBucket("bar", 20))); + NamedList parent = new NamedList<>(); + parent.add("count", 25); + final String facetName = "termsFacet"; + parent.add(facetName, terms); + NestableJsonFacet facet = new NestableJsonFacet(parent); + + Mockito.when(response.getJsonFacetingResponse()).thenReturn(facet); + + Map result = ResultHelper + .convertJsonFacetQueryResponseToFacetResultMap(createFacetQuery("foo"), response); + assertThat(result).isNotNull().hasSize(1).containsKey(facetName); + MultiBucketJsonFacetResult termsResult = (MultiBucketJsonFacetResult) result.get(facetName); + assertThat(termsResult).isNotNull(); + assertThat(termsResult.getCount()).isEqualTo(25); + List buckets = termsResult.getBuckets(); + assertThat(buckets).hasSize(2); + BucketFacetEntry bucket1 = buckets.get(0); + assertThat(bucket1.getKey()).isEqualTo("foo"); + assertThat(bucket1.getValueCount()).isEqualTo(5); + BucketFacetEntry bucket2 = buckets.get(1); + assertThat(bucket2.getKey()).isEqualTo("bar"); + assertThat(bucket2.getValueCount()).isEqualTo(20); + } + + @Test // DATASOLR-564 + public void testConvertJsonStatsFacetResult() { + NamedList facetsList = new NamedList<>(); + facetsList.add("count", 5); + facetsList.add("sum", 42d); + facetsList.add("unique", 3); + + NamedList relatedness = new SimpleOrderedMap<>(); + relatedness.add("relatedness", 4.2d); + relatedness.add("foreground_popularity", 1.5d); + relatedness.add("background_popularity", 1.1d); + facetsList.add("relatedness", relatedness); + + NestableJsonFacet facet = new NestableJsonFacet(facetsList); + Mockito.when(response.getJsonFacetingResponse()).thenReturn(facet); + + Map result = ResultHelper + .convertJsonFacetQueryResponseToFacetResultMap(createFacetQuery("bar"), response); + assertThat(result).isNotNull().hasSize(3).containsKeys("sum", "unique", "relatedness"); + SingleStatJsonFacetResult sumResult = (SingleStatJsonFacetResult) result.get("sum"); + assertThat(sumResult).isNotNull(); + assertThat(sumResult.getValue().doubleValue()).isEqualTo(42d); + SingleStatJsonFacetResult uniqueResult = (SingleStatJsonFacetResult) result.get("unique"); + assertThat(uniqueResult.getValue().intValue()).isEqualTo(3); + assertThat(uniqueResult.getCount()).isEqualTo(5); + SingleBucketJsonFacetResult relatednessResult = (SingleBucketJsonFacetResult) result.get("relatedness"); + assertThat(relatednessResult.getFacets()).hasSize(3).containsKeys("relatedness", "foreground_popularity", + "background_popularity"); + SingleStatJsonFacetResult relatednessStat = (SingleStatJsonFacetResult) relatednessResult.getFacets() + .get("relatedness"); + assertThat(relatednessStat.getValue().doubleValue()).isEqualTo(4.2d); + } + + private SimpleOrderedMap createTermsFacetBucket(String value, int count) { + SimpleOrderedMap bucket = new SimpleOrderedMap<>(); + bucket.add("val", value); + bucket.add("count", count); + return bucket; + } + private NamedList createFieldStatNameList(Object min, Object max, Double sum, Long count, Long missing, Object mean, Double stddev, Double sumOfSquares) { NamedList nl = new NamedList<>();