Skip to content

Querying

To build a search query use a SearchQuery builder.

In these samples we will utilize following document:

package samples.querying

import dev.evo.elasticmagic.doc.Document

object UserDoc : Document() {
    val id by int()
    val isActive by boolean("is_active")
    val groups by keyword()
    val rating by float()
    val about by text()
}

Query

You can pass a query to SearchQuery directly via its constructor:

package samples.querying

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

var q = SearchQuery(
    FunctionScore(
        UserDoc.about.match("fake"),
        listOf(
            FunctionScore.FieldValueFactor(
                UserDoc.rating,
                missing = 0.0F,
            )
        )
    )
)

Also it is possible to replace existing query using a query method:

package samples.querying

import dev.evo.elasticmagic.query.Bool
import dev.evo.elasticmagic.query.FunctionScore
import dev.evo.elasticmagic.query.match

val q2 = q.query(
    FunctionScore(
        Bool.should(
            UserDoc.about.match("fake"),
            UserDoc.about.match("real"),
        ),
        listOf(
            FunctionScore.FieldValueFactor(
                UserDoc.rating,
                missing = 0.0F,
            )
        )
    )
)

See full list of available query expressions.

Cloning

In the last example q and q2 variables point to the same object. For efficiency all SearchQuery methods modify current instance and return it for chaining method calls.

If you want to get independent query instance you should clone it explicitly:

package samples.querying

import dev.evo.elasticmagic.query.match

val clonedQuery = q.clone()
    .query(UserDoc.about.match("fake"))

Now you can modify clonedQuery without touching its ancestor q.

Filtering

Using filter method you can filter your query. All filters passed to the filter method will be combined using AND operation.

package samples.querying

import dev.evo.elasticmagic.query.Bool
import dev.evo.elasticmagic.query.match

// Select only active users that id is not equal 0
val activeUsersQuery = q
    .filter(UserDoc.isActive.eq(true))
    .filter(UserDoc.id.ne(0))

// The same as above
val activeUsersQuery2 = q
    .filter(
        UserDoc.isActive.eq(true),
        UserDoc.id.ne(0)
    )

// If you need one condition OR another use Bool.should expression.
// In the following example we select non-active users OR "fake" users
val activeUsersQuery3 = q
    .filter(
        Bool.should(
            UserDoc.isActive.ne(true),
            UserDoc.about.match("fake")
        )
    )

See Query Filter Context

Sorting

sort method has 2 flavors:

  • a shortcut that accepts document fields
  • and full-featured version accepting Sort expressions
package samples.querying

import dev.evo.elasticmagic.query.Sort

// Sort by user id with ascending order
val sortedByIdQuery = q.sort(UserDoc.id)

// Sort by user id descending
val sortedByIdDescQuery = q.sort(UserDoc.id.desc())

// Sort by several criteria
val multipleSortedQuery = q.sort(UserDoc.isActive.desc(), UserDoc.id.asc())

// Use Sort explicitly to customize sort behaviour
val sortedByRatingQuery = q.sort(Sort(UserDoc.rating, missing = Sort.Missing.First))

See:

Aggregations

Use aggs method to define aggregations:

package samples.querying

import dev.evo.elasticmagic.aggs.AvgAgg
import dev.evo.elasticmagic.aggs.HistogramAgg
import dev.evo.elasticmagic.aggs.TermsAgg

// Build aggregations by groups and calculate average rating as well as
// build histogram by rating for every group
val ratingHistogramQuery = q.aggs(
    "groups" to TermsAgg(
        UserDoc.groups,
        aggs = mapOf(
            "avg_rating" to AvgAgg(UserDoc.rating),
            "rating_histogram" to HistogramAgg(
                UserDoc.rating, interval = 10.0F
            ),
        )
    )
)

See Search Aggregations

Query Nodes

Sometimes you don't know final form of your query when creating it. For such a usecase it is possible to replace parts of the query after creation using special query expressions and queryNode method:

package samples.querying

import dev.evo.elasticmagic.SearchQuery
import dev.evo.elasticmagic.doc.BaseDocSource
import dev.evo.elasticmagic.doc.BoundField
import dev.evo.elasticmagic.doc.Document
import dev.evo.elasticmagic.doc.MappingField
import dev.evo.elasticmagic.doc.SubDocument
import dev.evo.elasticmagic.query.Bool
import dev.evo.elasticmagic.query.DisMax
import dev.evo.elasticmagic.query.FunctionScore
import dev.evo.elasticmagic.query.match
import dev.evo.elasticmagic.query.NodeHandle
import dev.evo.elasticmagic.query.QueryExpressionNode

import kotlin.random.Random

class TranslationDoc(field: BoundField<BaseDocSource, Nothing>) : SubDocument(field) {
    val en by text()
    val de by text()
    val ru by text()
}

object QuestionDoc : Document() {
    val title by obj(::TranslationDoc)
    val text by obj(::TranslationDoc)
    val rating by float()
    val votes by int()
}

val scoringHandle = NodeHandle<FunctionScore>()
val langHandle = NodeHandle<DisMax>()

val searchTerm = "hello world"
val skeletonQuery = SearchQuery(
    QueryExpressionNode(
        scoringHandle,
        FunctionScore(
            QueryExpressionNode(
                langHandle,
                DisMax(
                    queries = listOf(
                        Bool.should(
                            QuestionDoc.title.en.match(searchTerm),
                            QuestionDoc.text.en.match(searchTerm),
                        )
                    ),
                )
            ),
            functions = listOf(
                FunctionScore.FieldValueFactor(QuestionDoc.rating),
            ),
            scoreMode = FunctionScore.ScoreMode.MULTIPLY,
        )
    )
)

val boostByNumberOfVotes = Random.nextBoolean()
var boostedQuery = if (boostByNumberOfVotes) {
    skeletonQuery.queryNode(scoringHandle) { node ->
        node.copy(
            functions = node.functions + listOf(
                FunctionScore.Weight(
                    1.1F,
                    filter = QuestionDoc.votes.range(gte = 10, lt = 50)
                ),
                FunctionScore.Weight(
                    1.5F,
                    filter = QuestionDoc.votes.gte(50)
                ),
            )
        )
    }
} else {
    skeletonQuery
}

val additionalLanguages = listOf("de", "ru")
val userLang = additionalLanguages[Random.nextInt(additionalLanguages.size)]
val additionalLangFields: List<MappingField<String>> = listOfNotNull(
    QuestionDoc.title.getFieldByName(userLang),
    QuestionDoc.text.getFieldByName(userLang),
)
val langQuery = if (additionalLangFields.isNotEmpty()) {
    boostedQuery.queryNode(langHandle) { node ->
        node.copy(
            queries = node.queries + listOf(
                Bool.should(
                    *additionalLangFields.map { it.match(searchTerm) }.toTypedArray()
                )
            )
        )
    }
} else {
    boostedQuery
}