Posted in: Comments

So you've read Joels blogpost about Unified Search and want to try it out, great! It's actually — what I think — one of the cooler features of Find when it comes to actual search and not just querying for data. Too bad it's just for EPiServer 7, luckily some of the parts are backported to EPiServer 6.

So what's missing? We have to do most of the plumbing and hook up Find correctly, so everything is indexed, and we also have to add our own filters that resembles EPiServer's FilterForVisitors. All of this could easily be done by EPiServer in a couple of days, but hey, we don't want to wait for that and it's not certain that this ever will be fully backported to EPiServer 6.

First you need to install Find, if you're going with nuget remember to specify the correct version so we don't get the EPiServer 7 Integration. You can find the instructions under My Services > Download .NET Api at find.episerver.com.

If you're using Page Type Builder you can implement the ISearchContent interface in a base class, some of the methods are implemented by default and some we want to override. In my example we're not actually implementing the interface but just overriding some of the properties by adding properties with the same name.

namespace DV.PageTypes
{
    using System;
    using PageTypeBuilder;

    public abstract class PageTypeBase : TypedPageData
    {
        public string SearchTitle
        {
            get { return this.Property.ExistsLocally("PageTitle") && this["PageTitle"] != null ? this["PageTitle"].ToString() : this.PageName; }
        }

        public string SearchHitUrl
        {
            get { return this.ExternalURL(); }
        }

        public string SearchSection
        {
            get
            {
                PageData sectionPage = this;

                while (!sectionPage.ParentLink.CompareToIgnoreWorkID(PageReference.EmptyReference) &&
                       !sectionPage.ParentLink.CompareToIgnoreWorkID(PageReference.StartPage) &&
                       !sectionPage.ParentLink.CompareToIgnoreWorkID(PageReference.RootPage))
                {
                    sectionPage = DataFactory.Instance.GetPage(sectionPage.ParentLink);
                }

                if (!sectionPage.PageLink.CompareToIgnoreWorkID(PageReference.StartPage))
                {
                    return sectionPage.PageName;
                }

                return string.Empty;
            }
        }

        public string SearchHitTypeName
        {
            get { return "Web page"; }
        }

        public string SearchTypeName
        {
            get
            {
                if (this.PageTypeID == 1)
                {
                    return "News";
                }

                if (this.PageTypeID == 2)
                {
                    return "Contact persons";
                }

                return "Other";
            }
        }

        public DateTime? SearchPublishDate
        {
            get { return this.Changed; }
        }

        public DateTime? SearchUpdateDate
        {
            get { return this.StartPublish; }
        }
    }
}

We also have to tell Find which types we want in our Unified Search, in EPiServer 7 PageData and UnifiedFile is added automatically. This is preferredly done in an InitializableModule. Notice that we also adds a filter for each type, more on that later. For UnifiedFiles we can't obviously use a base class, so here we have to go with extension methods. For some reason a property with the name Attachment is added automatically, but Unified Search expects a property with the name SearchAttachment. So first we have to remove the default and add our own.

var fileTypesToIndex = new List<string>
{
    ".txt",
    ".pdf",
    ".doc",
    ".docx",
    ".rtf",
    ".htm",
    ".html",
    ".xls",
    ".xlsx"
};

SearchClient.Instance.Conventions.UnifiedSearchRegistry.Add<PageData>().PublicSearchFilter(c => c.BuildFilter<PageData>().FilterForVisitor<PageData>());
SearchClient.Instance.Conventions.UnifiedSearchRegistry.Add<UnifiedFile>().PublicSearchFilter(c => c.BuildFilter<UnifiedFile>().FilterOnUnifiedFileReadAccess());

FileIndexer.Instance.Conventions.ShouldIndexVPPConvention = new VisibleInFilemanagerVPPIndexingConvention();
FileIndexer.Instance.Conventions.ForInstancesOf<UnifiedFile>().ShouldIndex(x => fileTypesToIndex.Contains(x.Extension));

PageIndexer.Instance.Conventions.EnablePageFilesIndexing();

SearchClient.Instance.Conventions.ForInstancesOf<UnifiedFile>()
    .ExcludeField(file => file.Attachment()) // Exclude the default Attachment
    .IncludeField(file => file.SearchFilename())
    .IncludeField(file => file.SearchFileExtension())
    .IncludeField(file => file.SearchHitUrl())
    .IncludeField(file => file.SearchAttachment()) // Include our extened Attachment
    .IncludeField(file => file.SearchPublishDate())
    .IncludeField(file => file.SearchUpdateDate())
    .IncludeField(file => file.SearchHitTypeName())
    .IncludeField(file => file.SearchSubsection())
    .IncludeField(file => file.SearchTitle());

Here are some example extensions for UnifiedFile:

namespace DV.Extensions
{
    using EPiServer;
    using EPiServer.Find;
    using EPiServer.Find.Cms;
    using EPiServer.Web.Hosting;
    using System;
    using System.Text;

    public static class UnifiedFileExtensions
    {
        public static string SearchFilename(this UnifiedFile file)
        {
            return file.VirtualPath;
        }

        public static string SearchFileExtension(this UnifiedFile file)
        {
            return file.Extension;
        }

        public static Attachment SearchAttachment(this UnifiedFile file)
        {
            return file.Attachment();
        }

        public static string SearchTitle(this UnifiedFile file)
        {
            return file.Summary != null && !string.IsNullOrWhiteSpace(file.Summary.Title) ? file.Summary.Title : file.Name;
        }

        public static string SearchHitUrl(this UnifiedFile file)
        {
            var fileUrlBuilder = new UrlBuilder(file.PermanentLinkVirtualPath);

            Global.UrlRewriteProvider.ConvertToExternal(fileUrlBuilder, null, Encoding.UTF8);

            var url = new UriBuilder(EPiServer.Configuration.Settings.Instance.SiteUrl);

            url.Path = fileUrlBuilder.ToString();

            return url.Uri.AbsoluteUri;
        }

        public static DateTime? SearchPublishDate(this UnifiedFile file)
        {
            return file.Created;
        }

        public static DateTime? SearchUpdateDate(this UnifiedFile file)
        {
            return file.Changed;
        }

        public static string SearchHitTypeName(this UnifiedFile file)
        {
            if (!string.IsNullOrEmpty(file.Extension))
            {
                var extension = file.Extension.Replace(".", string.Empty).ToLower();

                switch (extension)
                {
                    case "asp":
                    case "aspx":
                    case "html":
                    case "htm":
                    case "jsp":
                    case "php":
                        return "Webpages";
                    case "docx":
                    case "doc":
                        return "Word";
                    case "pages":
                        return "Pages";
                    case "pdf":
                        return "PDF";
                    case "pps":
                    case "ppt":
                    case "ppsx":
                    case "pptx":
                        return "PowerPoint";
                    case "key":
                        return "Keynote";
                    case "xls":
                    case "xlsx":
                        return "Excel";
                    case "rtf":
                    case "txt":
                    case "text":
                        return "Text";
                    case "bmp":
                    case "dwg":
                    case "gif":
                    case "jpg":
                    case "jpeg":
                    case "png":
                    case "psd":
                    case "tif":
                        return "Images";
                    case "7z":
                    case "zip":
                    case "zipx":
                    case "rar":
                    case "sit":
                        return "Compressed files";
                    default:
                        return extension;
                }
            }

            return string.Empty;
        }

        public static string SearchTypeName(this UnifiedFile page)
        {
            return "Other";
        }
    }
}

If you're not using Page Type Builder you can create extension methods for PageData with the same name for every property in ISearchContent and add them to the indexing conventions. The implementations are quite easy and straight forward so I'm not gonna post example code for that. SearchText is already implemented in the EPiServer.Find.Cms namespace though, so add a using statement for that namespace.

SearchClient.Instance.Conventions.ForInstancesOf<PageData>()
    .IncludeField(page => page.SearchText())
    .IncludeField(page => page.SearchHitTypeName())
    .IncludeField(page => page.SearchHitUrl())
    .IncludeField(page => page.SearchPublishDate())
    .IncludeField(page => page.SearchSection())
    .IncludeField(page => page.SearchTitle())
    .IncludeField(page => page.SearchTypeName())
    .IncludeField(page => page.SearchUpdateDate());

As I mentioned before we added filters for each type. Those are added by default in the EPiServer 7 Integration, for EPiServer 6 they don't exists. So I've backported them by using Reflector and by verifying the outgoing requests' JSON. I've uploaded this code to EPiServer World because they are about about 200 lines of code, you can find them here.

There are some caveats using Unified Search with EPiServer 6, for instance pages are not re-indexed when the access rights are changed for pages and files. To be safe, enable the scheduled indexing job, so the ACLs are updated at least every night.

If you have any questions or found any bugs, please let me know.

Hope this helps and happy searching!