Skip to content

Query Filters

Query filters allow you to describe search query modifications declaratively. It is possible to filter, sort, paginate and build facets using query filters.

Let's describe our new document:

package samples.qf

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

enum class BikeKind {
    BMX, MTB, CITY, ROAD, CYCLOCROSS, GRAVEL, EBIKE;
}

object BikeDoc : Document() {
    val price by float()
    val manufacturer by keyword()
    val model by text()
    val kind by keyword().enum(BikeKind::name)
    val weight by float()
}

Now we can describe query filters for the BikeDoc:

package samples.qf

import dev.evo.elasticmagic.qf.FacetFilter
import dev.evo.elasticmagic.qf.FacetRangeFilter
import dev.evo.elasticmagic.qf.PageFilter
import dev.evo.elasticmagic.qf.QueryFilters
import dev.evo.elasticmagic.qf.SortFilter
import dev.evo.elasticmagic.qf.SortFilterValue

object BikeQueryFilters : QueryFilters() {
    val price by FacetRangeFilter(BikeDoc.price)
    val manufacturer by FacetFilter(BikeDoc.manufacturer)
    val kind by FacetFilter(BikeDoc.kind)
    val weight by FacetRangeFilter(BikeDoc.weight)

    val sort by SortFilter(
        SortFilterValue("price", listOf(BikeDoc.price)),
        SortFilterValue("-price", listOf(BikeDoc.price.desc())),
        SortFilterValue("weight", listOf(BikeDoc.weight)),
    )

    val page by PageFilter()
}

To apply it to a search query you need query filter parameters. They are just a mapping where keys are a pair of filter name and an operation, and values are a list of strings. For example, it could be transformed from http query parameters.

package samples.qf

import dev.evo.elasticmagic.SearchQuery

// You can imagine it could be converted from following http query parameters:
// manufacturer=Giant&manufacturer=Cannondale&
// kind=CITY&kind=CYCLOCROSS&kind=GRAVEL&
// price__lte=2000&
// sort=weight&
// page=2&
val qfParams = mapOf(
    listOf("manufacturer") to listOf("Giant", "Cannondale"),
    listOf("kind") to listOf("CITY", "CYCLOCROSS", "GRAVEL"),
    listOf("price", "lte") to listOf("2000"),
    listOf("sort") to listOf("weight"),
    listOf("page") to listOf("2"),
)

val searchQuery = SearchQuery()

val appliedFilters = BikeQueryFilters.apply(searchQuery, qfParams)

// Now searchQuery is filtered, sorted, paginated and corresponding aggregations
// to calculate facets are added

After executing query we are able to process its results:

package samples.qf

import dev.evo.elasticmagic.doc.DynDocSource
import samples.started.cluster

suspend fun process() {
    val filtersResult = appliedFilters.processResult(
        searchQuery.search(cluster["elasticmagic-samples_bike"])
    )

    val manufacturerFacet = filtersResult[BikeQueryFilters.manufacturer]
    println("Manufacturers:")
    for (manufacturer in manufacturerFacet) {
        val selectedMark = if (manufacturer.selected) "x" else " "
        println("  [$selectedMark] ${manufacturer.value} (${manufacturer.count})")
    }
    println()

    val kindFacet = filtersResult[BikeQueryFilters.kind]
    println("Kinds:")
    for (kind in kindFacet) {
        val selectedMark = if (kind.selected) "x" else " "
        println("  [$selectedMark] ${kind.value} (${kind.count})")
    }
    println()

    val page = filtersResult[BikeQueryFilters.page]
    println("Results:")
    for (hit in page) {
        val source = requireNotNull(hit.source) as DynDocSource

        println("  ${source[BikeDoc.manufacturer]} ${source[BikeDoc.model]} - ${source[BikeDoc.price]}")
    }
    println()
    println("Current page: ${page.page}")
    println("Total pages: ${page.totalPages}")
    println()
}

Run a full-fledged query filters sample

JVM version:

./gradlew :samples:runBikeshop -q --console=plain

Native version:

./gradlew :samples:linkBikeshopDebugExecutableNative
./samples/build/bin/native/bikeshopDebugExecutable/bikeshop.kexe

Both versions support following environment variables:

  • ELASTIC_URL - URL to your Elasticsearch cluster. Default is http://localhost:9200. If you want to use TLS change it to https://localhost:9200. Elasticsearch 8.x and Opensearch 2.x turn on TLS by default.
  • ELASTIC_USER - user for a basic authentication. Default is elastic. Change it to admin if you use Opensearch.
  • ELASTIC_PASSWORD - if this variable is set, basic authentication will be used. Set it to a real password of your cluster or leave empty if your cluster doesn't require authentication. Password for default configuration of Elasticsearch 8.x can be found in logs. Default password for Opensearch 2.x is admin.