Skip to content

Document (aka mapping)

Roughly speaking Document represents Elasticsearch's mapping. However, it is possible to merge multiple documents into a single mapping.

It is convenient to describe Document subclasses as singleton objects.

Read more about Elasticsearch mapping types

Simple fields

General way

You can use field method to describe a field in a document:

package samples.document.field

import dev.evo.elasticmagic.doc.Document
import dev.evo.elasticmagic.types.BooleanType
import dev.evo.elasticmagic.types.IntType
import dev.evo.elasticmagic.types.KeywordType
import dev.evo.elasticmagic.types.TextType

object UserDoc : Document() {
    // It is possible to pass field options via shortcuts like `index`
    // or specifying `params` argument which can include any options (see `about` field below)
    val id by field(IntType, index = false, store = true)

    val login by field(KeywordType)

    // By default the field name is equal to the property name
    // but that behaviour can be changed
    val isAdmin by field("is_admin", BooleanType)

    val about by field(TextType, params = mapOf("norms" to false))
}

Fields can be used when building a search query:

package samples.document.field

import dev.evo.elasticmagic.SearchQuery
import dev.evo.elasticmagic.query.match

val fakeAdmins = SearchQuery(UserDoc.about.match("fake"))
    .filter(UserDoc.isAdmin.eq(true))
    .sort(UserDoc.id)

Using shortcuts

There are some nice shortcuts for popular field types. You don't need to import all those field types:

package samples.document.shortcuts

import dev.evo.elasticmagic.doc.Document

object UserDoc : Document() {
    val id by int(index = false, store = true)
    val login by keyword()
    val isAdmin by boolean("is_admin")
    val about by text(norms = false)
}

Full list of available shortcuts can be found here

Enums

It is possible to map field values to kotlin enums. Use enum extension function for that:

package samples.document.enums

import dev.evo.elasticmagic.doc.Document
import dev.evo.elasticmagic.doc.enum

enum class UserStatus {
    ACTIVE, LOCKED, NO_PASSWORD
}

object UserDoc : Document() {
    val status by keyword().enum<UserStatus>()
}

Now you are able to use enum variants in your search queries:

package samples.document.enums

import dev.evo.elasticmagic.SearchQuery

val q = SearchQuery()
    .filter(
        UserDoc.status.eq(UserStatus.ACTIVE)
    )

Sub fields

It is possible to define sub-fields for any simple field:

package samples.document.subfields

import dev.evo.elasticmagic.doc.BoundField
import dev.evo.elasticmagic.doc.Document
import dev.evo.elasticmagic.doc.SubFields

class AboutSubFields(field: BoundField<String, String>) : SubFields<String>(field) {
    val sort by keyword(normalizer = "lowercase")
    val autocomplete by text(analyzer = "autocomplete")
}

object UserDoc : Document() {
    val about by text().subFields(::AboutSubFields)
}

Sub-fields also can be used in search queries:

package samples.document.subfields

import dev.evo.elasticmagic.SearchQuery
import dev.evo.elasticmagic.query.match

val maybeNiceUsers = SearchQuery(UserDoc.about.autocomplete.match("nic"))
    .sort(UserDoc.about.sort)

Note

It is a mistake to use sub-fields twice. Following example will fail at runtime.

package samples.document.subfields.mistake

import dev.evo.elasticmagic.doc.BoundField
import dev.evo.elasticmagic.doc.Document
import dev.evo.elasticmagic.doc.SubFields

class AboutSubFields(field: BoundField<String, String>) : SubFields<String>(field) {
    val sort by keyword(normalizer = "lowercase")
}

object UserDoc : Document() {
    val about by text().subFields(::AboutSubFields)
    val description by text().subFields { about }
}

fun main() {
    println(UserDoc)
}
Show an error
Exception in thread "main" java.lang.ExceptionInInitializerError
        at samples.document.subfields.mistake.MistakeKt.main(Mistake.kt:17)
        at samples.document.subfields.mistake.MistakeKt.main(Mistake.kt)
Caused by: java.lang.IllegalStateException: Field [description] has already been initialized as [about]
        at dev.evo.elasticmagic.SubFields$UnboundSubFields.provideDelegate(Document.kt:363)
        at samples.document.subfields.mistake.UserDoc.<clinit>(Mistake.kt:13)
        ... 2 more

Object fields

Object

Object type just represents a hierarchical structure. It is similar to sub-fields but every field in a sub-document can have its own source value:

package samples.document.`object`

import dev.evo.elasticmagic.doc.BaseDocSource
import dev.evo.elasticmagic.doc.BoundField
import dev.evo.elasticmagic.doc.Document
import dev.evo.elasticmagic.SearchQuery
import dev.evo.elasticmagic.doc.SubDocument

class GroupDoc(field: BoundField<BaseDocSource, Nothing>) : SubDocument(field) {
    val id by int()
    val name by keyword()
}

object UserDoc : Document() {
    val groups by obj(::GroupDoc)
}

val systemUsers = SearchQuery()
    .filter(UserDoc.groups.name.eq("system"))

Note

The same as with the sub-fields sub-document should not be a singleton object.

Read more:

Nested

Using nested type it you can work with sub-documents independently. In the example below we find all users that have a moderator role with both article and order permissions:

package samples.document.nested

import dev.evo.elasticmagic.doc.BaseDocSource
import dev.evo.elasticmagic.query.Bool
import dev.evo.elasticmagic.doc.BoundField
import dev.evo.elasticmagic.doc.Document
import dev.evo.elasticmagic.query.Nested
import dev.evo.elasticmagic.SearchQuery
import dev.evo.elasticmagic.doc.SubDocument

class RoleDoc(field: BoundField<BaseDocSource, Nothing>) : SubDocument(field) {
    val name by keyword()
    val permissions by keyword()
}

object UserDoc : Document() {
    val roles by nested(::RoleDoc)
}

val moderators = SearchQuery()
    .filter(
        Nested(
            UserDoc.roles,
            Bool.must(
                UserDoc.roles.name.eq("moderator"),
                UserDoc.roles.permissions.eq("article"),
                UserDoc.roles.permissions.eq("order"),
            )
        )
    )

If we tried to make it with object type, we would find users that have a moderator role with article permission and view role with order permission.

Read more:

Parent-child relationship

Parent/child relationship allows you to define a link between documents inside an index.

Join field

package samples.document.join

import dev.evo.elasticmagic.doc.Document

abstract class BaseQADoc : Document() {
    val id by int()
    val content by text()
    val join by join(relations = mapOf("question" to listOf("answer")))
}

object QuestionDoc : BaseQADoc() {
    val rating by float()
    val title by text()
}

object AnswerDoc : BaseQADoc() {
    val accepted by boolean()
}

Read more:

Meta fields

Elasticsearch mapping has metadata fields. Some of those fields can be customized. In following example we make a value for _routing field required and keep only name field in document source:

package samples.document.meta

import dev.evo.elasticmagic.doc.Document
import dev.evo.elasticmagic.doc.MetaFields

object ProductDoc : Document() {
    val name by text()
    val companyId by int()

    override val meta = object : MetaFields() {
        override val routing by RoutingField(required = true)
        override val source by SourceField(includes = listOf("name"))
    }
}

Now you must provide the required routing value when indexing documents otherwise Elasticsearch will throw routing_missing_exceptions.

Merge multiple documents

To create a mapping for multiple documents you can use mergeDocuments function. Documents that are merged should not contradict each other.

package samples.document.join

import dev.evo.elasticmagic.doc.mergeDocuments

val QAMapping = mergeDocuments(QuestionDoc, AnswerDoc)

Resulting document can be used when creating an index or updating an existing mapping.