In this part, I will talk about the search experience implemented in this sample. For now, to implement the search feature in a SharePoint intranet solution, you can typically use two main strategies:
- Leverage the OOTB search features*
- Implement your own solution
Because I’m a strong supporter of the OOTB approach, I opted for the first solution. SharePoint search features are very powerful and implement your own search experience can be very time consuming just to equal these capabilities so before choosing a custom approach, you have to evaluate the effective gain of doing this.
The main downside of using the OOTB search features is that they are isolated from the rest of the Office 365 environment. For example, if you want to benefit of the Graph API, you have no choice to implement you own components. In the very next future, the new SharePoint Framework may help to do build customized experience more easily ;).
* I don’t mention the search feature coming with the « SharePoint » tile because it is very limited.
The OOTB search features used in this sample are:
- Search Web Parts with custom display templates (Search Results, Content Search and Refinement panel).
- Results sources.
- Custom search schema.
- Query rules.
Search categories (verticals)
In an intranet solution, it is common to split search results into « verticals ». It allows to apply a pre-filtered query according to a main content type simplifying the results display. In this example, I split into three commons categories:
- « Intranet« : all web pages like news and static pages
- « Documents« : only documents
- « People« : Office 365 profiles
Except the people search category, they all have a dedicated search result source. The search box simply redirects to the correct search results page according to the selected type (Search.aspx for the « Intranet » category and « SearchDocuments.aspx » for « Documents). Entered keywords are passed to the page using the « k » query string parameter.
The special case of the people search
In Office 365, you have several ways to search for people:
- Delve
- The « Contacts » tile
- The SharePoint search via the « SharePoint » tile
- The SharePoint search via a dedicated search center site combined with the default « People » result source.
In order to save money and time, this is not necessary to implement a custom search page with custom display templates (or even a custom component). If you have budget for this fine, otherwise maybe using an existing solution among these mentioned above in not a bad idea. For example, in the solution, the people search simply redirects to the Delve search using the « q » parameter.
Here is the search function logic within the search box view model:
this.searchCategories = ko.observableArray([ new SearchCategory(i18n.t("intranetSearchCategory"), "ms-Icon--globe", i18n.t("intranetSearchPageUrl")), new SearchCategory(i18n.t("documentsSearchCategory"), "ms-Icon--documents", i18n.t("documentsSearchPageUrl")), new SearchCategory(i18n.t("peopleSearchCategory"), "ms-Icon--people", null, true), ]); ... public doSearch = () => { // Check if the input text is empty if (this.isSearchEmpty()) { this.isError(true); } else { let queryUrl: string = ""; // Check if people search. In this case, we use the Delve portal instead of SharePoint if (this.selectedCategory().isPeople) { let profileUrl = _spPageContextInfo["ProfileUrl"]; profileUrl = this.utilityModule.getLocation(profileUrl); // Build the search query for Delve queryUrl = profileUrl.protocol + "//" + profileUrl.hostname + "/_layouts/15/me.aspx?q=" + this.inputQuery(); // Open the page in a new tab window.open(queryUrl); } else { queryUrl = _spPageContextInfo.siteAbsoluteUrl + "/Pages/" + this.selectedCategory().searchPageUrl + "?k=" + this.inputQuery(); // Redirect to the correct page according to selected category window.location.href = queryUrl; } } } ...
Archive and category pages management
An « archive » page is typically the page accessed by clicking on the « See all news » link at the bottom of a list or a carousel component. Instead of implementing a dedicated search page to display all the news, this feature is mutualized with the global search results page. At the end, it is only a refined query on the « Intranet » search category only for the « news » content type. This query is built dynamically like this:
ko.bindingHandlers.getNewsSearchUrl = { init: () => { let newsContentTypeId = "0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF39000650D0E024D0AE42B88AF5AF825F709C02"; let refinementString = '{"k":"","r":[{"n":"ContentTypeId","t":["' + newsContentTypeId + '*"],"o":"and","k":false,"m":null}'; this.searchPageUrl(_spPageContextInfo.siteAbsoluteUrl + "/Pages/" + i18n.t("intranetSearchPageUrl") + "#Default=" + encodeURIComponent(refinementString)); }, };
You can apply the same behavior for pages, announcements, etc.
A category page is slightly different than an archive page. It represents a page for a specific navigation node. For example, the « News » node is a category page and if you click on it you might expect to see all the news of the portal. Because, like archive pages, we don’t necessary want to put the whole refinement query in the navigation link property manually (because it can be very long and complex), we can use search query rules to simplify instead:
Note: If you don’t want to manage category pages, you can just leave blank the navigation link for concerned taxonomy terms.
Search schema customizations for search implementation
Refinable search managed properties
To be able to use your own search properties inside the refinement panel Web Part, these have to be flagged as « Refinable » in the search schema. With SharePoint Online, you can’t modify directly a default created search managed property to enable this option.
To do this, you have to use one of the predefined managed property according to the type you want (text, date, int ,etc.) and map the correct crawled properties. These predefined properties follow the format: Refinable<Type><Number>:
Notes:
- As a best practice, I don’t modify any of the default search managed properties. I always reuse one of the predefined ones.
- I always use an alias name (instead of « RefinableStringXX« ) to easily reference properties in display template or search queries.
- Search schema customizations are done within the site collection scope to not interfere with the global tenant search schema configuration. I simply export the whole search configuration (search schema, result sources, etc.) as XML using the export option in the site settings. The import is done via the PnP cmdlet Set-SPOSearchConfiguration -Path $SearchConfigurationFilePath -Scope Site
Taxonomy refiners special case
For taxonomy refiners, I use the ows_taxid_xxx crawled properties instead of the « text » ones. Remember, in the previous article, I said refiners were not translated automatically by SharePoint so I want to get the term id and using code, get the right label according to the intranet current language.
It is done through a custom binding handler used in the display template:
... function outputFilter(refinementName, refinementCount, refiners, method, aClass, showCounts) { var aOnClick = "$getClientControl(this)." + method + "('" + $scriptEncode(Sys.Serialization.JavaScriptSerializer.serialize(refiners)) + "');"; var nameClass = "ms-ref-name " + (showCounts ? "ms-displayInline" : "ms-displayInlineBlock ms-ref-ellipsis"); var encodedRefinementName = $htmlEncode(refinementName); // Keep only terms (L0). The crawl property ows_taxid_xxx return term sets too. if (!/(GTSet|GPP|GP0)/i.test(encodedRefinementName)) { _#--> <div id='Value' name='Item' class="refinement-filter"> <a id='FilterLink' class='_#= $htmlEncode(aClass) =#_' onclick="_#= aOnClick =#_" href='javascript:{}'> <div id='RefinementName' class='_#= nameClass =#_' data-bind="localizedTermLabel: '_#= encodedRefinementName =#_'" ></div> ...
... ko.bindingHandlers.localizedTermLabel = { init: (element, valueAccessor) => { let value: string = ko.unwrap(valueAccessor()); // Check if the value seems to be a taxonomy term let isTerm = /L0\|#/i.test(value); if (isTerm) { // Extract the id let termId: Array<string> = value.match(/[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}/); if (termId.length > 0) { $(element).addClass("spinner"); this.taxonomyModule.init().then(() => { this.taxonomyModule.getTermById(new SP.Guid(termId[0])).then((term) => { $(element).text(term.get_name()); $(element).removeClass("spinner"); }); }).catch((errorMesssage) => { pnp.log.write(errorMesssage, pnp.log.LogLevel.Error); }); } } else { // Return the original value $(element).text(value); } }, }; ...
Contact the Author: Franck Cornu
– LinkedIn: HTTPS://CA.LINKEDIN.COM/IN/FRANCKCORNU
– Twitter: @FRANCKCORNU
– Blog: HTTP://THECOLLABORATIONCORNER.COM/
Reference:
Cornu, F. (2016). PnP – Office 365 Starter Intranet Solution (Part 6: The search implementation) [online] Available at: http://thecollaborationcorner.com/2016/09/08/part-6-the-search-implementation/ [Accessed 24 Mar. 2017].