SharePoint Online – Working with Search Settings with Office 365 PnP PowerShell

With the PnP PowerShell extensions we have a lot of cmdlets to customize our sites in SharePoint Online. There are also cmdlets to move the search configuration from a development environment to a test or production environment. That makes it simple to create new solutions in SharePoint Online. This walkthrough will show how we can configure the search configuration in our development environment and how to prepare a PowerShell script for deployment. You should be somewhat familiar with the configuration options via the web interface in SharePoint Online.

In this example we start with a simple team site. Using PowerShell we create a new library called “Library” (keep it simple) and in this library a new text field called “Freetext”. As we know in SharePoint, when we store documents in the library and store data in the field, the crawler will create a new crawled property. We will use this crawled property to create a new managed property. Additionally we will connect the crawled property to one of the managed properties prepared for the refinement. To restrict the search results to our site collection we will create a new result source. To test and use our search configuration we will create a subsite with the basic search center. In this search center we will modify the search results webpart and the refinement webpart. Finally we will create one single script to deploy our customizations to a new site collection. Let’s go.

A site collection we can use for our development steps is already created in SharePoint Online. First we will create our library and its field. This could be done with a simple PowerShell script (the first part of our deployment script):


$list = New-PnPList -Title "Library" -Template DocumentLibrary -Url "Library" -OnQuicklaunch

$field = Add-PnPField -List "Library" -DisplayName "Freetext" -InternalName "Freetext" -Type Text -AddToDefaultView

Next we need to upload a document to this new library and set the value of our field. This is necessary to create the crawled property in the search configuration. Now we need a little patience, because the crawler needs to crawl the content of our library. It might take 5-10min until the document and the metadata appears in the search results (you can test this using the search box in the top right of the site collection).

Now we can start modifying the search configuration. We will do this using the web interface and the site settings of our development site. We open the site settings and click the link “Search Schema” in the Site Collection Administration section. Next click the “New Managed Property” link. Enter a name for the property (do not use spaces or special characters in the name field), mark the property as

  • Searchable
  • Queryable
  • Retrievable

Scroll down to the section for the “Mappings to crawled properties” and click the “Add a Mapping” button. Search for the crawled property that matches to the field (in our case “ows_Freetext”) and add it to the mappings as we see in the following image.

Scroll down and click the OK button to create the managed property.

Now we need to configure the managed property for the refinements. The starting point is the search schema again. In the filter box for the managed properties search for “refinablestring”.

Click one of the properties found that does not have a mapped crawled property. This will open the settings for this managed property. Scroll down to the section for the “Mappings to crawled properties” and click the “Add a Mapping” button. Search for the crawled property that matches to the field (in our case “ows_Freetext”) and add it to the mappings. That were all settings we need to do in the search schema.

Next we will create a new result source that we use in the search results webpart to limit the search results to the current site collection. In the site settings of the site collection we click the link “Search Result Sources” in the Site Collection Administration section. Click the link “New Result Source”. Type a name for the result source (I used “Current Site Collection”) and scroll down to the Query Transform section. Click the button “Launch Query Builder”. In the Property Filter combobox select “–Show all managed properties–“. That is necessary, because we need to choose the property “SPSiteURL”. As the value select “This site collection”. The query should look like this, after you clicked “Add property filter”.

Now, after we have finalized our necessary search configuration, we can do the export of the configuration. To do this, we have two options:

  • Use Search Configuration Export from the site settings (Site Collection Administration section)
  • Use the Get-PnPSearchConfiguration cmdlet from the PnP PowerShell extensions and use the scope “Site”

Both will produce the same result and create an xml-file with the current search configuration of our site collection. This file will be used by our deployment script to set the search configuration in our target environment.

Next we need a subsite for our basic search center. To create this subsite I use a PowerShell script:


$SubSearchWebTemplate = "SRCHCENTERLITE#1"
$SubSearchDescription = "Search"
$SubSearchTitle = "Search"
$SubSearchUrl = "Search"
$SubSearchLanguage = "1033"
$searchWeb = New-PnPWeb -Title $SubSearchTitle -Url $SubSearchUrl -Description $SubSearchDescription -Locale $SubSearchLanguage -Template $SubSearchWebTemplate

When the subsite is created, open the subsite in a browser and run a search (we need to modify the results.aspx). On the results page (results.aspx) we need to modify the webpart for the refinement panel and the search results webpart.

In the webpart properties for the refinement webpart click the “Choose Refiners…” button and add the RefinableString managed property, we modified a few steps above. Don’t forget to set the display name for the refiner.

In the properties of the search results webpart, click the “Change query” button. In the query builder choose the result source we created in our site collection from the combobox near “Select a query”. That is all we need to configure for the search results webpart.

To make the modifications in the webparts available by our deployment script, we need to export the webpart settings. This could easily be done from the web interface.

Do this export for the refinement webpart and the search results webpart.

Now we have the following artefacts

  • Search-Config.xml
  • Refinement.webpart
  • SearchResults.webpart

So, let’s create the deployment script to make all the customizations in the target environment. The script should do the following:

1. Create the library and its field

2. Import the search configuration

3. Create the search subsite

4. Modifiy the refinement webpart and the search result webpart in the search subsite

5. Configure the search settings in the root site of the site collection (could be done by setting properties in the property bag of the web)

Finally the deployment script will look like this (yes, I know, do not use full qualified paths in these scripts, but it’s just a demo):

Param (
		[Parameter(Mandatory=$true)]
		[string] $Url
)

Function EstablishConnectionSilent
{
	Param (
		[Parameter(Mandatory=$true)]
		[string] $Url,
		$Credentials
	)
	
	if ($Credentials -eq $null)
	{
		$Credentials = Get-Credentials
	}
	
	Connect-PnPOnline -Url $Url -Credentials $Credentials -ErrorAction Stop
}

$cred = Get-Credential

Write-Host "Connect to site collection"
Connect-PnPOnline -Url $Url -Credentials $cred

Write-Host "Create library"
$list = New-PnPList -Title "Library" -Template DocumentLibrary -Url "Library" -OnQuicklaunch

Write-Host "Add field to library"
$field = Add-PnPField -List "Library" -DisplayName "Freetext" -InternalName "Freetext" -Type Text -AddToDefaultView

Write-Host "Import search configuration"
$sConfig = Set-PnPSearchConfiguration -Scope Site -Path C:\Scripts\Search-Demo\Search-Config.xml

Write-Host "Create search subweb"
$SubSearchWebTemplate = "SRCHCENTERLITE#1"
$SubSearchDescription = "Search"
$SubSearchTitle = "Search"
$SubSearchUrl = "Search"
$SubSearchLanguage = "1033"
$searchWeb = New-PnPWeb -Title $SubSearchTitle -Url $SubSearchUrl -Description $SubSearchDescription -Locale $SubSearchLanguage -Template $SubSearchWebTemplate

if ($searchWeb -ne $null)
{
	$searchWebUrl = $searchWeb.Url

	Write-Host "Switch context to search subweb"
	EstablishConnectionSilent -Url $searchWebUrl -Credentials $cred
	
	$resultsPage = $searchWeb.ServerRelativeUrl + "/results.aspx"

	Write-Host "Remove webparts"
	Remove-PnPWebPart -ServerRelativePageUrl $resultsPage -Title "Refinement"
	Remove-PnPWebPart -ServerRelativePageUrl $resultsPage -Title "Search Results"
	
	Write-Host "Add webparts"
	$wp = Add-PnPWebPartToWebPartPage -ServerRelativePageUrl $resultsPage -Path C:\Scripts\Search-Demo\Refinement.webpart -ZoneId "NavigationZone" -ZoneIndex 1
	$wp = Add-PnPWebPartToWebPartPage -ServerRelativePageUrl $resultsPage -Path C:\Scripts\Search-Demo\SearchResults.webpart -ZoneId "MainZone" -ZoneIndex 2

	Write-Host "Switch context to root web"
	EstablishConnectionSilent -Url $Url -Credentials $cred

	Write-Host "Set search settings in web property bag"
	Set-PnPPropertyBagValue -Key "SRCH_ENH_FTR_URL" -Value $searchWebUrl
	Set-PnPPropertyBagValue -Key "SRCH_ENH_FTR_URL_SITE" -Value $searchWebUrl
	$value = '{"Inherit":false,"ResultsPageAddress":"' + $resultsPage + '","ShowNavigation":false}'
	Set-PnPPropertyBagValue -Key "SRCH_SB_SET_SITE" -Value $value
}

Write-Host "Done."

To deploy our configuration to a new site collection, simply run the script with a proper url parameter:

Now we can upload new documents to our library, set the values for our Freetext field and wait for a crawl (remember, be patient). When the search index is updated, the search results should look similar to the next image.

Hope this will help in the next customizations, where modifications to the search configuration are necessary.

Hide Button in Ribbon with Office 365 PnP PowerShell

Sometimes it is necessary to hide a button in the ribbon of SharePoint. With the Office 365 PnP PowerShell extensions this task could be done very easy. In this example, we will hide the “Publish” button in the ribbon for a document library.

First, we need the ID/location of the element in the ribbon. This is the difficult part in the whole task. In most cases, we can find this element with the developer tools of the browser.

In our case the ID/location of the button is “Ribbon.Documents.Workflow.Publish”.

Next we need an xml file that will be used in the cmdlet from the PnP PowerShell extensions. In this simple xml file we need the ID/location, we found using the developer tools:

<CommandUIExtension>
    <CommandUIDefinitions>
        <CommandUIDefinition Location="Ribbon.Documents.Workflow.Publish" />
    </CommandUIDefinitions>
</CommandUIExtension> 

Last but not least, we can run the Set-SPOCustomAction cmdlet from the PnP PowerShell extensions:


$fileContent = Get-Content ".\DisablePublish.xml"
$fileContentAsString = [string]$fileContent

Add-SPOCustomAction -Name "DisablePublish" -Title "DisablePublish" -Description "-" -Group "a" -Location "CommandUI.Ribbon" -RegistrationType List -Sequence 10000 -RegistrationId 101 -CommandUIExtension $fileContentAsString

And that is the result:

To make the button visible again, use the cmdlet Remove-SPOCustomAction from the PnP PowerShell extensions. First, run Get-SPOCustomAction, to get the ID of the custom action, then run Remove-SPOCustomAction with this ID.

Add an assembly reference in an Azure Function

When we develop solutions in Visual Studio, we can add references to the assemblies we need in our code. When we create an Azure Function, we do not have this option in the user interface in our browser.

But adding a reference is easy. To add it, just write all reference names with a leading ‘#r’ on top of the csx-file, as shown in the following image.

Get current location in PowerApps with Azure Functions

When creating an app in PowerApps, we can use Azure Functions to add functionality that is not available in PowerApps. Additionally we can use Azure Functions to make business logic available in a PowerApps. In the PowerApps overview pages, Microsoft provides a simple example, how to add an Azure Function to a PowerApp.

This short article will show a practical example for an Azure function. In PowerApps we have access to the current location of the user. That could be helpful in a mobile app on the phone. But, PowerApps does not have function, to get the current location (the city) of the user.

With the help of the Google Maps API, we can create an Azure Function to get the current location from the longitude and the latitude. The code for our function is shown in the following listing.

#r "System.Web.Extensions"

using System.Net;
using System.Web;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    log.Info($"C# HTTP trigger function processed a request. RequestUri={req.RequestUri}");

    // parse query parameter
    string longitude = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "long", true) == 0)
        .Value;
        
    string latitude = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "lat", true) == 0)
        .Value; 
        
    // Get request body
    dynamic data = await req.Content.ReadAsAsync<object>();

    if ((longitude != null) && (latitude != null))
    {
        string str = DoSomething(latitude, longitude);

        return req.CreateResponse(HttpStatusCode.OK, str);
    }
    else
    {
        return req.CreateResponse(HttpStatusCode.BadRequest, "Please pass a langitude and a latitude on the query string or in the request body.");
    }
}

private static string DoSomething(string latitude, string longitude)
{
    string result = String.Empty;

    var webAddress = String.Format("https://maps.googleapis.com/maps/api/geocode/json?latlng={0},{1}", latitude, longitude);
    var client = new WebClient();
    var content = client.DownloadString(webAddress);

    var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();

    var dict = serializer.Deserialize<Dictionary<string, dynamic>>(content);

    var status = dict["status"];

    if (status == "OK")
    {
        var results = dict["results"][0];
        var address_components = results["address_components"];
        var locality = address_components[2]["long_name"];

        result = locality;
    }
    else
    {
        // error handling
    }

    return result;
}

To make the Azure Function available in PowerApps, we need a Swagger definition file, as in the following example.

{
  "swagger": "2.0",
  "info": {
    "version": "1.0.0",
    "title": "GetLocationFromGoogleMaps"
  },
  "host": "getcityfromgeolocation.azurewebsites.net",
  "paths": {
    "/api/getlocationfromgooglemaps": {
      "get": {
        "description": "Calls my azure function over https",
        "operationId": "Run",
        "parameters": [
          {
            "name": "code",
            "in": "query",
            "description": "code",
            "default": "{enter the key from your Azure Function here}",
            "type": "string"
          },
          {
            "name": "long",
            "in": "query",
            "required": true,
            "type": "string"
          },
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "type": "string"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "schema": {
              "title": "The response of the api.",
              "type": "string"
            }
          }
        }
      }
    }
  }
}

How to add this Swagger definition file to the PowerApps environment of our tenant is shown in the Microsoft example for using Azure Functions in PowerApps.

To use the function in our PowerApp, we use a very simple app. We have three fields to show the longitude, the latitude and our current location. The field for the location is a simple text field, where the Text property of the field was set to the result of the function call of our Azure Function.

The PowerApp will look like this, when we start it on a mobile phone:

In this example for the combination of PowerApps and Azure Functions I used the Google Maps API, but with a developer key we can also do the same with Bing Maps.

 

Add Lookup Field with Office 365 PnP PowerShell

The PowerShell extensions from the Office 365 Patterns and Practices provide a cmdlet to create a new field in a list (or in the site columns) from an xml. That could perfectly be used to create a lookup field. The xml file should look similar to this (in the example this file is named LookupField.xml):

<Field 
	ID="{e83578ab-f63e-4c46-bae5-b0e211dd5003}"
	DisplayName="Company"
	Name="Company"
	StaticName="Company"
	Type="Lookup"
	Required="FALSE"
	EnforceUniqueValues="FALSE"
	List="Companies"
	ShowField="Title" 
/>

But, to be able to create the field with this xml file, we need to replace the name of the list in the list attribute with the Id of the list. This could be done with the following script (PatchLookupFieldXml.ps1) that returns the modified xml from the file:

param (
    [string]$LookupFieldXml
)

$content = Get-Content $LookupFieldXml
$content = [string]$content

$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.LoadXml($content)

[System.Xml.XmlElement]$root = $xmlDoc.DocumentElement

$listName = $root.List

if ($listName -ne $null -and $listName -ne "")
{
    $list = Get-SPOList -Identity $listName

    $listId = $list.Id

    $root.List = "{$listid}"

    Write-Output $xmlDoc.InnerXml
}
else
{
    Write-Output $null
}

Now we can use these two simple lines to add our lookup field:

$fieldXml = .\PatchLookupFieldXml.ps1 -LookupFieldXml C:\Temp\LookupField.xml
Add-SPOFieldFromXml -FieldXml $fieldXml -List "Documents"

Done.

Filter data by the current user in PowerApps

In an app in PowerApps it could be necessary to filter the data by the current user, who is using the app. To achieve this set the Items property of the Gallery to something like this:

SortByColumns(Filter(Timesheet, Author.Email = User().Email), “Date”, If(SortDescending1, Descending, Ascending))

The trick is in the Filter function (also see https://powerapps.microsoft.com/de-de/tutorials/function-filter-lookup). By using the formula

Author.Email = User().Email

only data created by the current user is shown.

Instead of the Email member you can also use the FullName member. In this case the formula looks like

Author.FullName = User().FullName

Move PowerApps app to another tenant

When you have created a PowerApps App for a list in SharePoint Online and you need to move this app to another tenant (I often create these apps in demo tenants), then you can use the following steps to move your app to another tenant.

Extract the app and the list from the current tenant:

  1. Save your app in the PowerApps Studio as a file to your hard disk
  2. Save your list in SharePoint Online as a list template
  3. Download the saved list template to your hard disk

Add the list and the app to the new tenant:

  1. Upload the list template to the site collection where the list should be created
  2. Create the list in the desired site by using the list template. Use the same name for the list as in the source
  3. Open the PowerApps file in the PowerApps Studio (you should be logged in with a user for the new tenant)
  4. Remove the data source in the app
  5. Add a data source to the app, pointing to the list you created in the previous steps in SharePoint Online
  6. Save the app to the cloud

That’s it. Your app should now be operational in the new tenant. Depending on your app it might be necessary to set the permissions in the SharePoint list and to share the PowerApps app, so it could be used by other users in the tenant.