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.