From 9da2e0c3f8f6f3915928184be515beb0e98e7cd5 Mon Sep 17 00:00:00 2001 From: Philippus Baalman Date: Fri, 7 Jun 2024 15:53:36 +0200 Subject: [PATCH] Support highlighting in top hits aggregation (#3083) --- .../searches/aggs/TopHitsAggregation.scala | 16 +++++++++++++++- .../builders/TopHitsAggregationBuilder.scala | 6 ++++++ .../searches/TopHitsAggregationBuilderTest.scala | 9 ++++++++- .../aggs/responses/metrics/TopHits.scala | 3 ++- .../searches/aggs/TopHitsAggregationTest.scala | 14 +++++++++++++- 5 files changed, 44 insertions(+), 4 deletions(-) diff --git a/elastic4s-core/src/main/scala/com/sksamuel/elastic4s/requests/searches/aggs/TopHitsAggregation.scala b/elastic4s-core/src/main/scala/com/sksamuel/elastic4s/requests/searches/aggs/TopHitsAggregation.scala index ad8c2ffe3..2439f3c8c 100644 --- a/elastic4s-core/src/main/scala/com/sksamuel/elastic4s/requests/searches/aggs/TopHitsAggregation.scala +++ b/elastic4s-core/src/main/scala/com/sksamuel/elastic4s/requests/searches/aggs/TopHitsAggregation.scala @@ -4,6 +4,7 @@ import com.sksamuel.elastic4s.requests.common.FetchSourceContext import com.sksamuel.elastic4s.requests.script.Script import com.sksamuel.elastic4s.requests.searches.sort.Sort import com.sksamuel.elastic4s.ext.OptionImplicits._ +import com.sksamuel.elastic4s.requests.searches.{Highlight, HighlightField, HighlightOptions} case class TopHitsAggregation(name: String, explain: Option[Boolean] = None, @@ -16,7 +17,8 @@ case class TopHitsAggregation(name: String, scripts: Map[String, Script] = Map.empty, storedFields: Seq[String] = Nil, subaggs: Seq[AbstractAggregation] = Nil, - metadata: Map[String, AnyRef] = Map.empty) + metadata: Map[String, AnyRef] = Map.empty, + highlight: Option[Highlight] = None) extends Aggregation { type T = TopHitsAggregation @@ -45,6 +47,18 @@ case class TopHitsAggregation(name: String, def script(name: String, script: Script): T = copy(scripts = scripts + (name -> script)) + def highlighting(first: HighlightField, rest: HighlightField*): TopHitsAggregation = + highlighting(HighlightOptions(), first +: rest) + + def highlighting(fields: Iterable[HighlightField]): TopHitsAggregation = + highlighting(HighlightOptions(), fields) + + def highlighting(options: HighlightOptions, first: HighlightField, rest: HighlightField*): TopHitsAggregation = + highlighting(options, first +: rest) + + def highlighting(options: HighlightOptions, fields: Iterable[HighlightField]): TopHitsAggregation = + copy(highlight = Highlight(options, fields).some) + override def subAggregations(aggs: Iterable[AbstractAggregation]): T = sys.error("Top Hits does not support sub aggregations") override def metadata(map: Map[String, AnyRef]): T = copy(metadata = map) diff --git a/elastic4s-core/src/main/scala/com/sksamuel/elastic4s/requests/searches/aggs/builders/TopHitsAggregationBuilder.scala b/elastic4s-core/src/main/scala/com/sksamuel/elastic4s/requests/searches/aggs/builders/TopHitsAggregationBuilder.scala index 1b0866844..db9aee9be 100644 --- a/elastic4s-core/src/main/scala/com/sksamuel/elastic4s/requests/searches/aggs/builders/TopHitsAggregationBuilder.scala +++ b/elastic4s-core/src/main/scala/com/sksamuel/elastic4s/requests/searches/aggs/builders/TopHitsAggregationBuilder.scala @@ -1,6 +1,7 @@ package com.sksamuel.elastic4s.requests.searches.aggs.builders import com.sksamuel.elastic4s.handlers.common.FetchSourceContextBuilderFn +import com.sksamuel.elastic4s.handlers.searches.HighlightBuilderFn import com.sksamuel.elastic4s.handlers.searches.queries.sort.SortBuilderFn import com.sksamuel.elastic4s.json.{XContentBuilder, XContentFactory} import com.sksamuel.elastic4s.requests.searches.aggs.TopHitsAggregation @@ -25,6 +26,11 @@ object TopHitsAggregationBuilder { agg.fetchSource.foreach(FetchSourceContextBuilderFn(builder, _)) agg.explain.foreach(builder.field("explain", _)) + + agg.highlight.foreach { highlight => + builder.rawField("highlight", HighlightBuilderFn(highlight)) + } + agg.version.foreach(builder.field("version", _)) builder.endObject().endObject() diff --git a/elastic4s-core/src/test/scala/com/sksamuel/elastic4s/requests/searches/TopHitsAggregationBuilderTest.scala b/elastic4s-core/src/test/scala/com/sksamuel/elastic4s/requests/searches/TopHitsAggregationBuilderTest.scala index 6e0b39007..63b3ac155 100644 --- a/elastic4s-core/src/test/scala/com/sksamuel/elastic4s/requests/searches/TopHitsAggregationBuilderTest.scala +++ b/elastic4s-core/src/test/scala/com/sksamuel/elastic4s/requests/searches/TopHitsAggregationBuilderTest.scala @@ -7,7 +7,7 @@ import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers class TopHitsAggregationBuilderTest extends AnyFunSuite with Matchers { - test("top hits aggregation should generate expected json") { + test("top hits aggregation builder should generate expected json") { val q = TopHitsAggregation("top_items") .size(5) .from(10) @@ -17,4 +17,11 @@ class TopHitsAggregationBuilderTest extends AnyFunSuite with Matchers { TopHitsAggregationBuilder(q).string shouldBe """{"top_hits":{"size":5,"from":10,"sort":[{"price":{"mode":"median","order":"asc"}}],"explain":false,"version":true}}""" } + + test("top hits aggregation builder should support highlighting") { + val q = TopHitsAggregation("top_items") + .highlighting(HighlightField("name")) + TopHitsAggregationBuilder(q).string shouldBe + """{"top_hits":{"highlight":{"fields":{"name":{}}}}}""" + } } diff --git a/elastic4s-domain/src/main/scala/com/sksamuel/elastic4s/requests/searches/aggs/responses/metrics/TopHits.scala b/elastic4s-domain/src/main/scala/com/sksamuel/elastic4s/requests/searches/aggs/responses/metrics/TopHits.scala index 1938292de..ab7e2f3c3 100644 --- a/elastic4s-domain/src/main/scala/com/sksamuel/elastic4s/requests/searches/aggs/responses/metrics/TopHits.scala +++ b/elastic4s-domain/src/main/scala/com/sksamuel/elastic4s/requests/searches/aggs/responses/metrics/TopHits.scala @@ -15,7 +15,8 @@ case class TopHit(@JsonProperty("_index") index: String, @JsonProperty("_id") id: String, @JsonProperty("_score") score: Option[Double], sort: Seq[String], - @JsonProperty("_source") source: Map[String, Any]) extends Transformable { + @JsonProperty("_source") source: Map[String, Any], + highlight: Map[String, Any]) extends Transformable { @deprecated("types are deprecated in elasticsearch", "7.7") def ref: DocumentRef = DocumentRef(index, `type`, id) diff --git a/elastic4s-tests/src/test/scala/com/sksamuel/elastic4s/requests/searches/aggs/TopHitsAggregationTest.scala b/elastic4s-tests/src/test/scala/com/sksamuel/elastic4s/requests/searches/aggs/TopHitsAggregationTest.scala index e8cbaae4a..2749037cd 100644 --- a/elastic4s-tests/src/test/scala/com/sksamuel/elastic4s/requests/searches/aggs/TopHitsAggregationTest.scala +++ b/elastic4s-tests/src/test/scala/com/sksamuel/elastic4s/requests/searches/aggs/TopHitsAggregationTest.scala @@ -2,7 +2,7 @@ package com.sksamuel.elastic4s.requests.searches.aggs import com.sksamuel.elastic4s.AggReader import com.sksamuel.elastic4s.requests.common.RefreshPolicy -import com.sksamuel.elastic4s.requests.searches.Total +import com.sksamuel.elastic4s.requests.searches.{HighlightField, Total} import com.sksamuel.elastic4s.testkit.DockerTests import org.scalatest.freespec.AnyFreeSpec import org.scalatest.matchers.should.Matchers @@ -75,7 +75,19 @@ class TopHitsAggregationTest extends AnyFreeSpec with DockerTests with Matchers val agg = resp.aggs.terms("agg1") val tophits = agg.buckets.find(_.key == "london").get.tophits("agg2") tophits.hits.head.safeTo[String].get shouldBe "{\"name\":\"buckingham palace\",\"location\":\"london\"}" + } + + "should support highlighting" in { + val resp = client.execute { + search("tophits").matchQuery("name", "palace").aggs { + termsAgg("agg1", "location").addSubagg( + topHitsAgg("agg2").sortBy(fieldSort("name")).highlighting(HighlightField("name")) + ) + } + }.await.result + val tophits = resp.aggs.terms("agg1").buckets.find(_.key == "london").get.tophits("agg2") + tophits.hits.head.highlight shouldBe Map("name" -> List("buckingham palace")) } } }