MongoDB Indexes With Spring Data
Look at annotations that can be applied to a class to create indexes that have been specified within the class. See what indexes do and how they can decrease query times.
Join the DZone community and get the full member experience.
Join For FreeWhen working with large amounts of data, the use of indexes will greatly improve the time it takes for your queries to run by storing part of a collection’s data in a form that is easy to traverse. To add some indexes to your collections, you could run some functions directly via the Mongo Shell — or Spring Data can be used to handle it for you. As the title suggests, that's what we will be looking into in this post.
Let's start with some background information about why we should use indexes. As mentioned in the introduction indexes allows us to query vast amounts of data in a more efficient way which reduces the time taken to retrieve the results. This might seem negligible with smaller sets of data but as the size of documents and collections increase this time difference between having indexes or not is definitely recognizable.
Now let's get to what this post is about, applying indexes to documents using Spring Data. This is done through the use of the various index annotations that Spring Data provides for use with MongoDB, which include:
@Indexed
specifies a field that will be indexed by MongoDB.@CompoundIndex
specifies a class that will use compound indexes.@TextIndexed
specifies a field that will part of the text index.@GeoSpacialIndexed
specifies a field that will be indexed using MongoDB’s geospacial indexing feature.
This post will focus on the @Indexed
and @CompoundIndex
annotations.
A very important thing to mention before we go any further is that to use the index annotations within a document class the @Document
annotation needs to be applied. Without this annotation the documents will be created and used correctly but no indexes will be created.
In this post, I used Spring Boot to run and test the code although none of this code will be shown here, the required setup and foundation information needed for this post can be found in Getting started with Spring Data and MongoDB.
@Indexed
This annotation is how we mark a single field as being indexed which is the equivalent to the following MongoDB command.
db.COLLECTION_NAME.createIndex({FIELD_NAME: 1})
Where COLLECTION_NAME
is obviously the name of the collection, which when using Spring Data will be the name of the class that is being used or the name specified in the @Document
annotation that has been applied to the class. FIELD_NAME
is the name of field that the @Indexed
annotation has been applied to.
It also comes with various properties that allow us to control how the index is applied.
background
: When set to true, the index will be applied in the background, allowing read and write operations to occur while the index is being built. The equivalent MongoDB command is:db.COLLECTION_NAME.createIndex({FIELD_NAME:1}, {background: BOOLEAN})
direction
specifies the sort order of the index which is ascending by default. The equivalent MongoDB command is:
...wheredb.COLLECTION_NAME.createIndex({FIELD_NAME:SORT_ORDER})
SORT_ORDER
is 1 forIndexDirection.ASCENDING
and -1 forIndexDirection.DESCENDING
.dropDups
: When set to true, applies a unique index to the first occurrence of a key and removes all subsequent duplicated documents from the collection, although this command was deprecated in MongoDB 3.0.expireAfterSeconds
: Specifies the number of seconds that documents in the collection are retained for. When this property is used the index can be referred to as a TTL (Time-To-Live) index. This property can only be used on fields that represent a date. The equivalent MongoDB command is:db.COLLECTION_NAME.createIndex({FIELD_NAME:1}, {expireAfterSeconds: TIME})
name
: Provides a name for the index; otherwise, the name will be automatically generated to the name of the field. The equivalent MongoDB command is:db.COLLECTION_NAME.createIndex({FIELD_NAME:1}, {name: INDEX_NAME})
sparse
: When true, the index only references documents that contain the indexed field. The equivalent MongoDB command is:db.COLLECTION_NAME.createIndex({FIELD_NAME:1}, {sparse: BOOLEAN})
unique
: When set to true, reject all documents that contain a duplicate value for the indexed field. The equivalent MongoDB command is:db.COLLECTION_NAME.createIndex({FIELD_NAME:1}, {unique: BOOLEAN})
useGeneratedName
: When set to true, it will ignore the given index name from thename
property if provided and use the MongoDB generated name instead, which will look likefieldName_1
.
There are some remaining options that are available via MongoDB directly but not through the Spring Data annotations which will need to be applied manually to the collection via the shell if you wish to use them. These include: v
(the index version number) and weights
(which specifies the significance of an indexed field relative to other indexes).
An import piece of information to note, changing the properties in the annotation when the index has already been created for the collection will cause an exception to occur when Spring Data tries to create the new index. Therefore you might need to drop the original index or question if you are really meant to be changing it in the first place.
Now that we know the properties that are available, let's see them in action. Below there are two code snippets that make up an example that use @Indexed
on a few fields and makes use of some of the properties.
@Document
public class Person {
@Id private String id;
@Indexed(name = "first_name_index", direction = IndexDirection.DESCENDING)
private String firstName;
private String secondName;
@Indexed(name = "expire_after_seconds_index", expireAfterSeconds = 10)
private LocalDateTime dateOfBirth;
private Address address;
private String profession;
private int salary;
// constructor
// getters and setters
}
In the Person document, each index has been given a name. The first_name_index
is sorted in descending order and the expire_after_seconds_index
will cause documents to be removed after being in the collection for 10 seconds (yes, I know you probably wouldn’t actually put this on a date of birth field!).
@Document
public class Address {
@Indexed(name = "address_line_one_index")
private String addressLineOne;
private String addressLineTwo;
private String city;
private String country;
// constructor
// getters and setters
}
In the Address document, the index address_line_one_index
has been created. This is an embedded document that has been used inside the Person
document and will cause its index to be created slightly differently than the earlier piece of code. The index will be placed onto the field address.addressLineOne
where address
is a field in the Person
document and addressLineOne
is a field in the embedded Address document.
To test the use of the indexes I created “some” test data… 100,000 records, to be exact, so I could make the time difference between querying with and without indexes more significant. To check how long it took for the query to execute, I ran a find query with the explain method added on the end.
db.person.find({"firstName":"firstName_2500"}).explain("executionStats")
I don’t personally know anyone called firstName_2500
but it made creating the data much simpler. Anyway, the generated data contained documents that were duplicated four times, for example, 4 documents called firstName_2500
were created. By running the above query, we know that we are looking for four documents in a collection of 100,000… that’s a pretty small percentage of the total documents that we actually want.
When ran without an index on firstName
:
{
... more stats ...
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 4,
"executionTimeMillis" : 111,
"totalKeysExamined" : 0,
"totalDocsExamined" : 100000,
... more stats ...
}
From looking at the statistics, we can see that all 100,000 documents were examined for the query results even though only four were returned. This caused it to take 111 milliseconds, which doesn’t seem like much, but as the collection size keeps increasing, this time will only become greater. For consistency, I ran this query multiple times with execution times varying from 60 to 200 milliseconds.
When ran with an index on firstName
:
{
... more stats ...
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 4,
"executionTimeMillis" : 0,
"totalKeysExamined" : 4,
"totalDocsExamined" : 4,
... more stats ...
}
These execution results look much better. Only four documents were examined, compared to 100,000, which were read without the index and this leads to the execution time being much faster. In this example, it was actually so small it couldn’t display the actual time. I also ran this query multiple times and each time it returned an execution time of 0 milliseconds.
I also ran the query a bit later, some time after the 10-second time to live, which was marked by the expireAfterSeconds
property and no results were returned. So either I did something wrong and deleted the documents myself or the TTL index worked correctly.
That’s probably enough time spent on the @Indexed
annotation, which provides a good basis moving forward. Therefore, some information will be skipped over while explaining the @CompoundIndex
annotation.
@CompoundIndex
This annotation is placed onto a class that represents a document. The equivalent MongoDB command is:
db.COLLECTION_NAME.createIndex({FIELD_NAME_1: 1, FIELD_NAME_2: 1})
The format follows the same as the command for creating a singular index but instead takes in multiple fields. In this example, I have only used two fields but more could be added.
The annotation shares all of the properties that the @Indexed
annotation had available although direction has been deprecated as the sort order is specified in a different property as explained below:
@Document
@CompoundIndex(def = "{'firstName':1, 'salary':-1}", name = "compound_index")
public class Person {
@Id private String id;
private String firstName;
private String secondName;
private LocalDateTime dateOfBirth;
private Address address;
private String profession;
private int salary;
// constructor
// getters and setters
}
As demonstrated by this example, the fields that are added to the compound index are specified by the def
property inside the annotation. If you compare this to the equivalent MongoDB command to add the index manually, you can see that it is virtually the same. As mentioned earlier, the direction property has been deprecated from this annotation since the sort orders are specified within the def
property.
The order that the fields are specified within the def
property are important and represent the order that the index will sort the fields. In the example above, documents are ordered in ascending order of firstName
values and then salary
values in descending order. Another important piece of information is that when manually applying a sort the fields in the sort method must appear in the same order as the index and can only sort on the original sort order or it’s inverse, if these conditions are violated the sort order on the index will not be efficiently used or not used at all.
Therefore, the index in the example could be sorted by the following Mongo queries:
db.person.find({$and: [{"firstName":"firstName_2500"}, {"salary":{$gt:0}}]}).sort({"firstName":1, "salary":-1})
...and:
db.person.find({$and: [{"firstName":"firstName_2500"}, {"salary":{$gt:0}}]}).sort({"firstName":-1, "salary":1})
...but not by:
db.person.find({$and: [{"firstName":"firstName_2500"}, {"salary":{$gt:0}}]}).sort({"firstName":1, "salary":1})
...or:
db.person.find({$and: [{"firstName":"firstName_2500"}, {"salary":{$gt:0}}]}).sort({"salary":-1, "firstName":1})
More information about sorting compound indexes can be found in the MongoDB docs.
If you wanted to add multiple compound indexes to your document class, you will quickly realize that we need to go about it in a different way (can’t have multiple annotations of the same type applied to the class). Thankfully, there is a way around this with the aptly named @CompoundIndexes
annotation, which simply contains a collection of @CompoundIndex
annotations, which work as explained earlier.
There is not much to say about the @CompoundIndexes
annotation, so the example below shows it being added to a document class which will create the indexes when inserted.
@Document
@CompoundIndexes({
@CompoundIndex(def = "{'firstName':1, 'salary':-1}", name = "compound_index_1"),
@CompoundIndex(def = "{'secondName':1, 'profession':1}", name = "compound_index_2")
})
public class Person {
@Id private String id;
private String firstName;
private String secondName;
private LocalDateTime dateOfBirth;
private Address address;
private String profession;
private int salary;
// constructor
// getters and setters
}
I think it’s about time to wrap this post up. In this post, we looked at the @Indexed
and @CompoundIndex
annotations that can be applied to a class that is marked with @Document
which, when inserted, will create the indexes that have been specified within the class. We have also looked briefly at what indexes actually do and how they can decrease query times by a significant amount.
If you enjoyed this article and want to learn more about MongoDB, check out this collection of tutorials and articles on all things MongoDB.
Published at DZone with permission of Dan Newton, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments