Skip to content
This repository has been archived by the owner on Sep 19, 2023. It is now read-only.

DATASOLR-564 - Add support for JSON facets. #116

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -65,11 +72,14 @@
* @author Joachim Uhrlaß
* @author Petar Tahchiev
* @author Juan Manuel de Blas
* @author Joe Linn
*/
public class DefaultQueryParser extends QueryParserBase<SolrDataQuery> {

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}.
Expand All @@ -79,6 +89,11 @@ public class DefaultQueryParser extends QueryParserBase<SolrDataQuery> {
*/
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)));
}

/**
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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) {

Expand Down Expand Up @@ -566,4 +597,47 @@ private List<String> getFilterQueryStrings(List<FilterQuery> filterQueries, @Nul
}
return filterQueryStrings;
}

/**
* Used when serializing JSON facets for transmission to Solr. Lowercases Enum names.
*/
private static class LowerCaseEnumSerializer<T extends Enum> extends StdSerializer<T> {
public LowerCaseEnumSerializer(Class<T> 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<Criteria> {
protected CriteriaSerializer(Class<Criteria> 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<StatFacetFunction> {
protected StatFacetFunctionSerializer(Class<StatFacetFunction> t) {
super(t);
}

@Override
public void serialize(StatFacetFunction value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeString(createFunctionFragment(value, 0, null));
}
}
}
89 changes: 79 additions & 10 deletions src/main/java/org/springframework/data/solr/core/ResultHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,6 +61,7 @@
* @author Francisco Spaeth
* @author Venil Noronha
* @author Vitezslav Zak
* @author Joe Linn
*/
final class ResultHelper {

Expand Down Expand Up @@ -226,6 +222,79 @@ static List<FacetQueryEntry> 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<String, JsonFacetResult> 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<String, JsonFacetResult> convertNestableFacetsToFacetResultMap(NestableJsonFacet facet) {
Map<String, JsonFacetResult> 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<BucketFacetEntry> 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 <T> List<HighlightEntry<T>> convertAndAddHighlightQueryResponseToResultPage(@Nullable QueryResponse response,
@Nullable SolrResultPage<T> page) {
if (response == null || CollectionUtils.isEmpty(response.getHighlighting()) || page == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -461,6 +462,8 @@ private <T> SolrResultPage<T> createSolrResultPage(Query query, Class<T> clazz,
ResultHelper.convertFacetQueryResponseToFacetPivotMap((FacetQuery) query, response));
page.addAllRangeFacetFieldResultPages(
ResultHelper.convertFacetQueryResponseToRangeFacetPageMap((FacetQuery) query, response));
page.addAllJsonFacetResults(
ResultHelper.convertJsonFacetQueryResponseToFacetResultMap((FacetQuery) query, response));
}

if (query.getSpellcheckOptions() != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 extends AbstractJsonFacet> T setName(String name) {
this.name = name;
return (T) this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,6 +37,7 @@
*
* @author Christoph Strobl
* @author Francisco Spaeth
* @author Joe Linn
*/
public class FacetOptions {

Expand All @@ -50,6 +53,7 @@ public enum FacetSort {
private List<PivotField> facetOnPivotFields = new ArrayList<>(0);
private List<FieldWithRangeParameters<?, ?, ?>> facetRangeOnFields = new ArrayList<>(1);
private List<SolrDataQuery> facetQueries = new ArrayList<>(0);
private Map<String, JsonFacet> jsonFacets = new HashMap<>();

private int facetMinCount = DEFAULT_FACET_MIN_COUNT;
private int facetLimit = DEFAULT_FACET_LIMIT;
Expand Down Expand Up @@ -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();
}

/**
Expand All @@ -380,6 +384,31 @@ public Collection<FieldWithFacetParameters> getFieldsWithParameters() {
return result;
}

/**
* @return any configured JSON facets
*/
public Map<String, JsonFacet> 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) {
Expand Down
Loading