Lucene, GraphQL and Orchard Core
A brief guide on implementing Search using Orchard Core, Lucene and GraphQL.
Recently I was asked how one would go about enabling search functionality in a front-end single-page-application (SPA) that is powered by Orchard Core CMS.
Now, I already knew that Orchard provides search functionality using Lucene.NET, so I figured its Lucene module probably exposes a REST API that the SPA should be able to call. Sure enough, the documentation confirmed this.
What I didn’t know however, is that the Lucene module integrates with GraphQL, effectively allowing one to query Lucene indices using GraphQL.
Say what?
Yeah that’s right. We can query Lucene indices using GraphQL, right out of the box. GraphQL offers great flexibility when it comes to querying any number of data sources. But the consumer needn’t know nor care about any of that. It only specifies what type it wants to query, and what fields it needs back from it. Be it content items, other APIs, or Lucene indices. All uniformly available through a consistent API with GraphQL.
Let’s see how this works!
Enabling Lucene
The first thing to do before we can query anything, is to make sure that the Lucene feature is enabled. Depending on the recipe you selected during setup, this may or may not be the case already. If you selected the Blog recipe, then Lucene will be enabled for you.
If you’re not sure if Lucene is enabled, go to the Admin Dashboard, then select Configuration → Features from the left menu, and enable the feature called Lucene if it’s disabled.
Setting up a Lucene Query
To demonstrate how you can query Lucene indices, we’ll create a new Query object using the Admin Dashboard that returns all blog posts containing a user-provider search term.
On the left menu, select Search → Queries → All Queries, then select the Add Query button.
A modal dialog appears, showing all available Query Types. Orchard Core being a modular, extensible framework, might have any number of modules installed that adds to the types of queries we can use. We are just interested in creating a Lucene query, so select that one.
On the Lucene query editor screen, enter the following details:
- Name:
BlogPostsQuery
- Schema:
{
"type": "ContentItem/BlogPost"
} - Index:
Search
- Return Content Items:
checked
- Query:
{
"query": {
"bool": {
"must": [
{
"match": {
"Content.ContentItem.FullText": "{{ Term }}"
}
},
{
"term": {
"Content.ContentItem.ContentType": "BlogPost"
}
}
]
}
}
}
The above query is a liquified (templated using Liquid syntax) Lucene query that finds all content items that:
- Have a field called
Content.ContentItem.FullText
containing whatever string value returned by the Liquid variableTerm
(we’ll see how to provide a value for that shortly). - Have term field called
Content.ContentItem.ContentType
having the exact value ofBlogPost
.
This query effectively yields all content items of type BlogPost containing a yet-to-be-defined search term that we need to specify as a parameter.
Make sure to select the Save button before we try out our query.
Testing the Lucene Query
From the left menu, select Search → Queries → All Queries. You’ll see the query we just created, and 3 buttons called Run, Edit and Delete. Select the Run button.
On the next screen, we see our query template, as well as a Parameters field. That’s where we’ll supply the Term
parameter that we defined in the query template. As an example, we’ll try looking for all blog posts containing the word “explore” (which should yield exactly 1 result if you used the Blog recipe).
Now we’ve seen that that works, we’re ready to try out the query using an API call from Postman. After that, we’ll do the same, but using a GraphQL query API call.
Lucene + GraphQL
Finally, it’s time to query the Lucene index using GraphQL, which enables us to specify exactly what fields to be included in the response.
First, we’ll need to enable the GraphQL feature:
Before we see how to invoke the GraphQL endpoint using a HTTP request, let’s first try it out using GraphiQL. From the left menu, select Configuration → GraphiQL.
Notice that the Explorer view contains our BlogPostsQuery query:
Go ahead, and select/enter the following GraphQL query:
query MyQuery($parameters: String!) {
blogPostsQuery(parameters: $parameters) {
markdownBody {
html
}
subtitle
author
displayText
}
}
Notice that I’m passing in a GraphQL variable named $parameters
, so make sure to define that variable in the Query Variables view:
{
"parameters": "{\"Term\": \"explore\"}"
}
When selecting the Play button, you should receive the following response:
{
"data": {
"blogPostsQuery": [
{
"markdownBody": {
"html": "<p>Never in all their history have men been able truly to conceive of the world as one: a single sphere, a globe, having the qualities of a globe, a round earth in which all the directions eventually meet, in which there is no center because every point, or none, is center — an equal earth which all men occupy as equals. The airman's earth, if free men make it, will be truly round: a globe in practice, not in theory.</p>\n<p>Science cuts two ways, of course; its products can be used for both good and evil. But there's no turning back from science. The early warnings about technological dangers also come from science.</p>\n<p>What was most significant about the lunar voyage was not that man set foot on the Moon but that they set eye on the earth.</p>\n<p>A Chinese tale tells of some men sent to harm a young girl who, upon seeing her beauty, become her protectors rather than her violators. That's how I felt seeing the Earth for the first time. I could not help but love and cherish her.</p>\n<p>For those who have seen the Earth from space, and for the hundreds and perhaps thousands more who will, the experience most certainly changes your perspective. The things that we share in our world are far more valuable than those which divide us.</p>\n"
},
"subtitle": "Problems look mighty small from 150 miles up",
"author": "admin",
"displayText": "Man must explore, and this is exploration at its greatest"
}
]
}
}
Now let’s execute a GraphQL query using Postman.
Similar to what we did with the other two APIs, we first need to grant the Anonymous user role the necessary permission. In this case, the Execute GraphQL permission:
Now we can fire a GraphQL request using Postman:
POST /api/graphql HTTP/1.1
Host: localhost:8196
Content-Type: application/graphqlquery {
blogPostsQuery(parameters: "{ \"Term\":\"explore\" }") {
displayText
markdownBody {
html
}
}
}
Which results in the following response:
{
"data": {
"blogPostsQuery": [
{
"displayText": "Man must explore, and this is exploration at its greatest",
"markdownBody": {
"html": "<p>Never in all their history have men been able truly to conceive of the world as one: a single sphere, a globe, having the qualities of a globe, a round earth in which all the directions eventually meet, in which there is no center because every point, or none, is center — an equal earth which all men occupy as equals. The airman's earth, if free men make it, will be truly round: a globe in practice, not in theory.</p>\n<p>Science cuts two ways, of course; its products can be used for both good and evil. But there's no turning back from science. The early warnings about technological dangers also come from science.</p>\n<p>What was most significant about the lunar voyage was not that man set foot on the Moon but that they set eye on the earth.</p>\n<p>A Chinese tale tells of some men sent to harm a young girl who, upon seeing her beauty, become her protectors rather than her violators. That's how I felt seeing the Earth for the first time. I could not help but love and cherish her.</p>\n<p>For those who have seen the Earth from space, and for the hundreds and perhaps thousands more who will, the experience most certainly changes your perspective. The things that we share in our world are far more valuable than those which divide us.</p>\n"
}
}
]
}
}
Summary
We’ve seen how easy it is to enable search functionality and consume the available APIs from Postman using Lucene and GraphQL, allowing us to use a consistent API from our applications.
And the best part? We get all of these features out of the box. No coding required, just a bit of configuration, making your site’s content searchable.