So you've got an awesome query that you're handling with MongoTemplate but it's returning too many results? Maybe you should handle that problem with pagination.

If you're already familiar with pagination in Spring Data MongoDB, then you're probably accustomed to doing it via the MongoRepository interface.

Okay. That works.

But you've decided to go the MongoTemplate route for your current solution. And you're not sure how to handle pagination.

Stick around. I'll show you.

Picking Up Where We Left Off

I'm picking up from my previous guide on how to handle full text searches using MongoTemplate in a Spring Boot application.

If you read that guide, then you know I'm doing this full text search thing for my own blog. As in: the blog that you're reading right now.

I've successfully handled a full text search, but now I need to paginate the results because all good blogs paginate search results.

So here goes.

Updating the Service

Here's the updated BlogService class:

@Service
public class BlogService {

    private static final Logger LOG = LoggerFactory.getLogger(BlogService.class);
 
    
    @Autowired
    private MongoTemplate mongoTemplate;
    
    
    public Page<BlogPost> fullTextSearch(String searchPhrase, Pageable pageable) {
        TextCriteria criteria = TextCriteria
                                    .forDefaultLanguage()
                                    .matchingPhrase(searchPhrase);
        
        Query query = TextQuery
                        .queryText(criteria)
                        .with(pageable)
                        .skip(pageable.getPageSize() * pageable.getPageNumber())
                        .limit(pageable.getPageSize());
        
        List<BlogPost> posts = mongoTemplate.find(query, BlogPost.class);
        long count = mongoTemplate.count(query.skip(-1).limit(-1), BlogPost.class);
        
        Page<BlogPost> resultPage = new PageImpl<BlogPost>(posts, pageable, count);
        
        return resultPage;
    }    
}

Again, the only public method there is fullTextSearch(). It accepts a search string just like in the last guide.

Unlike in the last guide, though, the method now accepts a Pageable object as a second parameter.

That Pageable object contains important info about pagination, such as the number of results per page, the current page, and the sort order.

Next, the code instantiates a TextCriteria object just like last time. No changes there.

The Query object almost gets instantiated like last time. Except this time it uses withPageable() instead of sortByScore().

You see, that Pageable object is going to tell MongoDB how to sort the results. So there's no need to explicitly tell it to sort by score here.

Then the code invokes the skip() method to tell MongoDB how many results to skip.

Remember: the results are paginated. So the code just wants the results for the current page.

The number of results to skip is equal to the page number multiplied by the results per page. Keep in mind: the page numbering starts at 0.

And the limit() method is skip()'s kissing cousin when it comes to pagination. That tells MongoDB to return only a certain number of results at most.

The maximum number of results to return, of course, is equal to the number of results per page.

Next, the code uses the find() method on the MongoTemplate object to execute the query.

But that find() method isn't good enough. That's only going to return a subset of documents.

The application still needs to know the total number of documents in all the pages. Otherwise, it won't be able to handle pagination properly.

So the code invokes the count() method on the MongoTemplate object. But note that it's doing so with a slightly different query.

The skip and limit constraints have been reset so there are no skip and limit constraints. That's important because, as I just mentioned, the application needs to know the total number of documents that match the query or it will have no idea how many total pages are in the set.

Finally, the code instantiates PageImpl (specifically typed to BlogPost) with the results of the query, the Pageable object, and the total count of the results.

Testing It Out

You can run the above method with code that looks like this:

for (int i=0; i<3; i++) {
    Pageable pageable = PageRequest.of(i, 5, Sort.by("score").descending());
    Page<BlogPost> posts = blogService.fullTextSearch("spring boot", pageable);

    System.err.println(posts.getNumber());
    System.err.println(posts.getNumberOfElements());
    System.err.println(posts.getTotalPages());

    posts.forEach(post -> {
        System.err.println(post.getTitle() + " " + post.getScore());
    });
    
    System.err.println("---");
}

That will print out the titles and scores of the documents that matched.

But... it will only print out three pages worth of results with five results per page. 

If you take a look at the line that instantiates Pageable, you'll see it uses one of the more popular instantiation methods these days: of().

That of() method sets the current page (first parameter), the total number of results per page (second parameter), and the sort (third parameter).

By the way, as I teased above, the sort is now handled by the Pageable object. And you can see that in the code.

Wrapping It Up

There you have it. A nice little way to handle pagination manually with Spring Boot and MongoTemplate.

Although I'm sure you're not working with the same kinds of objects I'm using here, you can still adapt what you've learned today to suit your own requirements.

Have fun!

Photo by Wendy van Zyl from Pexels