kt-search

Multi platform kotlin client for Elasticsearch & Opensearch with easily extendable Kotlin DSLs for queries, mappings, bulk, and more.


Project maintained by jillesvangurp Hosted on GitHub Pages — Theme by mattgraham

Geo Spatial Queries

KT Search Manual Previous: Compound Queries Next: Specialized Queries
Github © Jilles van Gurp  

Elasticearch has some nice geo spatial query support that is of course supported in this client.

First, let’s create an index with some documents with a geospatial information:

@Serializable
data class TestDoc(val id: String, val name: String, val point: List<Double>)

client.createIndex(indexName) {
  mappings {
    keyword(TestDoc::id)
    text(TestDoc::name)
    geoPoint(TestDoc::point)
  }
}
val points = listOf(
  TestDoc(
    id = "bar",
    name = "Kommerzpunk",
    point = listOf(13.400544, 52.530286)
  ),
  TestDoc(
    id = "tower",
    name = "Tv Tower",
    point = listOf(13.40942173843226, 52.52082388531597)
  ),
  TestDoc(
    id = "tor",
    name = "Brandenburger Tor",
    point = listOf(13.377622382132417, 52.51632993824314)
  ),
  TestDoc(
    id = "tegel",
    name = "Tegel Airport",
    point = listOf(13.292043211510515, 52.55955614073912)
  ),
  TestDoc(
    id = "airport",
    name = "Brandenburg Airport",
    point = listOf(13.517282872748005, 52.367036750575814)
  )
).associateBy { it.id }

client.bulk(target = indexName) {
  // note longitude comes before latitude with geojson
  points.values.forEach { create(it) }
}

Bounding box searches return everything inside a bounding box specified by a top left and bottom right point.

client.search(indexName) {
  query = GeoBoundingBoxQuery(TestDoc::point) {
    topLeft(points["tegel"]!!.point)
    bottomRight(points["airport"]!!.point)
  }
}.parseHits(TestDoc.serializer()).map {
  it.id
}

You can specify points in a variety of ways:

GeoBoundingBoxQuery(TestDoc::point) {
  topLeft(latitude = 52.0, longitude = 13.0)
  // geojson coordinates
  topLeft(arrayOf(13.0, 52.0))
  // use lists or arrays
  topLeft(listOf(13.0, 52.0))
  // wkt notation
  topLeft("Point (52 13")
  // geohash prefix
  topLeft("u33")
}

Searching by distance is also possible.

client.search(indexName) {
  query = GeoDistanceQuery(TestDoc::point, "3km", points["tower"]!!.point)
}.parseHits(TestDoc.serializer()).map {
  it.id
}

Distance and bounding box searches are of course just syntactic sugar for geo shape queries. Using that, you can query by any valid geojson geometry. Shape is of course using JsonDsl. You can also construct shapes using withJsonDsl

// you can use the provided Shape sealed class
// to construct geometries
val polygon = Shape.Polygon(
  listOf(
    listOf(
      points["tegel"]!!.point,
      points["tor"]!!.point,
      points["airport"]!!.point,
      // last point has to be the same as the first
      points["tegel"]!!.point,
    )
  )
)

client.search(indexName) {
  query = GeoShapeQuery(TestDoc::point) {
    shape = polygon
    relation = GeoShapeQuery.Relation.contains
  }
}

client.search(indexName) {
  query = GeoShapeQuery(TestDoc::point) {
    // you can also use string literals for the geometry
    // this is useful if you have some other representation
    // of geojson that you can serialize to string
    shape(
      """
      {
        "type":"Polygon",
        "coordinates":[[
          [13.0,53.0],
          [14.0,53.0],
          [14.0,52.0],
          [13.0,52.0],
          [13.0,53.0]
        ]]
      }
    """.trimIndent()
    )
    relation = GeoShapeQuery.Relation.intersects
  }
}.parseHits(TestDoc.serializer()).map {
  it.id
}

A recent addition to Elasticsearch 8 is grid_search. Using grid search, you can search by geohash, Uber h3 hexagon ids, or map tiles. Of course this is translated into another geo_shape query.

client.search(indexName) {
  query = GeoGridQuery(TestDoc::point) {
    geotile = "6/50/50"
  }
}
client.search(indexName) {
  query = GeoGridQuery(TestDoc::point) {
    geohex = "8a283082a677fff"
  }
}
client.search(indexName) {
  query = GeoGridQuery(TestDoc::point) {
    geohash = "u33d"
  }
}.parseHits(TestDoc.serializer()).map {
  it.id
}

Fun fact: I contributed documentation to Elasticsearch 1.x for geojson based searches back in 2013. The POI, Wind und Wetter used here refers to a coffee bar / vegan restaurant / and cocktail bar that has been renamed and changed owners several times since then. Back in the day when I was building Localstre.am, my former startup, I was using that as my office while writing that documentation. Somehow, the modern day Elastic documentation still uses this point as the example for geo_shape queries. ;-)


KT Search Manual Previous: Compound Queries Next: Specialized Queries
Github © Jilles van Gurp