Writing Complex MongoDB Queries Using QueryBuilder
MongoDB provides a lot of query selectors for filtering documents from a collection. Writing complex queries for MongoDB in Java can be tricky sometimes.
Consider below data present in student_marks collection
The same query written in Java will look something like this -
DBObject query = new BasicDBObject();
List<BasicDBObject> andQuery = new ArrayList<BasicDBObject>();
andQuery.add(new BasicDBObject("lname", "Ford"));
andQuery.add(new BasicDBObject("marks.english", new BasicDBObject("$gt", 35)));
query.put("$and", andQuery);
Using MongoDB QueryBuilder we can rewrite above query as -
DBObject query = new QueryBuilder()
.start()
.and(new QueryBuilder().start().put("lname").is("Ford").get(),
new QueryBuilder().start().put("marks.english")
.greaterThan(35).get()).get();
You can see that by using QueryBuilder we can write complex queries with ease. QueryBuilder class provides many methods like and, not, greaterThan, exists, etc. which helps in writing MongoDB queries more efficiently and less prone to error/mistakes.
MongoDB Aggregation pipeline is a framework for data aggregation. It is modelled
on the concept of data processing pipelines. Documents enter a
multi-stage pipeline that transforms the documents into an aggregated
results. It was introduced in MongoDB 2.2 to do aggregation
operations without needing to use map-reduce.
Aggregation Pipeline
The $match and $sort pipeline operators can
take advantage of an index when they occur at the beginning of the
pipeline [Reference].
There are no restrictions on result size as a cursor is returned [Reference].
The output can be returned inline or written to a collection [Reference].
Pipeline stages have a limit of 100MB of RAM. To handle large datasets use allowDiskUse option [Reference].
Aggregation Pipeline have an optimization phase which
attempts to reshape the pipeline for improved performance [Reference].
For most aggregation operations, the
Aggregation Pipeline provides better performance and
more coherent interface. However, map-reduce operations provide
some flexibility that is presently not available in the aggregation
pipeline.
The syntax for aggregation pipeline is
db.collection.aggregate([{<stage>},...])
Stages
The MongoDB aggregation pipeline consists of stages. Each stage transforms the
documents as they pass through the pipeline. Pipeline stages do not
need to produce one output document for every input document; e.g.,
some stages may generate new documents or filter out documents.
Pipeline stages can appear multiple times in the pipeline.
Various stage operators supported by MongoDB are listed below-
Returns an ordered stream of documents based on the proximity to a
geospatial point. Incorporates the functionality of $match,
$sort, and $limit for geospatial data. The
output documents include an additional distance field and can
include a location identifier field.
Groups input documents by a specified identifier expression and
applies the accumulator expression(s), if specified, to each group.
Consumes all input documents and outputs one document per each
distinct group. The output documents only contain the identifier
field and, if specified, accumulated fields.
Passes the first n documents unmodified to the pipeline
where n is the specified limit. For each input document, outputs
either one document (for the first n documents) or zero documents
(after the first n documents).
Filters the document stream to allow only matching documents
to pass unmodified into the next pipeline stage. $match
uses standard MongoDB queries. For each input document, outputs
either one document (a match) or zero documents (no match).
Reshapes each document in the stream by restricting the content for
each document based on information stored in the documents
themselves. Incorporates the functionality of $project
and $match. Can be used to implement field level
redaction. For each input document, outputs either one or zero
document.
Skips the first n documents where n is the specified skip number
and passes the remaining documents unmodified to the pipeline. For
each input document, outputs either zero documents (for the first n
documents) or one document (if after the first n documents).
Reorders the document stream by a specified sort key. Only the order
changes; the documents remain unmodified. For each input document,
outputs one document.
Deconstructs an array field from the input documents to output a
document for each element. Each output document replaces the array
with an element value. For each input document, outputs n documents
where n is the number of array elements and can be zero for an
empty array.
Different expressions supported by MongoDB are listed here.
In MongoDB there are multiple guarantee levels available for reporting the success of a write operation, called Write Concerns. The strength of the
write concerns determine the level of guarantee. A weak Write Concern has better performance at the cost of lesser guarantee, while a strong Write Concern has higher guarantee as clients wait to confirm the write operations.
MongoDB provides different levels of write concern to better address
the specific needs of applications. Clients may adjust write concern to
ensure that the most important operations persist successfully to an
entire MongoDB deployment. For other less critical operations, clients
can adjust the write concern to ensure faster performance rather than
ensure persistence to the entire deployment.
Write Concern Levels
MongoDB has the following levels of conceptual write
concern, listed from weakest to strongest:
Unacknowledged
With an unacknowledged write concern, MongoDB does not acknowledge
the receipt of write operations. Unacknowledged is similar to
errors ignored; however, drivers will attempt to receive and handle
network errors when possible. The driver’s ability to detect network
errors depends on the system’s networking configuration.
Acknowledged
With a receipt acknowledged write concern, the mongod confirms the receipt of the write operation. Acknowledged write
concern allows clients to catch network, duplicate key, and other
errors. This is default write concern.
Journaled
With a journaled write concern, the MongoDB acknowledges the write
operation only after committing the data to the journal. This
write concern ensures that MongoDB can recover the data following a
shutdown or power interruption.
You must have journaling enabled to use this write concern.
Replica Acknowledged Replica sets present additional considerations
with regards to write concern. The default write concern only requires
acknowledgement from the primary.
With replica acknowledged write concern, you can guarantee that the
write operation propagates to additional members of the replica set.
Write operation to a replica set with write concern level of w:2 or write to the primary and at least one secondary.
Recently I worked on a project which uses MongoDB as source data system and uses R for analysis and MongoDB again for output storage.
In this project we faced a different problem. We were using R to process source data present in MongoDB and if we gave large number of documents for analysis to R it was becoming slower and a bottleneck. To avoid this bottleneck we had to implement processing of a fixed number of documents in R for a batch.
To achieve this we needed some kind of record number in MongoDB, but being a distributed database getting some sequential number in MongoDB was not supported. Also our MongoDB source was getting populated by a distributed real-time stream so implementing some logic on application side was also deterrent.
To have some batchId field for a fixed number of documents in MongoDB we implemented below algorithm : 1. Find for documents which didn't had batchId field. 2. Sort by some timestamp field. 3. Limit the number of documents (say 10000). 4. Append batchId field to documents and save them (get value of batchId from audit table).
MongoDB shell command for this is :
db['collection1'].find({batchId:null}).sort({systemTime:1}).limit(10000).forEach( function (e) { // get value of batchId from audit table e.batchId = 1; db['collection1'].save(e); } );
Using the above code we appeneded batchId to MongoDB documents and picked only current batchId for analysis in R.
MongoDB supports running Hadoop MapReduce programs.
In this post we shall use Enron dataset (more information about the dataset can be found here ) to build a list of the unique sender/recipient pairs, counting how many times each pair occurs.
Download the enron dataset from above link and load it to MongoDB using mongorestore to enron.messages collection.
For this MapReduce program we shall use a custom key which is defined below.
MailPair WritableComparable Class
MailPair is just a simple "POJO" that contains Strings for the from and to values, and implements WritableComparable so that it can be serialized across Hadoop nodes and sorted.
package com.example
public class MailPair implements WritableComparable{
String from;
String to;
public MailPair(){
}
public MailPair(String from, String to){
this.from = from;
this.to = to;
}
public void readFields(DataInput in) throws IOException{
this.from = in.readUTF();
this.to = in.readUTF();
}
public void write(DataOutput out) throws IOException{
out.writeUTF(this.from);
out.writeUTF(this.to);
}
@Override
public boolean equals(Object o) {
if (o instanceof MailPair) {
MailPair mp = (MailPair) o;
return from == mp.from && to == mp.to;
}
return false;
}
@Override
public int compareTo(Object o) {
if(!(o instanceof MailPair)){
return -1;
}
MailPair mp = (MailPair)o;
int first = from.compareTo(mp.from);
if(first != 0) return first;
int second = to.compareTo(mp.to);
if(second != 0) return second;
return 0;
}
}
Mapper Class
The mapper class will get the headers field from each document, parse out the sender from the From field and the recipients from the To field, and construct a MailPair object containing each pair which will act as the key. Then we emit the value 1 for each key.
package com.example
public class EnronMailMapper
extends Mapper
Reducer Class
The reduce class will take the collected values for each key, sum them together, and record the output.
package com.example
public class EnronMailReducer
extends Reducer {
private static final Log LOG = LogFactory.getLog( EnronMailReducer.class );
@Override
public void reduce( final MailPair pKey,
final Iterable pValues,
final Context pContext )
throws IOException, InterruptedException{
int sum = 0;
for ( final IntWritable value : pValues ){
sum += value.get();
}
BSONObject outDoc = new BasicDBObjectBuilder().start().add( "f" , pKey.from).add( "t" , pKey.to ).get();
BSONWritable pkeyOut = new BSONWritable(outDoc);
pContext.write( pkeyOut, new IntWritable(sum) );
}
}
Driver Class
package com.example
public class EnronMail extends MongoTool {
public int run( String[] args ) throws Exception{
final Configuration conf = getConf();
final Job job = new Job( conf, "enron-messgaes" );
job.setJarByClass(EnronMail.class);
job.setMapperClass(EnronMailMapper.class);
job.setReducerClass(EnronMailReducer.class);
job.setInputFormatClass(com.mongodb.hadoop.MongoInputFormat.class);
job.setMapOutputKeyClass(MailPair.class);
job.setMapOutputValueClass(IntWritable.class);
job.setOutputFormatClass(com.mongodb.hadoop.MongoOutputFormat.class);
job.setOutputKeyClass(org.apache.hadoop.io.Text.class);
job.setOutputValueClass(org.apache.hadoop.io.IntWritable.class);
return job.waitForCompletion( true ) ? 0 : 1;
}
public static void main( final String[] pArgs ) throws Exception{
System.exit( ToolRunner.run( new EnronMail(), pArgs ) );
}
}
Compile the jar and save it by the name enron.jar
To run this MapReduce use below command:
hadoop jar enron.jar com.example.EnronMail -Dmongo.input.uri=mongodb://localhost:27017/enron.messages -Dmongo.output.uri=mongodb://localhost:27017/enron.messages_out
After running the MapReduce program we can query enron.messages_out collection to see if it is populated properly.
Connecting Hive to MongoDB Using MongoStorageHandler
Connecting Hive to MongoDB Using MongoStorageHandler
Mongo-hadoop connector supports the creation of MongoDB-based Hive tables and BSON-based Hive tables. Both MongoDB-based Hive tables and BSON-based Hive tables can be:
Queried just like HDFS-based Hive tables.
Combined with HDFS-based Hive tables in joins and sub-queries.
Note - The current mongo-hadoop jars present in mongo-hadoop github doesn't contain MongoStorageHandler class so you need to build the jars yourself.
In this post we shall use MongoStorageHandler to create a MongoDB-based hive table.
Lets create a test.users collection in MongoDB with following data:
Now we shall create test.user table in Hive which is just a metadata created in Hive and the actual data will be stored in MongoDB.
The commands for this are given below:
create schema test;
use test;
CREATE TABLE users (id int, name string, age int)
STORED BY "com.mongodb.hadoop.hive.MongoStorageHandler"
WITH SERDEPROPERTIES('mongo.columns.mapping'='{"id":"_id","name":"name","age":"age"}')
TBLPROPERTIES ( "mongo.uri" = "mongodb://localhost:27017/test.users");
Note - Specifying BSONSerDe Mappings is optional. Without it, all the field names in the Hive table are expected to be the same as in the underlying MongoDB collection. If the exact field names in Hive cannot be found in a MongoDB document, their values in the Hive table will be null.
Now we can query the MongoDB collection in Hive also using HQL.
We shall now try INSERT INTO TABLE command in HQL to see if we can load MongoDB collection from Hive.
Lets create a temporary table in Hive, say users2 with same schema as users table and insert some data to it using a flat file.
cat users.txt
101,Scott,,10
104,Jesse,52
110,Mike,32
CREATE TABLE users2 (id int , name string , age int)
ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
STORED AS TEXTFILE;
LOAD DATA LOCAL INPATH "users.txt" OVERWRITE INTO TABLE test.users2;
Now we shall use INSERT INTO TABLE command to insert data from Hive table users2 to users table.
INSERT INTO TABLE test.mongo_users SELECT id,name,age from test.users2;
There are some limitations of using MongoStorageHandler which are given below:
Using a StorageHandler will mark the table as non-native. Data cannot be loaded into the table directory from a "LOAD DATA" command. Instead, the data can only come from the result of a query.
Files are only stored as a single output file, not one per reduce call. This will cause much lock leasing problems.
INSERT INTO a StorageHandler table behaves as INSERT OVERWRITE.
In some other post I shall write about using BSON files as storage format for Hive tables.