Saturday, December 18, 2010

Doing blended search results in SharePoint–Part 2: The Custom CoreResultsWebPart Way

(Part 1: The Hackish Way)
In Part 1 I used two Search Core Results Web Parts and a bit of jQuery magic to achive the look of blended search results.

This time we will create our own CoreResultsWebPart and inject the blended results into the result xml before it is transformed into html. In addition to blend in news results I decided to get some  images as well. I did this by importing a “Federated Location” for Flickr. The location definition can be found at “Flickr Search Connector for SharePoint Server, Search Server, and FAST Search for SharePoint”.

I started off by creating an empty SharePoint 2010 project in VS2010 and added a new web part.
image
After deploying and activating the feature I replaced the Search Core Results Web Part with my own Search Blended Core Results Web Part.
image
Since I use FS4SP as the search back-end remember to copy the xslt from the original web part as the default xslt is for internal SharePoint search.

By overriding the GetXPathNavigator we have full control over the result xml. The blended results are retrieved asynchronous while we get the local results from the page’s query manager on the main thread.

protected override XPathNavigator GetXPathNavigator(string viewPath)
{
QueryManager queryManager = SharedQueryManager.GetInstance(Page).QueryManager;
Func<QueryManager, StringBuilder> blend = GetBlendedResults;
IAsyncResult asyncResult = blend.BeginInvoke(queryManager, null, null);
// Get local results
XmlDocument xmlDocument = queryManager.GetResults(queryManager[0]);
// Get blended results
StringBuilder sb = blend.EndInvoke(asyncResult);
InsertBlendedResults(sb, xmlDocument);
return xmlDocument.CreateNavigator();
}

The blended searches have been hard coded for Bing News and Flickr, but you could very well modify the web part to include settings where you choose which federated locations to use for your blended results. In order to execute the search we create a new QueryManager object and add both search locations to it. This enables the QueryManager to execute both the news and the images search at the same time, and the results are concatenated as two channels inside the returning rss feed. The rest of the code is parsing the rss and appending it into the main xml results.

private XmlDocument GetBlendedResultsXml(QueryManager queryManager)
{
SPServiceContext context = SPServiceContext.GetContext(SPServiceApplicationProxyGroup.Default, SPSiteSubscriptionIdentifier.Default);
SearchServiceApplicationProxy searchProxy = context.GetDefaultProxy(typeof (SearchServiceApplicationProxy)) as SearchServiceApplicationProxy;
QueryManager blendedQuery = new QueryManager {UserQuery = queryManager.UserQuery};
LocationList locList = new LocationList();
Location internetLocation = new Location("InternetSearchResults", searchProxy) {ItemsPerPage = 3};
locList.Add(internetLocation);

Location flickrLocation = new Location("Flickr", searchProxy) {ItemsPerPage = 3};
locList.Add(flickrLocation);

blendedQuery.Add(locList);
blendedQuery.IsTriggered(locList);
return blendedQuery.GetResults(locList);
}

The complete code can be downloaded from my SkyDrive and includes result.xslt which is used for rendering the results. Click the icon below for the download.

Creating the xml for the blended results is fairly easy and for the images I add nodes to make the results look like they came from a SharePoint picture library in order to utilize image display already in the xslt. I have also added some markers which are used in the xslt to define the start and stop of the blended results so I can add a headline marking the blended section.

The following regular expression is used to pull out the image url from the RSS summary.

private static Regex _imgExtract = new Regex("img src=\"(?<url>.*?)\"", RegexOptions.Compiled);

And the code to build the result xml for each item:

private void CreateResultXmlFragment(XmlWriter writer, SyndicationItem rssItem, string positionType)
{
writer.WriteStartElement("Result");
writer.WriteElementString("title", rssItem.Title.Text);
writer.WriteElementString("description", rssItem.Summary.Text);
writer.WriteElementString("write", rssItem.PublishDate.Date.ToString("MM/dd/yyyy"));
writer.WriteElementString("url", rssItem.Links.First().Uri.OriginalString);
writer.WriteStartElement("imageurl");
writer.WriteAttributeString("imageurldescription", "Web Page");
writer.WriteString("/_layouts/images/html16.png");
writer.WriteEndElement();
Match m = _imgExtract.Match(rssItem.Summary.Text);
if (m.Success)
{
writer.WriteElementString("picturethumbnailurl", m.Groups["url"].Value);
writer.WriteElementString("contentclass", "STS_ListItem_PictureLibrary");
writer.WriteElementString("blendtype", "Images");
}
else
{
writer.WriteElementString("blendtype", "News");
}
writer.WriteElementString("blended", positionType);
writer.WriteEndElement();
}



XSLT modification for the rendering of blended sections:

<xsl:if test="blended = 'first'">
<div>                
<xsl:value-of select="blendtype" /> for <xsl:value-of select="$Keyword"/>
</div>
<xsl:text disable-output-escaping='yes'>&lt;div style="margin-left: 20px;background-color:#eee;font-size: 0.8em"&gt;</xsl:text>
</xsl:if>

By using the same internal sample data as in Part 1, the result looks like this with both news and images blended in:

image

Right now they take up too much space for production quality layout, but that’s an exercise left to the xsl savvy out there Smile. And you probably don´t want to blend both news and images after hit number three.

And I want to thank Corey Roth for his excellent posts on how to use the QueryManager class.

[Also postet at http://nuggets.comperio.no]