The 100% way of automatically updating Result Types (with PowerShell)

This will be a short and sweet post about how to update result types after display templates have changed. It’s a common scenario to update a display template with changes to managed properties. If your result types are using the display templates, you need to go to the result types and click update. Quite a cumbersome tasks if you are deploying changes to many display templates/result types.

Fortunately my superstar colleage Mikael Svenson has written about this before. This post is another way of doing the same as he is doing, but with PowerShell instead of server side code.

The process is quite simple:

  1. Get the result type names from search configuration file
  2. Get the display template used in the result type
  3. Get the managed properties used in the display template
  4. Update the result type with the display template properties

The relevant PowerShell snippet for this is as follows

function UpdateResultItemTypes([string]$Url, [string]$PathToSearchConfig) {
    Write-Host "Updating result type items at $Url"
    $site = Get-SPSite $Url
    [void] [Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server.Search") 
    
    $sspApp = Get-SPEnterpriseSSA
    if ($sspApp -eq $null) {
        Write-Error "Unable to get an instance of the Search Service application or proxy"
        return;
    }
    $fedManager = New-Object Microsoft.Office.Server.Search.Administration.Query.FederationManager($sspApp)
    $searchOwner = New-Object Microsoft.Office.Server.Search.Administration.SearchObjectOwner([Microsoft.Office.Server.Search.Administration.SearchObjectLevel]::SPSite, $site.RootWeb)

    Get-ResultItemTypeNames -PathToSearchConfig $PathToSearchConfig | % {
        $resultItemName = $_
        Write-Debug "Updating result item $resultItemName"
        $resultType = Get-SPEnterpriseSearchResultItemType -Owner $searchOwner -SearchApplication $sspApp | ? {$_.Name -eq $resultItemName}
        if ($resultType -ne $null) {
            $updatedProperties = GetResultItemUpdatedProperties -site $site -resultType $resultType
            if ($updatedProperties -ne $null) {
                Set-SPEnterpriseSearchResultItemType -Identity $resultType -SearchApplication $sspApp -Owner $searchOwner -DisplayProperties $updatedProperties
            }
        }
    }
}

function GetResultItemUpdatedProperties([Microsoft.SharePoint.SPSite]$site, $resultType) {
    $masterPageGallery = [Microsoft.SharePoint.SPList]$site.GetCatalog([Microsoft.SharePoint.SPListTemplateType]::MasterPageCatalog)
    $displayTemplateName = [System.IO.Path]::GetFileName($resultType.DisplayTemplateUrl)

    $displayTemplate = $masterPageGallery.Items | ? {$_.Name -eq $displayTemplateName}

    if ($displayTemplate -ne $null) {
        $properties = $displayTemplate["ManagedPropertyMapping"]

        [string]$propFormatted = ""
        $propArray = $properties.Split(",")
        $propArray | % {
            $pair = $_.Replace("'", "").Split(":")
            $propFormatted += $pair[$pair.length - 1] + ","
        }
        return $propFormatted.TrimEnd(",")
    }
    return $null
}

Content Filters – Search configuration made easy

In a recent project I’ve worked in we came up with the concept of Content Filters. In this post I will explain the general idea.

image

The rationale for creating the Content Filters is that to end users, configuring search results web parts can be hard. Even for SharePoint superusers and developers, you have to know a lot about how SharePoint search works, names of managed properties and fields etc. to be able to create advanced searches. We wanted to have a way for users to create powerful searches without having to configure search results web parts themselves.

image

We are storing queries into fields of the page, and then we have search results web parts provisioned on the page getting their queries from result sources which again are just picking up the query fields of the page. The result source query looks simply like this: {\Page.ContentFilterQuery1}

Since we have full control of the query, we can do cool things like letting the users write KQL, using fields on the page in different ways, include/exclude children terms for managed metadata searches, and more.

IntraDocumentStatus:Approved OR IntraDocumentStatus:Reviewed OR IntraDocumentStatus:"Pending Approval" OR IntraDocumentStatus:"Pending Reapproval" ContentTypeId:0x0101008C4E7537DBB64A9CB91C5D8112D44EA50113* owstaxIdIntraProductServiceMulti:#034ef7a04-73bf-4420-9e18-e8086d951de8

Code snippet: Example of a query picked up by the search web parts

We build the configuration model as a JSON object that is stored in a separate field on the page.

[{"_title":"Brochures","_model":{"_contentTypes":[{"Value":{"Id":"0x0101008C4E7537DBB64A9CB91C5D8112D44EA50113"}}],"_statuses":[{"Value":"Approved"},{"Value":"Reviewed"},{"Value":"Pending Approval"},{"Value":"Pending Reapproval"}],"_userInputQuery":"","_metadata":{}}},{"_title":"Media Content","_model":{"_contentTypes":[{"Value":{"Id":"0x0120D520A80800DF779D84BABF499C8C3CF9496D811B5001"}},{"Value":{"Id":"0x0101009148F5A04DDD49CBA7127AADA5FB792B00AADE34325A8B49CDA8BB4DB53328F21400C3AA00D5796C48639629BA0249C75A6D01"}},{"Value":{"Id":"0x0101009148F5A04DDD49CBA7127AADA5FB792B006973ACD696DC4858A76371B2FB2F439A008304A57B34E448A1B7D6A0C913EE1F8901"}}],"_statuses":[{}],"_userInputQuery":"","_metadata":{}}},{"_title":"Alerts","_model":{"_contentTypes":[{"Value":{"Id":"0x0101008C4E7537DBB64A9CB91C5D8112D44EA501010601"}}],"_statuses":[{"Value":"Approved"},{"Value":"Reviewed"},{"Value":"Pending Approval"},{"Value":"Pending Reapproval"}],"_userInputQuery":"","_metadata":{}}},{"_title":"Bulletins","_model":{"_contentTypes":[{"Value":{"Id":"0x0101008C4E7537DBB64A9CB91C5D8112D44EA501010602"}}],"_statuses":[{"Value":"Approved"},{"Value":"Reviewed"},{"Value":"Pending Approval"},{"Value":"Pending Reapproval"}],"_userInputQuery":"","_metadata":{}}},{"_title":"Lessons Learned","_model":{"_contentTypes":[{"Value":{"Id":"0x0101008C4E7537DBB64A9CB91C5D8112D44EA501010605"}}],"_statuses":[{"Value":"Approved"},{"Value":"Reviewed"},{"Value":"Pending Approval"},{"Value":"Pending Reapproval"}],"_userInputQuery":"","_metadata":{}}},{"_title":"Patents","_model":{"_contentTypes":[{"Value":{"Id":"0x010034F720BB1B564010999F8BAC472F7BEC0102"}}],"_userInputQuery":"","_metadata":{}}}]

Code snippet: Example of configuration object

Technically, the content filters are being created using JavaScript logic, building an array of content filter configurations. The logic is backed by a configuration file with names of managed properties, field names etc. that is being used to render the content filter form fields and “backend”. The frontend is using angular.js. The queries and fields are being saved to the fields on the page, without the end user knowing about it.

A very valid alternative to using search result web parts to pick up the queries is to roll your own search results. The benefit of this is that you can use the configuration object itself to perform the queries, and grab the results using the REST api. It’s an overall simpler approach and has less moving parts. The drawback of the approach is that you have to roll your own display templates for the results. With the result web parts approach we get the advantage of the SharePoint search toolstack, with result items, query rules, display templates etc.

image

Image above: The content filter web parts in display mode (with custom display template)

Search web parts show the same results after web parts are added to a page

Problem: After web parts were added to pages, visitors (users with read-only access) saw the same content in all search web parts.

Solution: The solution to this problems was to set the QueryGroupName property to unique GUID’s for all web part instances.

In my current project (SharePoint 2013, publishing site) we have a few page layout with quite a lot of search results web parts, both Search Results Web Parts and Content by Query Web Parts. In our provisioning, we use the same web part definition to provision all the web parts of the same type. This was working fine to our knowledge for a long while, until we granted visitors access to the site.

From time to time we upgrade the pages by removing all web parts and adding the updated web parts from the page layout. After these upgrades, we discovered that visitors saw the same content in all web parts that used the same web part definition for provisioning. We could fix the problem by having an user with owner access visit the page – then something wired up correctly in the background that fixed the web parts for all users. Of course this was not really a fix: In our solution we have hundreds of pages, and we upgrade all pages roughly once a month (after each sprint).

The QueryGroupName property of the web part

We found that the culprit was the QueryGroupName property of the web parts, which is a “native” property of the search web parts themselves, but it is also a property in the DataProviderJSON property of the web part defintion.

We first attempted to use the value “Default” as the property value, which was suggested by a few. Unfortunately this didn’t work. We also tried to not set the property at all, but leave it out of the web part definition, in hope that SharePoint could set a value automatically. This didn’t work either.

In all the errors above, ULS reported with the following error message for visitors (full stack trace left out):

System.UnauthorizedAccessException: Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED)), StackTrace:   
 at Microsoft.SharePoint.WebPartPages.SPWebPartManager.SaveChangesCore(SPLayoutProperties layoutProperties, Boolean httpGet, Boolean saveCompressed, Boolean skipRightsCheck, Boolean skipSafeAgainstScriptCheck, WebPartTypeInfo& newTypeId, Byte[]& newAllUsersProperties, Byte[]& newPerUserProperties, String[]& newLinks)    

Setting new GUID’s with a token

The way we provision page layouts and web parts allows us to tap into the web part definitions before we add them to the page. We could therefore introduce a token “|NewGuid|” as the value of the QueryGroupName property, and on provisioning we replaced this token with a new, random GUID. This made all the web parts work again for all users.

Picture below: From the updated web part definition

webpartproperties_newguid

We used this simple line of code to update the web part definition before we added it to the page:

webpartDefinitionXml.Replace("|NewGuid|", Guid.NewGuid().ToString());