Article Search

The ISearchableContentType interface provides support for making content searchable. When the search index job runs this plugin is used to gather information about your content.

Why should I make my content searchable?

Search is a powerful feature in any community. If making your content discoverable then planning on a search strategy is important. Consider what properties are important to index for making your content distinguishable from other content on the search page.

Creating an ISearchableContentType plugin

To add support for search you must implement the ISearchableContentType. It is defined in the Telligent.Evolution.Extensibility.Content.Version1 namespace of Telligent.Evolution.Core.dll.

It is important to note that one or more Core Services can be implemented in the same IContentType class.

Begin by defining a method to flag what content is or is not index by the search provider.

public void SetIndexStatus(Guid[] contentIds, bool isIndexed)
{
    LinksData.UpdateStatus(contentIds, isIndexed);
}

For the next method it is important to consider what fields will be indexed. You will need to build a list of SearchIndexDocuments by using the API Apis.Get<ISearchIndexing>().NewDocument().

public IList<SearchIndexDocument> GetContentToIndex()
{
    var docs = new List<SearchIndexDocument>();
    var links = LinksData.ListLinks().Where(link => !link.IsIndexed);

    foreach (var link in links)
    {
        if (link == null) { continue; }

        var doc = Apis.Get<ISearchIndexing>().NewDocument(link.ContentId, link.ContentTypeId, "linkitem", link.Url, link.HtmlName("web"), link.HtmlDescription("web"));
        doc.AddField("date", Apis.Get<ISearchIndexing>().FormatDate(link.CreatedDate));
        docs.Add(doc);
    }

    return docs;
}

Security trimming is another important feature of the search provider. It determines if a user has permission to view certain content. This will require you to return a list of roles that are allowed to view your content.

public int[] GetViewSecurityRoles(Guid contentId)
{
    var content = LinksData.GetLink(contentId);
    if (content == null) { return new int[] { }; }
    if (content.Application == null) { return new int[] { }; }

    return Apis.Get<IRoles>().Find("Registered Users").Select(r => r.Id.GetValueOrDefault()).ToArray();
}

One of the final methods to implement returns the html view for the search result. It is important to return a consistent format but you will have full control over the markup that is returned for your content.

public string GetViewHtml(IContent content, Target target)
{
    if (content == null) { return null; }

    var user = Apis.Get<IUsers>().Get(new UsersGetOptions { Id = content.CreatedByUserId });

    var author = String.Format(@"<a href=""{0}"" class=""internal-link view-user-profile""><span></span>{1}</a>", PublicApi.Html.EncodeAttribute(user.ProfileUrl), PublicApi.Html.Encode(user.DisplayName));
    
    var appLink = content.Application != null
        ? String.Format(@"<a href=""{0}"">{1}</a>", PublicApi.Html.EncodeAttribute(content.Application.Url), content.Application.HtmlName(target.ToString()))
        : String.Empty;
    
    var groupLink = content.Application != null && content.Application.Container != null
        ? String.Format(@"<a href=""{0}"">{1}</a>", PublicApi.Html.EncodeAttribute(content.Application.Container.Url), content.Application.Container.HtmlName(target.ToString()))
        : String.Empty;

    return String.Format(@"
<div class=""abbreviated-post-header""></div>
<div class=""abbreviated-post ui-searchresult"">
    <div class=""post-metadata"">
        <ul class=""property-list"">
            <li class=""property-item date"">{0}</li>
                <li class=""property-item author"">
                    <span class=""user-name"">{1}</span>
                </li>
            <li>
                <ul class=""details"">
                    <li class=""property-item type""></li>
                </ul>
            </li>
        </ul>
    </div>
    <h4 class=""post-name"">
        <a class=""internal-link view-post"" title=""{3}"" href=""{4}"">
            {2}
        </a>
    </h4>
    <div class=""post-summary"">{5}</div>
    <div class=""post-application"">
        {6}
        {7}
    </div>
</div>
<div class=""abbreviated-post-footer""></div>",
    Apis.Get<ILanguage>().FormatDate(content.CreatedDate),
    author,
    content.HtmlName("web"),
    content.HtmlName("web"),
    PublicApi.Html.EncodeAttribute(content.Url),
    content.HtmlDescription("web"),
    appLink,
    groupLink);
}

Here is the full sample.

using System;
using System.Collections.Generic;
using System.Linq;
using Telligent.Evolution.Extensibility;
using Telligent.Evolution.Extensibility.Api.Entities.Version1;
using Telligent.Evolution.Extensibility.Api.Version1;
using Telligent.Evolution.Extensibility.Content.Version1;
using IContent = Telligent.Evolution.Extensibility.Content.Version1.IContent;

namespace Samples.Links
{
    public class LinkItemContentType : IContentType, ISearchableContentType
    {
        IContentStateChanges _contentState = null;

        #region IPlugin Members

        public string Description
        {
            get { return "Items in a Links collection"; }
        }

        public void Initialize()
        {
        
        }

        public string Name
        {
            get { return "Link Items"; }
        }
        
        #endregion

        #region IContentType Members

        public Guid[] ApplicationTypes
        {
            get { return new Guid[] { ContentTypes.LinksApplicationId }; }
        }

        public void AttachChangeEvents(IContentStateChanges stateChanges)
        {
            _contentState = stateChanges;
        }

        public Guid ContentTypeId
        {
            get { return ContentTypes.LinksItemId; }
        }

        public string ContentTypeName
        {
            get { return "Links Item"; }
        }

        public IContent Get(Guid contentId)
        {
            return LinksData.GetLink(contentId);
        }

        #endregion

        #region ISearchableContentType

        public void SetIndexStatus(Guid[] contentIds, bool isIndexed)
        {
            LinksData.UpdateStatus(contentIds, isIndexed);
        }

        public IList<SearchIndexDocument> GetContentToIndex()
        {
            var docs = new List<SearchIndexDocument>();
            var links = LinksData.ListLinks().Where(link => !link.IsIndexed);

            foreach (var link in links)
            {
                if (link == null) { continue; }

                var doc = Apis.Get<ISearchIndexing>().NewDocument(link.ContentId, link.ContentTypeId, "linkitem", link.Url, link.HtmlName("web"), link.HtmlDescription("web"));
                doc.AddField("date", Apis.Get<ISearchIndexing>().FormatDate(link.CreatedDate));
                docs.Add(doc);
            }

            return docs;
        }

        public int[] GetViewSecurityRoles(Guid contentId)
        {
            var content = LinksData.GetLink(contentId);
            if (content == null) { return new int[] { }; }
            if (content.Application == null) { return new int[] { }; }

            return Apis.Get<IRoles>().Find("Registered Users").Select(r => r.Id.GetValueOrDefault()).ToArray();
        }

        public string GetViewHtml(IContent content, Target target)
        {
            if (content == null) { return null; }

            var user = Apis.Get<IUsers>().Get(new UsersGetOptions { Id = content.CreatedByUserId });

            var author = String.Format(@"<a href=""{0}"" class=""internal-link view-user-profile""><span></span>{1}</a>", PublicApi.Html.EncodeAttribute(user.ProfileUrl), PublicApi.Html.Encode(user.DisplayName));
            
            var appLink = content.Application != null
                ? String.Format(@"<a href=""{0}"">{1}</a>", PublicApi.Html.EncodeAttribute(content.Application.Url), content.Application.HtmlName(target.ToString()))
                : String.Empty;
            
            var groupLink = content.Application != null && content.Application.Container != null
                ? String.Format(@"<a href=""{0}"">{1}</a>", PublicApi.Html.EncodeAttribute(content.Application.Container.Url), content.Application.Container.HtmlName(target.ToString()))
                : String.Empty;

            return String.Format(@"
<div class=""abbreviated-post-header""></div>
<div class=""abbreviated-post ui-searchresult"">
    <div class=""post-metadata"">
        <ul class=""property-list"">
            <li class=""property-item date"">{0}</li>
                <li class=""property-item author"">
                    <span class=""user-name"">{1}</span>
                </li>
            <li>
                <ul class=""details"">
                    <li class=""property-item type""></li>
                </ul>
            </li>
        </ul>
    </div>
    <h4 class=""post-name"">
        <a class=""internal-link view-post"" title=""{3}"" href=""{4}"">
            {2}
        </a>
    </h4>
    <div class=""post-summary"">{5}</div>
    <div class=""post-application"">
        {6}
        {7}
    </div>
</div>
<div class=""abbreviated-post-footer""></div>",
            Apis.Get<ILanguage>().FormatDate(content.CreatedDate),
            author,
            content.HtmlName("web"),
            content.HtmlName("web"),
            PublicApi.Html.EncodeAttribute(content.Url),
            content.HtmlDescription("web"),
            appLink,
            groupLink);
        }

        public bool IsCacheable
        {
            get { return true; }
        }

        public bool VaryCacheByUser
        {
            get { return true; }
        }

        #endregion
    }
}