Sitecore Search User Profile Personalization with Personalize

45 min read

Published:

Welcome back to my blog, and today’s topic is a little different than previous content that I’ve been creating on my blog, but it’s an important topic as I ramp up to speak at SUGCON Europe this week on the topic of behavioral personalization. The second part of my presentation will introduce the topic of Sitecore Search and how pairing that product with XM Cloud and Personalize could allow you to achieve a more cohesive content personalization strategy based on the behavioral habits of the customers on your website.

This topic involves utilizing the user profile built by Sitecore Search for personalization with Sitecore Personalize. Technically, it's not strictly related to XM Cloud; you could implement this strategy with XM, XP, Content Hub One, or any other CMS. The focus is on configuring Sitecore Search, which can then influence how you use user profile information in Sitecore Personalize to drive personalized experiences on your website.

Configuring Sitecore Search

The very first step of this use case is to configure Sitecore Search. For this I am using an instance of Sitecore Search configured for the Developer Portal. Most of the search configuration has already been completed, but it must still be specifically configured for personalized search results. There are two types of personalization options available in Sitecore Search:

  • More Like This
  • Affinity Scoring

The key difference between these two personalized search results options is the basis of boosting search results. One prioritizes results based on a document's content, while the other, Affinity Scoring, which we will cover today, is based on user behavior on your website, such as browsing history and recent searches.

Personalized recommendations in your search results are beneficial, but it's even more advantageous to use the data that Sitecore Search collects about your site behaviors. This can help personalize the user experience in other ways on your website.

One advantage of this approach is that there is no need to focus on configuring Personas or tagging your content with specific attributes. Instead, Sitecore Search can automatically score your content based on search attributes.

I'm assuming you've already configured attributes for your search results (https://doc.sitecore.com/search/en/users/search-user-guide/attributes.html). Attributes are additional metadata about the content items being indexed for retrieval from Sitecore Search. For example, they could be additional page descriptions or classifications, such as the products a particular content item focuses on. If we are indexing this page: https://developers.sitecore.com/customer-data-management, there's a variety of metadata available about that page as I’m showing below in the following image.

We can take these attributes and use them for Sitecore Search affinity scoring. For instance, we might want to prioritize 'Site Name' as an affinity scoring attribute. As a user browses the Developer Portal, the system could infer that this user prefers content from the Developer Portal. Another possible attribute is 'product name', which isn't in the previous example but is available for some content in the Developer Portal or external indexed content. As a user navigates the site, we can learn which products they are interested in. This information can then be used to personalize search results and other content on the website.

Let's delve into a use case where the goal is to detect affinity towards specific Sitecore products. We can visit the Sitecore Search admin website and configure the Product attribute for future personalization needs.

There are two essential steps for configuring Personalization in Sitecore Search, which you can learn more about here: https://doc.sitecore.com/search/en/users/search-user-guide/walkthrough--configuring-the-personalization-feature.html.

Let's begin by configuring our attributes. It's quite straightforward - navigate to Administration → Domain Settings → Feature Configuration → Personalization. You'll see a list of attributes already set up for personalization. In our case, we can click on Add Attribute and select from the attributes we have already configured.

It's quite straightforward. For our use case of integrating this information with the User Profile API, this is all we need to configure. However, if you plan to personalize search results, you should also configure Global Widget Settings. This is mentioned in the documentation and details how to set up personalization with search results. Now that we've configured search affinity scores, let's examine how we can use the User Profile API to access the available data.

Calling Search User Profile API

There is two parts to calling the Sitecore Search User Profile API. The first is making sure we get the Sitecore Search’s user id, which if you are using the React SDK for search, should be as easy as running the following code:

1import { getUserId } from '@sitecore-search/data';
2
3const uuid = getUserId().uuid;

However this is not the only approach, and in some cases you might be integrating with the REST APIs directly, and your application is more responsible for the creation of this unique identifier. This identifier could be stored in a cookie and in your case, you may need to read from the cookie to access this information (to learn more about this unique identifier read the following documentation to learn more: https://doc.sitecore.com/search/en/developers/search-developer-guide/using-a-uuid-to-track-site-visitors.html).

Once you have the UUID, it can be used with the user profile API for Sitecore Search to retrieve browsing history, search requests, and any affinity information. This ID can be passed using a Full Stack Interactive Experience or stored in the Guest Context for future use. However, the latter approach may require additional work since it requires access to Sitecore CDP to use the REST APIs. In a future blog post, I may cover this specific scenario for those that want to store this ID in a more permanent nature.

To keep the example simple, we will not store the UUID in the guest context. Instead, we'll use the Full Stack Interactive Experience approach. Before we get to the Full Stack Interactive Experience, lets take a look at the specifics of what calling the User Profile API looks like.

To make a request, you will call into https://api.rfksrv.com/user/v4/user-profile/:domainId. The domain Id you can access from your Sitecore Search Console application. If you would like to see the full definition for this route you can access the postman collection here on gist: https://gist.github.com/dylanyoung-dev/053f27a6e2f4aa887fc99775dc66079a. But lets spend a few moment explaining a few things in that endpoint, so that you can modify it to fit your needs. Like I said, the first thing is that there is a path parameter for domain Id that you’ll need to collect. Second you’ll need to pass an auth header, which will be Authorization ${AuthorizationKey}. To get your authorization key, you’ll need to access the Sitecore Search Console application and generate your new access key, and it should be configured with a scope for User. To learn more about generating this API key, check out this resource: https://doc.sitecore.com/search/en/developers/search-developer-guide/api-authentication-and-authorization.html.

Then you’ll also need to pass the User Id that we covered how to access previously. To pass it to the API, you’ll just pass it in the body as the id. The full JSON required for the body, is below, which will highlight the last item we’ll also need to cover:

1{
2 "id": "Your ID",
3 "id_type": "uuid",
4 "request": {
5 "max_size": 50,
6 "personalization": 1,
7 "entities": [
8 {
9 "entity_type": "content",
10 "events": [
11 "views"
12 ],
13 "keyword": [
14 "sb",
15 "sp"
16 ],
17 "affinity": [
18 "name",
19 "product",
20 "products",
21 "product_names",
22 "site_name"
23 ]
24 }
25 ]
26 }
27}

You’ll need to customize the affinity entities item to match the attributes that you have defined. In our case we have several affinity attributes defined. You don’t have to pass them all in, and really you should only pass in the ones you need for the specific needs that you have. We are also requesting all the view events and search keyword queries as well. If you don’t need that information however, you may want to opt to not include these either.

Well that’s it, what I don’t mention however, is that you’ll want to configure this api as a connection in Sitecore Personalize before we move on to the Decision Model configuration.

Our Decision Model

Let's examine the crucial area: the Decision Model within Sitecore Personalize. Below is a brief overview of our configuration for this Model. We'll review each step/node in the model, starting from the bottom and working our way up.

So for the first node at the bottom, that is our Data System (Connection) to the User Profile API for Sitecore Search. Here we are passing in our Domain ID and User ID. This will then return back something like the following below:

1{
2 "domain_hash": "hidden",
3 "id": "hidden",
4 "entities": [
5 {
6 "entity_type": "content",
7 "events": {
8 "views": [
9 {
10 "id": "49164840cf8dfb8d1c55cb7a141a57913b2889d3fe8a5d783421a09a",
11 "ut": 1709126107992,
12 "n": 1,
13 "attributes": {
14 "event_name": "entity_page_view",
15 "source_id": "813854"
16 }
17 },
18 {
19 "id": "https___developers_sitecore_com_search",
20 "ut": 1709126096950,
21 "n": 18,
22 "attributes": {
23 "event_name": "entity_page_view",
24 "source_id": "848150"
25 }
26 },
27 {
28 "id": "https___sitecore_stackexchange_com_questions_8299_sitecore-okta-integration",
29 "ut": 1709125969227,
30 "n": 1,
31 "attributes": {
32 "event_name": "entity_page_view",
33 "source_id": "823430"
34 }
35 },
36 {
37 "id": "https___sitecore_stackexchange_com_questions_9618_sitecore-8-2-integrated-search-function-with-salesforce",
38 "ut": 1709125946137,
39 "n": 1,
40 "attributes": {
41 "event_name": "entity_page_view",
42 "source_id": "823430"
43 }
44 },
45 {
46 "id": "https___developers_sitecore_com",
47 "ut": 1709125924807,
48 "n": 11,
49 "attributes": {
50 "event_name": "entity_page_view",
51 "source_id": "848150"
52 }
53 },
54 {
55 "id": "d67377a6fcf44bc796f15fc2873ea51248b1cd8c3faf5f68a43e7da4",
56 "ut": 1709125765620,
57 "n": 1,
58 "attributes": {
59 "event_name": "entity_page_view",
60 "source_id": "813854"
61 }
62 },
63 {
64 "id": "https___developers_sitecore_com_learn",
65 "ut": 1709125746999,
66 "n": 7,
67 "attributes": {
68 "event_name": "entity_page_view",
69 "source_id": "848150"
70 }
71 },
72 {
73 "id": "https___developers_sitecore_com_changelog",
74 "ut": 1709124406293,
75 "n": 2,
76 "attributes": {
77 "event_name": "entity_page_view",
78 "source_id": "848150"
79 }
80 }
81 ]
82 },
83 "fitment": {},
84 "keyword": [
85 {
86 "kw": "Sitecore CDP",
87 "ut": 1709126102500
88 },
89 {
90 "kw": "Sitecore CDP to Search Integration",
91 "ut": 1709126008285
92 },
93 {
94 "kw": "Sitecore CDP ",
95 "ut": 1709125920859
96 },
97 {
98 "kw": "sitecore search react sdk",
99 "ut": 1709125762269
100 },
101 {
102 "kw": "sitecore search react sdk",
103 "ut": 1709124541964
104 },
105 {
106 "kw": "sitecore search and standard values",
107 "ut": 1709124434313
108 },
109 {
110 "kw": "sitecore search not working in bucket items folder for nonadmin roles in sitecore 102",
111 "ut": 1709124426754
112 },
113 {
114 "kw": "sitecore search manage documents directly in content collection",
115 "ut": 1709124426338
116 }
117 ],
118 "category": {},
119 "affinity": {
120 "name": [
121 {
122 "value": "Dashboards in Sitecore CDP | Sitecore Documentation",
123 "score": 0.25,
124 "views": 1,
125 "a2c": 0,
126 "orders": 0,
127 "is_score_reliable": false
128 },
129 {
130 "value": "Sitecore OKTA Integration",
131 "score": 0.25,
132 "views": 1,
133 "a2c": 0,
134 "orders": 0,
135 "is_score_reliable": false
136 },
137 {
138 "value": "Sitecore 8.2 integrated search function with Salesforce",
139 "score": 0.25,
140 "views": 1,
141 "a2c": 0,
142 "orders": 0,
143 "is_score_reliable": false
144 },
145 {
146 "value": "Introduction to Sitecore Search JS SDK for React | Sitecore Documentation",
147 "score": 0.25,
148 "views": 1,
149 "a2c": 0,
150 "orders": 0,
151 "is_score_reliable": false
152 }
153 ],
154 "product": [
155 {
156 "value": "Sitecore Search",
157 "score": 0.5,
158 "views": 1,
159 "a2c": 0,
160 "orders": 0,
161 "is_score_reliable": false
162 },
163 {
164 "value": "Sitecore CDP",
165 "score": 0.5,
166 "views": 1,
167 "a2c": 0,
168 "orders": 0,
169 "is_score_reliable": false
170 }
171 ],
172 "product_names": [
173 {
174 "value": "Sitecore CDP",
175 "score": 0.5,
176 "views": 1,
177 "a2c": 0,
178 "orders": 0,
179 "is_score_reliable": false
180 },
181 {
182 "value": "Sitecore Search",
183 "score": 0.5,
184 "views": 1,
185 "a2c": 0,
186 "orders": 0,
187 "is_score_reliable": false
188 }
189 ],
190 "site_name": [
191 {
192 "value": "Sitecore Documentation",
193 "score": 0.047619047619047616,
194 "views": 2,
195 "a2c": 0,
196 "orders": 0,
197 "is_score_reliable": false
198 },
199 {
200 "value": "Sitecore Developer Portal",
201 "score": 0.9047619047619048,
202 "views": 38,
203 "a2c": 0,
204 "orders": 0,
205 "is_score_reliable": true
206 },
207 {
208 "value": "Sitecore Stack Exchange",
209 "score": 0.047619047619047616,
210 "views": 2,
211 "a2c": 0,
212 "orders": 0,
213 "is_score_reliable": false
214 }
215 ]
216 }
217 }
218 ]
219}

This is my user profile for the Developer Portal. It includes my complete page view history and all my search queries. While these may not be particularly useful for our use case, my affinities are. In this use case, I have several affinities. Let's examine the product affinity in more detai:

1"product": [
2 {
3 "value": "Sitecore Search",
4 "score": 0.5,
5 "views": 1,
6 "a2c": 0,
7 "orders": 0,
8 "is_score_reliable": false
9 },
10 {
11 "value": "Sitecore CDP",
12 "score": 0.5,
13 "views": 1,
14 "a2c": 0,
15 "orders": 0,
16 "is_score_reliable": false
17 }
18],

Interestingly, this demonstrates my affinity or preference for products based on my browsing history. The scores here are both 50%, indicating no clear preference for either product. However, if I were to browse more content related to Sitecore Search or CDP, I suspect the weightings would increase accordingly.

Now, let's continue exploring our decision model. The next node is a Programmable, which contains the following code:

1function calculateTopAffinityScore(productAffinityScores) {
2 var highestScore = -Infinity;
3 var highestScoreObject = null;
4
5 for (var i = 0; i < productAffinityScores.length; i++) {
6 var currentObject = productAffinityScores[i];
7 if (currentObject.score > highestScore) {
8 highestScore = currentObject.score;
9 highestScoreObject = currentObject;
10 }
11 }
12
13 return highestScoreObject;
14}
15
16(function () {
17 let searchResponse = sitecoreSearchUserProfile.entities;
18
19 if (searchResponse !== undefined && searchResponse.length > 0 && searchResponse[0].affinity !== undefined) {
20 var topAffinityScore = calculateTopAffinityScore(searchResponse[0].affinity.product);
21
22 print(topAffinityScore);
23
24 return (topAffinityScore);
25 }
26
27 return undefined;
28})();

This code runs a function that examines the Data System's results, identifies its entities, and accesses the product affinity. It then processes this information through a method to identify the highest-scoring affinity. In this case, with a tie, the first affinity with 50% would be returned.

The next node in our decision model is optional. You could establish a connection with the decision model and return that as the result in the Full Stack Interactive Experience, leaving the rest to the trigger code. However, in our scenario, we use the Decision Table to return a title and description to the code that triggers the experience:

That concludes this blog post. If you're interested in more content from my Sitecore SUGCON Europe 2024 series, follow the link to learn more.

General

Home
About

Stay up to date


© 2024 All rights reserved.