Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.sksamuel.elastic4s.handlers.index

import com.sksamuel.elastic4s.handlers.fields.ElasticFieldBuilderFn
import com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest

object DynamicTemplateBuilderFn {

/** Deserializes a single dynamic template from the ES "array of single-key objects" format.
*
* ES encodes dynamic_templates as: [ { "template_name": { "match": "...", "mapping": {...} } }, ... ]
*
* @param name the template name (the single key in the outer object)
* @param values the inner object containing match conditions and the "mapping" sub-object
*/
def fromMap(name: String, values: Map[String, Any]): DynamicTemplateRequest = {
val mappingMap = values.get("mapping") match {
case Some(m: Map[_, _]) => m.asInstanceOf[Map[String, Any]]
case _ => Map.empty[String, Any]
}
val field = ElasticFieldBuilderFn.construct(name, mappingMap)

DynamicTemplateRequest(
name = name,
mapping = field,
`match` = values.get("match").map(_.toString),
unmatch = values.get("unmatch").map(_.toString),
pathMatch = values.get("path_match").map(_.toString),
pathUnmatch = values.get("path_unmatch").map(_.toString),
MatchPattern = values.get("match_pattern").map(_.toString),
matchMappingType = values.get("match_mapping_type").map(_.toString)
)
}

/** Converts the raw mappings map (from Jackson untyped deserialization) into a typed [[TemplateMappings]].
*
* Handles the ES dynamic_templates encoding (array of single-key objects) and reconstructs
* [[com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest]] instances.
*/
def fromMappingsMap(raw: Map[String, Any]): TemplateMappings = {
val dynamicTemplates: Seq[DynamicTemplateRequest] = raw.get("dynamic_templates") match {
case Some(list: List[_]) =>
list.flatMap {
case entry: Map[_, _] =>
entry.asInstanceOf[Map[String, Any]].map { case (templateName, value) =>
fromMap(templateName, value.asInstanceOf[Map[String, Any]])
}
case _ => Seq.empty
}
case _ => Seq.empty
}

val properties = raw.get("properties") match {
case Some(props: Map[_, _]) =>
props.asInstanceOf[Map[String, Any]].map { case (fieldName, value) =>
ElasticFieldBuilderFn.construct(fieldName, value.asInstanceOf[Map[String, Any]])
}.toSeq
case _ => Seq.empty
}

TemplateMappings(dynamicTemplates, properties)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,38 @@ case class IndexTemplate(
order: Int,
@JsonProperty("index_patterns") indexPatterns: Seq[String],
settings: Map[String, Any],
mappings: Map[String, Any],
mappings: TemplateMappings,
aliases: Map[String, Any],
version: Option[Int]
)

// Raw Jackson-deserialized intermediate types — not part of the public API.
// Composable index templates nest settings/mappings/aliases under a "template" sub-object.
private[index] case class RawGetIndexTemplatesResponse(
@JsonProperty("index_templates") indexTemplates: List[RawTemplateEntry]
)
private[index] case class RawTemplateEntry(name: String, @JsonProperty("index_template") template: RawIndexTemplate)
private[index] case class RawTemplateBody(
settings: Option[Map[String, Any]],
mappings: Option[Map[String, Any]],
aliases: Option[Map[String, Any]]
)
private[index] case class RawIndexTemplate(
order: Int,
@JsonProperty("index_patterns") indexPatterns: Seq[String],
template: Option[RawTemplateBody],
version: Option[Int]
) {
def toIndexTemplate: IndexTemplate = IndexTemplate(
order = order,
indexPatterns = indexPatterns,
settings = template.flatMap(_.settings).getOrElse(Map.empty),
mappings = template.flatMap(_.mappings).fold(TemplateMappings())(DynamicTemplateBuilderFn.fromMappingsMap),
aliases = template.flatMap(_.aliases).getOrElse(Map.empty),
version = version
)
}

trait IndexTemplateHandlers {

implicit object IndexTemplateExistsHandler extends Handler[IndexTemplateExistsRequest, IndexTemplateExists] {
Expand Down Expand Up @@ -70,8 +97,11 @@ trait IndexTemplateHandlers {
override def handle(response: HttpResponse): Either[ElasticError, GetIndexTemplatesResponse] =
response.statusCode match {
case 200 =>
val templates = ResponseHandler.fromResponse[GetIndexTemplatesResponse](response)
Right(templates)
val raw = ResponseHandler.fromResponse[RawGetIndexTemplatesResponse](response)
val typed = GetIndexTemplatesResponse(
raw.indexTemplates.map(t => Templates(t.name, t.template.toIndexTemplate))
)
Right(typed)
case _ => Left(ElasticErrorParser.parse(response))
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.sksamuel.elastic4s.handlers.index

import com.sksamuel.elastic4s.fields.ElasticField
import com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest

case class TemplateMappings(
dynamicTemplates: Seq[DynamicTemplateRequest] = Seq.empty,
properties: Seq[ElasticField] = Seq.empty
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"index_templates": [
{
"name": "my_template",
"index_template": {
"index_patterns": ["my_*", "bar_*"],
"template": {
"settings": {
"number_of_shards": 1
},
"mappings": {
"dynamic_templates": [
{
"es": {
"match": "*_es",
"match_pattern": "regex",
"match_mapping_type": "string",
"mapping": {
"type": "text",
"analyzer": "spanish"
}
}
},
{
"en": {
"match": "*",
"match_mapping_type": "string",
"mapping": {
"type": "text",
"analyzer": "english"
}
}
}
],
"properties": {
"title": {
"type": "keyword"
},
"created_at": {
"type": "date"
}
}
},
"aliases": {
"my_alias": {}
}
},
"priority": 500,
"version": 3,
"composed_of": []
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.sksamuel.elastic4s.handlers.index

import com.sksamuel.elastic4s.HttpEntity.StringEntity
import com.sksamuel.elastic4s.HttpResponse
import com.sksamuel.elastic4s.fields.{KeywordField, TextField}
import com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest
import org.scalatest.EitherValues
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

import scala.io.Source

class GetIndexTemplateResponseTest extends AnyFlatSpec with IndexTemplateHandlers with Matchers with EitherValues {

private def fixture(path: String): String =
Source.fromInputStream(getClass.getResourceAsStream(path)).mkString

"GetIndexTemplateHandler" should "deserialize dynamic_templates from the array-of-single-key-objects format" in {
val json = fixture("/json/index/get_index_template_response.json")
val response = HttpResponse(200, Some(StringEntity(json, None)), Map.empty)

val result = GetIndexTemplateHandler.responseHandler.handle(response).value

result.indexTemplates should have size 1
val tmpl = result.indexTemplates.head
tmpl.name shouldBe "my_template"

val mappings = tmpl.template.mappings
mappings.dynamicTemplates should have size 2
}

it should "correctly parse the first dynamic template (es)" in {
val json = fixture("/json/index/get_index_template_response.json")
val response = HttpResponse(200, Some(StringEntity(json, None)), Map.empty)

val mappings = GetIndexTemplateHandler.responseHandler.handle(response).value
.indexTemplates.head.template.mappings

val es = mappings.dynamicTemplates.find(_.name == "es").get
es.`match` shouldBe Some("*_es")
es.MatchPattern shouldBe Some("regex")
es.matchMappingType shouldBe Some("string")
es.mapping shouldBe a[TextField]
es.mapping.asInstanceOf[TextField].analyzer shouldBe Some("spanish")
}

it should "correctly parse the second dynamic template (en)" in {
val json = fixture("/json/index/get_index_template_response.json")
val response = HttpResponse(200, Some(StringEntity(json, None)), Map.empty)

val mappings = GetIndexTemplateHandler.responseHandler.handle(response).value
.indexTemplates.head.template.mappings

val en = mappings.dynamicTemplates.find(_.name == "en").get
en.`match` shouldBe Some("*")
en.matchMappingType shouldBe Some("string")
en.MatchPattern shouldBe None
en.mapping shouldBe a[TextField]
en.mapping.asInstanceOf[TextField].analyzer shouldBe Some("english")
}

it should "deserialize properties from mappings" in {
val json = fixture("/json/index/get_index_template_response.json")
val response = HttpResponse(200, Some(StringEntity(json, None)), Map.empty)

val mappings = GetIndexTemplateHandler.responseHandler.handle(response).value
.indexTemplates.head.template.mappings

mappings.properties should have size 2
val fieldNames = mappings.properties.map(_.name).toSet
fieldNames shouldBe Set("title", "created_at")

val title = mappings.properties.find(_.name == "title").get
title shouldBe a[KeywordField]
}

it should "return empty TemplateMappings when mappings is absent" in {
val json =
"""{
| "index_templates": [
| {
| "name": "empty_template",
| "index_template": {
| "index_patterns": ["empty_*"],
| "priority": 1,
| "version": 1,
| "composed_of": []
| }
| }
| ]
|}""".stripMargin
val response = HttpResponse(200, Some(StringEntity(json, None)), Map.empty)

val mappings = GetIndexTemplateHandler.responseHandler.handle(response).value
.indexTemplates.head.template.mappings

mappings.dynamicTemplates shouldBe empty
mappings.properties shouldBe empty
}

it should "return empty dynamic_templates when mappings has only properties" in {
val json =
"""{
| "index_templates": [
| {
| "name": "props_only",
| "index_template": {
| "index_patterns": ["props_*"],
| "template": {
| "mappings": {
| "properties": {
| "name": { "type": "keyword" }
| }
| }
| },
| "priority": 1,
| "version": 1,
| "composed_of": []
| }
| }
| ]
|}""".stripMargin
val response = HttpResponse(200, Some(StringEntity(json, None)), Map.empty)

val mappings = GetIndexTemplateHandler.responseHandler.handle(response).value
.indexTemplates.head.template.mappings

mappings.dynamicTemplates shouldBe empty
mappings.properties should have size 1
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.sksamuel.elastic4s.requests.admin

import com.sksamuel.elastic4s.fields.TextField
import com.sksamuel.elastic4s.requests.common.RefreshPolicy
import com.sksamuel.elastic4s.requests.indexes.CreateIndexTemplateRequest
import com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest
import com.sksamuel.elastic4s.testkit.DockerTests
import org.scalatest.concurrent.Eventually
import org.scalatest.matchers.should.Matchers
Expand Down Expand Up @@ -29,6 +31,13 @@ class IndexTemplateHttpTest
Thread.sleep(2000)
}

Try {
client.execute {
deleteIndexTemplate("dyntemplate_template")
}.await
Thread.sleep(2000)
}

"create template" should {
"create template" in {

Expand All @@ -50,6 +59,33 @@ class IndexTemplateHttpTest
resp.result.indexTemplates.find(_.name == "brewery_template").get.template.order shouldBe 0
}
}
"deserialize dynamic_templates from get template response" in {

val dynTemplate = DynamicTemplateRequest("es_fields", TextField("").analyzer("spanish"))
.matchMappingType("string")
.matching("*_es")

client.execute {
CreateIndexTemplateRequest("dyntemplate_template", Seq("dyntemplate*")).mappings(
properties(keywordField("id")).dynamicTemplates(dynTemplate)
)
}.await.result.acknowledged shouldBe true

eventually {
val resp = client.execute {
getIndexTemplate("dyntemplate_template")
}.await

val mappings = resp.result.indexTemplates.find(_.name == "dyntemplate_template").get.template.mappings
mappings.dynamicTemplates should have size 1

val tmpl = mappings.dynamicTemplates.head
tmpl.name shouldBe "es_fields"
tmpl.`match` shouldBe Some("*_es")
tmpl.matchMappingType shouldBe Some("string")
tmpl.mapping shouldBe a[TextField]
}
}
"apply template to new indexes that match the pattern" in {

// this should match the earlier template of brew*
Expand Down