(Quick Reference)

5 Querying

Version: 2023.3.0

5 Querying

5.1 Basic Querying

GORM for MongoDB supports all of the regular methods for executing GORM queries apart from HQL, which is a Hibernate specific query language more appropriate for SQL databases.

If you wish to execute a native MongoDB query you can use the find method that takes a Bson argument. For example:

import com.mongodb.client.FindIterable
import static com.mongodb.client.model.Filters.*
...
FindIterable findIterable = Product.find(eq("title", "coffee"))
findIterable.limit(10)
            .each { Product product ->
    println "Product title $product.title"
}

The find method will return a FindIterable instance that you can then use to further customize via filters, sorting and projections.

For the full MongoDB client model refer to the com.mongodb.client.model package.

The find method will return instances of your domain class for each query. If you wish to instead obtain MongoDB Document instance then you should use the collection property of the domain class:

import com.mongodb.client.FindIterable
import static com.mongodb.client.model.Filters.*
...
Document doc = Product.collection
                        .find(eq("title", "coffee"))
                        .first()

5.2 Querying Indexing

Basics

MongoDB doesn’t require that you specify indices to query, but like a relational database without specifying indices your queries will be significantly slower.

With that in mind it is important to specify the properties you plan to query using the mapping block:

class Person {
    String name
    static mapping = {
        name index:true
    }
}

With the above mapping a MongoDB index will be automatically created for you. You can customize the index options using the indexAttributes configuration parameter:

class Person {
    String name
    static mapping = {
        name index:true, indexAttributes: [unique:true, dropDups:true]
    }
}

You can use MongoDB Query Hints by passing the hint argument to any dynamic finder:

def people = Person.findByName("Bob", [hint:[name:1]])

Or in a criteria query using the query "arguments" method

Person.withCriteria {
        eq 'firstName', 'Bob'
    arguments hint:[1][firstName] }

Compound Indices

MongoDB supports the notion of compound keys. GORM for MongoDB enables this feature at the mapping level using the compoundIndex mapping:

class Person {
    ...
    static mapping = {
        compoundIndex name:1, age:-1
    }
}

As per the MongoDB docs 1 is for ascending and -1 is for descending.

Indexing using the 'index' method

In addition to the convenience features described above you can use the index method to define any index you want. For example:

static mapping = {
    index( [1],[person.address.postCode] [unique:true] )
}

In the above example I define an index on an embedded attribtue of the document. In fact what arguments you pass to the index method get passed to the underlying MongoDB createIndex method.

5.3 Geospacial Querying

MongoDB supports storing Geospacial data in both flat and spherical surface types.

To store data in a flat surface you use a "2d" index, whilst a "2dsphere" index used for spherical data. GORM for MongoDB supports both and the following sections describe how to define and query Geospacial data.

5.3.1 Geospacial 2D Sphere Support

Using a 2dsphere Index

MongoDB’s 2dsphere indexes support queries that calculate geometries on an earth-like sphere.

Although you can use coordinate pairs in a 2dsphere index, they are considered legacy by the MongoDB documentation and it is recommended you store data using GeoJSON Point types.

MongoDB legacy coordinate pairs are in latitude / longitude order, whilst GeoJSON points are stored in longitude / latitude order!

To support this GORM for MongoDB features a special type, grails.mongodb.geo.Point, that can be used within domain classes to store geospacial data:

import grails.mongodb.geo.*
...
class Restaurant {
    ObjectId id
    Point location

    static mapping = {
        location geoIndex:'2dsphere'
    }
}

The Point type gets persisted as a GeoJSON Point. A Point can be constructed from coordinates represented in longitude and latitude (the inverse of 2d index location coordinates!). Example:

Restaurant r = new Restaurant(location: new Point(50, 50))
r.id = "Dan's Burgers"
r.save(flush:true)

Restaurant.findByLocation(new Point(50,50))

Querying a 2dsphere Index

Once the 2dsphere index is in place you can use various MongoDB plugin specific dynamic finders to query, including:

  • findBy…​GeoWithin - Find out whether a Point is within a Box, Polygon, Circle or Sphere

  • findBy…​GeoIntersects - Find out whether a Point is within a Box, Polygon, Circle or Sphere

  • findBy…​Near - Find out whether any GeoJSON Shape is near the given Point

  • findBy…​NearSphere - Find out whether any GeoJSON Shape is near the given Point using spherical geometry.

Some examples:

Restaurant.findByLocationGeoWithin( Polygon.valueOf([ [0, 0], [100, 0], [100, 100], [0, 100], [0, 0] ]) )
Restaurant.findByLocationGeoWithin( Box.valueOf( [[25, 25], [100, 100]] ) )
Restaurant.findByLocationGeoWithin( Circle.valueOf( [[50, 50], 100] ) )
Restaurant.findByLocationGeoWithin( Sphere.valueOf( [[50, 50], 0.06]) )
Restaurant.findByLocationNear( Point.valueOf( 40, 40 ) )
Note that a Sphere differs from a Circle in that the radius is specified in radians. There is a special Distance class that can help with radian calculation.

Native Querying Support

In addition to being able to pass any Shape to geospacial query methods you can also pass a map that represents the native values to be passe to the underlying query. For example:

def results = Restaurant.findAllByLocationNear( [$geometry: [type:'Point', coordinates: [1,7]], $maxDistance:30000] )

In the above example the native query parameters are simply passed to the $near query

5.3.2 Geospacial 2D Index Support

MongoDB supports 2d indexes that store points on a two-dimensional plane. although they are considered legacy and you should use 2dsphere indexes instead.

It is possible to use a MongoDB 2d index by mapping a list or map property using the geoIndex mapping:

class Hotel {
    String name
    List location

    static mapping = {
        location geoIndex:'2d'
    }
}

By default the index creation assumes latitude/longitude and thus is configured for a -180..180 range. If you are indexing something else you can customise this with indexAttributes

class Hotel {
    String name
    List location

    static mapping = {
        location geoIndex:'2d', indexAttributes:[min:-500, max:500]
    }
}

You can then save Geo locations using a two dimensional list:

new Hotel(name:"Hilton", location:[50, 50]).save()

Alternatively you can use a map with keys representing latitude and longitude:

new Hotel(name:"Hilton", location:[lat: 40.739037d, long: 73.992964d]).save()
You must specify whether the number of a floating point or double by adding a 'd' or 'f' at the end of the number eg. 40.739037d. Groovy’s default type for decimal numbers is BigDecimal which is not supported by MongoDB.

Once you have your data indexed you can use MongoDB specific dynamic finders to find hotels near a given a location:

def h = Hotel.findByLocationNear([50, 60])
assert h.name == 'Hilton'

You can also find a location within a box (bound queries). Boxes are defined by specifying the lower-left and upper-right corners:

def box = [[40.73083d, -73.99756d], [40.741404d,  -73.988135d]]
def h = Hotel.findByLocationWithinBox(box)

You can also find a location within a circle. Circles are specified using a center and radius:

def center = [50, 50]
def radius = 10
def h = Hotel.findByLocationWithinCircle([center, radius])

If you plan on querying a location and some other value it is recommended to use a compound index:

class Hotel {
    String name
    List location
    int stars

    static mapping = {
        compoundIndex location:"2d", stars:1
    }
}

In the example above you an index is created for both the location and the number of stars a Hotel has.

5.3.3 GeoJSON Data Models

You can also store any GeoJSON shape using the grails.mongodb.geo.Shape super class:

import grails.mongodb.geo.*
...
class Entry {
    ObjectId id
    Shape shape

    static mapping = {
        shape geoIndex:'2dsphere'
    }
}
...
new Entry(shape: Polygon.valueOf([[[3, 1], [1, 2], [5, 6], [9, 2], [4, 3], [3, 1]]]) ).save()
new Entry(shape: LineString.valueOf([[5, 2], [7, 3], [7, 5], [9, 4]]) ).save()
new Entry(shape: Point.valueOf([5, 2])).save()

And then use the findBy*GeoIntersects method to figure out whether shapes intersect with each other:

assert Entry.findByShapeGeoIntersects( Polygon.valueOf( [[ [0,0], [3,0], [3,3], [0,3], [0,0] ]] ) )
assert Entry.findByShapeGeoIntersects( LineString.valueOf( [[1,4], [8,4]] ) )

5.4 Full Text Search

Using MongoDB 5.0 and above you can create full text search indices.

To create a "text" index using the index method inside the mapping block:

class Product {
    ObjectId id
    String title

    static mapping = {
        index title:"text"
    }
}

You can then search for instances using the search method:

assert Product.search("bake coffee cake").size() == 10
assert Product.search("bake coffee -cake").size() == 6

You can search for the top results by rank using the searchTop method:

assert Product.searchTop("cake").size() == 4
assert Product.searchTop("cake",3).size() == 3

And count the number of hits with the countHits method:

assert Product.countHits('coffee') == 5