Using elastic4s 7.12.1 with spray-json 1.3.6 (and scala 2.13.5):
Is there a way to read the _id of an Elasticsearch document into a field, e.g . id, of a case class instance,
using only an implicit spray-json RootJsonFormat, i. e. without wiriting a custom HitReader for elastic4s and if so, how?
The same goes for writing documents: Is there a way to insert an instance of a case class without serializing (making it part of the _source in ES) the id field using only the aforementioned RootJsonFormat, i. e. without writing a custom Indexable?
According to the elastic4s documentation, this should be possible using jackson, which I want to avoid because of it's numerous critical security issues, that come up all the time.
Consider this case class, which should be indexed into ES:
case class Foo(id: String, name: String)
Using spray-json, I would only need to define a RootJsonFormat:
implicit val fooJsonFormat: RootJsonFormat[Foo] = jsonFormat2(Foo)
And could use elastic4s this way to index and search 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)
}
However, there are major problems with this approach:
- I need to provide an
idwhen creating aFoo, while I actually want ES to generate the_idfor me when indexing the document. This is of course primarily caused by using acase class - When loading a
Foodocument, itsidfield contains the (meaningless) dummy value I used when I indexed it, not the actual_idunder which it's stored inside the ES node
To solve these problems (the first one only partially), I could of course write my own Indexable and HitReader like this:
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
}
This doesn't look too terrible in a small example, but I think it's obvious that this approach scales horribly, provides no type safety and is a refactoring nightmare, since the names of the fields (e. g. "name") need to be specified manually.
Bottomline: Is there a better a way to achieve a spring-data-elasticsearch-like experience or is elastic4s with spray-json just not suited for this task?
edit: Another possibility would be to remove the id field from Foo, introduce a wrapper case class, e.g. FooResultWrapper, which stores Foo search results by _id in a Map[String, Foo], use a RootJsonFormat[Foo] and HitReader[FooResultWrapper] that converts the _source to Foo and stores it by hit.id. But that's also not very satisfying.
Behold the brilliant solution I came up with (basically what I suggested in the edit of the question):
Removed the
idfields of my domaincase class(e. g.Foo) and introduced a genericcase classto wrap the results and force usingobjectsto implementreadfromelastic4sfor the specificcase class:along with a generic
trait, which contains the implementation for wrapping results of typeTinESResultWrapperinstances:Now all that's left for actual "domain" classes is to extend the
ESResultWrapperHitReader[T]traitwith the specific case class (for which aRootJsonFormatalso exists) and delegatinghittohitInternal, thereby implicitly providing aHitReader[T]through theRootJsonFormat[T]:Usage is pretty simple (sticking with the example from the question):
leads to e. g.:
ESResultWrapper(-XMSQXkB-5ze1JvrVWup,Foo("someFoo"))And the best part: Changig the wrapping implementation doesn't impact the domain classes.
I applaud myself for coming up with this on my 3rd day of using Scala. Good job.