Making Custom Features Searchable

To make your custom feature searchable you need to make an IndexBuilderProvider that can add your content to the search index. You do this by inheriting from mojoPortal.Business.WebHelpers.IndexBuilderProvider and then implementing the override for 2 methods, ContentChangedHandler and RebuildIndex.

All you do in these methods is create instances of IndexItem, set properties on it from the content of your feature and pass it to IndexHelper to actually add it to the search index.

You make your business class implement IIndexableContent which simply defines an event that will be fired when your content is created updated or deleted. So for example lets consider the business class mojoPortal.Business.HtmlContent, defined in its source like this:

public class HtmlContent : IIndexableContent

Implementing IIndexable content only requires defining this event:

#region IIndexableContent

public event ContentChangedEventHandler ContentChanged;

protected void OnContentChanged(ContentChangedEventArgs e)
{
if (ContentChanged != null)
{
ContentChanged(this, e);
}
}

#endregion

Then in your create, update, and delete methods you just fire the event at the end of your method like this:

ContentChangedEventArgs e = new ContentChangedEventArgs(); OnContentChanged(e);

in your delete method you indicate the deletion like this:

ContentChangedEventArgs e = new ContentChangedEventArgs();
e.IsDeleted = true;
OnContentChanged(e);

Now getting back to the IndexBuilderProvider,

ContentChangedHandler is an event handler that you will attach to the ContentChangedEvent which you will fire in your business object whenever content is updated. For example, in the HtmlEdit.aspx page we attach like this:

HtmlContent html = new HtmlContent();
IndexBuilderProvider indexBuilder = IndexBuilderManager.Providers["HtmlContentIndexBuilderProvider"];
if (indexBuilder != null)
{
html.ContentChanged += new ContentChangedEventHandler(indexBuilder.ContentChangedHandler);
}
html.Body = edContent.Text;
html.Save();

Now after saving the event will fire and the ContentChangedHandler will be called on the HtmlContentIndexBuilderProvider. Notice how we retieved it by name from the IndexBuilderManager.Providers collection. In order to add your provider to the collection you need to create a config file for your IndexBuilderProvider and drop it in the Web/Setup/indexbuilderconfig folder. This is simply an xml file with a .config extension, it doesn't matter what its named. The contents specify the type and assembly of your provider as shown in this example:

<?xml version="1.0" encoding="utf-8" ?>
<IndexBuilderProviders>
<providers>

<add name="HtmlContentIndexBuilderProvider"
type="mojoPortal.Business.WebHelpers.HtmlContentIndexBuilderProvider, mojoPortal.Business.WebHelpers"
description="An IndexBuilder to index Html module content for Lucene.NET Search Engine" />


</providers>
</IndexBuilderProviders>

Lets look at how this is impemented in the HtmlContentIndexBuilderProvider:

public override void ContentChangedHandler(
object sender,
ContentChangedEventArgs e)
{
HtmlContent content = (HtmlContent)sender;
if (e.IsDeleted)
{
// get list of pages where this module is published
List<PageModule> pageModules
= PageModule.GetPageModulesByModule(content.ModuleID);

foreach (PageModule pageModule in pageModules)
{
IndexHelper.RemoveIndexItem(
pageModule.PageID,
content.ModuleID,
content.ItemID);
}
}
else
{
IndexItem(content);
}

}

private static void IndexItem(HtmlContent content)
{
SiteSettings siteSettings
= CacheHelper.GetCurrentSiteSettings();

if (
(content == null)
|| (siteSettings == null)
)
{
return;
}

Guid htmlFeatureGuid
= new Guid("881e4e00-93e4-444c-b7b0-6672fb55de10");
ModuleDefinition htmlFeature
= new ModuleDefinition(htmlFeatureGuid);
Module module = new Module(content.ModuleID);

// get list of pages where this module is published
List<PageModule> pageModules
= PageModule.GetPageModulesByModule(content.ModuleID);

foreach (PageModule pageModule in pageModules)
{
PageSettings pageSettings
= new PageSettings(
siteSettings.SiteID,
pageModule.PageID);

IndexItem indexItem = new IndexItem();
indexItem.SiteID = siteSettings.SiteID;
indexItem.PageID = pageSettings.PageID;
indexItem.PageName = pageSettings.PageName;
indexItem.ViewRoles = pageSettings.AuthorizedRoles;
if (pageSettings.UseUrl)
{
indexItem.ViewPage = pageSettings.Url.Replace("~/", string.Empty);
indexItem.UseQueryStringParams = false;
}

indexItem.FeatureName = htmlFeature.FeatureName;
indexItem.FeatureResourceFile = htmlFeature.ResourceFile;

indexItem.ItemID = content.ItemID;
indexItem.ModuleID = content.ModuleID;
indexItem.ModuleTitle = module.ModuleTitle;
indexItem.Title = content.Title;
indexItem.Content = content.Body;
indexItem.PublishBeginDate = pageModule.PublishBeginDate;
indexItem.PublishEndDate = pageModule.PublishEndDate;

IndexHelper.ReIndex(indexItem);
}

log.Debug("Indexed " + content.Title);

}

The meaningful work is in the private IndexItem method where we actually create an IndexItem and pass it to the IndexHelper.ReIndex method. We store quite a bit of information about the content in the index including the roles that can view the page containing the content. This is so we can filter results of search according to the user's role so its very important. We also store the publish begin and end dates for filtering purposes, we don't want results with links to content that is no longer there. The indexItem.Content and indexItem.Title are the actual searchable content. Whatever you assign will be tokenized into the index for searching whereas the other properties are just stored in the index but not searchable.

Another important setting is the indexItem.ViewPage which is used to build a link to the content in the search results. By default it will be Default.aspx and the pageid will be passed in the query string. In this example we have checked if the page uses a friendly url and if so we use that for the viewpage. This approach works for the HtmlContent because its a simple feature and the content will be on the page containing the module instance. However, in more complex features like the forums or blogs we need to build a different url for the search results because the indexed content is not displayed right on the page. For example in a blog the item is on the page but eventually newer posts push it off the page so we need to build a link to BlogView.aspx and pass the paramters to view the post. This is also true of the Forums in which case we must link to ForumThreadView.aspx. Heres how we do it in the ForumThreadIndexBuilderProvider:

indexItem.ViewPage = "ForumThreadView.aspx";
indexItem.QueryStringAddendum = "&thread="
+ forumThread.ThreadID.ToString()
+ "&postid=" + forumThread.PostID.ToString();

Notice that standard things like pageid, moduleid etc will be automatically added to the query string of the url, but in the forums a few additional params are needed and these are assinged to the QueryStringAddendum

Now we have covered the ContentChangedHandler implementation but we must also implement the RebuildIndex method. In this case a pageSettings object is passed in and we must index any content on the page so the HtmlIndexBuilder for example must find any instances of HtmlContent on the page and index all of them.

public override void RebuildIndex(
PageSettings pageSettings,
string indexPath)
{
if (pageSettings == null)
{
log.Error("pageSettings passed in to HtmlContentIndexBuilderProvider.RebuildIndex was null");
return;
}

log.Info("HtmlContentIndexBuilderProvider indexing page - "
+ pageSettings.PageName);

try
{
Guid htmlFeatureGuid
= new Guid("881e4e00-93e4-444c-b7b0-6672fb55de10");
ModuleDefinition htmlFeature
= new ModuleDefinition(htmlFeatureGuid);

List<PageModule> pageModules
= PageModule.GetPageModulesByPage(pageSettings.PageID);

DataTable dataTable = HtmlContent.GetHtmlContentByPage(
pageSettings.SiteID,
pageSettings.PageID);

foreach (DataRow row in dataTable.Rows)
{
IndexItem indexItem = new IndexItem();
indexItem.SiteID = pageSettings.SiteID;
indexItem.PageID = pageSettings.PageID;
indexItem.PageName = pageSettings.PageName;
indexItem.ViewRoles = pageSettings.AuthorizedRoles;
if (pageSettings.UseUrl)
{
indexItem.ViewPage = pageSettings.Url.Replace("~/", string.Empty);
indexItem.UseQueryStringParams = false;
}

indexItem.FeatureName = htmlFeature.FeatureName;
indexItem.FeatureResourceFile = htmlFeature.ResourceFile;

indexItem.ItemID = Convert.ToInt32(row["ItemID"]);
indexItem.ModuleID = Convert.ToInt32(row["ModuleID"]);
indexItem.ModuleTitle = row["ModuleTitle"].ToString();
indexItem.Title = row["Title"].ToString();
indexItem.Content = row["Body"].ToString();

// lookup publish dates
foreach (PageModule pageModule in pageModules)
{
if (indexItem.ModuleID == pageModule.ModuleID)
{
indexItem.PublishBeginDate = pageModule.PublishBeginDate;
indexItem.PublishEndDate = pageModule.PublishEndDate;
}
}

IndexHelper.ReIndex(indexItem, indexPath);

log.Debug("Indexed " + indexItem.Title);

}
}
catch (Exception ex)
{
log.Error(ex);
}

}

This method will be called whenever the page permissions are changed. Remember we store view permissions in the index so we must keep them up to date any time they change. This method is also called for all pages in the site if the index is being rebuilt from scratch.

In the Edit page for your feature, you wire up the ContentChangedHandler for your feature so it is called upon saving your feature content. Then after saving you call 

SiteUtils.QueueIndexing();

To start the background task that processes the index items from the mp_IndexingQueue table into the Lucene search index in /Data/Sites/[SiteID]/index

Note: To rebuild the search index for the whole site, just delete the contents of the Data/Sites/[SiteID]/index folder then do a search. If no files are found it will launch a new thread and start building the index.

Last Updated 2012-02-29 by Joe Audette

Rate this Content 14 Votes