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 be59f2217..73a360a78 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/query/InnerHits.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/query/InnerHits.scala @@ -17,29 +17,145 @@ package zio.elasticsearch.query import zio.Chunk +import zio.elasticsearch.ElasticPrimitive.ElasticPrimitiveOps +import zio.elasticsearch.Field +import zio.elasticsearch.highlights.Highlights import zio.json.ast.Json -import zio.json.ast.Json.{Num, Obj, Str} +import zio.json.ast.Json.{Arr, Num, Obj, Str} final case class InnerHits private[elasticsearch] ( + private val excluded: Chunk[String], + private val included: Chunk[String], private val from: Option[Int], + private val highlights: Option[Highlights], private val name: Option[String], private val size: Option[Int] ) { self => + + /** + * Specifies one or more type-safe fields to be excluded in the response of a [[zio.elasticsearch.query.InnerHits]]. + * + * @param field + * a type-safe field to be excluded + * @param fields + * type-safe fields to be excluded + * @tparam S + * document which fields are excluded + * @return + * an instance of a [[zio.elasticsearch.query.InnerHits]] with specified fields to be excluded. + */ + def excludes[S](field: Field[S, _], fields: Field[S, _]*): InnerHits = + self.copy(excluded = excluded ++ (field.toString +: fields.map(_.toString))) + + /** + * Specifies one or more fields to be excluded in the response of a [[zio.elasticsearch.query.InnerHits]]. + * + * @param field + * a field to be excluded + * @param fields + * fields to be excluded + * @return + * an instance of a [[zio.elasticsearch.query.InnerHits]] with specified fields to be excluded. + */ + def excludes(field: String, fields: String*): InnerHits = + self.copy(excluded = excluded ++ (field +: fields)) + + /** + * Specifies the starting offset of the [[zio.elasticsearch.query.InnerHits]] to be returned. + * + * @param value + * the starting offset value + * @return + * an instance of a [[zio.elasticsearch.query.InnerHits]] with the specified starting offset. + */ def from(value: Int): InnerHits = self.copy(from = Some(value)) + /** + * Specifies the highlighting configuration for the [[zio.elasticsearch.query.InnerHits]]. + * + * @param value + * the [[zio.elasticsearch.highlights.Highlights]] configuration + * @return + * an instance of a [[zio.elasticsearch.query.InnerHits]] with the specified highlighting configuration. + */ + def highlights(value: Highlights): InnerHits = + self.copy(highlights = Some(value)) + + /** + * Specifies one or more type-safe fields to be included in the response of a [[zio.elasticsearch.query.InnerHits]]. + * + * @param field + * a type-safe field to be included + * @param fields + * type-safe fields to be included + * @tparam S + * document which fields are included + * @return + * an instance of a [[zio.elasticsearch.query.InnerHits]] with specified fields to be included. + */ + def includes[S](field: Field[S, _], fields: Field[S, _]*): InnerHits = + self.copy(included = included ++ (field.toString +: fields.map(_.toString))) + + /** + * Specifies one or more fields to be included in the response of a [[zio.elasticsearch.query.InnerHits]]. + * + * @param field + * a field to be included + * @param fields + * fields to be included + * @return + * an instance of a [[zio.elasticsearch.query.InnerHits]] with specified fields to be included. + */ + def includes(field: String, fields: String*): InnerHits = + self.copy(included = included ++ (field +: fields)) + + /** + * Specifies the name of the [[zio.elasticsearch.query.InnerHits]]. + * + * @param value + * the name of the [[zio.elasticsearch.query.InnerHits]] + * @return + * an instance of a [[zio.elasticsearch.query.InnerHits]] with the specified name. + */ def name(value: String): InnerHits = self.copy(name = Some(value)) + /** + * Specifies the maximum number of [[zio.elasticsearch.query.InnerHits]] to be returned. + * + * @param value + * the maximum number of [[zio.elasticsearch.query.InnerHits]] + * @return + * an instance of a [[zio.elasticsearch.query.InnerHits]] with the specified size. + */ def size(value: Int): InnerHits = self.copy(size = Some(value)) - def toStringJsonPair: (String, Json) = + private[elasticsearch] def toStringJsonPair: (String, Json) = { + val sourceJson: Option[Json] = + (included, excluded) match { + case (Chunk(), Chunk()) => + None + case (included, excluded) => + val includes = if (included.isEmpty) Obj() else Obj("includes" -> Arr(included.map(_.toJson))) + val excludes = if (excluded.isEmpty) Obj() else Obj("excludes" -> Arr(excluded.map(_.toJson))) + Some(includes merge excludes) + } + "inner_hits" -> Obj( - Chunk(from.map("from" -> Num(_)), size.map("size" -> Num(_)), name.map("name" -> Str(_))).flatten + Chunk( + from.map("from" -> Num(_)), + size.map("size" -> Num(_)), + name.map("name" -> Str(_)), + highlights.map("highlight" -> _.toJson), + sourceJson.map("_source" -> _) + ).flatten ) + } } object InnerHits { - def apply(): InnerHits = InnerHits(from = None, name = None, size = None) + def apply(): InnerHits = + InnerHits(excluded = Chunk(), included = Chunk(), from = None, highlights = None, name = None, size = None) } diff --git a/modules/library/src/test/scala/zio/elasticsearch/ElasticQuerySpec.scala b/modules/library/src/test/scala/zio/elasticsearch/ElasticQuerySpec.scala index 2994c96c4..fb6d22f76 100644 --- a/modules/library/src/test/scala/zio/elasticsearch/ElasticQuerySpec.scala +++ b/modules/library/src/test/scala/zio/elasticsearch/ElasticQuerySpec.scala @@ -17,6 +17,7 @@ package zio.elasticsearch import zio.Chunk +import zio.elasticsearch.ElasticHighlight.highlight import zio.elasticsearch.ElasticQuery._ import zio.elasticsearch.domain._ import zio.elasticsearch.query.DistanceType.Plane @@ -761,7 +762,16 @@ object ElasticQuerySpec extends ZIOSpecDefault { query = MatchAll(boost = None), scoreMode = None, ignoreUnmapped = None, - innerHitsField = Some(InnerHits(from = Some(0), name = Some("innerHitName"), size = Some(3))) + innerHitsField = Some( + InnerHits( + excluded = Chunk(), + included = Chunk(), + from = Some(0), + highlights = None, + name = Some("innerHitName"), + size = Some(3) + ) + ) ) ) ) && @@ -772,7 +782,16 @@ object ElasticQuerySpec extends ZIOSpecDefault { query = MatchAll(boost = None), scoreMode = None, ignoreUnmapped = None, - innerHitsField = Some(InnerHits(None, None, None)) + innerHitsField = Some( + InnerHits( + excluded = Chunk(), + included = Chunk(), + from = None, + highlights = None, + name = None, + size = None + ) + ) ) ) ) && @@ -794,7 +813,16 @@ object ElasticQuerySpec extends ZIOSpecDefault { query = MatchAll(boost = None), scoreMode = Some(ScoreMode.Max), ignoreUnmapped = Some(false), - innerHitsField = Some(InnerHits(None, Some("innerHitName"), None)) + innerHitsField = Some( + InnerHits( + excluded = Chunk(), + included = Chunk(), + from = None, + highlights = None, + name = Some("innerHitName"), + size = None + ) + ) ) ) ) @@ -2027,7 +2055,15 @@ object ElasticQuerySpec extends ZIOSpecDefault { val queryWithNested = nested(TestDocument.subDocumentList, nested("items", term("testField", "test"))) val queryWithIgnoreUnmapped = nested(TestDocument.subDocumentList, matchAll).ignoreUnmappedTrue val queryWithInnerHits = - nested(TestDocument.subDocumentList, matchAll).innerHits(InnerHits().from(0).size(3).name("innerHitName")) + nested(TestDocument.subDocumentList, matchAll).innerHits( + InnerHits() + .from(0) + .size(3) + .name("innerHitName") + .highlights(highlight("stringField")) + .excludes("longField") + .includes("intField") + ) val queryWithInnerHitsEmpty = nested(TestDocument.subDocumentList, matchAll).innerHits val queryWithScoreMode = nested(TestDocument.subDocumentList, matchAll).scoreMode(ScoreMode.Avg) val queryWithAllParams = nested(TestDocument.subDocumentList, matchAll).ignoreUnmappedFalse @@ -2091,7 +2127,20 @@ object ElasticQuerySpec extends ZIOSpecDefault { | "inner_hits": { | "from": 0, | "size": 3, - | "name": "innerHitName" + | "name": "innerHitName", + | "highlight" : { + | "fields" : { + | "stringField" : {} + | } + | }, + | "_source" : { + | "includes" : [ + | "intField" + | ], + | "excludes" : [ + | "longField" + | ] + | } | } | } |} diff --git a/modules/library/src/test/scala/zio/elasticsearch/InnerHitsSpec.scala b/modules/library/src/test/scala/zio/elasticsearch/InnerHitsSpec.scala new file mode 100644 index 000000000..6b016f576 --- /dev/null +++ b/modules/library/src/test/scala/zio/elasticsearch/InnerHitsSpec.scala @@ -0,0 +1,244 @@ +package zio.elasticsearch + +import zio.Chunk +import zio.elasticsearch.ElasticHighlight.highlight +import zio.elasticsearch.domain.TestDocument +import zio.elasticsearch.highlights.{HighlightField, Highlights} +import zio.elasticsearch.query.InnerHits +import zio.elasticsearch.utils.RichString +import zio.json.ast.Json.Obj +import zio.test.Assertion.equalTo +import zio.test.{Spec, TestEnvironment, ZIOSpecDefault, assert} + +object InnerHitsSpec extends ZIOSpecDefault { + + def spec: Spec[TestEnvironment, Any] = + suite("InnerHits")( + test("constructing") { + val innerHits = InnerHits() + val innerHitsWithExcluded = InnerHits().excludes(TestDocument.doubleField, TestDocument.dateField) + val innerHitsWithFrom = InnerHits().from(2) + val innerHitsWithHighlights = InnerHits().highlights(highlight("stringField")) + val innerHitsWithIncluded = InnerHits().includes(TestDocument.intField) + val innerHitsWithName = InnerHits().name("innerHitName") + val innerHitsWithSize = InnerHits().size(5) + val innerHitsWithAllParams = + InnerHits() + .excludes("longField") + .includes("intField") + .from(2) + .highlights(highlight("stringField")) + .name("innerHitName") + .size(5) + + assert(innerHits)( + equalTo( + InnerHits(excluded = Chunk(), from = None, highlights = None, included = Chunk(), name = None, size = None) + ) + ) && assert(innerHitsWithExcluded)( + equalTo( + InnerHits( + excluded = Chunk("doubleField", "dateField"), + included = Chunk(), + from = None, + highlights = None, + name = None, + size = None + ) + ) + ) && assert(innerHitsWithFrom)( + equalTo( + InnerHits( + excluded = Chunk(), + included = Chunk(), + from = Some(2), + highlights = None, + name = None, + size = None + ) + ) + ) && assert(innerHitsWithHighlights)( + equalTo( + InnerHits( + excluded = Chunk(), + included = Chunk(), + from = None, + highlights = Some(Highlights(fields = Chunk(HighlightField("stringField")), config = Map.empty)), + name = None, + size = None + ) + ) + ) && assert(innerHitsWithIncluded)( + equalTo( + InnerHits( + excluded = Chunk(), + included = Chunk("intField"), + from = None, + highlights = None, + name = None, + size = None + ) + ) + ) && assert(innerHitsWithName)( + equalTo( + InnerHits( + excluded = Chunk(), + included = Chunk(), + from = None, + highlights = None, + name = Some("innerHitName"), + size = None + ) + ) + ) && assert(innerHitsWithSize)( + equalTo( + InnerHits( + excluded = Chunk(), + included = Chunk(), + from = None, + highlights = None, + name = None, + size = Some(5) + ) + ) + ) && assert(innerHitsWithAllParams)( + equalTo( + InnerHits( + excluded = Chunk("longField"), + included = Chunk("intField"), + from = Some(2), + highlights = Some(Highlights(fields = Chunk(HighlightField("stringField")), config = Map.empty)), + name = Some("innerHitName"), + size = Some(5) + ) + ) + ) + }, + test("encoding as JSON") { + val innerHits = InnerHits() + val innerHitsWithExcluded = InnerHits().excludes(TestDocument.doubleField, TestDocument.dateField) + val innerHitsWithFrom = InnerHits().from(2) + val innerHitsWithHighlights = InnerHits().highlights(highlight("stringField")) + val innerHitsWithIncluded = InnerHits().includes(TestDocument.intField) + val innerHitsWithName = InnerHits().name("innerHitName") + val innerHitsWithSize = InnerHits().size(5) + val innerHitsWithAllParams = + InnerHits() + .excludes("longField") + .includes("intField") + .from(2) + .highlights(highlight("stringField")) + .name("innerHitName") + .size(5) + + val expected = + """ + |{ + | "inner_hits": { + | + | } + |} + |""".stripMargin + + val expectedWithExcluded = + """ + |{ + | "inner_hits": { + | "_source" : { + | "excludes" : [ + | "doubleField", + | "dateField" + | ] + | } + | } + |} + |""".stripMargin + + val expectedWithFrom = + """ + |{ + | "inner_hits": { + | "from": 2 + | } + |} + |""".stripMargin + + val expectedWithHighlights = + """ + |{ + | "inner_hits": { + | "highlight" : { + | "fields" : { + | "stringField" : {} + | } + | } + | } + |} + |""".stripMargin + + val expectedWithIncluded = + """ + |{ + | "inner_hits": { + | "_source" : { + | "includes" : [ + | "intField" + | ] + | } + | } + |} + |""".stripMargin + + val expectedWithName = + """ + |{ + | "inner_hits": { + | "name": "innerHitName" + | } + |} + |""".stripMargin + + val expectedWithSize = + """ + |{ + | "inner_hits": { + | "size": 5 + | } + |} + |""".stripMargin + + val expectedWithAllParams = + """ + |{ + | "inner_hits": { + | "from": 2, + | "size": 5, + | "name": "innerHitName", + | "highlight" : { + | "fields" : { + | "stringField" : {} + | } + | }, + | "_source" : { + | "includes" : [ + | "intField" + | ], + | "excludes" : [ + | "longField" + | ] + | } + | } + |} + |""".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)) + } + ) +}