The task
So you finished developing a solution for a client of yours. The solution has gone into production and the editors have put 10.000s of content items in three different websites. You created a news template for the editors. You made it possible for the editor to enter news everywhere in the content tree, as they may want to create different areas for news. Everything works great and the client is very happy. However they noticed that not many visitors navigate to the news sections, so now they want a page with a list of all the latest news from the different sites.
Your project manager says that this is an easy task and tells the client it will take no more than 3 hours to implement. (Aren’t project managers always like that?). You are assigned to the task and after you looked at the content tree, you see that there is no way that you can iterate through all content items and keep performance at an acceptable level. You tell your project manager that you can’t create a simple XSL for this task. You need to use an index. You somehow convince the project manager that this takes a bit longer then first assumed (This is a hypothetical situation).
So… you now have the time to implement the list using an index. The requirements to the task are as follows:
- The list should only contain news items.
- On a news item it is possible to categorize the item using a multilist field, where it is possible to assign globally defined categories. It should be possible to filter the news that is included with these categories.
- As it is a multisite solution it should be possible to define a root node. Only news that are placed under that node should be included in the list.
- There is a content field on the news template. It should be possible to specify a word and only news with that word in the content field should be included in the list.
The Sitecore templates and content hierarchy
The news template has the following fields:
- Title (single-line text)
- Content (Rich text)
- Categories (Multilist)
You now create a template for the news list. On this template you want to be able to define the constraints and filters for the list, so you create a newslist template with the following fields:
- Category (Droplink – for selecting the category filter)
- RootNode (Internal Link – for defining the root node, from which all news should be included)
- Keyword (Single-Line text – used to define a keyword that should be in the content field of the news item)
You have now got your data templates, and you also create a sublayout for presenting the list.
The index definition
In this article I won’t dig into how you define your index in the web.config or how you build a custom indexer so you can index the path. This will be covered in the third article in the series. and you can take a closer look at the Sitecore description of the indexes here
For now all you need to know, is that I created a custom index, where only the news items are included and there is the following fields: Categories, Content, Date and Path. The index is called newsindex.
The sublayout - Newslist
You have created a sublayout for presenting the news. For now it is very simple and just iterates over some items and prints out the title of the news. For this you have created a simple placeholder, to which you add the news titles. The Page_Load event then looks like this:
protected void Page_Load(object sender, EventArgs e)
{
/*First get the constraints from the current item
Normally you should check for null references*/
string category = Sitecore.Context.Item["Category"];
string rootNode = Sitecore.Context.Item["RootNode"];
string keyword = Sitecore.Context.Item["Keyword"];
/*Here goes the logic that will retrieve the news
You will see how it is implemented in a minute*/
NewsRetriever retriever = new NewsRetriever("newsIndex");
Hits news = retriever.GetNews(category, rootNode, keyword);
/* Iterate over all the items and print the title */
for (int i = 0; i < news.Length(); i++ )
{
/* Get a sitecore item from a hit using Sitecore API */
Item newsItem = Sitecore.Data.Indexing.Index.GetItem(news.Doc(i), Sitecore.Context.Database);
/* Add the title to the page*/
ListPlaceholder.Controls.Add(new LiteralControl(newsItem["Title"] + " "));
}
}
This is pretty simple. The fun stuff is about to unravel :)
The NewsRetriever
This class is where all the logic is put. If you plan on implementing something similar, you could consider making it more generic so it would retrieve all types of items. Further you could consider making a better implementation making constraints a generic thing. But for the sake of simplicity, this class is pretty static.
You take a look at the different constraints and soon you discover that you need to use different query types. For the category you decide to use a TermQuery, as the indexed item needs to contain a specific ID. For the rootNode you decide to use a PrefixQuery, as the indexed item path needs to be prefixed with the root path. For the keyword you decide to use a WildcardQuery, as the keyword just need to be present in the content field.
This gives the following methods that create each of the queries:
private WildcardQuery GetWildCardQuery(string field, string keyword)
{
/* First we need to remove any potential wildcard operators from the string */
keyword = keyword.Replace("*", "");
keyword = keyword.Replace("?", "");
/* now we can create the term. We need * on both sides of the keyword, to indicate that it doesn't matter where in the text, the keyword is placed */
Term term = new Term(field, "*" + keyword + "*");
return new WildcardQuery(term);
}
private PrefixQuery GetPrefixQuery(string fieldName, string rootNode)
{
/*Create the term and the Query and return it*/
Term term = new Term(fieldName, rootNode);
return new PrefixQuery(term);
}
private TermQuery GetTermQuery(string fieldName, string termString)
{
/*The TermQuery don't like chars like { and } so we need to remove them*/
termString = termString.Replace("}", "");
termString = termString.Replace("{", "");
/* make the string lowercased */
termString = termString.ToLower();
/* create the term by giving in the fieldname and the termstring. Create the query and return it*/
Term term = new Term(fieldName, termString);
return new TermQuery(term);
}
Now all you got to do is concatenate the queries, so that you can perform the search. You create a method that does this and returns the hits. You call it GetNews:
//The main method for retrieving news, parsing in the different constraints.
public Hits GetNews(string category, string rootNode, string keyword)
{
/*First we need to create a BooleanQuery so we can concatenate the different queries*/
BooleanQuery completeQuery = new BooleanQuery();
if(!String.IsNullOrEmpty(category))
completeQuery.Add(GetTermQuery("category",category), BooleanClause.Occur.MUST);
if (!String.IsNullOrEmpty(rootNode))
completeQuery.Add(GetPrefixQuery("path", rootNode), BooleanClause.Occur.MUST);
if (!String.IsNullOrEmpty(keyword))
completeQuery.Add(GetWildCardQuery("content", keyword), BooleanClause.Occur.MUST);
/* You want the hits sorted by date, so you create the sort to pass on to the search method. This method takes a fieldname as parameter. You pass in the name of the date field.The second parameter tells the sort if it should invert the sort, which is true as it sorts from lowest to highest*/
Sort sort = new Sort("date", true);
/* Now we can perform the search */
Index newsIndex = Sitecore.Context.Database.Indexes["newsIndex"];
IndexSearcher searcher = newsIndex.GetSearcher(Sitecore.Context.Database);
Hits
hits;
try
{
hits = searcher.Search(completeQuery, sort);
}
finally
{
//Allways remember to close the searcher when done
searcher.Close();
}
return
hits;
}
Wow! You just made your first index based constrained list! And it didn’t even take more than 3 hours. You can now goof off for a while and then tell your project manager that you’re done. :)
Read the third part of the article here.