diff --git a/docs/overview/aggregations/elastic_aggregation_range.md b/docs/overview/aggregations/elastic_aggregation_range.md new file mode 100644 index 000000000..30404ce4c --- /dev/null +++ b/docs/overview/aggregations/elastic_aggregation_range.md @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala b/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala index 55ecd2b09..f1bac96bd 100644 --- a/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala +++ b/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala @@ -146,6 +146,38 @@ object HttpExecutorSpec extends IntegrationSpec { .asMinAggregation("aggregationInt") } yield assert(aggsRes.head.value)(equalTo(23.0)) } + } @@ around( + Executor.execute(ElasticRequest.createIndex(firstSearchIndex)), + Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie + ), + test("aggregate using range aggregation") { + + // TODO + + val expectedResponse = ("aggregationInt", MinAggregationResult(value = 23.0)) + 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 = 200)) + ) + _ <- Executor.execute( + ElasticRequest + .upsert[TestDocument](firstSearchIndex, secondDocumentId, secondDocument.copy(intField = 23)) + .refreshTrue + ) + aggregation = minAggregation(name = "aggregationInt", field = TestDocument.intField) + aggsRes <- Executor + .execute(ElasticRequest.aggregate(index = firstSearchIndex, aggregation = aggregation)) + .asMinAggregation("aggregationInt") + } yield assert(aggsRes.head.value)(equalTo(23.0)) + } + + + + } @@ around( Executor.execute(ElasticRequest.createIndex(firstSearchIndex)), Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie diff --git a/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala b/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala index 70eae388b..d2b50fdfa 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/ElasticAggregation.scala @@ -17,7 +17,7 @@ package zio.elasticsearch import zio.Chunk -import zio.elasticsearch.aggregation._ +import zio.elasticsearch.aggregation.{Range => IRange, SingleRange => Range, _} import zio.elasticsearch.script.Script object ElasticAggregation { @@ -178,6 +178,34 @@ object ElasticAggregation { final def multipleAggregations: MultipleAggregations = Multiple(aggregations = Chunk.empty) + // TODO: Add docs + final def rangeAggregation[A: Numeric]( + name: String, + field: Field[_, A], + range: Range, + ranges: Range* + ): RangeAggregation = + IRange( + name = name, + field = field.toString, + ranges = Chunk.fromIterable(ranges.prepended(range)), + keyed = None + ) + + // TODO: Add docs + final def rangeAggregation( + name: String, + field: String, + range: Range, + ranges: Range* + ): RangeAggregation = + IRange( + name = name, + field = field, + ranges = Chunk.fromIterable(ranges.prepended(range)), + keyed = None + ) + /** * Constructs a type-safe instance of [[zio.elasticsearch.aggregation.SumAggregation]] 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 780c56c0f..0d2f56b7f 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/aggregation/Aggregations.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/aggregation/Aggregations.scala @@ -199,6 +199,68 @@ private[elasticsearch] final case class Multiple(aggregations: Chunk[SingleElast aggregations.map(_.toJson).reduce(_ merge _) } +private[elasticsearch] sealed 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 { + + // TODO: Scala Doc + 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 SumAggregation extends SingleElasticAggregation with HasMissing[SumAggregation] with WithAgg private[elasticsearch] final case class Sum(name: String, field: String, missing: Option[Double]) diff --git a/modules/library/src/main/scala/zio/elasticsearch/package.scala b/modules/library/src/main/scala/zio/elasticsearch/package.scala index fcbdc349b..221703b1f 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 RoutingNewtype { def asMinAggregation(name: String): RIO[R, Option[MinAggregationResult]] = aggregationAs[MinAggregationResult](name) + // TODO asRangeAggregation + // TODO asKeyedRangeAggregation + /** * 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 621fa2883..a67e1f0e4 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/result/AggregationResult.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/result/AggregationResult.scala @@ -32,6 +32,11 @@ final case class MinAggregationResult private[elasticsearch] (value: Double) ext final case class SumAggregationResult private[elasticsearch] (value: Double) extends AggregationResult +final case class RangeAggregationResult private[elasticsearch] ( + + ) extends AggregationResult + + final case class TermsAggregationResult private[elasticsearch] ( docErrorCount: Int, sumOtherDocCount: Int, diff --git a/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala b/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala index 70418bedc..1b816163f 100644 --- a/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala +++ b/modules/library/src/test/scala/zio/elasticsearch/ElasticAggregationSpec.scala @@ -212,6 +212,73 @@ object ElasticAggregationSpec extends ZIOSpecDefault { ) ) }, + 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).key("secondKey"), + SingleRange.to(12).from(0).key("thirdKey") + ).keyed(true) + + assert(aggregationTo)( + equalTo( + Range( + "aggregation1", + "testField", + Chunk.from(List(SingleRange(from = None, to = Some(23.9), key = None))), + None + ) + ) + ) && assert(aggregationFrom)( + equalTo( + Range( + "aggregation2", + "testField", + Chunk.from(List(SingleRange(from = Some(2), to = None, key = None))), + None + ) + ) + ) && assert(aggregationFromTo)( + equalTo( + Range( + "aggregation3", + "testField", + Chunk.from(List(SingleRange(from = Some(4), to = Some(344.0), key = None))), + Some(false) + ) + ) + ) && assert(aggregationKeyed)( + equalTo( + Range( + "aggregation4", + "testField", + Chunk.from(List(SingleRange(from = None, to = Some(139), key = None))), + Some(true) + ) + ) + ) && assert(aggregationMultiple)( + equalTo( + Range( + "aggregation5", + "testField", + Chunk.from( + List( + SingleRange(from = None, to = Some(23.9), key = None), + SingleRange(from = Some(3), to = None, key = Some("secondKey")), + SingleRange(from = Some(0), to = Some(12), key = Some("thirdKey")) + ) + ), + Some(true) + ) + ) + ) + }, test("subAggregation") { val aggregation1 = termsAggregation(name = "first", field = TestDocument.stringField).withSubAgg( termsAggregation(name = "second", field = TestSubDocument.stringField.raw) @@ -713,6 +780,97 @@ object ElasticAggregationSpec extends ZIOSpecDefault { assert(aggregation.toJson)(equalTo(expected.toJson)) && assert(aggregationWithSubAggregation.toJson)(equalTo(expectedWithSubAggregation.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).key("secondKey"), + SingleRange.to(12).from(0).key("thirdKey") + ).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("subAggregation") { val aggregation = termsAggregation("first", TestDocument.stringField) diff --git a/website/sidebars.js b/website/sidebars.js index 29b069fa1..9fd4d0519 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -42,6 +42,7 @@ module.exports = { 'overview/aggregations/elastic_aggregation_cardinality', 'overview/aggregations/elastic_aggregation_max', 'overview/aggregations/elastic_aggregation_min', + 'overview/aggregations/elastic_aggregation_range', 'overview/aggregations/elastic_aggregation_terms', 'overview/aggregations/elastic_aggregation_sum', ],