Implement Full-Text Search over a GraphQL API in Atlas
Rate this tutorial
GraphQL can be an extremely powerful and efficient way to create APIs, and MongoDB Atlas 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 Atlas for GraphQL in 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 . If you can't map collection fields to a schema from within Atlas and you need to write custom logic, 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 Atlas for these scenarios, but you can leverage GraphQL custom resolvers within Atlas App Services instead.
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.
To be successful with this tutorial, you'll need a couple of things:
The expectation is that your Atlas cluster already has network access rules, users, data, etc., all configured so we can jump right into the GraphQL side of things. We're also expecting that you have an App Services application created, even if it isn't configured. If you need help with configurations, check out on the subject.
At this point, you should at least have a blank Atlas application. To get up and running with GraphQL, we need to accomplish the following:
- Configure authentication methods.
- Define a 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 App Services 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 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 suitable 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:
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 App Services 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.
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 App Services 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
- 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
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 Atlas 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.
Let's break down what's happening for this particular resolver function.
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:
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.
Before we can make use of the function, we need to create an Atlas Search index because we are using the
$search stage in our pipeline.
In the main Atlas dashboard, click "Browse Collections" for the deployment we plan to use. Next, click the "Search" tab where we'll be able to create and manage indexes specific to Atlas Search.
There are a few ways we can go about this index based on the code we have in our aggregation pipeline.
- We can create a very dynamic default index at the cost of performance.
- We can create specific index with static mappings for the fields we plan to use.
We're going to take a look at both, starting with the default index. You can use the JSON editor to add the following:
The above index will map every field, current, or future. If we created just this index and gave it a name of "default," the function and GraphQL would work fine. However, performance would not be optimized so your performance results may vary.
Instead, we could do something like the following:
We know that we are searching on the
name path in our aggregation pipeline and nothing else. The above index reflects that we are doing a static mapping on that one field. This is a better choice.
If you name the index "default," you won't need to update your function code. However, if you name it something else, you can update your function code to the following:
Because we have an Atlas Search index created, the GraphQL custom resolver should be usable.
So how can we confirm it’s working? We can test it in GraphiQL, Postman, or anything else that can make HTTP requests.
From the “GraphQL” tab of the App Services dashboard, visit the “Explore” sub-tab if it isn't already selected.
Include the following in the GraphiQL editor:
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 App Services dashboard, you'd need to use it in your own applications. With Atlas's authentication options, you can utilize Atlas Search via GraphQL in your client-side and backend applications.
You can add a lot of power to your GraphQL APIs with custom resolvers and when done with MongoDB Atlas Functions, you don't even need to deploy your own infrastructure. You can take full advantage of serverless and the entire MongoDB Atlas ecosystem.