elastic4s: как автоматически считывать идентификатор документа в экземпляр класса case?

Использование elastic4s 7.12.1 с spray-json 1.3.6 (и scala 2.13.5):
Есть ли способ прочитать _id документа Elasticsearch в поле, например . id экземпляра case class,
использующего только неявный spray-json RootJsonFormat, т.е. е. без создания пользовательского HitReader для elastic4s, и если да, то как?
То же самое касается написания документов: есть ли способ вставить экземпляр case class без сериализации (сделав его частью _source в ES) поля id с помощью только вышеупомянутый RootJsonFormat, т.е. е. без написания пользовательского 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)
      }

Однако у этого подхода есть серьезные проблемы:

  • Мне нужно предоставить id при создании Foo, в то время как я действительно хочу, чтобы ES генерировал для меня _id при индексации документа. Это, конечно, в первую очередь вызвано использованием case class
  • При загрузке документа Foo его поле id содержит (бессмысленное) фиктивное значение, которое я использовал при его индексации, а не фактическое _id, под которым он хранится внутри узла ES.

Чтобы решить эти проблемы (первая только частично), я мог бы, конечно, написать свои собственные Indexable и HitReader вот так:

  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, или elastic4s с spray-json просто не подходит для этой задачи?


Изменить: Другой возможностью было бы удалить поле id из Foo, ввести оболочку case class, например. FooResultWrapper, который сохраняет Foo результаты поиска по _id в Map[String, Foo], используйте RootJsonFormat[Foo] и HitReader[FooResultWrapper], которые преобразуют _source в Foo и сохраняют их в hit.id. Но это тоже не очень радует.


person aebblcraebbl    schedule 06.05.2021    source источник


Ответы (1)


Вот блестящее решение, которое я придумал (в основном то, что я предложил при редактировании вопроса):
Удалены поля id моего домена case class (например, Foo) и введен общий case class для переноса результатов и принудительного использования objects для реализовать read из elastic4s для конкретного case class:

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

вместе с общим trait, который содержит реализацию для упаковки результатов типа T в экземпляры ESResultWrapper:

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 конкретным классом case (для которого также существует RootJsonFormat) и делегировать hit hitInternal, тем самым неявно предоставляя HitReader[T] через RootJsonFormat[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)
      }

приводит к эл. g.: ESResultWrapper(-XMSQXkB-5ze1JvrVWup,Foo("someFoo"))
И самое приятное: изменение реализации оболочки не влияет на классы предметной области.

Я хвалю себя за то, что придумал это на третий день использования Scala. Отличная работа.

person aebblcraebbl    schedule 06.05.2021