user672009 user672009 - 1 month ago 16
Java Question

Picking random entries from mongodb using spring data

I'm trying to fetch x random entries from mongodb using spring.

My repository looks as follows

public interface StoryRepository extends MongoRepository<Story, Long> {
@Query("{$sample: {size: ?0} }")
List<Story> findRandom(int quantity);

And the error I'm getting looks like this

com.mongodb.BasicDBObject cannot be cast to

I've also tried the following which gives exactly the same error

public List<Story> findRandom(final int quantity) {
CustomAggregationOperation customAggregationOperation = new CustomAggregationOperation(new BasicDBObject("$sample", new BasicDBObject("size", quantity)));
TypedAggregation<Story> aggregation = new TypedAggregation<>(Story.class, customAggregationOperation);
AggregationResults<Story> aggregationResults = mongoTemplate.aggregate(aggregation, Story.class);
return aggregationResults.getMappedResults();

My story class looks as follows

public class Story {
private long id;
private String by;
private int descendants;
private List<Long> kids;
private int score;
private long time;
private String title;
private String type;
private String url;

private By author;

public long getId() {
return id;


And my pom files as follows

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="" xmlns:xsi=""


<description>Zmags Hacker News Backend</description>

<relativePath/> <!-- lookup parent from repository -->





Here is the complete project. Note that should be updated with the proper mongodb settings.

Any clues?


Why it fails

If you open spring's QueryMapper.getMappedKeyword (which is the method raising the exception), you'll see :

protected DBObject getMappedKeyword(Keyword keyword, MongoPersistentEntity<?> entity) {
    if (keyword.isSample()) {
        return exampleMapper.getMappedExample(keyword.<Example<?>> getValue(), entity);

public boolean isSample() {
    return "$sample".equalsIgnoreCase(key);

It parses the queries and tries to use Example when it finds the word $sample. That explains your error.

Now the question would be : how to achieve what you want without $sample ?

Or perhaps you need to use Example when using $sample, and thus you need to dig into Example.

(1) Alternative

As an alternative, you could :

  • get a random number in Java (assuming you first retrieve the number of documents in the collection)
  • In an aggregate query
    • limit(randomNumber)
    • sort ascending
    • limit(1)

Anyway you do it: limit with a random number and get the last document. I'm sure that are better solution, but that would still work.

=> db.story.aggregate([{$limit: RANDOM_NUMBER},{$sort: {_id: 1}}, {$limit: 1}])

(2) Trying to use $sample with Example - unsuccessful

The following does not work but seems like a step forward in using Example with $sample:

CustomAggregationOperation customAggregationOperation = new CustomAggregationOperation(new BasicDBObject("$sample", Example.of(new Story())));
TypedAggregation<Story> typedAggr = Aggregation.newAggregation(Story.class, 

AggregationResults<Example> aggregationResults = mongoTemplate.aggregate(typedAggr, Example.class);

Raises this error: Command failed with error 16435: 'A pipeline stage specification object must contain exactly one field.' on server localhost:27017. The full response is { "ok" : 0.0, "errmsg" : "A pipeline stage specification object must contain exactly one field.", "code" : 16435 }

(3) This works - implement a CustomSampleOperation (instead of a generic one)

Simply use the customAggregator operation to return the valid $sample query.

CustomSampleOperation customSampleOperation = new CustomSampleOperation(1);
TypedAggregation<Story> typedAggr = Aggregation.newAggregation(Story.class, 

AggregationResults<Story> aggregationResults = mongoTemplate.aggregate(typedAggr, Story.class);


public class CustomSampleOperation implements AggregationOperation {
    private int size;
    public CustomSampleOperation(int size){
        this.size = size;   

    public DBObject toDBObject(final AggregationOperationContext context){
        return new BasicDBObject("$sample", new BasicDBObject("size", size));

If you look at how other Operations are written, we're right on (LimitOperation):

public class LimitOperation implements AggregationOperation {
    private final long maxElements;
    public LimitOperation(long maxElements) {
        this.maxElements = maxElements;

    public DBObject toDBObject(AggregationOperationContext context) {    
        return new BasicDBObject("$limit", maxElements);

Plus, this is how it was implemented by someone else who requested $sample support:

(4) To keep the CustomOperation generic

To keep your CustomOperation generic, you could define it like this:

CustomGenericOperation customGenericOperation = 
    new CustomGenericOperation(new BasicDBObject("$sample", new BasicDBObject("size", 1)));    

public class CustomGenericOperation implements AggregationOperation {
    private DBObject dbObject;
    public CustomGenericOperation(DBObject dbObject){
        this.dbObject = dbObject;

    public DBObject toDBObject(final AggregationOperationContext context) {
        return dbObject;