The content you're reading is getting on in years
This post is on the older side and its content may be out of date.
Be sure to visit our blogs homepage for our latest news, updates and information.
If you haven’t already you should read part one of this series where I describe how to get web API up and running within your Sitefinity project.
Everyone knows that a large part of the user experience is performance. You want your pages to load as fast as possible and while there are many factors in achieving a quick page load, one of them, and the one that’s often missed, is processing large amounts of data. With any search engine this can be an issue as content grows on your site. In order to prevent your search widgets from appearing sluggish or unresponsive you can remove the data queries from the page load and use a technique like the one we will discuss below to query for the search results after the page has already been displayed for the user.
In this instance we are going to use the Sitefinity API to build a web api controller that serves a JavaScript based widget (fitted for Sitefinity’s widget system) that helps return search results. This widget can help improve page load speeds as well as provide you a mechanism for extending your search capabilities on your site. As an example, we are going to build this widget with search by categories in mind.
Building the model
In the case of this widget we are going to build a model. The model will stand to make sure we return only the data we need. We can also use it to collect additional information from our results that is not a part of the IEnumberable<IDocument> object that would normally be returned from a search result.
Our model will just be a class model. It doesn’t inherit from anything. In this case I’ve named it SearchResultsModel.cs and placed it in my models folder we created adjacent to the controllers folder.
namespace
SitefinityWebApp.Data.Models
{
public
class
SearchResultsModel
{
//default properties
public
string
Title {
get
;
set
; }
public
string
Summary {
get
;
set
; }
public
string
Link {
get
;
set
; }
public
int
Count {
get
;
set
; }
public
int
Total {
get
;
set
; }
public
string
Type {
get
;
set
; }
public
SearchResultsModel() { }
public
SearchResultsModel(IDocument searchResult,
string
query,
int
count,
int
total)
{
var sum = searchResult.GetValue(
"Content"
);
this
.Summary = Highlighter(sum, query);
this
.Title = Highlighter(searchResult.GetValue(
"Title"
), query);
this
.Link = searchResult.GetValue(
"Link"
);
this
.Total = total;
this
.Count = count;
this
.Type = TypeResolutionService.ResolveType(searchResult.GetValue(
"ContentType"
)).Name;
}
private
string
Highlighter(
string
summary,
string
query)
{
var newSum = summary;
foreach
(var word
in
query.Split(
'+'
))
{
var regex =
new
Regex(word, RegexOptions.IgnoreCase);
newSum = regex.Replace(newSum,
"<span class='pdSearchHighlight'>"
+ word +
"</span>"
);
}
if
(newSum.Length < 500)
{
return
newSum;
}
else
{
return
newSum.Remove(500) +
"..."
;
}
}
}
}
As you can see the model is fairly small but it has an important job. In the primary method SearchResultsModel(opt + 4) we take the incoming search result, the query, the count of the current document being passed (in order) and the total number of results. We then “map” the values we need to the model’s public variables. This is how we return the data we need in a format we can expect without the extra junk that comes along. Some information we will be returning other than the title/summary/link include the Type of the content item (News, Blogs, etc) as well as the counts. The type is handy to return so we can do extra frontend work such as mapping images to specific types, etc.
You will also notice the Highlight method. This method takes the summary and the query and appends an additional class around matching words. You can then use CSS to highlight these words however you’d like by using the “pdSearchHighlight” class.
Building the controller
Following the techniques demonstrated in my first post we are going to build a new controller that will handle all the data operations of our search.
Here’s what the main method looks like to implement search.
//primary search function
public
List<SearchResultsModel> GetSearchResultsFiltered(
string
query,
string
index,
int
take,
int
skip,
string
filter)
{
//if we are not filtering, use the generic method
if
(filter ==
"null"
|| filter ==
null
|| filter == String.Empty)
{
return
GetSearchResults(query, index, take, skip);
}
else
{
//initialize the search manager and execute the search
var sm = SearchManager.GetManager();
var raw = sm.Find(index, query);
//set content links
var results = raw.SetContentLinks();
var opl =
new
List<SearchResultsModel>();
//get the filters that we need
var categoryArray = BuildFilters(filter);
//pass some information to our model
int
i = skip;
int
x = raw.Count();
foreach
(var doc
in
results)
{
//loop through the results and match items to the category in the filter
//add the item if it matches to our list in the form of our model
i++;
if
(ItemInCategory(doc, categoryArray[
"Categories"
]))
{
opl.Add(
new
SearchResultsModel(doc, query, i, x));
}
}
//return the result to the client
return
opl.Skip(skip).Take(take).ToList();
}
}
It’s first going to check whether or not a filter was passed. If there’s no filter it runs a more generic search method. Otherwise it continues and calls the search manager. The search manager then accepts an index and a query. It returns an IResult set which contains a bunch of IDocuments matching our search.
Our next step is to take our filters and build our categories to search for.
//Build the passed filters for use by Lucene
private
Dictionary<
string
,
string
[]> BuildFilters(
string
query)
{
var newMatch =
new
Dictionary<
string
,
string
[]>();
var parsedQuery = HttpUtility.ParseQueryString(query);
newMatch.Add(
"Categories"
, SetFilterItem(parsedQuery[
"inCategory"
]));
//We can add support for these later
//newMatch.Add("Tags", SetFilterItem(parsedQuery["hasTag"]));
//newMatch.Add("Types", SetFilterItem(parsedQuery["ofType"]));
return
newMatch;
}
Inside this method we initialize a new Dictionary object to store our matches. We then parse the querystring that we passed that contains our filters. We build the dictionary and return it to be used later.
Now that we have our filters we are going to loop through the raw results and find matches. The values x and i are sent to our model for counts. We are going to check if the item is a match by the ItemInCategory method.
//check if the found item is in the category
private
bool
ItemInCategory(IDocument resultItem,
string
[] categories)
{
var type = TypeResolutionService.ResolveType(resultItem.GetValue(
"ContentType"
));
var manager = ManagerBase.GetMappedManager(type.FullName);
var actualItem = manager.GetItem(type, Guid.Parse(resultItem.GetValue(
"Id"
)));
//check for categories
var tm = TaxonomyManager.GetManager();
foreach
(var category
in
categories)
{
category.Replace(
"-"
,
" "
);
var actualCategory = GetCategoryByTitle(category);
if
(actualCategory !=
null
)
{
var taxonId = tm.GetTaxa<HierarchicalTaxon>().Where(t => t.Id == actualCategory.Id).SingleOrDefault().Id;
try
{
if
(actualItem.FieldValue<TrackedList<Guid>>(
"Category"
).Contains(taxonId))
{
return
true
;
}
}
catch
{
//continue;
}
}
}
return
false
;
}
This method uses the Manager base to determine the content type and check if there’s a match on the category. If there’s a match, it returns true. Otherwise we return false which excludes the documents from the actual results we will be returning.
After we loop through all of the results we return the list after implementing our skip/take for paging.
Building the widget
Attached to this project is the full source for you to look at so we will just take a look at the important methods in the javascript for this widget. In this method we parse the query strings and we present the query to Sitefinity as close as possible to the default widget. This is so you can continue to use your default search widget with the async results.
function
getSearchResults(query, index, skip, take) {
if
(getUrlVars()[
"searchQuery"
] !== undefined) {
//if the blogs have been loaded don't load again -> change of new blog during visit is low comp to performance
$(
"#searchLoader"
).show();
$(
'#searchResults'
).empty();
var
total = 0;
var
pages = 1;
var
filter = getUrlVars()[
"inCategory"
];
if
(filter == undefined) {
filter =
"null"
;
}
else
{
filter =
"inCategory="
+ filter
}
$.getJSON(
'/api/searchresults/getsearchresultsfiltered/'
+ query +
"/"
+ index +
"/"
+ take +
"/"
+ skip +
"/"
+ filter,
function
(data) {
// Loop through the list of products.
if
(data.length > 0) {
$.each(data,
function
(key, val) {
var
newItem = $(
"#searchtemplate"
).html().replace(/{ Summary }/g, val.Summary).replace(/{ Link }/g, val.Link).replace(/{ Title }/g, val.Title);
$(newItem)
// Append to the list
.appendTo($(
'#searchResults'
));
$(
"#searchLoader"
).hide();
total = val.Total;
});
if
(total > take) {
pages = Math.round(total / take);
$(
"#pager"
).show();
$(
"#pager"
).html(displayPager(pages));
}
}
else
{
$(
"#searchLoader"
).hide();
var
listItem =
'<li class="pdNoResults">Sorry, no matches were found.</li>'
;
$(listItem)
// Append to the list
.appendTo($(
'#searchResults'
));
}
});
}
else
{
//do something because there was no search.
}
}
You can find the template and the helper classes (for parsing querystrings, etc) in the full source example. The purpose of this function in the JavaScript file is to take the search data and call the web api we built. If it returns data we render the output based on our template. In the .ascx file there is a template that allows you to easily set up your results.
<%-- Template --%>
<
div
id
=
"searchtemplate"
data-type
=
"template"
style
=
"display: none;"
>
<
li
class
=
"sfSearchResultItem"
>
<
div
>
<
a
href
=
"{ Link }"
>{ Title }</
a
>
</
div
>
<
div
>
<
a
class
=
"sfSearchLink"
href
=
"{ Link }"
>{ Link }</
a
>
</
div
>
<
div
>
{ Summary }
</
div
>
</
li
>
</
div
>
View all posts from Patrick Dunn on the Progress blog. Connect with us about all things application development and deployment, data integration and digital business.
Let our experts teach you how to use Sitefinity's best-in-class features to deliver compelling digital experiences.
Learn MoreSubscribe to get all the news, info and tutorials you need to build better business apps and sites