Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Implement Full-Text Search over a GraphQL API in MongoDB Atlas

TwitterFacebookRedditLinkedInHacker News

GraphQL can be an extremely powerful and efficient way to create APIs and MongoDB Realm makes it easy by allowing you to connect your collections to GraphQL schemas without writing a single line of code. I wrote about some of the basics behind configuring MongoDB and Realm for GraphQL in an announcement tutorial a while back.

As you find yourself needing to do more advanced things with GraphQL, you’re going to need to familiarize yourself with custom resolvers. If you can’t map collection fields to a schema from within Realm and you need to write custom logic using a serverless function instead, this is where the custom resolvers come into play. Take the example of needing to use an aggregation pipeline within MongoDB. The complex logic that you add to your aggregation pipeline isn’t something you can map. The good news is that you don’t need to abandon MongoDB Realm for these scenarios, but you can leverage Realm’s custom resolvers instead.

In this tutorial we’re going to see how to create a custom resolver that implements Atlas Search for our GraphQL API using Realm Functions, enabling you to add fast, relevant full-text search to your applications.

To be clear, MongoDB will instantly create a GraphQL API for basic CRUD operations without having to create a function or write any code. However, you can easily extend the functionality to do things like full-text search, which is what we’re going to accomplish here.

The Requirements

To be successful with this tutorial, you’ll need a few things:

  • A MongoDB Cloud account, free tier, or higher.
  • A basic understanding of GraphQL, not specific to MongoDB or Realm.

We’ll be using a MongoDB Atlas cluster for our data, a Realm Function for our custom resolver, and some basic Realm configurations for our API. All of this can be accessed from your MongoDB Cloud account.

The expectation is that your Atlas cluster already has network access rules, users, data, etc., all defined and configured so we can jump right into the GraphQL side of things. We’re also expecting that you have a Realm application created, even if it isn’t configured. If you need help with configurations, check out this tutorial on the subject.

Configuring Realm to use GraphQL

At this point, you should at least have a blank slate in terms of a Realm application. To get up and running with GraphQL, we need to accomplish the following:

  • Configure authentication methods.
  • Define a Realm JSON Schema.
  • Establish access rules for authenticated users.

It might sound like a lot of work, but our configuration is mostly point and click.

Within your Realm application, navigate to the “Authentication” tab. From this dashboard, you’re going to want to enable “API Keys” and create a new API key.

The name of your API key is not too important for this example, but it could be if you’re in production and need to keep track of your keys.

After making note of your API key, click the “Schema” tab from within the Realm dashboard.

If you don’t already have a schema defined for the collection that you plan to use, click “Configure Collection” and then “Generate Schema” from within the “Schema” sub-tab. Generating a schema will analyze your collection and create a Realm schema for what it finds in the sample data. You can also define your own schema if you don’t want it automatically generated.

It’s not too important for this example, but my schema looks something like this:

{
    "title": "recipe",
    "properties": {
        "_id": {
            "bsonType": "objectId"
        },
        "ingredients": {
            "bsonType": "array",
             "items": {
                "bsonType": "string"
            }
        },
        "name": {
            "bsonType": "string"
        }
    }
}

The documents in my collection have three fields—a recipe name, a string-based array of ingredients, and a document id.

The final step to configure is the rules on who can access the data from your API.

Within the Realm dashboard, click the “Rules” tab.

Find the collection you would like to apply a rule to and then make the default rule read-only by selecting the checkboxes. In a production scenario that is outside of the scope of this example, you’ll probably want better-defined rules.

At this point, we can work on our custom resolver.

Creating a GraphQL Custom Resolver Function within Realm

The GraphQL API for our collection should work as of now. You can test it with GraphiQL from the “GraphQL” tab or using your GraphQL client of choice.

From the “GraphQL" tab of the Realm dashboard, click the “Custom Resolvers” sub-tab. Click the “Add a Custom Resolver” button to be brought to a configuration screen.

From this screen we’re going to configure the following:

  • GraphQL Field Name
  • Parent Type
  • Function
  • Input Type
  • Payload Type

The GraphQL field name is the field that you’ll be using to query with. Since we are going to do an Atlas Search query, it might make sense to call it a search field.

The parent type is how we plan to access the field. Do we plan to access it as just another field within our collection, do we plan to use it as part of a mutation for creating or updating documents, or do we plan to use it for querying? Because we want to provide a search query, it makes sense to make the parent type a Query as the choice.

The function is where all the magic is going to happen. Choose to create a new function and give it a name that works for you. Before we start writing our custom resolver logic, let’s complete the rest of the configuration.

The input type is the type of data that we plan to send to our function from a GraphQL query. Since we plan to provide a text search query, we plan to use string data. For this, choose Scalar Type and String when prompted. Search queries aren’t limited to strings, so you could also use numerical or temporal if needed.

Finally, we have the payload type which is the expected response from the custom resolver. We’re searching for documents so it makes sense to return however many of that document type come back. This means we can choose Existing Type (List) because we might receive more than one, and [Recipe] for the type. In my circumstance recipe is the name of my collection and Realm is referring to it as Recipe within the schema. Your existing type might differ.

We need to add some logic to the custom resolver function now.

Add the following JavaScript code:

exports = async (query) => {
    const cluster = context.services.get("mongodb-atlas");
    const recipes = cluster.db("food").collection("recipes");
    const result = await recipes.aggregate([
        {
            "$search": {
                "text": {
                    "query": query,
                    "path": "name",
                    "fuzzy": {
                        "maxEdits": 2,
                        "prefixLength": 2
                    }
                }
            }
        }
    ]).toArray();
    return result;
};

Let’s break down what’s happening for this particular resolver function.

const cluster = context.services.get("mongodb-atlas");
const recipes = cluster.db("food").collection("recipes");

In the above code, we are getting a handle to our recipes collection from within the food database. This is the database and collection I chose to use for this example so yours may differ depending on what you did for the previous steps of this tutorial.

Next, we run a single-stage aggregation pipeline:

const result = await recipes.aggregate([
    {
        "$search": {
            "text": {
                "query": query,
                "path": "name",
                "fuzzy": {
                    "maxEdits": 2,
                    "prefixLength": 2
                }
            }
        }
    }
]).toArray();

In the above code, we are doing a text search using the client-provided query string. Remember we defined an input type in the previous step. We’re searching the name field for our documents and we’re using a fuzzy search.

The results are transformed into an array and returned to the GraphQL client.

Testing the GraphQL API and Atlas Search Query

So how can we confirm it is working? We can test it in GraphiQL, Postman, or anything else that can make HTTP requests.

From the “GraphQL” tab of the Realm dashboard, visit the “Explore” sub-tab if it isn’t already selected.

Include the following in the GraphiQL editor:

query {
    recipes {
        _id
        ingredients
        name
    }
    search(input:"chip") {
        name
    }
}

The first recipes query will return all documents in the collection while the second search query will return whatever is found in our function.

While we didn’t use the API key when using the GraphiQL editor that was included in the Realm dashboard, you’d need to use it in your own applications. With Realm’s authentication options you can utilize Atlas Search via GraphQL in your client-side and backend applications.

Conclusion

You can add a lot of power to your GraphQL APIs with custom resolvers and when done with MongoDB Realm, you don’t even need to deploy your own infrastructure. You can take full advantage of serverless and the entire Realm and MongoDB ecosystem.

Don’t forget to stop by the MongoDB Community Forums to see what everyone else is doing with GraphQL!

This content first appeared on MongoDB.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.