@@ -148,11 +148,27 @@ trait ElasticConversion {
148148
149149 case (None , Some (aggs)) =>
150150 // Case 2 : only aggregations
151- parseAggregations(aggs, Map .empty, fieldAliases, aggregations)
151+ val ret = parseAggregations(aggs, Map .empty, fieldAliases, aggregations)
152+ val groupedRows : Map [String , Seq [Map [String , Any ]]] =
153+ ret.groupBy(_.getOrElse(" bucket_root" , " " ).toString)
154+ groupedRows.values.foldLeft(Seq (Map .empty[String , Any ])) { (acc, group) =>
155+ for {
156+ accMap <- acc
157+ groupMap <- group
158+ } yield accMap ++ groupMap
159+ }
152160
153161 case (Some (hits), Some (aggs)) if hits.isEmpty =>
154162 // Case 3 : aggregations with no hits
155- parseAggregations(aggs, Map .empty, fieldAliases, aggregations)
163+ val ret = parseAggregations(aggs, Map .empty, fieldAliases, aggregations)
164+ val groupedRows : Map [String , Seq [Map [String , Any ]]] =
165+ ret.groupBy(_.getOrElse(" bucket_root" , " " ).toString)
166+ groupedRows.values.foldLeft(Seq (Map .empty[String , Any ])) { (acc, group) =>
167+ for {
168+ accMap <- acc
169+ groupMap <- group
170+ } yield accMap ++ groupMap
171+ }
156172
157173 case (Some (hits), Some (aggs)) if hits.nonEmpty =>
158174 // Case 4 : Hits + global aggregations + top_hits aggregations
@@ -355,7 +371,7 @@ trait ElasticConversion {
355371 }
356372 } else if (bucketAggs.isEmpty) {
357373 // No buckets : it is a leaf aggregation (metrics or top_hits)
358- val metrics = extractMetrics(aggsNode)
374+ val metrics = extractMetrics(aggsNode, aggregations )
359375 val allTopHits = extractAllTopHits(aggsNode, fieldAliases, aggregations)
360376
361377 if (allTopHits.nonEmpty) {
@@ -369,6 +385,7 @@ trait ElasticConversion {
369385 // Handle each aggregation with buckets
370386 bucketAggs.flatMap { case (aggName, buckets, _) =>
371387 buckets.flatMap { bucket =>
388+ val metrics = extractMetrics(bucket, aggregations)
372389 val allTopHits = extractAllTopHits(bucket, fieldAliases, aggregations)
373390
374391 val bucketKey = extractBucketKey(bucket)
@@ -379,7 +396,7 @@ trait ElasticConversion {
379396 val currentContext = parentContext ++ Map (
380397 aggName -> bucketKey,
381398 s " ${aggName}_doc_count " -> docCount
382- ) ++ allTopHits
399+ ) ++ metrics ++ allTopHits
383400
384401 // Check for sub-aggregations
385402 val subAggFields = bucket
@@ -468,62 +485,76 @@ trait ElasticConversion {
468485
469486 /** Extract metrics from an aggregation node
470487 */
471- def extractMetrics (aggsNode : JsonNode ): Map [String , Any ] = {
488+ def extractMetrics (
489+ aggsNode : JsonNode ,
490+ aggregations : Map [String , ClientAggregation ]
491+ ): Map [String , Any ] = {
472492 if (! aggsNode.isObject) return Map .empty
473- aggsNode
474- .properties()
475- .asScala
476- .flatMap { entry =>
477- val name = normalizeAggregationKey(entry.getKey)
478- val value = entry.getValue
479-
480- // Detect simple metric values
481- Option (value.get(" value" ))
482- .filter(! _.isNull)
483- .map { metricValue =>
484- val numericValue = if (metricValue.isIntegralNumber) {
485- metricValue.asLong()
486- } else if (metricValue.isFloatingPointNumber) {
487- metricValue.asDouble()
488- } else {
489- metricValue.asText()
490- }
491- name -> numericValue
493+ var bucketRoot : Option [String ] = None
494+ val metrics =
495+ aggsNode
496+ .properties()
497+ .asScala
498+ .flatMap { entry =>
499+ val name = normalizeAggregationKey(entry.getKey)
500+ aggregations.get(name) match {
501+ case Some (agg) =>
502+ bucketRoot = Some (agg.bucketRoot)
503+ case _ =>
492504 }
493- .orElse {
494- // Stats aggregations
495- if (value.has( " count " ) && value.has( " sum " ) && value.has( " avg " )) {
496- Some (
497- name -> Map (
498- " count " -> value.get( " count " ).asLong(),
499- " sum " -> Option (value.get( " sum " )).filterNot(_.isNull).map(_.asDouble()),
500- " avg " -> Option (value.get( " avg " )).filterNot(_.isNull).map(_.asDouble()),
501- " min " -> Option (value.get( " min " )).filterNot(_.isNull).map(_.asDouble()),
502- " max " -> Option (value.get( " max " )).filterNot(_.isNull).map(_. asDouble() )
503- ).collect { case (k, Some (v)) => k -> v; case (k, v : Long ) => k -> v }
504- )
505- } else {
506- None
505+ val value = entry.getValue
506+
507+ // Detect simple metric values
508+ Option (value.get( " value " ))
509+ .filter( ! _.isNull)
510+ .map { metricValue =>
511+ val numericValue = if (metricValue.isIntegralNumber) {
512+ metricValue.asLong()
513+ } else if (metricValue.isFloatingPointNumber) {
514+ metricValue. asDouble()
515+ } else {
516+ metricValue.asText( )
517+ }
518+ name -> numericValue
507519 }
508- }
509- .orElse {
510- // Percentiles
511- if (value.has( " values " ) && value.get( " values " ).isObject) {
512- val percentiles = value
513- .get(" values " )
514- .properties()
515- .asScala
516- .map { pEntry =>
517- pEntry.getKey -> pEntry.getValue. asDouble()
518- }
519- .toMap
520- Some (name -> percentiles)
521- } else {
522- None
520+ .orElse {
521+ // Stats aggregations
522+ if (value.has( " count " ) && value.has( " sum " ) && value.has( " avg " )) {
523+ Some (
524+ name -> Map (
525+ " count " -> value .get(" count " ).asLong(),
526+ " sum " -> Option (value.get( " sum " )).filterNot(_.isNull).map(_.asDouble()),
527+ " avg " -> Option (value.get( " avg " )).filterNot(_.isNull).map(_.asDouble()),
528+ " min " -> Option (value.get( " min " )).filterNot(_.isNull).map(_.asDouble()),
529+ " max " -> Option (value.get( " max " )).filterNot(_.isNull).map(_. asDouble() )
530+ ).collect { case (k, Some (v)) => k -> v; case (k, v : Long ) => k -> v }
531+ )
532+ } else {
533+ None
534+ }
523535 }
524- }
525- }
526- .toMap
536+ .orElse {
537+ // Percentiles
538+ if (value.has(" values" ) && value.get(" values" ).isObject) {
539+ val percentiles = value
540+ .get(" values" )
541+ .properties()
542+ .asScala
543+ .map { pEntry =>
544+ pEntry.getKey -> pEntry.getValue.asDouble()
545+ }
546+ .toMap
547+ Some (name -> percentiles)
548+ } else {
549+ None
550+ }
551+ }
552+ }
553+ .toMap
554+ bucketRoot match {
555+ case Some (root) => metrics + (" bucket_root" -> root)
556+ case None => metrics
557+ }
527558 }
528559
529560 /** Extract all top_hits aggregations with their names and hits */
@@ -533,6 +564,7 @@ trait ElasticConversion {
533564 aggregations : Map [String , ClientAggregation ]
534565 ): Map [String , Any ] = {
535566 if (! aggsNode.isObject) return Map .empty
567+ var bucketRoot : Option [String ] = None
536568 val allTopHits =
537569 aggsNode
538570 .properties()
@@ -553,13 +585,20 @@ trait ElasticConversion {
553585 // Process each top_hits aggregation with their names
554586 val row = allTopHits.map { case (topHitName, hits) =>
555587 // Determine if it is a multivalued aggregation (array_agg, ...)
556- val hasMultipleValues = aggregations.get(topHitName) match {
588+ val agg = aggregations.get(topHitName)
589+ val hasMultipleValues = agg match {
557590 case Some (agg) => agg.multivalued
558591 case None =>
559592 // Fallback on naming convention if aggregation is not found
560593 ! topHitName.toLowerCase.matches(" (first|last)_.*" )
561594 }
562595
596+ agg match {
597+ case Some (agg) =>
598+ bucketRoot = Some (agg.bucketRoot)
599+ case _ =>
600+ }
601+
563602 val processedHits = hits.map { hit =>
564603 val source = extractSource(hit, fieldAliases)
565604 if (hasMultipleValues) {
@@ -582,7 +621,7 @@ trait ElasticConversion {
582621 } else {
583622 val metadata = extractHitMetadata(hit)
584623 val innerHits = extractInnerHits(hit, fieldAliases)
585- source ++ metadata ++ innerHits
624+ source ++ metadata ++ innerHits ++ Map ( " bucket_root " -> bucketRoot)
586625 }
587626 }
588627
@@ -600,7 +639,10 @@ trait ElasticConversion {
600639 }
601640 }
602641
603- row
642+ bucketRoot match {
643+ case Some (root) => row + (" bucket_root" -> root)
644+ case None => row
645+ }
604646 }
605647
606648 /** Extract global metrics from aggregations (for hits + aggs case)
0 commit comments