MongoDB Full Text Search vs. Regular Expressions

Today we would like to introduce you to the new MongoDB Full Text Search and compare its capabilities and performance with simple regular expressions, which are currently state of the art for searching in MongoDB. We will provide code snippets explaining how to use both features in a Java application as well as an empirical performance evaluation.

What is MongoDB Full Text Search?

MongoDB Full Text Search is a new feature in MongoDB 2.4. However, up to now it is in beta state and not recommended to use in production systems.

Why not continue to use regular expressions?

Basically, there are two major reasons. First, regular expressions have their natural limitations because they lack any stemming functionality and cannot handle convenient search queries such as “action -superhero” in a trivial way. Second, they cannot use traditional indexes which makes queries in large datasets really slow.

Nevertheless, searching via regular expressions is really easy to implement using Spring Data as demonstrated by the following code snippet:

import org.springframework.data.mongodb.core.query.*;
import org.springframework.data.mongodb.core.MongoOperations;

public List<Movie> searchInDescription(String searchString, int limit, int offset) {
   Criteria criteria = Criteria.where("description").regex(searchString);
   Query query = Query.query(criteria);
   // apply pagination, sorting would also be specified here
   query.limit(limit);
   query.skip(offset);
   return mongoOperations.find(query, Movie.class);
}

How to use the MongoDB Full Text Search?

Unfortunately, this is a little harder as Spring Data does not yet support the feature. For implementing our own solution, we have to understand how a full text search can be executed in the Mongo shell:

> db.collection.runCommand("text", {search:"action", project:{"_id":1}})
{ "queryDebugString" : "day||||||",
  "language" : "english",
  "results" : [
     { "score" : 0.5089285714285714,
       "obj" : {
          "_id" : ObjectId("51c175a20364281420b1d17d")
          }
     }
  ],
  "stats" : {
     "nscanned" : 1,
     "nscannedObjects" : 0,
     "n" : 1,
     "nfound" : 1,
     "timeMicros" : 77
  },
  "ok" : 1
}

The command returns a single json document with all objects that match the query and some statistics on the search that has just been executed. Translating this into Java code that extracts the IDs of all matches works as follows.

import com.mongodb.*;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.MongoOperations;

public Collection<ObjectId> findMatchingIds(String searchString) {
   CommandResult result = executeFullTextSearch(searchString);
   return extractSearchResultIds(result);
}

private CommandResult executeFullTextSearch(String searchString) {
   BasicDBObject textSearch = new BasicDBObject();
   textSearch.put("text", Movie.COLLECTION_NAME);
   textSearch.put("search", searchString);
   textSearch.put("limit", SEARCH_LIMIT); // override default of 100
   textSearch.put("project", new BasicDBObject("_id", 1));
   return mongoOperations.executeCommand(textSearch);
}

private Collection<ObjectId> extractSearchResultIds(CommandResult result) {
   Set<ObjectId> objectIds = new HashSet<ObjectId>();
   BasicDBList resultList = (BasicDBList) commandResult.get("results");
   Iterator<Object> it = resultList.iterator();
   while (it.hasNext()) {
      BasicDBObject resultContainer = (BasicDBObject) it.next();
      BasicDBObject resultObj = (BasicDBObject) resultContainer.get("obj");
      ObjectId resultId = (ObjectId) resultObj.get("_id");
      objectIds.add(resultId);
   }
   return objectIds;
}

Note that there is no indicator for the field to search in! As MongoDB supports only one text index per collection this information is implicitly specified after defining it in the shell

db.collection.ensureIndex({"description":"text"})

or from the Java application

mongoOperations.getCollection(Movie.COLLECTION_NAME)
   .ensureIndex(new BasicDBObject("description", "text"));

In order to provide search results with pagination and custom sorting for the application’s UI layer, we need another standard Spring Data query that does exactly that.

import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.query.*;
import org.springframework.data.mongodb.core.MongoOperations;

public List<Movie> searchInDescription(String searchString, int limit, int offset) {
   Collection<ObjectId> searchResultIds = findMatchingIds(searchString);
   Criteria criteria = Criteria.where("_id").in(searchResultIds);
   Query query = Query.query(criteria);
   // apply pagination, sorting would also be specified here
   query.limit(limit);
   query.skip(offset);
   return mongoOperations.find(query, Movie.class);
}

This two-step approach ensures we can use all the functionality of a regular MongoDB query (sorting, pagination, additional criteria, …) while taking advantage of MongoDB’s current full text search implementation.

What are limitations of the MongoDB Full Text Search?

The full text search does not work properly for really large datasets as all matches are returned as a single document and the command does not support a “skip” parameter to retrieve results page-by-page. Despite of projecting to nothing but the “_id” field a huge set of matches will not be returned in its entirety if the result exceeds Mongo’s 16MB per document limit.

How does the MongoDB Full Text Search perform compared to regular expressions?

To get a feeling how fast the MongoDB Full Text Search works in different cases, we built a small demo application which imports data from The Movie Database and displays them in a list. Entering a search term in the search field, one can decide to run it with or without the MongoDB Full Text Search. The time results in ms are printed to the console.

For our example we imported 100,000 movies and searched for three different words, always retrieving the first page with up to 15 entries but counting the number of all matches (for calculating the number of required pages):

– “movie” which delivers 3,533 matches with the full text search and 3,317 with regular expressions (the number differs due to the full text search’s stemming functionality)

– “newspaper” which delivers 318 matches with the full text search and 320 with regular expressions

– “Mayzie” which delivers 2 matches in both cases

The following bar chart illustrates the corresponding performances for counting the results and retrieving them:

MongoDB Full Text Search vs. Regular Expressions

The chart indicates two major trends. The regular expression search takes longer for queries with just a few results while the full text search gets faster and is clearly superior in those cases. Why is that?

Let’s explain the results for searching with regular expressions first. The time for counting the number of matches among the 100,000 entries is pretty consistent at about 200ms. Obviously, this is the time required to scan the entire collection document by document as no index can be used here. On the other hand, the time to retrieve one page goes up tremendously for a smaller number of results. This is due to the fact that MongoDB uses an index for iterating over all documents in the correct sorting order and can stop immediately as soon as 15 entries for the first page have been found. For a query with about 3,500 matches in 100,000 documents (“movie”) the expected value of documents to be scanned is only 15/0.035=428.6 while the entire collection needs to be checked for a very rare search term such as “Mayzie”.

Explaining the full text search performance is quite straightforward. In this case, MongoDB can use an index and hence the query is always efficient. It only requires slightly more time for an increasing number of matches. An important issue to understand here is that the time to retrieve the first page includes executing the full text search, extracting all result IDs in the application and running another standard query as explained above. The time required for the extraction step increases linearly with the number of matches which is the major reason for the rise of retrieval time for bigger result sets even though only 15 entries are returned.

What do you need to run this demo yourself?

Our demo application uses Spring, Spring Data, Apache Wicket, Gradle and MongoDB.

To get started download the code from https://github.com/comsysto/mongo-full-text-search-movie-showcase.

To start your MongoDB with full text search enabled, shutdown your mongod if it’s currently running and then command:

mongod --setParameter textSearchEnabled=true

Alternatively, you can add this line to your mongodb.conf file for permanently enabling the feature (not recommended in a production environment):

setParameter = textSearchEnabled=true

If you haven’t installed gradle, follow this manual. Then command

gradle clean build

To start the application command

gradle jettyRun

What may help if you have problems?

If the full text search is not properly configured you will always obtain an empty result list no matter which term you were searching for. Additionally, the message “### MongoDB Full Text Search does not work properly – cannot retrieve any results.” will be printed to the console.

This behavior can have multiple causes:

  1. You haven’t started your mongod server with the textSearchEnabled=true option as described above.
  2. You have specified more than one text index for the collection which cannot be handled by MongoDB. You can look this up by calling the following in a mongo shell:
    use movie 
    db.movie.getIndexes()

How can this demo be extended?

Starting with this little demo you can extend it as you wish to. If you are using The Movie Database, please create your own account here and use your own API key.

Any questions?

If you have any feedback, please write to Christan.Kroemer@comsysto.com or Elisabeth.Engel@comsysto.com!

Advertisements

6 thoughts on “MongoDB Full Text Search vs. Regular Expressions

    1. E_Engel Post author

      Dear Abhi,

      your are right, the regex can use indexes, but it might not be very efficient, especially if the regex has no “prefix expressions”.

      Thanks for the correction!

      Reply
  1. Pingback: Mongoose.js Find With Full Text Search - SnipRef

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s