diff --git a/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala b/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala index 916847521..81f190f14 100644 --- a/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala +++ b/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala @@ -27,12 +27,14 @@ import zio.elasticsearch.domain.{PartialTestDocument, TestDocument, TestSubDocum import zio.elasticsearch.executor.Executor import zio.elasticsearch.query.DistanceUnit.Kilometers import zio.elasticsearch.query.FunctionScoreFunction.randomScoreFunction +import zio.elasticsearch.query.{FunctionScoreBoostMode, FunctionScoreFunction, InnerHits} import zio.elasticsearch.query.sort.SortMode.Max import zio.elasticsearch.query.sort.SortOrder._ import zio.elasticsearch.query.sort.SourceType.NumberType import zio.elasticsearch.query.{Distance, FunctionScoreBoostMode, FunctionScoreFunction} import zio.elasticsearch.request.{CreationOutcome, DeletionOutcome} import zio.elasticsearch.result._ +import zio.elasticsearch.result.{Item, MaxAggregationResult, UpdateByQueryResult} import zio.elasticsearch.script.{Painless, Script} import zio.json.ast.Json.{Arr, Str} import zio.schema.codec.JsonCodec @@ -1141,6 +1143,55 @@ object HttpExecutorSpec extends IntegrationSpec { Executor.execute(ElasticRequest.createIndex(firstSearchIndex)), Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie ), + test("successfully find inner hit document with highlight") { + checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) { + (firstDocumentId, firstDocument, secondDocumentId, secondDocument) => + for { + _ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll)) + _ <- + Executor.execute( + ElasticRequest.upsert[TestDocument](firstSearchIndex, firstDocumentId, firstDocument) + ) + _ <- Executor.execute( + ElasticRequest + .upsert[TestDocument](firstSearchIndex, secondDocumentId, secondDocument) + .refreshTrue + ) + query = nested( + path = TestDocument.subDocumentList, + query = must( + matches( + TestSubDocument.stringField, + secondDocument.subDocumentList.headOption.map(_.stringField).getOrElse("foo") + ) + ) + ).innerHits( + InnerHits().highlights(highlight(TestSubDocument.stringField)) + ) + result <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)) + items <- result.items + res = items + .flatMap(_.innerHit("subDocumentList")) + .flatten + .flatMap(_.highlight("subDocumentList.stringField")) + .flatten + } yield assert(res)( + Assertion.contains( + secondDocument.subDocumentList.headOption + .map(doc => s"${doc.stringField}") + .getOrElse("foo") + ) + ) + } + } @@ around( + Executor.execute( + ElasticRequest.createIndex( + firstSearchIndex, + """{ "mappings": { "properties": { "subDocumentList": { "type": "nested" } } } }""" + ) + ), + Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie + ), test("successfully find document with highlight using field accessor") { checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) { (firstDocumentId, firstDocument, secondDocumentId, secondDocument) => diff --git a/modules/library/src/main/scala/zio/elasticsearch/ElasticRequest.scala b/modules/library/src/main/scala/zio/elasticsearch/ElasticRequest.scala index 15843b2d2..e4efefb0b 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/ElasticRequest.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/ElasticRequest.scala @@ -660,7 +660,7 @@ object ElasticRequest { val sizeJson: Json = size.fold(Obj())(s => Obj("size" -> s.toJson)) - val highlightsJson: Json = highlights.fold(Obj())(h => Obj("highlight" -> h.toJson)) + val highlightsJson: Json = highlights.fold(Obj())(h => Obj("highlight" -> h.toJson(None))) val searchAfterJson: Json = searchAfter.fold(Obj())(sa => Obj("search_after" -> sa)) @@ -751,7 +751,7 @@ object ElasticRequest { val sizeJson: Json = size.fold(Obj())(s => Obj("size" -> s.toJson)) - val highlightsJson: Json = highlights.fold(Obj())(h => Obj("highlight" -> h.toJson)) + val highlightsJson: Json = highlights.fold(Obj())(h => Obj("highlight" -> h.toJson(None))) val searchAfterJson: Json = searchAfter.fold(Obj())(sa => Obj("search_after" -> sa)) diff --git a/modules/library/src/main/scala/zio/elasticsearch/executor/HttpExecutor.scala b/modules/library/src/main/scala/zio/elasticsearch/executor/HttpExecutor.scala index a154b69e3..9596b2b2f 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/executor/HttpExecutor.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/executor/HttpExecutor.scala @@ -40,6 +40,7 @@ import zio.elasticsearch.executor.response.{ CreateResponse, DocumentWithHighlightsAndSort, GetResponse, + Hit, SearchWithAggregationsResponse, UpdateByQueryResponse } @@ -382,7 +383,7 @@ private[elasticsearch] final class HttpExecutor private (esConfig: ElasticConfig case HttpOk => response.body.fold( e => ZIO.fail(new ElasticException(s"Exception occurred: ${e.getMessage}")), - value => + value => { ZIO .fromEither(value.innerHitsResults) .map { innerHitsResults => @@ -395,6 +396,7 @@ private[elasticsearch] final class HttpExecutor private (esConfig: ElasticConfig ) } .mapError(error => DecodingException(s"Could not parse inner_hits: $error")) + } ) case _ => ZIO.fail(handleFailuresFromCustomResponse(response)) @@ -610,7 +612,7 @@ private[elasticsearch] final class HttpExecutor private (esConfig: ElasticConfig private def itemsFromDocumentsWithHighlightsSortAndInnerHits( results: Chunk[DocumentWithHighlightsAndSort], - innerHits: Chunk[Map[String, Chunk[Json]]] + innerHits: Chunk[Map[String, Chunk[Hit]]] ): Chunk[Item] = results.zip(innerHits).map { case (r, innerHits) => Item(raw = r.source, highlight = r.highlight, innerHits = innerHits, sort = r.sort) 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 bea0c69bd..ccb9e102e 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 @@ -35,11 +35,11 @@ private[elasticsearch] final case class SearchWithAggregationsResponse( hits: Hits, aggregations: Option[Json] ) { - lazy val innerHitsResults: Either[String, Chunk[Map[String, Chunk[Json]]]] = + lazy val innerHitsResults: Either[String, Chunk[Map[String, Chunk[Hit]]]] = Validation .validateAll( hits.hits - .map(_.innerHits.fold[Validation[String, Map[String, Chunk[Json]]]](Validation.succeed(Map.empty)) { + .map(_.innerHits.fold[Validation[String, Map[String, Chunk[Hit]]]](Validation.succeed(Map.empty)) { innerHits => Validation .validateAll( @@ -47,7 +47,7 @@ private[elasticsearch] final case class SearchWithAggregationsResponse( Validation.fromEither( response .as[InnerHitsResponse] - .map(innerHitsResponse => (name, innerHitsResponse.hits.hits.map(_.source))) + .map(innerHitsResponse => (name, innerHitsResponse.hits.hits)) ) } ) diff --git a/modules/library/src/main/scala/zio/elasticsearch/highlights/Highlights.scala b/modules/library/src/main/scala/zio/elasticsearch/highlights/Highlights.scala index da7ab8a39..e7ad18f97 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/highlights/Highlights.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/highlights/Highlights.scala @@ -99,15 +99,15 @@ private[elasticsearch] final case class Highlights( def withHighlight(field: String, config: HighlightConfig): Highlights = self.copy(fields = HighlightField(field, config) +: self.fields) - private[elasticsearch] def toJson: Json = Obj(configChunk) merge fieldsJson + private[elasticsearch] def toJson(fieldPath: Option[String]): Json = Obj(configChunk) merge fieldsJson(fieldPath) private lazy val configChunk: Chunk[(String, Json)] = Chunk.fromIterable(config) - private lazy val fieldsJson: Json = + private def fieldsJson(fieldPath: Option[String]): Json = if (explicitFieldOrder) { - Obj("fields" -> Arr(fields.reverse.map(_.toJsonObj))) + Obj("fields" -> Arr(fields.reverse.map(_.toJsonObj(fieldPath)))) } else { - Obj("fields" -> Obj(fields.reverse.map(_.toStringJsonPair))) + Obj("fields" -> Obj(fields.reverse.map(_.toStringJsonPair(fieldPath)))) } } @@ -116,7 +116,8 @@ object Highlights { } private[elasticsearch] final case class HighlightField(field: String, config: HighlightConfig = Map.empty) { - def toStringJsonPair: (String, Obj) = field -> Obj(Chunk.fromIterable(config)) + def toStringJsonPair(fieldPath: Option[String]): (String, Obj) = + fieldPath.map(_ + "." + field).getOrElse(field) -> Obj(Chunk.fromIterable(config)) - def toJsonObj: Json = Obj(toStringJsonPair) + def toJsonObj(fieldPath: Option[String]): Json = Obj(toStringJsonPair(fieldPath)) } diff --git a/modules/library/src/main/scala/zio/elasticsearch/query/InnerHits.scala b/modules/library/src/main/scala/zio/elasticsearch/query/InnerHits.scala index 73a360a78..d4ec3aa5b 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/query/InnerHits.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/query/InnerHits.scala @@ -132,7 +132,7 @@ final case class InnerHits private[elasticsearch] ( def size(value: Int): InnerHits = self.copy(size = Some(value)) - private[elasticsearch] def toStringJsonPair: (String, Json) = { + private[elasticsearch] def toStringJsonPair(fieldPath: Option[String]): (String, Json) = { val sourceJson: Option[Json] = (included, excluded) match { case (Chunk(), Chunk()) => @@ -148,7 +148,7 @@ final case class InnerHits private[elasticsearch] ( from.map("from" -> Num(_)), size.map("size" -> Num(_)), name.map("name" -> Str(_)), - highlights.map("highlight" -> _.toJson), + highlights.map("highlight" -> _.toJson(fieldPath)), sourceJson.map("_source" -> _) ).flatten ) diff --git a/modules/library/src/main/scala/zio/elasticsearch/query/Queries.scala b/modules/library/src/main/scala/zio/elasticsearch/query/Queries.scala index 960433211..892612d8f 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/query/Queries.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/query/Queries.scala @@ -466,7 +466,7 @@ private[elasticsearch] final case class HasChild[S]( Some("type" -> childType.toJson), Some("query" -> query.toJson(None)), ignoreUnmapped.map("ignore_unmapped" -> _.toJson), - innerHitsField.map(_.toStringJsonPair), + innerHitsField.map(_.toStringJsonPair(None)), maxChildren.map("max_children" -> _.toJson), minChildren.map("min_children" -> _.toJson), scoreMode.map("score_mode" -> _.toString.toLowerCase.toJson) @@ -544,7 +544,7 @@ private[elasticsearch] final case class HasParent[S]( boost.map("boost" -> _.toJson), ignoreUnmapped.map("ignore_unmapped" -> _.toJson), score.map("score" -> _.toJson), - innerHitsField.map(_.toStringJsonPair) + innerHitsField.map(_.toStringJsonPair(None)) ).flatten ) ) @@ -614,7 +614,7 @@ private[elasticsearch] final case class Nested[S]( Some("query" -> query.toJson(fieldPath.map(_ + "." + path).orElse(Some(path)))), scoreMode.map("score_mode" -> _.toString.toLowerCase.toJson), ignoreUnmapped.map("ignore_unmapped" -> _.toJson), - innerHitsField.map(_.toStringJsonPair) + innerHitsField.map(_.toStringJsonPair(fieldPath.map(_ + "." + path).orElse(Some(path)))) ).flatten ) ) diff --git a/modules/library/src/main/scala/zio/elasticsearch/result/Item.scala b/modules/library/src/main/scala/zio/elasticsearch/result/Item.scala index aef0732a9..abf927a51 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/result/Item.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/result/Item.scala @@ -18,6 +18,7 @@ package zio.elasticsearch.result import zio.Chunk import zio.elasticsearch.Field +import zio.elasticsearch.executor.response.Hit import zio.json.DecoderOps import zio.json.ast.{Json, JsonCursor} import zio.prelude.Validation @@ -27,8 +28,8 @@ import zio.schema.codec.JsonCodec.JsonDecoder final case class Item( raw: Json, - private val highlight: Option[Json] = None, - private val innerHits: Map[String, Chunk[Json]] = Map.empty, + highlight: Option[Json] = None, + private val innerHits: Map[String, Chunk[Hit]] = Map.empty, sort: Option[Json] = None ) { def documentAs[A](implicit schema: Schema[A]): Either[DecodeError, A] = JsonDecoder.decode(schema, raw.toString) @@ -43,14 +44,17 @@ final case class Item( def highlight(field: Field[_, _]): Option[Chunk[String]] = highlight(field.toString) + def innerHit(name: String): Option[Chunk[Item]] = + innerHits.get(name).map(_.map(hit => Item(hit.source, hit.highlight))) + def innerHitAs[A](name: String)(implicit schema: Schema[A]): Either[DecodingException, Chunk[A]] = for { - innerHitsJson <- innerHits.get(name).toRight(DecodingException(s"Could not find inner hits with name $name")) + innerHitItems <- innerHit(name).toRight(DecodingException(s"Could not find inner hits with name $name")) innerHits <- Validation .validateAll( - innerHitsJson.map(json => - Validation.fromEither(JsonDecoder.decode(schema, json.toString)).mapError(_.message) + innerHitItems.map(item => + Validation.fromEither(JsonDecoder.decode(schema, item.raw.toString)).mapError(_.message) ) ) .toEitherWith(errors => diff --git a/modules/library/src/test/scala/zio/elasticsearch/ElasticQuerySpec.scala b/modules/library/src/test/scala/zio/elasticsearch/ElasticQuerySpec.scala index bb446459f..fcf820459 100644 --- a/modules/library/src/test/scala/zio/elasticsearch/ElasticQuerySpec.scala +++ b/modules/library/src/test/scala/zio/elasticsearch/ElasticQuerySpec.scala @@ -2550,7 +2550,7 @@ object ElasticQuerySpec extends ZIOSpecDefault { | "name": "innerHitName", | "highlight" : { | "fields" : { - | "stringField" : {} + | "subDocumentList.stringField" : {} | } | }, | "_source" : { diff --git a/modules/library/src/test/scala/zio/elasticsearch/HighlightsSpec.scala b/modules/library/src/test/scala/zio/elasticsearch/HighlightsSpec.scala index ce03fd814..94f9af33f 100644 --- a/modules/library/src/test/scala/zio/elasticsearch/HighlightsSpec.scala +++ b/modules/library/src/test/scala/zio/elasticsearch/HighlightsSpec.scala @@ -228,32 +228,32 @@ object HighlightsSpec extends ZIOSpecDefault { |} |""".stripMargin - assert(highlightObject.toJson)( + assert(highlightObject.toJson(None))( equalTo( expected.toJson ) - ) && assert(highlightWithHighlight.toJson)( + ) && assert(highlightWithHighlight.toJson(None))( equalTo( expectedWithFirstName.toJson ) - ) && assert(highlightWithHighlightAndGlobalConfig.toJson)( + ) && assert(highlightWithHighlightAndGlobalConfig.toJson(None))( equalTo( expectedPlainWithFirstName.toJson ) - ) && assert(highlightWithConfig.toJson)( + ) && assert(highlightWithConfig.toJson(None))( equalTo( expectedPlainWithRequiredFieldMatch.toJson ) - ) && assert(highlightWithConfigAndHighlight.toJson)( + ) && assert(highlightWithConfigAndHighlight.toJson(None))( equalTo( expectedPlainWithMatchedFields.toJson ) - ) && assert(highlightWithConfigHighlightAndExplicitFieldOrder.toJson)( + ) && assert(highlightWithConfigHighlightAndExplicitFieldOrder.toJson(None))( equalTo( expectedPlainWithArrayOfFields.toJson ) ) && - assert(highlightWithMultipleConfig.toJson)( + assert(highlightWithMultipleConfig.toJson(None))( equalTo( expectedFvhType.toJson ) diff --git a/modules/library/src/test/scala/zio/elasticsearch/InnerHitsSpec.scala b/modules/library/src/test/scala/zio/elasticsearch/InnerHitsSpec.scala index 6b016f576..ef73df8f4 100644 --- a/modules/library/src/test/scala/zio/elasticsearch/InnerHitsSpec.scala +++ b/modules/library/src/test/scala/zio/elasticsearch/InnerHitsSpec.scala @@ -231,14 +231,14 @@ object InnerHitsSpec extends ZIOSpecDefault { |} |""".stripMargin - assert(Obj(innerHits.toStringJsonPair))(equalTo(expected.toJson)) && - assert(Obj(innerHitsWithExcluded.toStringJsonPair))(equalTo(expectedWithExcluded.toJson)) && - assert(Obj(innerHitsWithFrom.toStringJsonPair))(equalTo(expectedWithFrom.toJson)) && - assert(Obj(innerHitsWithHighlights.toStringJsonPair))(equalTo(expectedWithHighlights.toJson)) && - assert(Obj(innerHitsWithIncluded.toStringJsonPair))(equalTo(expectedWithIncluded.toJson)) && - assert(Obj(innerHitsWithName.toStringJsonPair))(equalTo(expectedWithName.toJson)) && - assert(Obj(innerHitsWithSize.toStringJsonPair))(equalTo(expectedWithSize.toJson)) && - assert(Obj(innerHitsWithAllParams.toStringJsonPair))(equalTo(expectedWithAllParams.toJson)) + assert(Obj(innerHits.toStringJsonPair(None)))(equalTo(expected.toJson)) && + assert(Obj(innerHitsWithExcluded.toStringJsonPair(None)))(equalTo(expectedWithExcluded.toJson)) && + assert(Obj(innerHitsWithFrom.toStringJsonPair(None)))(equalTo(expectedWithFrom.toJson)) && + assert(Obj(innerHitsWithHighlights.toStringJsonPair(None)))(equalTo(expectedWithHighlights.toJson)) && + assert(Obj(innerHitsWithIncluded.toStringJsonPair(None)))(equalTo(expectedWithIncluded.toJson)) && + assert(Obj(innerHitsWithName.toStringJsonPair(None)))(equalTo(expectedWithName.toJson)) && + assert(Obj(innerHitsWithSize.toStringJsonPair(None)))(equalTo(expectedWithSize.toJson)) && + assert(Obj(innerHitsWithAllParams.toStringJsonPair(None)))(equalTo(expectedWithAllParams.toJson)) } ) }