Jim Hendricks Jim Hendricks - 3 months ago 48
Scala Question

Elastic4s search case class example errors when result document is missing a field

I've been working with this example from the Elastic4s manual. It is working fine until it attempts to retrieve a document that does not have a field specified in the case class.

In this example from the manual, let's say one result only had

name
and was missing the
location
field. It would yield this error:


java.util.NoSuchElementException: key not found: location


I'm looking for a good approach to deal with search results that have varying fields.

Code sample:

case class Character(name: String, location: String)

implicit object CharacterHitAs extends HitAs[Character] {
override def as(hit: RichSearchHit): Character = {
Character(hit.sourceAsMap("name").toString, hit.sourceAsMap("location").toString) }}

val resp = client.execute {
search in "gameofthrones" / "characters" query "kings landing"
}.await

val characters :Seq[Character] = resp.as[Character]

Answer

When developing a case class with optional parameters, use Option:

case class Character(name: String, location: Option[String])

Character("Tyrion Lannister", None)

Then all you have to do is modify your data extractor to pass a None Option if it doesn't find the data:

val tyrion = Map("location" -> "King's Landing", "name" -> "Cersei Lannister")
val cersei = Map("father" -> "Tywin Lannister?", "name" -> "Cersei Lannister")
val jaime = Map("father" -> "Tywin Lannister", "location" -> "Tower of the Hand")
val characters = List(tyrion, cersei, jaime)

case class Character(name: String, location: Option[String])

characters.map(x => Character(x.getOrElse("name", "A CHARACTER HAS NO NAME"), x.get("location")))

The result of characters.map(...) is this:

res0: List[Character] = List(
        Character(Cersei Lannister,Some(King's Landing)), 
        Character(Cersei Lannister,None), 
        Character(A CHARACTER HAS NO NAME NAME,Some(Tower of the Hand)))

From the source code for RichSearchHit, sourceAsMap should return a Map object:

def sourceAsMap: Map[String, AnyRef] = if (java.sourceAsMap == null) Map.empty else java.sourceAsMap.asScala.toMap

Given that you're using a Map shorthand, you should be able to convert your code to:

case class Character(name: String, location: Option[String])

implicit object CharacterHitAs extends HitAs[Character] {
  override def as(hit: RichSearchHit): Character = {
Character(hit.sourceAsMap.getOrElse("name", "A CHARACTER HAS NO NAME"), hit.sourceAsMap.get("location")) }}