Searching with IPublishedContentQuery in Umbraco
I recently realized that I don’t think Umbraco’s APIs on IPublishedContentQuery are documented so hopefully this post may inspire some docs to be written or at least guide some folks on some functionality they may not know about.
A long while back even in Umbraco v7 UmbracoHelper was split into different components and UmbracoHelper just wrapped these. One of these components was called ITypedPublishedContentQuery and in v8 is now called IPublishedContentQuery, and this component is responsible for executing queries for content and media on the front-end in razor templates. In v8 a lot of methods were removed or obsoleted from UmbracoHelper so that it wasn’t one gigantic object and tries to steer developers to use these sub components directly instead. For example if you try to access UmbracoHelper.ContentQuery you’ll see that has been deprecated saying:
Inject and use an instance of IPublishedContentQuery in the constructor for using it in classes or get it from Current.PublishedContentQuery in views
and the UmbracoHelper.Search methods from v7 have been removed and now only exist on IPublishedContentQuery.
There are API docs for IPublishedContentQuery which are a bit helpful, at least will tell you what all available methods and parameters are. The main one’s I wanted to point out are the Search methods.
Strongly typed search responses
When you use Examine directly to search you will get an Examine ISearchResults object back which is more or less raw data. It’s possible to work with that data but most people want to work with some strongly typed data and at the very least in Umbraco with IPublishedContent. That is pretty much what IPublishedContentQuery.Search methods are solving. Each of these methods will return an IEnumerable<PublishedSearchResult> and each PublishedSearchResult contains an IPublishedContent instance along with a Score value. A quick example in razor:
@inherits Umbraco.Web.Mvc.UmbracoViewPage
@using Current = Umbraco.Web.Composing.Current;
@{
var search = Current.PublishedContentQuery.Search(Request.QueryString["query"]);
}
<div>
<h3>Search Results</h3>
<ul>
@foreach (var result in search)
{
<li>
Id: @result.Content.Id
<br/>
Name: @result.Content.Name
<br />
Score: @result.Score
</li>
}
</ul>
</div>
The ordering of this search is by Score so the highest score is first. This makes searching very easy while the underlying mechanism is still Examine. The IPublishedContentQuery.Search methods make working with the results a bit nicer.
Paging results
You may have noticed that there’s a few overloads and optional parameters to these search methods too. 2 of the overloads support paging parameters and these take care of all of the quirks with Lucene paging for you. I wrote a previous post about paging with Examine and you need to make sure you do that correctly else you’ll end up iterating over possibly tons of search results which can have performance problems. To expand on the above example with paging is super easy:
@inherits Umbraco.Web.Mvc.UmbracoViewPage
@using Current = Umbraco.Web.Composing.Current;
@{
var pageSize = 10;
var pageIndex = int.Parse(Request.QueryString["page"]);
var search = Current.PublishedContentQuery.Search(
Request.QueryString["query"],
pageIndex * pageSize, // skip
pageSize, // take
out var totalRecords);
}
<div>
<h3>Search Results</h3>
<ul>
@foreach (var result in search)
{
<li>
Id: @result.Content.Id
<br/>
Name: @result.Content.Name
<br />
Score: @result.Score
</li>
}
</ul>
</div>
Simple search with cultures
Another optional parameter you might have noticed is the culture parameter. The docs state this about the culture parameter:
When the
culture
is not specified or is *, all cultures are searched. To search for only invariant documents and fields use null. When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. While enumerating results, the ambient culture is changed to be the searched culture.
What this is saying is that if you aren’t using culture variants in Umbraco then don’t worry about it. But if you are, you will also generally not have to worry about it either! What?! By default the simple Search method will use the “ambient” (aka ‘Current’) culture to search and return data. So if you are currently browsing your “fr-FR” culture site this method will automatically only search for your data in your French culture but will also search on any invariant (non-culture) data. And as a bonus, the IPublishedContent returned also uses this ambient culture so any values you retrieve from the content item without specifying the culture will just be the ambient/default culture.
So why is there a “culture” parameter? It’s just there in case you want to search on a specific culture instead of relying on the ambient/current one.
Search with IQueryExecutor
IQueryExecutor is the resulting object created when creating a query with the Examine fluent API. This means you can build up any complex Examine query you want, even with raw Lucene, and then pass this query to one of the IPublishedContentQuery.Search overloads and you’ll get all the goodness of the above queries. There’s also paging overloads with IQueryExecutor too. To further expand on the above example:
@inherits Umbraco.Web.Mvc.UmbracoViewPage
@using Current = Umbraco.Web.Composing.Current;
@{
// Get the external index with error checking
if (ExamineManager.Instance.TryGetIndex(
Constants.UmbracoIndexes.ExternalIndexName, out var index))
{
throw new InvalidOperationException(
$"No index found with name {Constants.UmbracoIndexes.ExternalIndexName}");
}
// build an Examine query
var query = index.GetSearcher().CreateQuery()
.GroupedOr(new [] { "pageTitle", "pageContent"},
Request.QueryString["query"].MultipleCharacterWildcard());
var pageSize = 10;
var pageIndex = int.Parse(Request.QueryString["page"]);
var search = Current.PublishedContentQuery.Search(
query, // pass the examine query in!
pageIndex * pageSize, // skip
pageSize, // take
out var totalRecords);
}
<div>
<h3>Search Results</h3>
<ul>
@foreach (var result in search)
{
<li>
Id: @result.Content.Id
<br/>
Name: @result.Content.Name
<br />
Score: @result.Score
</li>
}
</ul>
</div>
The base interface of the fluent parts of Examine’s queries are IQueryExecutor so you can just pass in your query to the method and it will work.
Recap
The IPublishedContentQuery.Search overloads are listed in the API docs, they are:
- Search(String term, String culture, String indexName)
- Search(String term, Int32 skip, Int32 take, out Int64 totalRecords, String culture, String indexName)
- Search(IQueryExecutor query)
- Search(IQueryExecutor query, Int32 skip, Int32 take, out Int64 totalRecords)
Should you always use this instead of using Examine directly? As always it just depends on what you are doing. If you need a ton of flexibility with your search results than maybe you want to use Examine’s search results directly but if you want simple and quick access to IPublishedContent results, then these methods will work great.
Does this all work with ExamineX ? Absolutely!! One of the best parts of ExamineX is that it’s completely seamless. ExamineX is just an index implementation of Examine itself so all Examine APIs and therefore all Umbraco APIs that use Examine will ‘just work’.