Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HSEARCH-5133 Support basic metrics aggregations #4144

Merged
merged 10 commits into from
Sep 5, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.search.backend.elasticsearch.search.aggregation.impl;

import java.util.List;
import java.util.Optional;

import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor;
import org.hibernate.search.backend.elasticsearch.search.common.impl.AbstractElasticsearchCodecAwareSearchQueryElementFactory;
import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexScope;
import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexValueFieldContext;
import org.hibernate.search.backend.elasticsearch.search.predicate.impl.ElasticsearchSearchPredicate;
import org.hibernate.search.backend.elasticsearch.types.codec.impl.ElasticsearchFieldCodec;
import org.hibernate.search.engine.backend.types.converter.runtime.FromDocumentValueConvertContext;
import org.hibernate.search.engine.backend.types.converter.spi.ProjectionConverter;
import org.hibernate.search.engine.search.aggregation.spi.FieldMetricAggregationBuilder;
import org.hibernate.search.engine.search.common.ValueModel;
import org.hibernate.search.util.common.AssertionFailure;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

/**
* @param <F> The type of field values.
* @param <K> The type of returned value. It can be {@code F}, {@link Double}
* or a different type if value converters are used.
*/
public class ElasticsearchMetricFieldAggregation<F, K> extends AbstractElasticsearchNestableAggregation<K> {

private static final JsonAccessor<JsonObject> SUM_PROPERTY_ACCESSOR =
JsonAccessor.root().property( "sum" ).asObject();

private static final JsonAccessor<JsonObject> MIN_PROPERTY_ACCESSOR =
JsonAccessor.root().property( "min" ).asObject();

private static final JsonAccessor<JsonObject> MAX_PROPERTY_ACCESSOR =
JsonAccessor.root().property( "max" ).asObject();

private static final JsonAccessor<JsonObject> AVG_PROPERTY_ACCESSOR =
JsonAccessor.root().property( "avg" ).asObject();

private static final JsonAccessor<String> FIELD_PROPERTY_ACCESSOR =
JsonAccessor.root().property( "field" ).asString();

private static final JsonAccessor<Double> VALUE_ACCESSOR =
JsonAccessor.root().property( "value" ).asDouble();

public static <F> ElasticsearchMetricFieldAggregation.Factory<F> sum(ElasticsearchFieldCodec<F> codec) {
return new ElasticsearchMetricFieldAggregation.Factory<>( codec, SUM_PROPERTY_ACCESSOR );
}

public static <F> ElasticsearchMetricFieldAggregation.Factory<F> min(ElasticsearchFieldCodec<F> codec) {
return new ElasticsearchMetricFieldAggregation.Factory<>( codec, MIN_PROPERTY_ACCESSOR );
}

public static <F> ElasticsearchMetricFieldAggregation.Factory<F> max(ElasticsearchFieldCodec<F> codec) {
return new ElasticsearchMetricFieldAggregation.Factory<>( codec, MAX_PROPERTY_ACCESSOR );
}

public static <F> ElasticsearchMetricFieldAggregation.Factory<F> avg(ElasticsearchFieldCodec<F> codec) {
return new ElasticsearchMetricFieldAggregation.Factory<>( codec, AVG_PROPERTY_ACCESSOR );
}

private final String absoluteFieldPath;
private final ProjectionConverter<F, ? extends K> fromFieldValueConverter;
private final ElasticsearchFieldCodec<F> codec;
private final JsonAccessor<JsonObject> operation;

private ElasticsearchMetricFieldAggregation(Builder<F, K> builder) {
super( builder );
this.absoluteFieldPath = builder.field.absolutePath();
this.fromFieldValueConverter = builder.fromFieldValueConverter;
this.codec = builder.codec;
this.operation = builder.operation;
}

@Override
protected final JsonObject doRequest(AggregationRequestContext context) {
JsonObject outerObject = new JsonObject();
JsonObject innerObject = new JsonObject();

operation.set( outerObject, innerObject );
FIELD_PROPERTY_ACCESSOR.set( innerObject, absoluteFieldPath );
return outerObject;
}

@Override
protected Extractor<K> extractor(AggregationRequestContext context) {
return new MetricFieldExtractor( nestedPathHierarchy, filter );
}

private static class Factory<F>
extends
AbstractElasticsearchCodecAwareSearchQueryElementFactory<FieldMetricAggregationBuilder.TypeSelector, F> {

private final JsonAccessor<JsonObject> operation;

private Factory(ElasticsearchFieldCodec<F> codec, JsonAccessor<JsonObject> operation) {
super( codec );
this.operation = operation;
}

@Override
public FieldMetricAggregationBuilder.TypeSelector create(ElasticsearchSearchIndexScope<?> scope,
ElasticsearchSearchIndexValueFieldContext<F> field) {
return new ElasticsearchMetricFieldAggregation.TypeSelector<>( codec, scope, field, operation );
}
}

private static class TypeSelector<F> implements FieldMetricAggregationBuilder.TypeSelector {
private final ElasticsearchFieldCodec<F> codec;
private final ElasticsearchSearchIndexScope<?> scope;
private final ElasticsearchSearchIndexValueFieldContext<F> field;
private final JsonAccessor<JsonObject> operation;

private TypeSelector(ElasticsearchFieldCodec<F> codec,
ElasticsearchSearchIndexScope<?> scope, ElasticsearchSearchIndexValueFieldContext<F> field,
JsonAccessor<JsonObject> operation) {
this.codec = codec;
this.scope = scope;
this.field = field;
this.operation = operation;
}

@Override
public <T> Builder<F, T> type(Class<T> expectedType, ValueModel valueModel) {
ProjectionConverter<F, ? extends T> projectionConverter = null;
if ( useProjectionConverter( expectedType, valueModel ) ) {
projectionConverter = field.type().projectionConverter( valueModel )
.withConvertedType( expectedType, field );
}
return new Builder<>( codec, scope, field,
projectionConverter,
operation
);
}

private <T> boolean useProjectionConverter(Class<T> expectedType, ValueModel valueModel) {
if ( !Double.class.isAssignableFrom( expectedType ) ) {
if ( ValueModel.RAW.equals( valueModel ) ) {
throw new AssertionFailure(
"Raw projection converter is not supported with metric aggregations at the moment" );
}
return true;
}

// expectedType == Double.class
if ( ValueModel.RAW.equals( valueModel ) ) {
return false;
}
return field.type().projectionConverter( valueModel ).valueType().isAssignableFrom( Double.class );
}
}

private class MetricFieldExtractor extends AbstractExtractor<K> {
protected MetricFieldExtractor(List<String> nestedPathHierarchy, ElasticsearchSearchPredicate filter) {
super( nestedPathHierarchy, filter );
}

@Override
@SuppressWarnings("unchecked")
protected K doExtract(JsonObject aggregationResult, AggregationExtractContext context) {
FromDocumentValueConvertContext convertContext = context.fromDocumentValueConvertContext();
Optional<Double> value = VALUE_ACCESSOR.get( aggregationResult );
JsonElement valueAsString = aggregationResult.get( "value_as_string" );

if ( fromFieldValueConverter == null ) {
Double decode = value.orElse( null );
return (K) decode;
}
return fromFieldValueConverter.fromDocumentValue(
codec.decodeAggregationValue( value, valueAsString ),
convertContext
);
}
}

private static class Builder<F, K> extends AbstractBuilder<K>
implements FieldMetricAggregationBuilder<K> {

private final ElasticsearchFieldCodec<F> codec;
private final ProjectionConverter<F, ? extends K> fromFieldValueConverter;
private final JsonAccessor<JsonObject> operation;

private Builder(ElasticsearchFieldCodec<F> codec, ElasticsearchSearchIndexScope<?> scope,
ElasticsearchSearchIndexValueFieldContext<F> field,
ProjectionConverter<F, ? extends K> fromFieldValueConverter, JsonAccessor<JsonObject> operation) {
super( scope, field );
this.codec = codec;
this.fromFieldValueConverter = fromFieldValueConverter;
this.operation = operation;
}

@Override
public ElasticsearchMetricFieldAggregation<F, K> build() {
return new ElasticsearchMetricFieldAggregation<>( this );
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.search.backend.elasticsearch.search.aggregation.impl;

import java.util.List;

import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor;
import org.hibernate.search.backend.elasticsearch.gson.impl.JsonElementTypes;
import org.hibernate.search.backend.elasticsearch.search.common.impl.AbstractElasticsearchCodecAwareSearchQueryElementFactory;
import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexScope;
import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexValueFieldContext;
import org.hibernate.search.backend.elasticsearch.search.predicate.impl.ElasticsearchSearchPredicate;
import org.hibernate.search.backend.elasticsearch.types.codec.impl.ElasticsearchFieldCodec;
import org.hibernate.search.engine.search.aggregation.spi.SearchFilterableAggregationBuilder;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

public class ElasticsearchMetricLongAggregation extends AbstractElasticsearchNestableAggregation<Long> {

private static final JsonAccessor<JsonObject> COUNT_PROPERTY_ACCESSOR =
JsonAccessor.root().property( "value_count" ).asObject();

private static final JsonAccessor<JsonObject> COUNT_DISTINCT_PROPERTY_ACCESSOR =
JsonAccessor.root().property( "cardinality" ).asObject();

private static final JsonAccessor<String> FIELD_PROPERTY_ACCESSOR =
JsonAccessor.root().property( "field" ).asString();

public static <F> ElasticsearchMetricLongAggregation.Factory<F> count(ElasticsearchFieldCodec<F> codec) {
return new ElasticsearchMetricLongAggregation.Factory<>( codec, COUNT_PROPERTY_ACCESSOR );
}

public static <F> ElasticsearchMetricLongAggregation.Factory<F> countDistinct(ElasticsearchFieldCodec<F> codec) {
return new ElasticsearchMetricLongAggregation.Factory<>( codec, COUNT_DISTINCT_PROPERTY_ACCESSOR );
}

private final String absoluteFieldPath;
private final JsonAccessor<JsonObject> operation;

private ElasticsearchMetricLongAggregation(Builder builder) {
super( builder );
this.absoluteFieldPath = builder.field.absolutePath();
this.operation = builder.operation;
}

@Override
protected final JsonObject doRequest(AggregationRequestContext context) {
JsonObject outerObject = new JsonObject();
JsonObject innerObject = new JsonObject();

operation.set( outerObject, innerObject );
FIELD_PROPERTY_ACCESSOR.set( innerObject, absoluteFieldPath );
return outerObject;
}

@Override
protected Extractor<Long> extractor(AggregationRequestContext context) {
return new MetricLongExtractor( nestedPathHierarchy, filter );
}

private static class Factory<F>
extends
AbstractElasticsearchCodecAwareSearchQueryElementFactory<SearchFilterableAggregationBuilder<Long>, F> {

private final JsonAccessor<JsonObject> operation;

private Factory(ElasticsearchFieldCodec<F> codec, JsonAccessor<JsonObject> operation) {
super( codec );
this.operation = operation;
}

@Override
public SearchFilterableAggregationBuilder<Long> create(ElasticsearchSearchIndexScope<?> scope,
ElasticsearchSearchIndexValueFieldContext<F> field) {
return new Builder( scope, field, operation );
}
}

private static class MetricLongExtractor extends AbstractExtractor<Long> {
protected MetricLongExtractor(List<String> nestedPathHierarchy, ElasticsearchSearchPredicate filter) {
super( nestedPathHierarchy, filter );
}

@Override
protected Long doExtract(JsonObject aggregationResult, AggregationExtractContext context) {
JsonElement value = aggregationResult.get( "value" );
return JsonElementTypes.LONG.fromElement( value );
}
}

private static class Builder extends AbstractBuilder<Long> implements SearchFilterableAggregationBuilder<Long> {
private final JsonAccessor<JsonObject> operation;

private Builder(ElasticsearchSearchIndexScope<?> scope, ElasticsearchSearchIndexValueFieldContext<?> field,
JsonAccessor<JsonObject> operation) {
super( scope, field );
this.operation = operation;
}

@Override
public ElasticsearchMetricLongAggregation build() {
return new ElasticsearchMetricLongAggregation( this );
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.hibernate.search.backend.elasticsearch.gson.impl.JsonElementTypes;
import org.hibernate.search.backend.elasticsearch.logging.impl.Log;
import org.hibernate.search.engine.cfg.spi.NumberScaleConstants;
import org.hibernate.search.engine.cfg.spi.NumberUtils;
import org.hibernate.search.util.common.logging.impl.LoggerFactory;

import com.google.gson.Gson;
Expand Down Expand Up @@ -65,6 +66,11 @@ public BigDecimal decode(JsonElement element) {
return JsonElementTypes.BIG_DECIMAL.fromElement( element );
}

@Override
public BigDecimal decode(Double value) {
return NumberUtils.toBigDecimal( value ).setScale( decimalScale, RoundingMode.FLOOR );
}

@Override
public boolean isCompatibleWith(ElasticsearchFieldCodec<?> obj) {
if ( this == obj ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.hibernate.search.backend.elasticsearch.gson.impl.JsonElementTypes;
import org.hibernate.search.backend.elasticsearch.logging.impl.Log;
import org.hibernate.search.engine.cfg.spi.NumberScaleConstants;
import org.hibernate.search.engine.cfg.spi.NumberUtils;
import org.hibernate.search.util.common.logging.impl.LoggerFactory;

import com.google.gson.Gson;
Expand Down Expand Up @@ -67,6 +68,11 @@ public BigInteger decode(JsonElement element) {
return JsonElementTypes.BIG_INTEGER.fromElement( element );
}

@Override
public BigInteger decode(Double value) {
return NumberUtils.toBigInteger( value );
}

@Override
public BigInteger decodeAggregationKey(JsonElement key, JsonElement keyAsString) {
if ( key == null || key.isJsonNull() ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public Boolean decode(JsonElement element) {
return JsonElementTypes.BOOLEAN.fromElement( element );
}

@Override
public Boolean decode(Double value) {
return value != 0;
}

@Override
public Boolean decodeAggregationKey(JsonElement key, JsonElement keyAsString) {
if ( key == null || key.isJsonNull() ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package org.hibernate.search.backend.elasticsearch.types.codec.impl;

import org.hibernate.search.backend.elasticsearch.gson.impl.JsonElementTypes;
import org.hibernate.search.engine.cfg.spi.NumberUtils;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
Expand Down Expand Up @@ -33,6 +34,11 @@ public Byte decode(JsonElement element) {
return JsonElementTypes.BYTE.fromElement( element );
}

@Override
public Byte decode(Double value) {
return NumberUtils.toByte( value );
}

@Override
public boolean isCompatibleWith(ElasticsearchFieldCodec<?> other) {
return other instanceof ElasticsearchByteFieldCodec;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public Double decode(JsonElement element) {
return JsonElementTypes.DOUBLE.fromElement( element );
}

@Override
public Double decode(Double value) {
return value;
}

@Override
public boolean isCompatibleWith(ElasticsearchFieldCodec<?> other) {
return other instanceof ElasticsearchDoubleFieldCodec;
Expand Down
Loading