【问题标题】:elastic4s: How to automagically read document id into case class instance?elastic4s:如何自动将文档 ID 读入案例类实例?
【发布时间】:2021-07-28 17:36:15
【问题描述】:

使用elastic4s 7.12.1spray-json 1.3.6(和scala 2.13.5):
有没有办法将 Elasticsearch 文档的 _id 读入字段,例如 . idcase class 实例,
仅使用隐式 spray-json RootJsonFormat,即。 e.无需为elastic4s 编写自定义HitReader,如果是这样,怎么办?
编写文档也是如此:有没有办法插入case class 的实例而不序列化(使其成为ES 中_source 的一部分)id 字段,仅使用上述RootJsonFormat,即。 e.无需编写自定义Indexable
根据elastic4s 文档,这应该可以使用jackson 来实现,我想避免这种情况,因为它总是会出现许多关键的安全问题。

考虑这个case类,它应该被ES索引:

case class Foo(id: String, name: String)

使用spray-json,我只需要定义一个RootJsonFormat

implicit val fooJsonFormat: RootJsonFormat[Foo] = jsonFormat2(Foo)

并且可以使用elastic4s这种方式来索引和搜索Foos:

val someFoo = Foo("idWhichShouldBeOverwrittenByES", "someName")
client.execute {
  indexInto("foos").doc(someFoo)
}

val result: Response[SearchResponse] = client.execute {
      search("foos").query {
        boolQuery().must {
          matchQuery("name", "someName")
        }
      }
    }.await

result match {
        case RequestSuccess(_, _, _, result) => result.to[Foo].foreach(println)
        case RequestFailure(_, _, _, error) => println(error.toString)
      }

但是,这种方法存在重大问题:

  • 我需要在创建Foo 时提供id,而实际上我希望ES 在索引文档时为我生成_id。这当然主要是由于使用case class
  • 加载Foo 文档时,它的id 字段包含我在索引它时使用的(无意义的)虚拟值,而不是它存储在ES 节点中的实际_id

为了解决这些问题(第一个只是部分解决),我当然可以像这样写自己的IndexableHitReader

  implicit object FooHitReader extends HitReader[Foo] {
    override def read(hit: Hit): Try[Foo] = Try({
      val source = hit.sourceAsMap
      Foo(
        id = hit.id,
        name = source("name").toString
      )
    })
  }

  implicit object FooIndexable extends Indexable[Foo] {
    override def json(t: Foo): String =
      JsObject(
        "name" -> JsString(t.name),
      ).compactPrint
  }

这在一个小例子中看起来并不算太糟糕,但我认为很明显这种方法可扩展性很差,不提供类型安全性并且是重构的噩梦,因为字段的名称(例如 "name")需要手动指定。

底线: 有没有更好的方法来获得类似spring-data-elasticsearch 的体验,或者elastic4sspray-json 不适合这项任务?


编辑: 另一种可能性是从Foo 中删除id 字段,引入包装器case class,例如FooResultWrapper,将Foo 的搜索结果存储在Map[String, Foo] 中,使用RootJsonFormat[Foo]HitReader[FooResultWrapper]_source 转换为Foo 并将其存储在hit.id 中。但这也不是很令人满意。

【问题讨论】:

    标签: scala elasticsearch spray-json elastic4s


    【解决方案1】:

    看看我想出的绝妙解决方案(基本上是我在问题编辑中提出的建议):
    删除了我的域case classid 字段(例如Foo)并引入了一个通用的case class 来包装结果并强制使用objects 来实现elastic4s 中的read 用于特定的case class

    case class ESResultWrapper[T](id: String, result: T)
    

    还有一个通用的trait,它包含在ESResultWrapper 实例中包装T 类型结果的实现:

    trait ESResultWrapperHitReader[T] extends HitReader[ESResultWrapper[T]] {
      def readInternal(hit: Hit)(implicit reader: HitReader[T]): Try[ESResultWrapper[T]] = Try({
        ESResultWrapper(
          id = hit.id,
          result = hit.to[T]
        )
      })
    }
    

    现在真正的“域”类剩下的就是用特定的案例类扩展ESResultWrapperHitReader[T]trait(也存在RootJsonFormat)并将hit委派给hitInternal,从而隐式通过RootJsonFormat[T] 提供HitReader[T]

      implicit object FooResultWrapperHitReader extends ESResultWrapperHitReader[Foo] {
        override def read(hit: Hit): Try[ESResultWrapper[Foo]] = readInternal(hit)
      } 
    

    用法非常简单(坚持问题中的示例):

    result match {
            case RequestSuccess(_, _, _, result) => result.to[ESResultWrapper[Foo]].foreach(println)
            case RequestFailure(_, _, _, error) => println(error.toString)
          }
    

    导致e。 G。: ESResultWrapper(-XMSQXkB-5ze1JvrVWup,Foo("someFoo"))
    最好的部分:Changig 包装实现不会影响域类。

    我为自己在使用 Scala 的第三天提出这个想法而鼓掌。干得好。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-09
      • 2015-03-03
      • 2018-12-05
      • 1970-01-01
      • 1970-01-01
      • 2019-05-27
      相关资源
      最近更新 更多