Monday, April 14, 2014

Workaround to show Contact items in People Search

The good thing about community is that it spawns off a lot of ideas for blog posts. The idea for this post I got after my search buddy Matthew McDermott wondered if there is a way to blend results from the Local SharePoint search index with the Local People search index. And his question was spawned from this SO question, asked by Alvaro Monton.

Technically both SharePoint content and People search are in the same search engine, but they have been separated and use different search schemas and ranking.

[Update - 2014-04-15]
In SharePoint 2013 on-premises it's still possible (noticed by Alvaro), but you have to do the other way around. Choose "SharePoint Search Results" as the type, and include people results explicitly. Something like:
{?(({searchTerms}) ContentClass=urn:content-class:SPSPeople)} {?OR (({searchTerms}) spcontenttype:contact)}

[Update – 2014-04-14]
It is indeed possible, but only in SharePoint Online it seems. I tried in SPO on the tenant level and did the following. Create a new result source, pick “People Search Results” as the type, and change the query to the following:
{?({searchTerms})} {?OR (({searchTerms}) spcontenttype:contact)}
You still need to do work on the control and possibly display template to get it to show correct.
If you are on-premises, you might have to resort to the blend workaround.


What doesn’t work is to create a new Result Source which searches both, as you have to pick “SharePoint Search Results” or “People Search Results” as your index source.

What does work, is creating a query rule for people search which add a result block with for example Contact items.

Disclaimer: The following sample is not production ready, but show the concept of blending results.
Topics covered:
  • Create a new result type
  • Create a query rule with routing
  • Create a custom control template to merge results
First I created a new Contact list and added a couple of items.

image

Next set up a new result type so that Contact items are rendered as People results. Basically saying that ContentType=Contact should be rendered as People Item.

image

Now that I have the basis it’s time create a query rule for People search which will add the contact results.

On the Query Rule page, create a new query rule which triggers for “Local People Results”. Remove the trigger conditions to make sure it will always try to add a block with Contact items. Next add a Result Block targeting the LocalSearchIndex (or Local SharePoint Results) and filter the query to return only Contact items.

{?({subjectTerms})} SPContentType:Contact
The {?..} notation means that if subjectTerms is missing, it will remove the expression from the query. I also added () inside it, to be order to capture more complex queries with for example an OR statement.

image

As you can see from the settings above I have chosen to retrieve 10 items, which unfortunately is the limit for this work around :(

The nugget in this post is perhaps the next step. Click the Routing option, and choose to route the results to a name of your choice, in this case I picked Contacts. This will give you the option to target a result table with that specific name later on. (I’ll cover routing in more detail in episode 10 of my SharePoint Search Queries Explained series)

image

Clicking OK and the settings for the query rule should look something like this.

image

So far I’ve set up that I want Contact items to be rendered as People results and I have created a query rule to pull contact items into a people search query. The difference is that I’m not showing it as a block on the people search result page, but naming the results with a unique label.

Next is creating a new control template which will do the actual merging of the results. You can for example take a copy of /_catalogs/masterpage/Display Templates/Search/Control_SearchResults.html as your starting point. I named mine Control_SearchResults_ContactMerge.html.

The first step is setting a unique name in the <title> tag in order to find the control template later on.

image

Next is adding the javascript which will merge the results. Add this below <div id="Control_SearchResults">

image

In the script below I specifically look for a result table by the name of Contacts, as specified in the routing.

Type.registerNamespace('mAdcOW.Search');
mAdcOW.Search.MergeContacts = function() {
    var peopleTable = ctx.ListData.ResultTables.filter(function( table ) {
      return table.TableType == "RelevantResults";
    });
    
    var contactTable = ctx.ListData.ResultTables.filter(function( table ) {
      return table.TableType == "Contacts";
    });
    if( $isEmptyArray(peopleTable) || $isEmptyArray(contactTable) ) return;
    for (var i=0, item; item = contactTable[0].ResultRows[i]; i++) {
      // TODO: Map more properties to show more values (firstname, office etc)
      // Set preferred name to Title which is Last name
      item.PreferredName = item.Title;
    }

    // TODO: Logic to handle if peopleTable is empty
    // append items from the result block table to the main table
    peopleTable[0].ResultRows.push.apply(peopleTable[0].ResultRows,contactTable[0].ResultRows)
}
mAdcOW.Search.MergeContacts();


Once the template is saved, edit the result page and pick the new control template.

image

And we have the final result!

image

A couple of things which has to be worked out:
  • The result count is not updated, and the property does not have a setter in CSOM. As you see it shows one, while we display two.
  • Need to pull in more managed properties to get for example first name, image url, and show content in hover panel
  • Max 10 results from Contacts will be returned
  • Might want to add some sorting to results