A Common Optimizely Unified Search Query in Detail
In my previous post, I showed a unified search query which allows to return different results for your multi-language site. In this blog post I am going to explain in more detail each line of the query so you can understand what is happening. So without further due lets begin.
This is the full code we had in the previous blog. What we do in simple terms is query all FoundationPageData pages in the site which are published in an specific language for an specific query.
public interface ISearchService
{
ContentSearchViewModel SearchContent(FilterOptionViewModel filterOptions);
}
public class SearchService : ISearchService
{
private readonly IClient _findClient;
public SearchService(IClient findClient)
{
_findClient = findClient;
}
public ContentSearchViewModel SearchContent(FilterOptionViewModel filterOptions)
{
var model = new ContentSearchViewModel
{
FilterOption = filterOptions
};
if (!filterOptions.Q.IsNullOrEmpty())
{
_findClient.Conventions.UnifiedSearchRegistry
.ForInstanceOf<FoundationPageData>()
.ProjectTitleFrom(x => x.Heading);
var siteId = SiteDefinition.Current.Id;
var mainLanguage = SearchHelper.GetCurrentLanguage(filterOptions.Lang); // Get current language
var query = _findClient.UnifiedSearchFor(filterOptions.Q, mainLanguage)
.UsingSynonyms()
.TermsFacetFor(x => x.SearchSection)
.FilterFacet("AllSections", x => x.SearchSection.Exists())
.Filter(x => (x.MatchTypeHierarchy(typeof(FoundationPageData)) & (((FoundationPageData)x).SiteId().Match(siteId.ToString())) | (x.MatchTypeHierarchy(typeof(PageData)) & x.MatchTypeHierarchy(typeof(MediaData)))))
.Skip((filterOptions.Page - 1) * filterOptions.PageSize)
.Take(filterOptions.PageSize)
.ApplyBestBets();
//Include images in search results
if (!filterOptions.IncludeImagesContent)
{
query = query.Filter(x => !x.MatchType(typeof(ImageMediaData)));
}
//Exclude content from search
query = query.Filter(x => !(x as FoundationPageData).ExcludeFromSearch.Exists() | (x as FoundationPageData).ExcludeFromSearch.Match(false));
// obey DNT
var doNotTrackHeader = System.Web.HttpContext.Current.Request.Headers.Get("DNT");
if ((doNotTrackHeader == null || doNotTrackHeader.Equals("0")) && filterOptions.TrackData)
{
query = query.Track();
}
if (!string.IsNullOrWhiteSpace(filterOptions.SectionFilter))
{
query = query.FilterHits(x => x.SearchSection.Match(filterOptions.SectionFilter));
}
var hitSpec = new HitSpecification
{
HighlightTitle = filterOptions.HighlightTitle,
HighlightExcerpt = filterOptions.HighlightExcerpt
};
model.Hits = query.StaticallyCacheFor(TimeSpan.FromMinutes(5)).GetResult(hitSpec);
filterOptions.TotalCount = model.Hits.TotalMatching;
filterOptions.TotalPages = (int)Math.Ceiling((double)model.Hits.TotalMatching / filterOptions.PageSize);
}
return model;
}
}
Now, we will go line by line. First, we have the interface which should be added to the structure map initialization module. This interface allows you to create other search service implementations without having to be worried about how it is handled behind the scenes.
It uses the FilterOptionViewModel class to receive the filters from the Api controller and the ContentSearchViewModel class to return the results. You can see those classes implementations in the previous blog post.
public interface ISearchService
{
ContentSearchViewModel SearchContent(FilterOptionViewModel filterOptions);
}
The following line creates a search client which can handle normal search queries using the For method or unified search queries like the one we are going to use.
private readonly IClient _findClient;
Using dependency injection we initialize the find client variable in the constructor.
public SearchService(IClient findClient)
{
_findClient = findClient;
}
Now we will continue with the Search Content method which receives the filter options variable
public ContentSearchViewModel SearchContent(FilterOptionViewModel filterOptions)
We then initialize the model that we are going to return with the filter options variable send to the method in order to keep the information about the query, language, current page, page size, if we will highlight the title and the excerpt, if we are going to include images in the content, and if we going to track data.
var model = new ContentSearchViewModel
{
FilterOption = filterOptions
};
Then we check if the query is not empty and proceed to add a convention for all FoundationPageData pages where we project as title the Heading of the page
if (!filterOptions.Q.IsNullOrEmpty())
{
_findClient.Conventions.UnifiedSearchRegistry
.ForInstanceOf<FoundationPageData>()
.ProjectTitleFrom(x => x.Heading);
Now, we get the site id where the user is in, useful if you are in a multi site solution, and then we get the current language for the query using the lang variable that comes in the filter options variable and the search helper class which you can find it in the previous blog.
var siteId = SiteDefinition.Current.Id;
var mainLanguage = SearchHelper.GetCurrentLanguage(filterOptions.Lang);
Then we create the main query where we send the term, the language and choose some options available for unified search. Pay special attention to the comments in the code for more in deep detail.
var query = _findClient.UnifiedSearchFor(filterOptions.Q, mainLanguage) // Set query and language for unified search
.UsingSynonyms() // Use synonyms for the current query if available
.TermsFacetFor(x => x.SearchSection) // Get the facets in the query for the search section of the page
.FilterFacet("AllSections", x => x.SearchSection.Exists()) // Filter the found facets which exists
.Filter(x => (x.MatchTypeHierarchy(typeof(FoundationPageData)) &
(((FoundationPageData)x).SiteId().Match(siteId.ToString())) |
(x.MatchTypeHierarchy(typeof(PageData)) &
x.MatchTypeHierarchy(typeof(MediaData))))) // Filter all pages which do not belong to the current site, that they inherit from PageData or FoundationPageData or MediaData
.Skip((filterOptions.Page - 1) * filterOptions.PageSize) // For pagination purposes, skip # of pages using page size
.Take(filterOptions.PageSize) // Take n number of items
.ApplyBestBets(); // Apply best bets to get the best results for this query
After that, we continue filtering a little bit more the query. In this case we check if the filter options variable will not include images content and if that is the case we add the filter to remove the items which inherit from ImageMediaData.
//Include images in search results
if (!filterOptions.IncludeImagesContent)
{
query = query.Filter(x => !x.MatchType(typeof(ImageMediaData)));
}
We also have a property ExcludeFromSearch in the FoundationPageData class that allows the user to exclude a page from the results. In this case this line of code filters all pages which have that property in true.
//Exclude content from search
query = query.Filter(x => !(x as FoundationPageData).ExcludeFromSearch.Exists() | (x as FoundationPageData).ExcludeFromSearch.Match(false));
Then, we check if the user is allowing us to track the query and if is allowed we track it with the method Track()
// obey DNT
var doNotTrackHeader = System.Web.HttpContext.Current.Request.Headers.Get("DNT");
if ((doNotTrackHeader == null || doNotTrackHeader.Equals("0")) && filterOptions.TrackData)
{
query = query.Track();
}
We also add another filter if the section filter from the filter options variable is set to something. We filter directly from the hits not from the query itself.
if (!string.IsNullOrWhiteSpace(filterOptions.SectionFilter))
{
query = query.FilterHits(x => x.SearchSection.Match(filterOptions.SectionFilter));
}
We then create a HitSpecification class which can define if the query is going to highlight the title and/or the excerpt in the results. When a title or excerpt is highlighted it will add the html tag <em> to the results.
var hitSpec = new HitSpecification
{
HighlightTitle = filterOptions.HighlightTitle,
HighlightExcerpt = filterOptions.HighlightExcerpt
};
At this point we execute the query with all the previous filters and save the results to the Hits property of the results model. We use the method StaticallyCacheFor to specify for how long the query will be cached if the same query is executed again.
model.Hits = query.StaticallyCacheFor(TimeSpan.FromMinutes(5)).GetResult(hitSpec);
We then add to the filter options the total number of results, and how many pages we have based on the page size.
filterOptions.TotalCount = model.Hits.TotalMatching;
filterOptions.TotalPages = (int)Math.Ceiling((double)model.Hits.TotalMatching / filterOptions.PageSize);
Finally, we send the model with the results we obtained from the query.
}
return model;
And that is it. If you have any question let me know. I hope it will help someone and as always keep learning !!!
Leave a Reply