From c641f1a670322d45a5554ef0528ffc3a3949bd74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Tepi=C4=87?= Date: Sat, 14 Jun 2025 18:12:03 +0200 Subject: [PATCH 1/8] Support Range aggregation --- .../aggregations/elastic_aggregation_range.md | 36 ++++ .../zio/elasticsearch/HttpExecutorSpec.scala | 47 +++++- .../elasticsearch/ElasticAggregation.scala | 29 ++++ .../aggregation/Aggregations.scala | 58 +++++++ .../response/AggregationResponse.scala | 61 +++++++ .../scala/zio/elasticsearch/package.scala | 3 + .../result/AggregationResult.scala | 24 +++ .../ElasticAggregationSpec.scala | 156 ++++++++++++++++++ website/sidebars.js | 1 + 9 files changed, 413 insertions(+), 2 deletions(-) create mode 100644 docs/overview/aggregations/elastic_aggregation_range.md diff --git a/docs/overview/aggregations/elastic_aggregation_range.md b/docs/overview/aggregations/elastic_aggregation_range.md new file mode 100644 index 000000000..022ec6f15 --- /dev/null +++ b/docs/overview/aggregations/elastic_aggregation_range.md @@ -0,0 +1,36 @@ +--- +id: elastic_aggregation_range +title: "Range Aggregation" +--- + +The `Range` aggregation is a multi-value aggregation enables the user to define a set of ranges. During the aggregation process, the values extraced from each document will be checked against each bucket range. + +In order to use the `Range` aggregation import the following: +```scala +import zio.elasticsearch.aggregation.RangeAggregation +import zio.elasticsearch.ElasticAggregation.RangeAggregation +``` + +You can create a `Range` aggregation using the `rangeAggregation` method this way: +```scala +val aggregation: RangeAggregation = rangeAggregation(name = "rangeAggregation", field = "testField", range = SingleRange.to(23.9)) +``` + +You can create a [type-safe](https://lambdaworks.github.io/zio-elasticsearch/overview/overview_zio_prelude_schema) `Range` aggregation using the `rangeAggregation` method this way: +```scala +// Document.intField must be number value, because of Min aggregation +val aggregation: RangeAggregation = rangeAggregation(name = "rangeAggregation", field = Document.intField, range = SingleRange.to(23.9)) +``` + +// TODO: check this +If you want to change the `missing` parameter, you can use `missing` method: +```scala +val aggregationWithMissing: MinAggregation = minAggregation(name = "minAggregation", field = Document.intField).missing(10.0) +``` +// TODO: check this +If you want to add aggregation (on the same level), you can use `withAgg` method: +```scala +val multipleAggregations: MultipleAggregations = minAggregation(name = "minAggregation1", field = Document.intField).withAgg(minAggregation(name = "minAggregation2", field = Document.doubleField)) +``` + +You can find more information about `Range` aggregation [here](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-aggregations-bucket-range-aggregation.html). diff --git a/modules/integration/src/test/scala/zio/elasticsearch/HttpExecutorSpec.scala b/modules/integration/src/test/scala/zio/elasticsearch/HttpExecutorSpec.scala index 329c93bd4..d20cc833f 100644 --- a/modules/integration/src/test/scala/zio/elasticsearch/HttpExecutorSpec.scala +++ b/modules/integration/src/test/scala/zio/elasticsearch/HttpExecutorSpec.scala @@ -21,7 +21,7 @@ import zio.elasticsearch.ElasticAggregation._ import zio.elasticsearch.ElasticHighlight.highlight import zio.elasticsearch.ElasticQuery.{script => _, _} import zio.elasticsearch.ElasticSort.sortBy -import zio.elasticsearch.aggregation.AggregationOrder +import zio.elasticsearch.aggregation.{AggregationOrder, SingleRange} import zio.elasticsearch.data.GeoPoint import zio.elasticsearch.domain.{PartialTestDocument, TestDocument, TestSubDocument} import zio.elasticsearch.executor.Executor @@ -33,7 +33,7 @@ import zio.elasticsearch.query.sort.SortOrder._ import zio.elasticsearch.query.sort.SourceType.NumberType import zio.elasticsearch.query.{Distance, FunctionScoreBoostMode, FunctionScoreFunction, InnerHits} import zio.elasticsearch.request.{CreationOutcome, DeletionOutcome} -import zio.elasticsearch.result.{FilterAggregationResult, Item, MaxAggregationResult, UpdateByQueryResult} +import zio.elasticsearch.result._ import zio.elasticsearch.script.{Painless, Script} import zio.json.ast.Json.{Arr, Str} import zio.schema.codec.JsonCodec @@ -256,6 +256,49 @@ object HttpExecutorSpec extends IntegrationSpec { Executor.execute(ElasticRequest.createIndex(firstSearchIndex)), Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie ), + test("aggregate using range aggregation") { + checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) { + (firstDocumentId, firstDocument, secondDocumentId, secondDocument) => + for { + _ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll)) + _ <- Executor.execute( + ElasticRequest + .upsert[TestDocument](firstSearchIndex, firstDocumentId, firstDocument.copy(intField = 120)) + ) + _ <- + Executor.execute( + ElasticRequest + .upsert[TestDocument](firstSearchIndex, secondDocumentId, secondDocument.copy(intField = 180)) + .refreshTrue + ) + aggregation = rangeAggregation( + name = "aggregationInt", + field = TestDocument.intField, + range = SingleRange(from = 100.0, to = 200.0) + ) + aggsRes <- + Executor + .execute(ElasticRequest.aggregate(selectors = firstSearchIndex, aggregation = aggregation)) + .asRangeAggregation("aggregationInt") + } yield assert(aggsRes.head)( + equalTo( + RegularRangeAggregationResult( + Chunk( + RegularRangeAggregationBucketResult( + key = "100.0-200.0", + from = Some(100.0), + to = Some(200.0), + docCount = 2 + ) + ) + ) + ) + ) + } + } @@ around( + Executor.execute(ElasticRequest.createIndex(firstSearchIndex)), + Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie + ), test("aggregate using percentile ranks aggregation") { val expectedResult = Map("500.0" -> 55.55555555555555, "600.0" -> 100.0) checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument, genDocumentId, genTestDocument) { diff --git a/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala b/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala index 22c2b08b0..1a0f87608 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala @@ -17,6 +17,7 @@ package zio.elasticsearch import zio.Chunk +//import zio.elasticsearch.aggregation.{Range => IRange, SingleRange => Range, _} import zio.elasticsearch.aggregation._ import zio.elasticsearch.query.ElasticQuery import zio.elasticsearch.script.Script @@ -216,6 +217,34 @@ object ElasticAggregation { final def minAggregation(name: String, field: String): MinAggregation = Min(name = name, field = field, missing = None) + // TODO: Add docs + final def rangeAggregation[A: Numeric]( + name: String, + field: Field[_, A], + range: SingleRange, + ranges: SingleRange* + ): RangeAggregation = + Range( + name = name, + field = field.toString, + ranges = Chunk.fromIterable(ranges.prepended(range)), + keyed = None + ) + + // TODO: Add docs + final def rangeAggregation( + name: String, + field: String, + range: SingleRange, + ranges: SingleRange* + ): RangeAggregation = + Range( + name = name, + field = field, + ranges = Chunk.fromIterable(ranges.prepended(range)), + keyed = None + ) + /** * Constructs a type-safe instance of [[zio.elasticsearch.aggregation.MissingAggregation]] using the specified * parameters. diff --git a/modules/library/src/main/scala/zio/elasticsearch/aggregation/Aggregations.scala b/modules/library/src/main/scala/zio/elasticsearch/aggregation/Aggregations.scala index ec194dc27..ae12ae4f6 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/aggregation/Aggregations.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/aggregation/Aggregations.scala @@ -241,6 +241,64 @@ private[elasticsearch] final case class Min(name: String, field: String, missing } } +private[elasticsearch] final case class SingleRange( + from: Option[Double], + to: Option[Double], + key: Option[String] +) { self => + def from(value: Double): SingleRange = self.copy(from = Some(value)) + def to(value: Double): SingleRange = self.copy(to = Some(value)) +} + +object SingleRange { + + def from(value: Double): SingleRange = + SingleRange(from = Some(value), to = None, key = None) + + def to(value: Double): SingleRange = + SingleRange(from = None, to = Some(value), key = None) + + def apply(from: Double, to: Double): SingleRange = + SingleRange(from = Some(from), to = Some(to), key = None) + +} + +sealed trait RangeAggregation extends SingleElasticAggregation with WithAgg { + + def keyed(value: Boolean): Range +} + +private[elasticsearch] final case class Range( + name: String, + field: String, + ranges: Chunk[SingleRange], + keyed: Option[Boolean] +) extends RangeAggregation { self => + + def keyed(value: Boolean): Range = + self.copy(keyed = Some(value)) + + def withAgg(agg: SingleElasticAggregation): MultipleAggregations = + multipleAggregations.aggregations(self, agg) + + private[elasticsearch] def toJson: Json = { + val keyedJson: Json = keyed.fold(Obj())(m => Obj("keyed" -> m.toJson)) + + Obj( + name -> Obj( + "range" -> (Obj( + "field" -> field.toJson, + "ranges" -> Arr(ranges.map { r => + r.from.fold(Obj())(m => Obj("from" -> m.toJson)) merge + r.to.fold(Obj())(m => Obj("to" -> m.toJson)) merge + r.key.fold(Obj())(m => Obj("key" -> m.toJson)) + }) + ) merge keyedJson) + ) + ) + } +} + sealed trait MissingAggregation extends SingleElasticAggregation with WithAgg private[elasticsearch] final case class Missing(name: String, field: String) extends MissingAggregation { self => diff --git a/modules/library/src/main/scala/zio/elasticsearch/executor/response/AggregationResponse.scala b/modules/library/src/main/scala/zio/elasticsearch/executor/response/AggregationResponse.scala index 12497006e..676d52146 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/executor/response/AggregationResponse.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/executor/response/AggregationResponse.scala @@ -81,6 +81,30 @@ object AggregationResponse { MaxAggregationResult(value) case MinAggregationResponse(value) => MinAggregationResult(value) + case RegularRangeAggregationResponse(buckets) => + RegularRangeAggregationResult( + buckets.map(b => + RegularRangeAggregationBucketResult( + key = b.key, + to = b.to, + from = b.from, + docCount = b.docCount + ) + ) + ) + case KeyedRangeAggregationResponse(buckets) => + KeyedRangeAggregationResult( + buckets.map { case (k, v) => + ( + k, + KeyedRangeAggregationBucketResult( + to = v.to, + from = v.from, + docCount = v.docCount + ) + ) + } + ) case MissingAggregationResponse(value) => MissingAggregationResult(value) case PercentileRanksAggregationResponse(values) => @@ -297,6 +321,43 @@ private[elasticsearch] object MinAggregationResponse { implicit val decoder: JsonDecoder[MinAggregationResponse] = DeriveJsonDecoder.gen[MinAggregationResponse] } +private[elasticsearch] sealed trait RangeAggregationResponse extends AggregationResponse +private[elasticsearch] object RangeAggregationBucketsResponse { + implicit val RangeAggregationResponseDecoder: JsonDecoder[RangeAggregationResponse] = + DeriveJsonDecoder.gen[RangeAggregationResponse] +} + +private[elasticsearch] final case class RegularRangeAggregationBucketResponse( + key: String, + to: Option[Double], + from: Option[Double], + @jsonField("doc_count") + docCount: Int +) +private[elasticsearch] object RegularRangeAggregationBucketResponse { + implicit val RegularRangeAggregationBucketResponseDecoder: JsonDecoder[RegularRangeAggregationBucketResponse] = + DeriveJsonDecoder.gen[RegularRangeAggregationBucketResponse] +} + +private[elasticsearch] final case class KeyedRangeAggregationBucketResponse( + to: Option[Double], + from: Option[Double], + @jsonField("doc_count") + docCount: Int +) +private[elasticsearch] object KeyedRangeAggregationBucketResponse { + implicit val KeyedRangeAggregationBucketResponseDecoder: JsonDecoder[KeyedRangeAggregationBucketResponse] = + DeriveJsonDecoder.gen[KeyedRangeAggregationBucketResponse] +} + +private[elasticsearch] final case class RegularRangeAggregationResponse( + buckets: Chunk[RegularRangeAggregationBucketResponse] +) extends RangeAggregationResponse + +private[elasticsearch] final case class KeyedRangeAggregationResponse( + buckets: Map[String, KeyedRangeAggregationBucketResponse] +) extends RangeAggregationResponse + private[elasticsearch] final case class MissingAggregationResponse(@jsonField("doc_count") docCount: Int) extends AggregationResponse diff --git a/modules/library/src/main/scala/zio/elasticsearch/package.scala b/modules/library/src/main/scala/zio/elasticsearch/package.scala index 8c7d1eefd..ab22368ae 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/package.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/package.scala @@ -120,6 +120,9 @@ package object elasticsearch extends IndexNameNewtype with IndexPatternNewtype w def asMinAggregation(name: String): RIO[R, Option[MinAggregationResult]] = aggregationAs[MinAggregationResult](name) + def asRangeAggregation(name: String): RIO[R, Option[RangeAggregationResult]] = + aggregationAs[RangeAggregationResult](name) + /** * Executes the [[ElasticRequest.SearchRequest]] or the [[ElasticRequest.SearchAndAggregateRequest]]. * diff --git a/modules/library/src/main/scala/zio/elasticsearch/result/AggregationResult.scala b/modules/library/src/main/scala/zio/elasticsearch/result/AggregationResult.scala index 0f6037cf4..00f1f360a 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/result/AggregationResult.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/result/AggregationResult.scala @@ -57,6 +57,30 @@ final case class MaxAggregationResult private[elasticsearch] (value: Double) ext final case class MinAggregationResult private[elasticsearch] (value: Double) extends AggregationResult +private[elasticsearch] sealed trait RangeAggregationResult extends AggregationResult + +private[elasticsearch] final case class RegularRangeAggregationBucketResult( + key: String, + to: Option[Double], + from: Option[Double], + docCount: Int +) + +private[elasticsearch] final case class KeyedRangeAggregationBucketResult( + to: Option[Double], + from: Option[Double], + docCount: Int +) + +private[elasticsearch] final case class RegularRangeAggregationResult( + buckets: Chunk[RegularRangeAggregationBucketResult] +) extends RangeAggregationResult + +private[elasticsearch] final case class KeyedRangeAggregationResult( + buckets: Map[String, KeyedRangeAggregationBucketResult] +) extends RangeAggregationResult + + final case class MissingAggregationResult private[elasticsearch] (docCount: Int) extends AggregationResult final case class PercentileRanksAggregationResult private[elasticsearch] (values: Map[String, Double]) diff --git a/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala b/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala index 202950431..59f618b52 100644 --- a/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala +++ b/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala @@ -216,6 +216,71 @@ object ElasticAggregationSpec extends ZIOSpecDefault { equalTo(Min(name = "aggregation", field = "intField", missing = Some(20.0))) ) }, + test("range") { + val aggregationTo = rangeAggregation("aggregation1", "testField", SingleRange.to(23.9)) + val aggregationFrom = rangeAggregation("aggregation2", "testField", SingleRange.from(2)) + val aggregationFromTo = + rangeAggregation("aggregation3", "testField", SingleRange.from(4).to(344.0)).keyed(false) + val aggregationKeyed = rangeAggregation("aggregation4", "testField", SingleRange.to(139)).keyed(true) + val aggregationMultiple = rangeAggregation( + "aggregation5", + "testField", + SingleRange.to(23.9), + SingleRange.from(3), + SingleRange.to(12).from(0) + ).keyed(true) + + assert(aggregationTo)( + equalTo( + Range( + "aggregation1", + "testField", + Chunk.from(List(SingleRange.to(23.9))), + None + ) + ) + ) && assert(aggregationFrom)( + equalTo( + Range( + "aggregation2", + "testField", + Chunk.from(List(SingleRange.from(2.0))), + None + ) + ) + ) && assert(aggregationFromTo)( + equalTo( + Range( + "aggregation3", + "testField", + Chunk.from(List(SingleRange(from = 4, to = 344.0))), + Some(false) + ) + ) + ) && assert(aggregationKeyed)( + equalTo( + Range( + "aggregation4", + "testField", + Chunk.from(List(SingleRange.to(139))), + Some(true) + ) + ) + ) && assert(aggregationMultiple)( + equalTo( + Range( + "aggregation5", + "testField", + Chunk.from( + List( + SingleRange.to(23.9) + ) + ), + Some(true) + ) + ) + ) + }, test("missing") { val aggregation = missingAggregation("aggregation", "testField") val aggregationTs = missingAggregation("aggregation", TestSubDocument.stringField) @@ -1053,6 +1118,97 @@ object ElasticAggregationSpec extends ZIOSpecDefault { assert(aggregationTs.toJson)(equalTo(expectedTs.toJson)) && assert(aggregationWithMissing.toJson)(equalTo(expectedWithMissing.toJson)) }, + test("range") { + val aggregationTo = rangeAggregation("aggregation1", "testField", SingleRange.to(23.9)) + val aggregationFrom = rangeAggregation("aggregation2", "testField", SingleRange.from(2)) + val aggregationFromTo = + rangeAggregation("aggregation3", "testField", SingleRange.from(4).to(344.0)).keyed(false) + val aggregationKeyed = rangeAggregation("aggregation4", "testField", SingleRange.to(139)).keyed(true) + val aggregationMultiple = rangeAggregation( + "aggregation5", + "testField", + SingleRange.to(23.9), + SingleRange.from(3), + SingleRange.to(12).from(0) + ).keyed(true) + + val expectedTo = + """ + |{ + | "aggregation1": { + | "range": { + | "field": "testField", + | "ranges": [ + | { "to": 23.9 } + | ] + | } + | } + |} + |""".stripMargin + val expectedFrom = + """ + |{ + | "aggregation2": { + | "range": { + | "field": "testField", + | "ranges": [ + | { "from": 2.0 } + | ] + | } + | } + |} + |""".stripMargin + val expectedFromTo = + """ + |{ + | "aggregation3": { + | "range": { + | "field": "testField", + | "keyed": false, + | "ranges": [ + | { "from": 4.0, "to": 344.0 } + | ] + | } + | } + |} + |""".stripMargin + val expectedKeyed = + """ + |{ + | "aggregation4": { + | "range": { + | "field": "testField", + | "keyed": true, + | "ranges": [ + | { "to": 139.0 } + | ] + | } + | } + |} + |""".stripMargin + val expectedMultiple = + """ + |{ + | "aggregation5": { + | "range": { + | "field": "testField", + | "keyed": true, + | "ranges": [ + | { "to": 23.9 }, + | { "key": "secondKey", "from": 3.0 }, + | { "key": "thirdKey", "from": 0.0, "to": 12.0 } + | ] + | } + | } + |} + |""".stripMargin + + assert(aggregationTo.toJson)(equalTo(expectedTo.toJson)) && + assert(aggregationFrom.toJson)(equalTo(expectedFrom.toJson)) && + assert(aggregationFromTo.toJson)(equalTo(expectedFromTo.toJson)) && + assert(aggregationKeyed.toJson)(equalTo(expectedKeyed.toJson)) && + assert(aggregationMultiple.toJson)(equalTo(expectedMultiple.toJson)) + }, test("missing") { val aggregation = missingAggregation("aggregation", "testField") val aggregationTs = missingAggregation("aggregation", TestDocument.stringField) diff --git a/website/sidebars.js b/website/sidebars.js index 8c11fe381..72c4b7721 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -55,6 +55,7 @@ module.exports = { 'overview/aggregations/elastic_aggregation_filter', 'overview/aggregations/elastic_aggregation_max', 'overview/aggregations/elastic_aggregation_min', + 'overview/aggregations/elastic_aggregation_range', 'overview/aggregations/elastic_aggregation_missing', 'overview/aggregations/elastic_aggregation_percentile_ranks', 'overview/aggregations/elastic_aggregation_percentiles', From 40ed18d88327a93824d37dbbb0d5593694352c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Tepi=C4=87?= Date: Sat, 14 Jun 2025 18:48:06 +0200 Subject: [PATCH 2/8] Update ElasticAggregationSpec --- .../aggregations/elastic_aggregation_range.md | 13 +- .../elasticsearch/ElasticAggregation.scala | 1 - .../aggregation/Aggregations.scala | 3 +- .../scala/zio/elasticsearch/package.scala | 9 ++ .../result/AggregationResult.scala | 1 - .../ElasticAggregationSpec.scala | 114 +++++++++++++----- 6 files changed, 99 insertions(+), 42 deletions(-) diff --git a/docs/overview/aggregations/elastic_aggregation_range.md b/docs/overview/aggregations/elastic_aggregation_range.md index 022ec6f15..828f584d9 100644 --- a/docs/overview/aggregations/elastic_aggregation_range.md +++ b/docs/overview/aggregations/elastic_aggregation_range.md @@ -3,7 +3,7 @@ id: elastic_aggregation_range title: "Range Aggregation" --- -The `Range` aggregation is a multi-value aggregation enables the user to define a set of ranges. During the aggregation process, the values extraced from each document will be checked against each bucket range. +The `Range` aggregation is a multi-value aggregation enables the user to define a set of ranges. During the aggregation process, the values extracted from each document will be checked against each bucket range. In order to use the `Range` aggregation import the following: ```scala @@ -22,15 +22,4 @@ You can create a [type-safe](https://lambdaworks.github.io/zio-elasticsearch/ove val aggregation: RangeAggregation = rangeAggregation(name = "rangeAggregation", field = Document.intField, range = SingleRange.to(23.9)) ``` -// TODO: check this -If you want to change the `missing` parameter, you can use `missing` method: -```scala -val aggregationWithMissing: MinAggregation = minAggregation(name = "minAggregation", field = Document.intField).missing(10.0) -``` -// TODO: check this -If you want to add aggregation (on the same level), you can use `withAgg` method: -```scala -val multipleAggregations: MultipleAggregations = minAggregation(name = "minAggregation1", field = Document.intField).withAgg(minAggregation(name = "minAggregation2", field = Document.doubleField)) -``` - You can find more information about `Range` aggregation [here](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-aggregations-bucket-range-aggregation.html). diff --git a/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala b/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala index 1a0f87608..9b2d49410 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala @@ -17,7 +17,6 @@ package zio.elasticsearch import zio.Chunk -//import zio.elasticsearch.aggregation.{Range => IRange, SingleRange => Range, _} import zio.elasticsearch.aggregation._ import zio.elasticsearch.query.ElasticQuery import zio.elasticsearch.script.Script diff --git a/modules/library/src/main/scala/zio/elasticsearch/aggregation/Aggregations.scala b/modules/library/src/main/scala/zio/elasticsearch/aggregation/Aggregations.scala index ae12ae4f6..ab27e2ceb 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/aggregation/Aggregations.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/aggregation/Aggregations.scala @@ -248,6 +248,7 @@ private[elasticsearch] final case class SingleRange( ) { self => def from(value: Double): SingleRange = self.copy(from = Some(value)) def to(value: Double): SingleRange = self.copy(to = Some(value)) + def key(value: String): SingleRange = self.copy(key = Some(value)) } object SingleRange { @@ -287,7 +288,7 @@ private[elasticsearch] final case class Range( Obj( name -> Obj( "range" -> (Obj( - "field" -> field.toJson, + "field" -> field.toJson, "ranges" -> Arr(ranges.map { r => r.from.fold(Obj())(m => Obj("from" -> m.toJson)) merge r.to.fold(Obj())(m => Obj("to" -> m.toJson)) merge diff --git a/modules/library/src/main/scala/zio/elasticsearch/package.scala b/modules/library/src/main/scala/zio/elasticsearch/package.scala index ab22368ae..465d9e4fa 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/package.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/package.scala @@ -120,6 +120,15 @@ package object elasticsearch extends IndexNameNewtype with IndexPatternNewtype w def asMinAggregation(name: String): RIO[R, Option[MinAggregationResult]] = aggregationAs[MinAggregationResult](name) + /** + * Executes the [[ElasticRequest.SearchRequest]] or the [[ElasticRequest.SearchAndAggregateRequest]]. + * + * @param name + * the name of the aggregation to retrieve + * @return + * a [[RIO]] effect that, when executed, will produce the aggregation as instance of + * [[result.RangeAggregationResult]]. + */ def asRangeAggregation(name: String): RIO[R, Option[RangeAggregationResult]] = aggregationAs[RangeAggregationResult](name) diff --git a/modules/library/src/main/scala/zio/elasticsearch/result/AggregationResult.scala b/modules/library/src/main/scala/zio/elasticsearch/result/AggregationResult.scala index 00f1f360a..72f6bdcdb 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/result/AggregationResult.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/result/AggregationResult.scala @@ -80,7 +80,6 @@ private[elasticsearch] final case class KeyedRangeAggregationResult( buckets: Map[String, KeyedRangeAggregationBucketResult] ) extends RangeAggregationResult - final case class MissingAggregationResult private[elasticsearch] (docCount: Int) extends AggregationResult final case class PercentileRanksAggregationResult private[elasticsearch] (values: Map[String, Double]) diff --git a/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala b/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala index 59f618b52..e12ee1e1b 100644 --- a/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala +++ b/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala @@ -217,17 +217,28 @@ object ElasticAggregationSpec extends ZIOSpecDefault { ) }, test("range") { - val aggregationTo = rangeAggregation("aggregation1", "testField", SingleRange.to(23.9)) - val aggregationFrom = rangeAggregation("aggregation2", "testField", SingleRange.from(2)) + val aggregationTo = rangeAggregation("aggregation1", "testField", SingleRange.to(23.9)) + val aggregationFrom = rangeAggregation("aggregation2", "testField", SingleRange.from(2)) val aggregationFromTo = - rangeAggregation("aggregation3", "testField", SingleRange.from(4).to(344.0)).keyed(false) - val aggregationKeyed = rangeAggregation("aggregation4", "testField", SingleRange.to(139)).keyed(true) - val aggregationMultiple = rangeAggregation( - "aggregation5", + rangeAggregation("aggregation3", "testField", SingleRange.from(4).to(344.0)) + val aggregationRegular = rangeAggregation( + "aggregation4", "testField", SingleRange.to(23.9), SingleRange.from(3), SingleRange.to(12).from(0) + ) + val aggregationKeyed = rangeAggregation( + "aggregation5", + "testField", + SingleRange.from(30).to(60), + SingleRange.from(60).to(100) + ).keyed(true) + val aggregationNamedKeyed = rangeAggregation( + "aggregation6", + "testField", + SingleRange.from(30).to(60).key("Low"), + SingleRange.from(60).to(100).key("High") ).keyed(true) assert(aggregationTo)( @@ -254,26 +265,47 @@ object ElasticAggregationSpec extends ZIOSpecDefault { "aggregation3", "testField", Chunk.from(List(SingleRange(from = 4, to = 344.0))), - Some(false) + None ) ) - ) && assert(aggregationKeyed)( + ) && assert(aggregationRegular)( equalTo( Range( "aggregation4", "testField", - Chunk.from(List(SingleRange.to(139))), - Some(true) + Chunk.from( + List( + SingleRange.to(23.9), + SingleRange.from(3), + SingleRange.to(12).from(0) + ) + ), + None ) ) - ) && assert(aggregationMultiple)( + ) && assert(aggregationKeyed)( equalTo( Range( "aggregation5", "testField", Chunk.from( List( - SingleRange.to(23.9) + SingleRange.from(30).to(60), + SingleRange.from(60).to(100) + ) + ), + Some(true) + ) + ) + ) && assert(aggregationNamedKeyed)( + equalTo( + Range( + "aggregation6", + "testField", + Chunk.from( + List( + SingleRange.from(30).to(60).key("Low"), + SingleRange.from(60).to(100).key("High") ) ), Some(true) @@ -1119,17 +1151,28 @@ object ElasticAggregationSpec extends ZIOSpecDefault { assert(aggregationWithMissing.toJson)(equalTo(expectedWithMissing.toJson)) }, test("range") { - val aggregationTo = rangeAggregation("aggregation1", "testField", SingleRange.to(23.9)) - val aggregationFrom = rangeAggregation("aggregation2", "testField", SingleRange.from(2)) + val aggregationTo = rangeAggregation("aggregation1", "testField", SingleRange.to(23.9)) + val aggregationFrom = rangeAggregation("aggregation2", "testField", SingleRange.from(2)) val aggregationFromTo = - rangeAggregation("aggregation3", "testField", SingleRange.from(4).to(344.0)).keyed(false) - val aggregationKeyed = rangeAggregation("aggregation4", "testField", SingleRange.to(139)).keyed(true) - val aggregationMultiple = rangeAggregation( - "aggregation5", + rangeAggregation("aggregation3", "testField", SingleRange.from(4).to(344.0)) + val aggregationRegular = rangeAggregation( + "aggregation4", "testField", SingleRange.to(23.9), SingleRange.from(3), SingleRange.to(12).from(0) + ) + val aggregationKeyed = rangeAggregation( + "aggregation5", + "testField", + SingleRange.from(30).to(60), + SingleRange.from(60).to(100) + ).keyed(true) + val aggregationNamedKeyed = rangeAggregation( + "aggregation6", + "testField", + SingleRange.from(30).to(60).key("Low"), + SingleRange.from(60).to(100).key("High") ).keyed(true) val expectedTo = @@ -1164,7 +1207,6 @@ object ElasticAggregationSpec extends ZIOSpecDefault { | "aggregation3": { | "range": { | "field": "testField", - | "keyed": false, | "ranges": [ | { "from": 4.0, "to": 344.0 } | ] @@ -1172,21 +1214,24 @@ object ElasticAggregationSpec extends ZIOSpecDefault { | } |} |""".stripMargin - val expectedKeyed = + + val expectedRegular = """ |{ | "aggregation4": { | "range": { | "field": "testField", - | "keyed": true, | "ranges": [ - | { "to": 139.0 } + | { "to": 23.9 }, + | { "from": 3.0 }, + | { "from": 0.0, "to": 12.0 } | ] | } | } |} |""".stripMargin - val expectedMultiple = + + val expectedKeyed = """ |{ | "aggregation5": { @@ -1194,9 +1239,23 @@ object ElasticAggregationSpec extends ZIOSpecDefault { | "field": "testField", | "keyed": true, | "ranges": [ - | { "to": 23.9 }, - | { "key": "secondKey", "from": 3.0 }, - | { "key": "thirdKey", "from": 0.0, "to": 12.0 } + | { "from": 30.0, "to": 60.0 }, + | { "from": 60.0, "to": 100.0 } + | ] + | } + | } + |} + |""".stripMargin + val expectedNamedKeyed = + """ + |{ + | "aggregation6": { + | "range": { + | "field": "testField", + | "keyed": true, + | "ranges": [ + | { "from": 30.0, "to": 60.0, "key": "Low" }, + | { "from": 60.0, "to": 100.0, "key": "High" } | ] | } | } @@ -1206,8 +1265,9 @@ object ElasticAggregationSpec extends ZIOSpecDefault { assert(aggregationTo.toJson)(equalTo(expectedTo.toJson)) && assert(aggregationFrom.toJson)(equalTo(expectedFrom.toJson)) && assert(aggregationFromTo.toJson)(equalTo(expectedFromTo.toJson)) && + assert(aggregationRegular.toJson)(equalTo(expectedRegular.toJson)) && assert(aggregationKeyed.toJson)(equalTo(expectedKeyed.toJson)) && - assert(aggregationMultiple.toJson)(equalTo(expectedMultiple.toJson)) + assert(aggregationNamedKeyed.toJson)(equalTo(expectedNamedKeyed.toJson)) }, test("missing") { val aggregation = missingAggregation("aggregation", "testField") From 841697f1a2896db769922c657f06edd6754e5058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Tepi=C4=87?= Date: Sat, 14 Jun 2025 19:02:25 +0200 Subject: [PATCH 3/8] Add docs --- .../elasticsearch/ElasticAggregation.scala | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala b/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala index 9b2d49410..08d3b5986 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala @@ -216,7 +216,24 @@ object ElasticAggregation { final def minAggregation(name: String, field: String): MinAggregation = Min(name = name, field = field, missing = None) - // TODO: Add docs + /** + * Constructs an instance of [[zio.elasticsearch.aggregation.RangeAggregation]] using the specified parameters. + * + * @param name + * aggregation name + * @param field + * the field for which min aggregation will be executed + * @tparam A + * expected number type + * @param range + * the first range to be evaluated and transformed to bucket in [[zio.elasticsearch.aggregation.RangeAggregation]] + * @param ranges + * an array of ranges to be evaluated and transformed to buckets in + * [[zio.elasticsearch.aggregation.RangeAggregation]] + * @return + * an instance of [[zio.elasticsearch.aggregation.RangeAggregation]] that represents min aggregation to be + * performed. + */ final def rangeAggregation[A: Numeric]( name: String, field: Field[_, A], @@ -226,11 +243,26 @@ object ElasticAggregation { Range( name = name, field = field.toString, - ranges = Chunk.fromIterable(ranges.prepended(range)), + ranges = Chunk.single(range).prependedAll(ranges), keyed = None ) - // TODO: Add docs + /** + * Constructs an instance of [[zio.elasticsearch.aggregation.RangeAggregation]] using the specified parameters. + * + * @param name + * aggregation name + * @param field + * the field for which min aggregation will be executed + * @param range + * the first range to be evaluated and transformed to bucket in [[zio.elasticsearch.aggregation.RangeAggregation]] + * @param ranges + * an array of ranges to be evaluated and transformed to buckets in + * [[zio.elasticsearch.aggregation.RangeAggregation]] + * @return + * an instance of [[zio.elasticsearch.aggregation.RangeAggregation]] that represents min aggregation to be + * performed. + */ final def rangeAggregation( name: String, field: String, @@ -240,7 +272,7 @@ object ElasticAggregation { Range( name = name, field = field, - ranges = Chunk.fromIterable(ranges.prepended(range)), + ranges = Chunk.single(range).prependedAll(ranges), keyed = None ) From b5cd54238bd12b3947982d67f74a231d73437e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Tepi=C4=87?= Date: Sat, 14 Jun 2025 22:17:50 +0200 Subject: [PATCH 4/8] Support constructing ranges for Scala 2.12 --- .../src/main/scala/zio/elasticsearch/ElasticAggregation.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala b/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala index 08d3b5986..cbf27fba4 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala @@ -243,7 +243,7 @@ object ElasticAggregation { Range( name = name, field = field.toString, - ranges = Chunk.single(range).prependedAll(ranges), + ranges = Chunk.fromIterable(range +: ranges), keyed = None ) @@ -272,7 +272,7 @@ object ElasticAggregation { Range( name = name, field = field, - ranges = Chunk.single(range).prependedAll(ranges), + ranges = Chunk.fromIterable(range +: ranges), keyed = None ) From bb19bfd09beafe6e02f85ecb991f5f1130c75f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Tepi=C4=87?= Date: Sat, 14 Jun 2025 22:43:43 +0200 Subject: [PATCH 5/8] Update Aggregation tests --- .../zio/elasticsearch/ElasticAggregationSpec.scala | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala b/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala index e12ee1e1b..5cbbd14ec 100644 --- a/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala +++ b/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala @@ -246,7 +246,7 @@ object ElasticAggregationSpec extends ZIOSpecDefault { Range( "aggregation1", "testField", - Chunk.from(List(SingleRange.to(23.9))), + Chunk.fromIterable(List(SingleRange.to(23.9))), None ) ) @@ -255,7 +255,7 @@ object ElasticAggregationSpec extends ZIOSpecDefault { Range( "aggregation2", "testField", - Chunk.from(List(SingleRange.from(2.0))), + Chunk.fromIterable(List(SingleRange.from(2.0))), None ) ) @@ -264,7 +264,7 @@ object ElasticAggregationSpec extends ZIOSpecDefault { Range( "aggregation3", "testField", - Chunk.from(List(SingleRange(from = 4, to = 344.0))), + Chunk.fromIterable(List(SingleRange(from = 4, to = 344.0))), None ) ) @@ -273,7 +273,7 @@ object ElasticAggregationSpec extends ZIOSpecDefault { Range( "aggregation4", "testField", - Chunk.from( + Chunk.fromIterable( List( SingleRange.to(23.9), SingleRange.from(3), @@ -288,7 +288,7 @@ object ElasticAggregationSpec extends ZIOSpecDefault { Range( "aggregation5", "testField", - Chunk.from( + Chunk.fromIterable( List( SingleRange.from(30).to(60), SingleRange.from(60).to(100) @@ -302,7 +302,7 @@ object ElasticAggregationSpec extends ZIOSpecDefault { Range( "aggregation6", "testField", - Chunk.from( + Chunk.fromIterable( List( SingleRange.from(30).to(60).key("Low"), SingleRange.from(60).to(100).key("High") @@ -1214,7 +1214,6 @@ object ElasticAggregationSpec extends ZIOSpecDefault { | } |} |""".stripMargin - val expectedRegular = """ |{ @@ -1230,7 +1229,6 @@ object ElasticAggregationSpec extends ZIOSpecDefault { | } |} |""".stripMargin - val expectedKeyed = """ |{ From c1f9ddc827f17ee59d2d5afc4738372ef971c7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Tepi=C4=87?= Date: Sun, 15 Jun 2025 00:55:33 +0200 Subject: [PATCH 6/8] Fix Json decoders --- .../response/AggregationResponse.scala | 27 +++++++++++++------ .../SearchWithAggregationsResponse.scala | 2 ++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/modules/library/src/main/scala/zio/elasticsearch/executor/response/AggregationResponse.scala b/modules/library/src/main/scala/zio/elasticsearch/executor/response/AggregationResponse.scala index 676d52146..a1af2397a 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/executor/response/AggregationResponse.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/executor/response/AggregationResponse.scala @@ -321,12 +321,6 @@ private[elasticsearch] object MinAggregationResponse { implicit val decoder: JsonDecoder[MinAggregationResponse] = DeriveJsonDecoder.gen[MinAggregationResponse] } -private[elasticsearch] sealed trait RangeAggregationResponse extends AggregationResponse -private[elasticsearch] object RangeAggregationBucketsResponse { - implicit val RangeAggregationResponseDecoder: JsonDecoder[RangeAggregationResponse] = - DeriveJsonDecoder.gen[RangeAggregationResponse] -} - private[elasticsearch] final case class RegularRangeAggregationBucketResponse( key: String, to: Option[Double], @@ -335,7 +329,7 @@ private[elasticsearch] final case class RegularRangeAggregationBucketResponse( docCount: Int ) private[elasticsearch] object RegularRangeAggregationBucketResponse { - implicit val RegularRangeAggregationBucketResponseDecoder: JsonDecoder[RegularRangeAggregationBucketResponse] = + implicit val decoder: JsonDecoder[RegularRangeAggregationBucketResponse] = DeriveJsonDecoder.gen[RegularRangeAggregationBucketResponse] } @@ -346,17 +340,34 @@ private[elasticsearch] final case class KeyedRangeAggregationBucketResponse( docCount: Int ) private[elasticsearch] object KeyedRangeAggregationBucketResponse { - implicit val KeyedRangeAggregationBucketResponseDecoder: JsonDecoder[KeyedRangeAggregationBucketResponse] = + implicit val decoder: JsonDecoder[KeyedRangeAggregationBucketResponse] = DeriveJsonDecoder.gen[KeyedRangeAggregationBucketResponse] } +private[elasticsearch] sealed trait RangeAggregationResponse extends AggregationResponse + private[elasticsearch] final case class RegularRangeAggregationResponse( buckets: Chunk[RegularRangeAggregationBucketResponse] ) extends RangeAggregationResponse +private[elasticsearch] object RegularRangeAggregationResponse { + implicit val decoder: JsonDecoder[RegularRangeAggregationResponse] = + DeriveJsonDecoder.gen[RegularRangeAggregationResponse] +} private[elasticsearch] final case class KeyedRangeAggregationResponse( buckets: Map[String, KeyedRangeAggregationBucketResponse] ) extends RangeAggregationResponse +private[elasticsearch] object KeyedRangeAggregationResponse { + implicit val decoder: JsonDecoder[KeyedRangeAggregationResponse] = + DeriveJsonDecoder.gen[KeyedRangeAggregationResponse] +} + +private[elasticsearch] object RangeAggregationResponse { + implicit val decoder: JsonDecoder[RangeAggregationResponse] = + RegularRangeAggregationResponse.decoder + .widen[RangeAggregationResponse] + .orElse(KeyedRangeAggregationResponse.decoder.widen[RangeAggregationResponse]) +} private[elasticsearch] final case class MissingAggregationResponse(@jsonField("doc_count") docCount: Int) extends AggregationResponse diff --git a/modules/library/src/main/scala/zio/elasticsearch/executor/response/SearchWithAggregationsResponse.scala b/modules/library/src/main/scala/zio/elasticsearch/executor/response/SearchWithAggregationsResponse.scala index f02886732..eca9c5791 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/executor/response/SearchWithAggregationsResponse.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/executor/response/SearchWithAggregationsResponse.scala @@ -86,6 +86,8 @@ private[elasticsearch] final case class SearchWithAggregationsResponse( MaxAggregationResponse.decoder.decodeJson(data.toString).map(field.split("#")(1) -> _) case str if str.contains("min#") => MinAggregationResponse.decoder.decodeJson(data.toString).map(field.split("#")(1) -> _) + case str if str.contains("range#") => + RangeAggregationResponse.decoder.decodeJson(data.toString).map(field.split("#")(1) -> _) case str if str.contains("missing#") => MissingAggregationResponse.decoder.decodeJson(data.toString).map(field.split("#")(1) -> _) case str if str.contains("percentile_ranks#") => From ddce4fb3918fef7f78c541c8c0f4fb8de2660f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Tepi=C4=87?= Date: Sun, 15 Jun 2025 09:48:16 +0200 Subject: [PATCH 7/8] Update docs --- .../main/scala/zio/elasticsearch/ElasticAggregation.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala b/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala index cbf27fba4..e8cf5aae7 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala @@ -222,7 +222,7 @@ object ElasticAggregation { * @param name * aggregation name * @param field - * the field for which min aggregation will be executed + * the field for which range aggregation will be executed * @tparam A * expected number type * @param range @@ -231,7 +231,7 @@ object ElasticAggregation { * an array of ranges to be evaluated and transformed to buckets in * [[zio.elasticsearch.aggregation.RangeAggregation]] * @return - * an instance of [[zio.elasticsearch.aggregation.RangeAggregation]] that represents min aggregation to be + * an instance of [[zio.elasticsearch.aggregation.RangeAggregation]] that represents range aggregation to be * performed. */ final def rangeAggregation[A: Numeric]( @@ -253,14 +253,14 @@ object ElasticAggregation { * @param name * aggregation name * @param field - * the field for which min aggregation will be executed + * the field for which range aggregation will be executed * @param range * the first range to be evaluated and transformed to bucket in [[zio.elasticsearch.aggregation.RangeAggregation]] * @param ranges * an array of ranges to be evaluated and transformed to buckets in * [[zio.elasticsearch.aggregation.RangeAggregation]] * @return - * an instance of [[zio.elasticsearch.aggregation.RangeAggregation]] that represents min aggregation to be + * an instance of [[zio.elasticsearch.aggregation.RangeAggregation]] that represents range aggregation to be * performed. */ final def rangeAggregation( From 228ef56f14f8a18be8781f0b1259b69e1fec102e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Tepi=C4=87?= Date: Mon, 16 Jun 2025 23:33:03 +0200 Subject: [PATCH 8/8] Address code remarks --- .../aggregation/Aggregations.scala | 116 +++-- .../SearchWithAggregationsResponse.scala | 4 +- .../scala/zio/elasticsearch/package.scala | 24 +- .../result/AggregationResult.scala | 16 +- .../ElasticAggregationSpec.scala | 428 +++++++++--------- website/sidebars.js | 2 +- 6 files changed, 294 insertions(+), 296 deletions(-) diff --git a/modules/library/src/main/scala/zio/elasticsearch/aggregation/Aggregations.scala b/modules/library/src/main/scala/zio/elasticsearch/aggregation/Aggregations.scala index ab27e2ceb..d969a4d1e 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/aggregation/Aggregations.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/aggregation/Aggregations.scala @@ -241,65 +241,6 @@ private[elasticsearch] final case class Min(name: String, field: String, missing } } -private[elasticsearch] final case class SingleRange( - from: Option[Double], - to: Option[Double], - key: Option[String] -) { self => - def from(value: Double): SingleRange = self.copy(from = Some(value)) - def to(value: Double): SingleRange = self.copy(to = Some(value)) - def key(value: String): SingleRange = self.copy(key = Some(value)) -} - -object SingleRange { - - def from(value: Double): SingleRange = - SingleRange(from = Some(value), to = None, key = None) - - def to(value: Double): SingleRange = - SingleRange(from = None, to = Some(value), key = None) - - def apply(from: Double, to: Double): SingleRange = - SingleRange(from = Some(from), to = Some(to), key = None) - -} - -sealed trait RangeAggregation extends SingleElasticAggregation with WithAgg { - - def keyed(value: Boolean): Range -} - -private[elasticsearch] final case class Range( - name: String, - field: String, - ranges: Chunk[SingleRange], - keyed: Option[Boolean] -) extends RangeAggregation { self => - - def keyed(value: Boolean): Range = - self.copy(keyed = Some(value)) - - def withAgg(agg: SingleElasticAggregation): MultipleAggregations = - multipleAggregations.aggregations(self, agg) - - private[elasticsearch] def toJson: Json = { - val keyedJson: Json = keyed.fold(Obj())(m => Obj("keyed" -> m.toJson)) - - Obj( - name -> Obj( - "range" -> (Obj( - "field" -> field.toJson, - "ranges" -> Arr(ranges.map { r => - r.from.fold(Obj())(m => Obj("from" -> m.toJson)) merge - r.to.fold(Obj())(m => Obj("to" -> m.toJson)) merge - r.key.fold(Obj())(m => Obj("key" -> m.toJson)) - }) - ) merge keyedJson) - ) - ) - } -} - sealed trait MissingAggregation extends SingleElasticAggregation with WithAgg private[elasticsearch] final case class Missing(name: String, field: String) extends MissingAggregation { self => @@ -410,6 +351,63 @@ private[elasticsearch] final case class Percentiles( } } +private[elasticsearch] final case class SingleRange( + from: Option[Double], + to: Option[Double], + key: Option[String] +) { self => + def from(value: Double): SingleRange = self.copy(from = Some(value)) + def to(value: Double): SingleRange = self.copy(to = Some(value)) + def key(value: String): SingleRange = self.copy(key = Some(value)) +} + +object SingleRange { + + def from(value: Double): SingleRange = + SingleRange(from = Some(value), to = None, key = None) + + def to(value: Double): SingleRange = + SingleRange(from = None, to = Some(value), key = None) + + def apply(from: Double, to: Double): SingleRange = + SingleRange(from = Some(from), to = Some(to), key = None) + +} + +sealed trait RangeAggregation extends SingleElasticAggregation with WithAgg { + def asKeyed: Range +} + +private[elasticsearch] final case class Range( + name: String, + field: String, + ranges: Chunk[SingleRange], + keyed: Option[Boolean] +) extends RangeAggregation { self => + + def asKeyed: Range = self.copy(keyed = Some(true)) + + def withAgg(agg: SingleElasticAggregation): MultipleAggregations = + multipleAggregations.aggregations(self, agg) + + private[elasticsearch] def toJson: Json = { + val keyedJson: Json = keyed.fold(Obj())(m => Obj("keyed" -> m.toJson)) + + Obj( + name -> Obj( + "range" -> (Obj( + "field" -> field.toJson, + "ranges" -> Arr(ranges.map { r => + r.from.fold(Obj())(m => Obj("from" -> m.toJson)) merge + r.to.fold(Obj())(m => Obj("to" -> m.toJson)) merge + r.key.fold(Obj())(m => Obj("key" -> m.toJson)) + }) + ) merge keyedJson) + ) + ) + } +} + sealed trait StatsAggregation extends SingleElasticAggregation with HasMissing[StatsAggregation] with WithAgg private[elasticsearch] final case class Stats(name: String, field: String, missing: Option[Double]) diff --git a/modules/library/src/main/scala/zio/elasticsearch/executor/response/SearchWithAggregationsResponse.scala b/modules/library/src/main/scala/zio/elasticsearch/executor/response/SearchWithAggregationsResponse.scala index eca9c5791..528066c09 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/executor/response/SearchWithAggregationsResponse.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/executor/response/SearchWithAggregationsResponse.scala @@ -86,14 +86,14 @@ private[elasticsearch] final case class SearchWithAggregationsResponse( MaxAggregationResponse.decoder.decodeJson(data.toString).map(field.split("#")(1) -> _) case str if str.contains("min#") => MinAggregationResponse.decoder.decodeJson(data.toString).map(field.split("#")(1) -> _) - case str if str.contains("range#") => - RangeAggregationResponse.decoder.decodeJson(data.toString).map(field.split("#")(1) -> _) case str if str.contains("missing#") => MissingAggregationResponse.decoder.decodeJson(data.toString).map(field.split("#")(1) -> _) case str if str.contains("percentile_ranks#") => PercentileRanksAggregationResponse.decoder.decodeJson(data.toString).map(field.split("#")(1) -> _) case str if str.contains("percentiles#") => PercentilesAggregationResponse.decoder.decodeJson(data.toString).map(field.split("#")(1) -> _) + case str if str.contains("range#") => + RangeAggregationResponse.decoder.decodeJson(data.toString).map(field.split("#")(1) -> _) case str if str.contains("stats#") => StatsAggregationResponse.decoder.decodeJson(data.toString).map(field.split("#")(1) -> _) case str if str.contains("sum#") => diff --git a/modules/library/src/main/scala/zio/elasticsearch/package.scala b/modules/library/src/main/scala/zio/elasticsearch/package.scala index 465d9e4fa..567849912 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/package.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/package.scala @@ -120,18 +120,6 @@ package object elasticsearch extends IndexNameNewtype with IndexPatternNewtype w def asMinAggregation(name: String): RIO[R, Option[MinAggregationResult]] = aggregationAs[MinAggregationResult](name) - /** - * Executes the [[ElasticRequest.SearchRequest]] or the [[ElasticRequest.SearchAndAggregateRequest]]. - * - * @param name - * the name of the aggregation to retrieve - * @return - * a [[RIO]] effect that, when executed, will produce the aggregation as instance of - * [[result.RangeAggregationResult]]. - */ - def asRangeAggregation(name: String): RIO[R, Option[RangeAggregationResult]] = - aggregationAs[RangeAggregationResult](name) - /** * Executes the [[ElasticRequest.SearchRequest]] or the [[ElasticRequest.SearchAndAggregateRequest]]. * @@ -168,6 +156,18 @@ package object elasticsearch extends IndexNameNewtype with IndexPatternNewtype w def asPercentilesAggregation(name: String): RIO[R, Option[PercentilesAggregationResult]] = aggregationAs[PercentilesAggregationResult](name) + /** + * Executes the [[ElasticRequest.SearchRequest]] or the [[ElasticRequest.SearchAndAggregateRequest]]. + * + * @param name + * the name of the aggregation to retrieve + * @return + * a [[RIO]] effect that, when executed, will produce the aggregation as instance of + * [[result.RangeAggregationResult]]. + */ + def asRangeAggregation(name: String): RIO[R, Option[RangeAggregationResult]] = + aggregationAs[RangeAggregationResult](name) + /** * Executes the [[ElasticRequest.SearchRequest]] or the [[ElasticRequest.SearchAndAggregateRequest]]. * diff --git a/modules/library/src/main/scala/zio/elasticsearch/result/AggregationResult.scala b/modules/library/src/main/scala/zio/elasticsearch/result/AggregationResult.scala index 72f6bdcdb..eb33651d8 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/result/AggregationResult.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/result/AggregationResult.scala @@ -57,6 +57,14 @@ final case class MaxAggregationResult private[elasticsearch] (value: Double) ext final case class MinAggregationResult private[elasticsearch] (value: Double) extends AggregationResult +final case class MissingAggregationResult private[elasticsearch] (docCount: Int) extends AggregationResult + +final case class PercentileRanksAggregationResult private[elasticsearch] (values: Map[String, Double]) + extends AggregationResult + +final case class PercentilesAggregationResult private[elasticsearch] (values: Map[String, Double]) + extends AggregationResult + private[elasticsearch] sealed trait RangeAggregationResult extends AggregationResult private[elasticsearch] final case class RegularRangeAggregationBucketResult( @@ -80,14 +88,6 @@ private[elasticsearch] final case class KeyedRangeAggregationResult( buckets: Map[String, KeyedRangeAggregationBucketResult] ) extends RangeAggregationResult -final case class MissingAggregationResult private[elasticsearch] (docCount: Int) extends AggregationResult - -final case class PercentileRanksAggregationResult private[elasticsearch] (values: Map[String, Double]) - extends AggregationResult - -final case class PercentilesAggregationResult private[elasticsearch] (values: Map[String, Double]) - extends AggregationResult - final case class StatsAggregationResult private[elasticsearch] ( count: Int, min: Double, diff --git a/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala b/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala index 5cbbd14ec..cd31b3938 100644 --- a/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala +++ b/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala @@ -216,103 +216,6 @@ object ElasticAggregationSpec extends ZIOSpecDefault { equalTo(Min(name = "aggregation", field = "intField", missing = Some(20.0))) ) }, - test("range") { - val aggregationTo = rangeAggregation("aggregation1", "testField", SingleRange.to(23.9)) - val aggregationFrom = rangeAggregation("aggregation2", "testField", SingleRange.from(2)) - val aggregationFromTo = - rangeAggregation("aggregation3", "testField", SingleRange.from(4).to(344.0)) - val aggregationRegular = rangeAggregation( - "aggregation4", - "testField", - SingleRange.to(23.9), - SingleRange.from(3), - SingleRange.to(12).from(0) - ) - val aggregationKeyed = rangeAggregation( - "aggregation5", - "testField", - SingleRange.from(30).to(60), - SingleRange.from(60).to(100) - ).keyed(true) - val aggregationNamedKeyed = rangeAggregation( - "aggregation6", - "testField", - SingleRange.from(30).to(60).key("Low"), - SingleRange.from(60).to(100).key("High") - ).keyed(true) - - assert(aggregationTo)( - equalTo( - Range( - "aggregation1", - "testField", - Chunk.fromIterable(List(SingleRange.to(23.9))), - None - ) - ) - ) && assert(aggregationFrom)( - equalTo( - Range( - "aggregation2", - "testField", - Chunk.fromIterable(List(SingleRange.from(2.0))), - None - ) - ) - ) && assert(aggregationFromTo)( - equalTo( - Range( - "aggregation3", - "testField", - Chunk.fromIterable(List(SingleRange(from = 4, to = 344.0))), - None - ) - ) - ) && assert(aggregationRegular)( - equalTo( - Range( - "aggregation4", - "testField", - Chunk.fromIterable( - List( - SingleRange.to(23.9), - SingleRange.from(3), - SingleRange.to(12).from(0) - ) - ), - None - ) - ) - ) && assert(aggregationKeyed)( - equalTo( - Range( - "aggregation5", - "testField", - Chunk.fromIterable( - List( - SingleRange.from(30).to(60), - SingleRange.from(60).to(100) - ) - ), - Some(true) - ) - ) - ) && assert(aggregationNamedKeyed)( - equalTo( - Range( - "aggregation6", - "testField", - Chunk.fromIterable( - List( - SingleRange.from(30).to(60).key("Low"), - SingleRange.from(60).to(100).key("High") - ) - ), - Some(true) - ) - ) - ) - }, test("missing") { val aggregation = missingAggregation("aggregation", "testField") val aggregationTs = missingAggregation("aggregation", TestSubDocument.stringField) @@ -437,6 +340,103 @@ object ElasticAggregationSpec extends ZIOSpecDefault { ) ) }, + test("range") { + val aggregationTo = rangeAggregation("aggregation1", TestDocument.intField, SingleRange.to(23.9)) + val aggregationFrom = rangeAggregation("aggregation2", TestDocument.intField, SingleRange.from(2)) + val aggregationFromTo = + rangeAggregation("aggregation3", "testField", SingleRange.from(4).to(344.0)) + val aggregationRegular = rangeAggregation( + "aggregation4", + "testField", + SingleRange.to(23.9), + SingleRange.from(3), + SingleRange.to(12).from(0) + ) + val aggregationKeyed = rangeAggregation( + "aggregation5", + TestDocument.intField, + SingleRange.from(30).to(60), + SingleRange.from(60).to(100) + ).asKeyed + val aggregationNamedKeyed = rangeAggregation( + "aggregation6", + "testField", + SingleRange.from(30).to(60).key("Low"), + SingleRange.from(60).to(100).key("High") + ).asKeyed + + assert(aggregationTo)( + equalTo( + Range( + "aggregation1", + "testField", + Chunk.fromIterable(List(SingleRange.to(23.9))), + None + ) + ) + ) && assert(aggregationFrom)( + equalTo( + Range( + "aggregation2", + "testField", + Chunk.fromIterable(List(SingleRange.from(2.0))), + None + ) + ) + ) && assert(aggregationFromTo)( + equalTo( + Range( + "aggregation3", + "testField", + Chunk.fromIterable(List(SingleRange(from = 4, to = 344.0))), + None + ) + ) + ) && assert(aggregationRegular)( + equalTo( + Range( + "aggregation4", + "testField", + Chunk.fromIterable( + List( + SingleRange.to(23.9), + SingleRange.from(3), + SingleRange.to(12).from(0) + ) + ), + None + ) + ) + ) && assert(aggregationKeyed)( + equalTo( + Range( + "aggregation5", + "testField", + Chunk.fromIterable( + List( + SingleRange.from(30).to(60), + SingleRange.from(60).to(100) + ) + ), + Some(true) + ) + ) + ) && assert(aggregationNamedKeyed)( + equalTo( + Range( + "aggregation6", + "testField", + Chunk.fromIterable( + List( + SingleRange.from(30).to(60).key("Low"), + SingleRange.from(60).to(100).key("High") + ) + ), + Some(true) + ) + ) + ) + }, test("stats") { val aggregation = statsAggregation("aggregation", "testField") val aggregationTs = statsAggregation("aggregation", TestDocument.intField) @@ -1150,123 +1150,6 @@ object ElasticAggregationSpec extends ZIOSpecDefault { assert(aggregationTs.toJson)(equalTo(expectedTs.toJson)) && assert(aggregationWithMissing.toJson)(equalTo(expectedWithMissing.toJson)) }, - test("range") { - val aggregationTo = rangeAggregation("aggregation1", "testField", SingleRange.to(23.9)) - val aggregationFrom = rangeAggregation("aggregation2", "testField", SingleRange.from(2)) - val aggregationFromTo = - rangeAggregation("aggregation3", "testField", SingleRange.from(4).to(344.0)) - val aggregationRegular = rangeAggregation( - "aggregation4", - "testField", - SingleRange.to(23.9), - SingleRange.from(3), - SingleRange.to(12).from(0) - ) - val aggregationKeyed = rangeAggregation( - "aggregation5", - "testField", - SingleRange.from(30).to(60), - SingleRange.from(60).to(100) - ).keyed(true) - val aggregationNamedKeyed = rangeAggregation( - "aggregation6", - "testField", - SingleRange.from(30).to(60).key("Low"), - SingleRange.from(60).to(100).key("High") - ).keyed(true) - - val expectedTo = - """ - |{ - | "aggregation1": { - | "range": { - | "field": "testField", - | "ranges": [ - | { "to": 23.9 } - | ] - | } - | } - |} - |""".stripMargin - val expectedFrom = - """ - |{ - | "aggregation2": { - | "range": { - | "field": "testField", - | "ranges": [ - | { "from": 2.0 } - | ] - | } - | } - |} - |""".stripMargin - val expectedFromTo = - """ - |{ - | "aggregation3": { - | "range": { - | "field": "testField", - | "ranges": [ - | { "from": 4.0, "to": 344.0 } - | ] - | } - | } - |} - |""".stripMargin - val expectedRegular = - """ - |{ - | "aggregation4": { - | "range": { - | "field": "testField", - | "ranges": [ - | { "to": 23.9 }, - | { "from": 3.0 }, - | { "from": 0.0, "to": 12.0 } - | ] - | } - | } - |} - |""".stripMargin - val expectedKeyed = - """ - |{ - | "aggregation5": { - | "range": { - | "field": "testField", - | "keyed": true, - | "ranges": [ - | { "from": 30.0, "to": 60.0 }, - | { "from": 60.0, "to": 100.0 } - | ] - | } - | } - |} - |""".stripMargin - val expectedNamedKeyed = - """ - |{ - | "aggregation6": { - | "range": { - | "field": "testField", - | "keyed": true, - | "ranges": [ - | { "from": 30.0, "to": 60.0, "key": "Low" }, - | { "from": 60.0, "to": 100.0, "key": "High" } - | ] - | } - | } - |} - |""".stripMargin - - assert(aggregationTo.toJson)(equalTo(expectedTo.toJson)) && - assert(aggregationFrom.toJson)(equalTo(expectedFrom.toJson)) && - assert(aggregationFromTo.toJson)(equalTo(expectedFromTo.toJson)) && - assert(aggregationRegular.toJson)(equalTo(expectedRegular.toJson)) && - assert(aggregationKeyed.toJson)(equalTo(expectedKeyed.toJson)) && - assert(aggregationNamedKeyed.toJson)(equalTo(expectedNamedKeyed.toJson)) - }, test("missing") { val aggregation = missingAggregation("aggregation", "testField") val aggregationTs = missingAggregation("aggregation", TestDocument.stringField) @@ -1480,6 +1363,123 @@ object ElasticAggregationSpec extends ZIOSpecDefault { assert(aggregationWithMissing.toJson)(equalTo(expectedWithMissing.toJson)) && assert(aggregationWithAllParams.toJson)(equalTo(expectedWithAllParams.toJson)) }, + test("range") { + val aggregationTo = rangeAggregation("aggregation1", "testField", SingleRange.to(23.9)) + val aggregationFrom = rangeAggregation("aggregation2", "testField", SingleRange.from(2)) + val aggregationFromTo = + rangeAggregation("aggregation3", "testField", SingleRange.from(4).to(344.0)) + val aggregationRegular: RangeAggregation = rangeAggregation( + "aggregation4", + TestDocument.intField, + SingleRange.to(23.9), + SingleRange.from(3), + SingleRange.to(12).from(0) + ) + val aggregationKeyed = rangeAggregation( + "aggregation5", + TestDocument.intField, + SingleRange.from(30).to(60), + SingleRange.from(60).to(100) + ).asKeyed + val aggregationNamedKeyed = rangeAggregation( + "aggregation6", + TestDocument.intField, + SingleRange.from(30).to(60).key("Low"), + SingleRange.from(60).to(100).key("High") + ).asKeyed + + val expectedTo = + """ + |{ + | "aggregation1": { + | "range": { + | "field": "testField", + | "ranges": [ + | { "to": 23.9 } + | ] + | } + | } + |} + |""".stripMargin + val expectedFrom = + """ + |{ + | "aggregation2": { + | "range": { + | "field": "testField", + | "ranges": [ + | { "from": 2.0 } + | ] + | } + | } + |} + |""".stripMargin + val expectedFromTo = + """ + |{ + | "aggregation3": { + | "range": { + | "field": "testField", + | "ranges": [ + | { "from": 4.0, "to": 344.0 } + | ] + | } + | } + |} + |""".stripMargin + val expectedRegular = + """ + |{ + | "aggregation4": { + | "range": { + | "field": "testField", + | "ranges": [ + | { "to": 23.9 }, + | { "from": 3.0 }, + | { "from": 0.0, "to": 12.0 } + | ] + | } + | } + |} + |""".stripMargin + val expectedKeyed = + """ + |{ + | "aggregation5": { + | "range": { + | "field": "testField", + | "keyed": true, + | "ranges": [ + | { "from": 30.0, "to": 60.0 }, + | { "from": 60.0, "to": 100.0 } + | ] + | } + | } + |} + |""".stripMargin + val expectedNamedKeyed = + """ + |{ + | "aggregation6": { + | "range": { + | "field": "testField", + | "keyed": true, + | "ranges": [ + | { "from": 30.0, "to": 60.0, "key": "Low" }, + | { "from": 60.0, "to": 100.0, "key": "High" } + | ] + | } + | } + |} + |""".stripMargin + + assert(aggregationTo.toJson)(equalTo(expectedTo.toJson)) && + assert(aggregationFrom.toJson)(equalTo(expectedFrom.toJson)) && + assert(aggregationFromTo.toJson)(equalTo(expectedFromTo.toJson)) && + assert(aggregationRegular.toJson)(equalTo(expectedRegular.toJson)) && + assert(aggregationKeyed.toJson)(equalTo(expectedKeyed.toJson)) && + assert(aggregationNamedKeyed.toJson)(equalTo(expectedNamedKeyed.toJson)) + }, test("stats") { val aggregation = statsAggregation("aggregation", "testField") val aggregationTs = statsAggregation("aggregation", TestDocument.intField) diff --git a/website/sidebars.js b/website/sidebars.js index 72c4b7721..df607d201 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -55,10 +55,10 @@ module.exports = { 'overview/aggregations/elastic_aggregation_filter', 'overview/aggregations/elastic_aggregation_max', 'overview/aggregations/elastic_aggregation_min', - 'overview/aggregations/elastic_aggregation_range', 'overview/aggregations/elastic_aggregation_missing', 'overview/aggregations/elastic_aggregation_percentile_ranks', 'overview/aggregations/elastic_aggregation_percentiles', + 'overview/aggregations/elastic_aggregation_range', 'overview/aggregations/elastic_aggregation_stats', 'overview/aggregations/elastic_aggregation_sum', 'overview/aggregations/elastic_aggregation_terms',