A client recently presented me with a requirement that sounded simple in concept, but the implementation ended up being quite complex. This client operates a nation-wide retail outlet with thousands of locations. Their users have had constant issues with being swamped with information that is not relevant to them.
To ensure users would only see relevant information, the client wanted to classify content in many ways:
- One or more manually selected outlets
- Types of outlets
- Outlets in a particular state
- Outlets that offer particular products or services
- Users in a particular role (e.g.: store manager).
Content may or may not be tagged with any of the above conditions, which should then affect whether or not it would be displayed in rollups throughout the site.
With Office 365 and SharePoint Online, we can do this!
The first problem to solve was to be able to identify which outlet a user belongs to. This required a script to synchronise data from their existing HR system into Azure Active Directory (AAD). We then had another script to synchronise data from AAD into custom SharePoint user profile properties. This gave us the user’s role, and an internal ID representing the user’s outlet. With that ID, the client then provided us with an API to query for further information about the outlet, such as its location and the services offered. This gives us everything we need to know about the user.
The next step is tagging the content. We created a handful of site columns, mostly managed metadata. This was straightforward for the most part, however the outlet’s services introduced additional complexity. The outlet details API would give us the service in the form of an ID (e.g.: 25), which would make no sense to the content authors tagging the content. For the services term set, we created the hierarchy with friendly names for the terms (allowing users to easily tag content), but added the ID as an ‘other label’ for each term so we could programmatically find the term matching the service.
The other complication we faced with tagging content was allowing users to select an individual outlet. With thousands of outlets spread throughout Australia, we needed to ensure this process would be simple and seamless for our users. What we settled on was to use client-side rendering (CSR) for one of our site columns.
With CSR, we loaded the field as a React component (as discussed earlier), and stored the underlying data in a JSON blob in a multi-line text field. We presented the user with a textbox, which on typing would query an external API for a list of suburbs to present for selection. Once the user had selected their suburb, we would query the same API for a list of outlets matching that location and present it to the user.
Once we had our user details and content loaded, we were faced with the difficult task of forming the query to retrieve the right data. As we were planning this out, we had another spanner thrown in the works. Our content authors wanted to be able to tag content to match an entire category of services (by tagging with a particular term), and if the outlet offered any of the sub-services (identified by being a child of that term), then the content should be returned.
We were able to achieve this by performing our matches against the a managed property mapped to the term’s ID (ows_taxId_MyColumn) and not its label. Running a search against this property for ‘#0<Term ID>’ will match selections for that term exactly. Searching for ‘#<Term ID>’ will match selections of that term and any children of that term. Perfect!
Additionally, content not tagged with any particular outlet details (such as general nation-wide communications or announcements) should also be displayed. Given that we can’t easily search for “null” matches, we needed to find a simple way of finding items that did not have values.
One final complication we encountered is that SharePoint Online Keyword Query Language queries only allow up to 4096 characters. In particular instances, where an outlet would offer a very large number of services to match against, we found we were exceeding this limit. This lead us to do a few optimisation passes of the query to reduce the length. Shortening the names of our managed properties and implementing parent matching (if all of a term’s children should be matched, do a single match for the parent and all of its children instead of one match for each child) made the biggest difference here.
This left us with a powerful reusable pattern for our React data retrieval to inject into any search-based custom rollup. With HTML5 client storage, we were able to cache a lot of the more expensive calculations to ensure our users had a silky-smooth experience and were only presented with information that matters to them!