Extending EPiServer search - part 2

Introduction

In previous blog post, I presented a custom search solution based on Lucene.NET which listens to IContentEvents.PublishedContent event add re-indexes all affected pages.

Here's the link: http://dcaric.com/blog/episerver-simple-search-and-shared-blocks

In this blog post, we will see how to extend that search engine and build custom functionality.

Requirements

Let's say we have the following page type:

[ContentType(GUID = "e4391223-9111-4c64-b254-cad9b19e61c2")]
public class BlogPost : PageData, ISearchablePage
{
    [Display(
        GroupName = SystemTabNames.Content,
        Order = 100)]
    public virtual string Title { get; set; }

    [Display(
        GroupName = SystemTabNames.Content,
        Order = 200)]
    [UIHint(UIHint.Textarea)]
    public virtual string DisplayText { get; set; }

    [Display(
        GroupName = SystemTabNames.Content,
        Order = 300)]
    public virtual XhtmlString MainContent { get; set; }

    [Display(
        GroupName = SystemTabNames.Content,
        Order = 400)]
    public virtual string Author { get; set; }

    [Display(
        GroupName = SystemTabNames.Content,
        Order = 500)]
    public virtual string Tags { get; set; }

    [Display(
        GroupName = SystemTabNames.Content,
        Order = 600)]
    public virtual bool ExcludeFromSearch { get; set; }
}

We want to build a functionality for showing latest 5 blog posts.

This is what we need to do:

- Update the search engine to index additional fields for BlogPost page(display text, publish date, etc.)

- Tell SimpleSearch Scheduled Job to use our new logic for indexing PageData

- Create a new method for fetching latest blog posts

Implementation

Derive a new class, called MySearchService, from the SearchService class, as follows:

public class LatestBlogPostSearchHit
{
    public string Url { get; set; }
    public string Title { get; set; }
    public string DisplayText { get; set; }
}

public class LatestBlogPostsSearchResult
{
    public int TotalHits { get; set; }
    public List<LatestBlogPostSearchHit> Hits { get; set; }
}

public class MySearchService : DC.EPi.SimpleSearch.SearchService
{
    public MySearchService(IWebPageParser webPageParser)
        : base(webPageParser)
    {
    }

    public override void IndexPage(PageData pageData)
    {
        // check if the page should be index:
        // - has template
        // - published
        // - visible to everyone
        // etc.
        if (ShouldIndexPage(pageData))
        {
            var document = GetDefaultDocument(pageData);

            // index additional fields for blog posts
            var blogPost = pageData as BlogPost;
            if (blogPost != null)
            {
                // index display text that will be displayed on search results page
                document.Add(new Field("display_text",
                                       blogPost.DisplayText ?? "",
                                       Field.Store.YES,
                                       Field.Index.NOT_ANALYZED));

                // index last published date which is used to get the latest blog posts
                document.Add(new Field("publish_date",
                                       blogPost.Saved.ToString("yyyyMMddhhmmss"),
                                       Field.Store.YES,
                                       Field.Index.NOT_ANALYZED));
            }

            IndexDocumentWithoutCommit(document);
        }
    }

    public LatestBlogPostsSearchResult GetLatestBlogPosts()
    {
        using (var searcher = new IndexSearcher(_luceneDirectory))
        {
            // get only blog posts
            var query = new TermQuery(new Term("type", typeof(BlogPost).FullName));
            // sorted by last published date in DESC order
            var sort = new Sort(new SortField("publish_date", SortField.LONG, true));
            // get the latest 5 blog posts
            var topDocs = searcher.Search(query, null, 5, sort);

            var result = new LatestBlogPostsSearchResult
            {
                TotalHits = topDocs.TotalHits,
                Hits = new List<LatestBlogPostSearchHit>()
            };

            for (int i = 0; i < topDocs.ScoreDocs.Length; i++)
            {
                int luceneDocumentId = topDocs.ScoreDocs[i].Doc;
                var luceneDocument = searcher.Doc(luceneDocumentId);

                var hit = new LatestBlogPostSearchHit
                {
                    Title = luceneDocument.Get("title"),
                    Url = luceneDocument.Get("url"),
                    DisplayText = luceneDocument.Get("display_text")
                };

                result.Hits.Add(hit);
            }

            return result;
        }
    }
}

Create a new initialization module:

[InitializableModule]
[ModuleDependency(typeof(ServiceContainerInitialization))]
public class SearchServiceInitModue : IConfigurableModule
{
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        context.Container.Configure(x =>
        {
            // We want to use MySearchService.IndexPage() method
            // when SimpleSearch Scheduled Job is executed
            x.For<ISearchService>().Use<MySearchService>();
        });
    }

    public void Initialize(InitializationEngine context)
    {
    }

    public void Uninitialize(InitializationEngine context)
    {
    }
}

Re-index the website by running SimpleSearch Scheduled Job and that's it.

var searchService = ServiceLocator.Current.GetInstance<MySearchService>();
var latestBlogPosts = searchService.GetLatestBlogPosts();
comments powered by Disqus