Walkthrough: Create a simple Tag Cloud Web Part based on search results

Let's see how we can extend the SharePoint 2010 search user experience by programmatically implementing a Tag Cloud Web Part. We'll use the Federation OM, the object model (OM) Web Parts in SharePoint 2010 are based on, to create a Web Part like in the screen shot below. 

Tag Cloud Web Part

Background

The communication model between the search Web Parts changed from SharePoint 2007 to SharePoint 2010. Now the Web Parts use the Federation OM together with a public class called the SharedQueryManager that is shared by all synchronous Web Parts. There is one shared instance of the SharedQueryManager per search page, and through this class you can access the other classes that are part of the Federation OM (see also previous post for an overview of the query integration points). Using the Federation OM you can hook into the query path; you can e.g. fetch the search results (as in the sample code below) after the query has been executed, or you can modify the query before submitting it to the search backend (e.g. add query terms before request is submitted).

Setup and Design

We want the Tag Cloud (aka. Word Cloud) Web Part to visually display the most important terms in our result set. To keep it simple we'll leverage the document vectors of the first few results. These document vectors are created during document processing (before indexing), and are available in the managed property "docvector" when querying against FAST Search for SharePoint. A document vector indicates the most important terms/concepts in a document and the corresponding weight, i.e. like this: "[string1,weight1][string2,weight2]...[stringN,weightN]". Note also that these vectors are used for similarity search, a feature available with FAST Search on the object models.

Anyway, the goal of this walkthrough is to create a simple web part that will work nicely with the other out-of-box search web parts in SP2010. Here' are the setup steps.

To implement this new web part, I'm using Visual Studio 2010 RC1;

  • Open Visual Studio 2010 
    •  Download here first if needed
  • Create a new Visual Studio project and solution
    • Select e.g. the Visual Web Part or the Web Part template under SharePoint 2010 in VS2010, and click OK
    • Provide a URL to your FAST Search Center site, and click Finish.
  • Add a reference to the Microsoft.Office.Server.Search.dll in your newly created project
    • E.g. from C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\ISAPI

You are ready to start coding!

Implementation 

I've included the code below that implements a TagCloudWebPart class. Here I access the SharedQueryManager in the OnInit() method, and keep a reference to the QueryManager. This is so we in the OnPreRender() method can access the result set, and collect the document vectors we want (assumption is that we will have one FAST Search Location available on this page). Here we also remove the surrounding brackets from these vectors, and store the terms and associated weight in a dictionary. The final part happens in the RenderContents()  method where we output the terms from the dictionary, sorted descending by weight (giving a larger fontsize to terms with highest weight).

Here's the code (note: code is for illustration purposes only, not meant to be production ready)

// Copyright © Microsoft Corporation. All Rights Reserved.

// This code released under the terms of the

// Microsoft Public License (MS-PL, http://opensource.org/licenses/ms-pl.html.)

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Linq;

using System.Web.UI;

using System.Web.UI.WebControls.WebParts;

using System.Xml.XPath;

using Microsoft.Office.Server.Search.Query;

using Microsoft.Office.Server.Search.WebControls;

namespace MyVisualWebPartProject.TagCloudWebPart

{

    [ToolboxItemAttribute(false)]

    public class TagCloudWebPart : WebPart

    {

        // Visual Studio might automatically update this path when you change the Visual Web Part project item.

        private const string _ascxPath = @"~/_CONTROLTEMPLATES/MyVisualWebPartProject/TagCloudWebPart/TagCloudWebPartUserControl.ascx";

        // To store the terms and weights used to render the tag cloud

        private Dictionary<string, double> dict = new Dictionary<string, double>();

        // To access the search results

        private QueryManager queryManager;

        protected override void OnInit(EventArgs e)

        {

            queryManager = SharedQueryManager.GetInstance(this.Page).QueryManager;

            base.OnInit(e);

        }

        protected override void CreateChildControls()

        {

            Control control = Page.LoadControl(_ascxPath);

            Controls.Add(control);

        }

        protected override void OnPreRender(EventArgs e)

        {

            LocationList locList = queryManager[0];

            if (locList == null)

                return;

            Location location = locList[0];

            dict.Clear();

            var nav = location.Result.CreateNavigator();

            XPathNodeIterator iterResults = nav.Select("All_Results/Result");

            string myContent = "";

            // concatenate document vectors

            foreach (XPathNavigator res in iterResults)

            {

                var docVectorNode = res.SelectSingleNode("docvector");

                if (null != docVectorNode)

                    myContent += docVectorNode.Value; // [term1, weight1]..[termN, weightN]

            }

            if (myContent.StartsWith("["))

            {

                // remove surrounding brackets, and split term and weight

                myContent = myContent.Remove(0, 1);

                myContent = myContent.Remove(myContent.Length - 1, 1);

                var array = myContent.Split(new string[] { "][" }, StringSplitOptions.RemoveEmptyEntries);

                for (int i = 0; i < array.Length; i++)

                {

                    string[] keyvalue = array[i].Split(',');

                    string key = keyvalue[0];

                    string val = keyvalue[1];

                    if (dict.ContainsKey(key))

                        dict[key] = dict[key] + Double.Parse(val);

                    else

                        dict.Add(key, Double.Parse(val));

                    // only keep 10 docvector items for each result

                    if (i >= 10)

                        break;

                }

            }

            base.OnPreRender(e);

        }

        protected override void RenderContents(HtmlTextWriter writer)

        {

            if (dict.Count > 0)

            {

                // order terms in dictionary by weight

                var words = from k in dict.Keys

                            orderby dict[k] descending

                            select k;

                int fontsize = 30;

                string color = "3333CC";

                int step = (30 - 8) / dict.Count;

                writer.Write("<p><center>");

                foreach(string word in words)

                {

                    // output one term...

writer.Write("<a href=\"results.aspx?k=" + word + "\" title=\"" + word + "\" style=\"color:#" + color + ";font-size:" + fontsize + "pt\">" + word + "</a> &nbsp;&nbsp; ");

                    // ...set smaller font for next term

                    fontsize = fontsize - step;

                    // ...and alternate color

                    if ("3333CC".Equals(color))

                        color = "9999FF";

                    else

                        color = "3333CC";

    }

                writer.Write("</center></p>");

                base.RenderContents(writer);

            }

        }

    }

}

Testing 

To test the web part you need a FAST Search Center on your SharePoint installation, plus some content indexed and searchable. Build and deploy the Web Part by hitting F5 in Visual Studio, and you are ready to start testing (and debugging!). Add the new Web Part to the result page in your search center; Click Edit Pag > Click Add Web Part in e.g. Bottom Zone > Select TagCloudWebPart from Custom category > Click Publish. Execute a query, and you will see a tag cloud like in the image at the top of this blog post.

Summary 

The key point with this exercise was to show how you can easily create new search Web Parts that play nicely together with other search Web Parts. With SharePoint 2010 this is done through the SharedQueryManager; here you can  hook in to get the results, or hook in to manipulate the query. Either way, the programming model is the same for FAST Search and SharePoint Search (and OpenSearch). You work with the QueryManager and the Locations in the Federation OM.

Links

See links below for more resources;
- The WebPart class on MSDN
Visual Studio 2010 support for SharePoint development