summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization-processing.js
blob: 538e7088e91794d2900c1f3b5aa6ca757cded5dc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
import { isNothing } from "@jet/environment";
import * as serverData from "../../foundation/json-parsing/server-data";
import { diversifyDataItems, getOrderedAppIds, getUpdatedScoreAfterBoosting, PersonalizedData, } from "./on-device-recommendations-common";
/**
 * This utility class simplifies processing the raw data, by decorating with some key properties.
 *  */
class PersonalizedDataDefault extends PersonalizedData {
    constructor(rawData) {
        super();
        this.rawData = rawData;
        this.isExactMatch = false;
        this.isWildcardMatch = false;
        this.isUnpersonalizedMatch = false;
        this.isFallbackMatch = false;
        this.appId = null;
        this.groupId = null;
        this.score = 0;
        this.modifiedScore = 0;
        this.onDeviceScore = 0;
    }
}
// Represents a "match all" wildcard segment. Any data items that have this segment are always considered a match.
const alwaysMatchUserSegment = "-1";
/**
 * Converts a list of raw data blobs into a list that has been personalized for the user, based upon on device personalization data.
 *
 * If use_segment_scores is true, the rules we follow here are:
 * 1. Choose the data items that have personalization segments which match the user
 * 2. Remove some data items so that there is only one per group
 * 3. Bring any data items where the user exactly matches the personalization segment to the front of the list
 *
 * If needed, we may also include fallback results to reach a preferred number of results. For any group where no matches are found, the last
 * item in that group can be used as a fallback. We can only ever have one item per group, so it may not always be possible to reach the
 * preferred number of results.
 *
 * If use_signals is true, we rerank content using the on-device scores
 *
 * @param dataItems The raw data blobs.
 * @param onDevicePersonalizationDataContainer The on device personalization data container for the user, used for matching segments against the dataItems.
 * @param includeItemsWithNoPersonalizationData Whether dataItems without any valid personalization data should always be included in the results.
 * @param allowUnmatchedFallbackResults Whether to allow fallback results to be included in the results. This will only be utilised in order to reach a preferredResultCount.
 * @param preferredResultCount The preferred number of items to be included in the results.
 * @param parentAppId An optional appID, which is the parent for all the dataItems. Currently only used for search.
 * @param diversify An optional flag that determines if we should diverse the personalized results on the basis of server side apps ranking
 * @returns The personalized set of data. This will be a subset (or all) of the original dataItems, and metrics data.
 */
export function personalizeDataItems(objectGraph, dataItems, onDevicePersonalizationDataContainer, includeItemsWithNoPersonalizationData, allowUnmatchedFallbackResults, preferredResultCount, parentAppId, diversify) {
    var _a;
    let sortResult = { sortedDataItems: [], processingType: 0 };
    const useSignals = (_a = onDevicePersonalizationDataContainer === null || onDevicePersonalizationDataContainer === void 0 ? void 0 : onDevicePersonalizationDataContainer.metricsData["use_signals"]) !== null && _a !== void 0 ? _a : false;
    if (!useSignals) {
        // First decorate our raw dataItems with segment and group information
        const personalizedDataItems = personalizedDataItemsFromDataItems(objectGraph, dataItems, onDevicePersonalizationDataContainer === null || onDevicePersonalizationDataContainer === void 0 ? void 0 : onDevicePersonalizationDataContainer.personalizationData, includeItemsWithNoPersonalizationData, parentAppId);
        // Get server side ordering of app Ids to be used for diversification
        const serverSideAppIdsOrdering = getOrderedAppIds(personalizedDataItems);
        // Now iterate through the list of personalizedDataItems, and choose one per group
        const matchedDataItemsIncludingFallback = filterDataItemsIntoOnePerGroup(objectGraph, personalizedDataItems);
        // Now sort the data items, respecting our preferredResultCount if needed
        sortResult = sortDataItems(objectGraph, matchedDataItemsIncludingFallback, allowUnmatchedFallbackResults, serverSideAppIdsOrdering, preferredResultCount, diversify);
    }
    else {
        // First decorate our raw dataItems with frequency, recency, usage information
        const personalizedDataItems = personalizedDataItemsFromDataItemsOnDeviceSignals(objectGraph, dataItems, onDevicePersonalizationDataContainer === null || onDevicePersonalizationDataContainer === void 0 ? void 0 : onDevicePersonalizationDataContainer.personalizationData, includeItemsWithNoPersonalizationData, parentAppId);
        // Now sort the data items
        const sortedDataItems = getUpdatedScoreAfterBoosting(personalizedDataItems, onDevicePersonalizationDataContainer === null || onDevicePersonalizationDataContainer === void 0 ? void 0 : onDevicePersonalizationDataContainer.metricsData);
        const orderWasNotChanged = personalizedDataItems.every((dataItem, index) => {
            return dataItem === sortedDataItems[index];
        });
        sortResult = {
            sortedDataItems: sortedDataItems,
            processingType: orderWasNotChanged
                ? 0 /* onDevicePersonalization.ProcessingType.contentsNotChanged */
                : 2 /* onDevicePersonalization.ProcessingType.contentsSorted */,
        };
        if (serverData.isDefinedNonNull(preferredResultCount) &&
            sortResult.sortedDataItems.length >= preferredResultCount) {
            sortResult.sortedDataItems = sortResult.sortedDataItems.slice(0, preferredResultCount);
        }
    }
    // We only need to return the raw data blobs, so remove the personalization decoration
    const finalDataItems = sortResult.sortedDataItems.map((personalizedDataItem) => personalizedDataItem.rawData);
    // Generate the processing type value
    const filterType = dataItems.length !== finalDataItems.length
        ? 1 /* onDevicePersonalization.ProcessingType.contentsFiltered */
        : 0 /* onDevicePersonalization.ProcessingType.contentsNotChanged */;
    const processingType = filterType + sortResult.processingType;
    return {
        personalizedData: finalDataItems,
        processingType: processingType,
    };
}
/**
 * Creates a list of `PersonalizedData` objects, based on the input raw data items.
 *
 * @param dataItems The raw data blobs.
 * @param onDevicePersonalizationData The on device personalization data, used for matching personalization segments against the dataItems.
 * @param includeItemsWithNoPersonalizationData Whether dataItems without any valid personalization data should be included in the results.
 * @param parentAppId An optional appID, which is the parent for all the dataItems. Currently only used for search.
 * @returns A list of PersonalizedData objects.
 */
function personalizedDataItemsFromDataItemsOnDeviceSignals(objectGraph, dataItems, onDevicePersonalizationData, includeItemsWithNoPersonalizationData, parentAppId) {
    const personalizedDataItems = [];
    for (const data of dataItems) {
        const personalizedData = new PersonalizedDataDefault(data);
        // Filter out invalid data
        const score = serverData.asNumber(data, "meta.personalizationData.score");
        let appId = serverData.asString(data, "meta.personalizationData.appId");
        if ((isNothing(appId) || appId.length === 0) && (parentAppId === null || parentAppId === void 0 ? void 0 : parentAppId.length) > 0) {
            // If we have a parentAppId this means we are coming from search, where `appId` is not provided.
            appId = parentAppId;
        }
        if (isNothing(appId) || appId.length === 0) {
            // Personalization data is missing or invalid. This may sometimes be valid, eg. evergreen today stories for when reco times out.
            if (includeItemsWithNoPersonalizationData) {
                personalizedData.isUnpersonalizedMatch = true;
                personalizedDataItems.push(personalizedData);
            }
            continue;
        }
        if (serverData.isDefinedNonNull(onDevicePersonalizationData)) {
            const onDevicePersonalizationDataForApp = onDevicePersonalizationData[appId];
            if (serverData.isDefinedNonNull(onDevicePersonalizationDataForApp) &&
                serverData.isDefinedNonNull(onDevicePersonalizationDataForApp.onDeviceSignals)) {
                personalizedData.onDeviceScore = +onDevicePersonalizationDataForApp.onDeviceSignals;
            }
        }
        personalizedData.appId = appId;
        personalizedData.score = score !== null && score !== void 0 ? score : 0;
        personalizedDataItems.push(personalizedData);
    }
    return personalizedDataItems;
}
/**
 * Creates a list of `PersonalizedData` objects, based on the input raw data items.
 *
 * @param dataItems The raw data blobs.
 * @param onDevicePersonalizationData The on device personalization data, used for matching personalization segments against the dataItems.
 * @param includeItemsWithNoPersonalizationData Whether dataItems without any valid personalization data should be included in the results.
 * @param parentAppId An optional appID, which is the parent for all the dataItems. Currently only used for search.
 * @returns A list of PersonalizedData objects.
 */
function personalizedDataItemsFromDataItems(objectGraph, dataItems, onDevicePersonalizationData, includeItemsWithNoPersonalizationData, parentAppId) {
    const personalizedDataItems = [];
    for (const data of dataItems) {
        const personalizedData = new PersonalizedDataDefault(data);
        // Filter out invalid data
        const rawDataUserSegments = serverData.asString(data, "meta.personalizationData.segId");
        let appId = serverData.asString(data, "meta.personalizationData.appId");
        let groupId = serverData.asString(data, "meta.personalizationData.grpId");
        if ((isNothing(appId) || appId.length === 0) && (parentAppId === null || parentAppId === void 0 ? void 0 : parentAppId.length) > 0) {
            // If we have a parentAppId this means we are coming from search, where `appId` and `grpId` are not provided.
            // Normally we filter our data items to only allow one item per group, so in this case we allocate a random
            // group ID, so that none of the data items get filtered out for that reason. Later on as part of search
            // results processing we will pick the first (valid) result, but only after ODP has finished.
            appId = parentAppId;
            groupId = objectGraph.random.nextUUID();
        }
        if (serverData.isNullOrEmpty(rawDataUserSegments) ||
            serverData.isNullOrEmpty(appId) ||
            serverData.isNullOrEmpty(groupId)) {
            // Personalization data is missing or invalid. This may sometimes be valid, eg. evergreen today stories for when reco times out.
            if (includeItemsWithNoPersonalizationData) {
                personalizedData.isUnpersonalizedMatch = true;
                personalizedDataItems.push(personalizedData);
            }
            continue;
        }
        // Check if the data has the match all user segment
        const dataUserSegments = rawDataUserSegments.split(",");
        if (dataUserSegments.includes(alwaysMatchUserSegment)) {
            personalizedData.isWildcardMatch = true;
        }
        // Check if any of the data segments match with the on device personalization data
        if (serverData.isDefinedNonNull(onDevicePersonalizationData)) {
            const onDevicePersonalizationDataForApp = onDevicePersonalizationData[appId];
            if (serverData.isDefinedNonNull(onDevicePersonalizationDataForApp)) {
                for (const dataUserSegment of dataUserSegments) {
                    if (onDevicePersonalizationDataForApp.userSegments.includes(dataUserSegment)) {
                        personalizedData.isExactMatch = true;
                        break;
                    }
                }
            }
        }
        personalizedData.appId = appId;
        personalizedData.groupId = groupId;
        personalizedDataItems.push(personalizedData);
    }
    return personalizedDataItems;
}
/**
 * Iterates through the list of given data items, and ensures we only have one per group.
 *
 * @param dataItems The data items to processed.
 * @returns A subset of dataItems, with only one dataItem per group.
 */
function filterDataItemsIntoOnePerGroup(objectGraph, dataItems) {
    var _a;
    const filledGroupIds = new Set();
    const matchedDataItemsIncludingMultipleFallbacksPerGroup = [];
    // Determine which groups have any exact matches
    const groupIdsWithExactMatchesArray = dataItems
        .filter((dataItem) => {
        return dataItem.isExactMatch;
    })
        .map((dataItem) => {
        return dataItem.groupId;
    });
    const groupIdsWithExactMatches = new Set(groupIdsWithExactMatchesArray);
    // Now iterate through our data items, and filter out any we don't need
    dataItems.forEach((dataItem, index) => {
        // If an item has no group, we always include it. This would only happen for
        // data which is missing valid personalization metadata, and we have specifically
        // opted in to including these items in the results.
        if (serverData.isNullOrEmpty(dataItem.groupId)) {
            matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem);
            return;
        }
        // We already have a match for this group, so move onto the next item
        if (filledGroupIds.has(dataItem.groupId)) {
            return;
        }
        // This item is an unpersonalized match, which will only occur if we permit this.
        // These are always added to the result set.
        if (dataItem.isUnpersonalizedMatch) {
            matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem);
            return;
        }
        // This item is the first exact match for this group, so add it into our result set
        if (dataItem.isExactMatch) {
            filledGroupIds.add(dataItem.groupId);
            matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem);
            return;
        }
        // If we know we have an exact match somewhere else for this group, we can just
        // continue on to the next item, as the exact match will be picked later.
        if (groupIdsWithExactMatches.has(dataItem.groupId)) {
            return;
        }
        // We have no exact matches for this group, so we can now take wildcard matches.
        if (dataItem.isWildcardMatch) {
            filledGroupIds.add(dataItem.groupId);
            matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem);
            return;
        }
        // This item is not a match. As we don't have any matches for this group yet,
        // we can mark it as a fallback. This does not necessarily mean it will be used,
        // but it does mean it becomes available for use. groupIDs are not necessarily in
        // sequential order, so we mark all of these as fallbacks, and filter them further below.
        dataItem.isFallbackMatch = true;
        matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem);
    });
    // We now need to remove all the fallback items except for the last one in each group, so iterate
    // through in reverse order and filter out any duplicates
    const matchedDataItemsWithOneFallbackPerGroup = [];
    const reversedMatchedDataItems = matchedDataItemsIncludingMultipleFallbacksPerGroup.slice().reverse();
    for (const dataItem of reversedMatchedDataItems) {
        if (dataItem.isFallbackMatch) {
            if (filledGroupIds.has(dataItem.groupId)) {
                continue;
            }
        }
        matchedDataItemsWithOneFallbackPerGroup.push(dataItem);
        if (((_a = dataItem.groupId) === null || _a === void 0 ? void 0 : _a.length) > 0) {
            filledGroupIds.add(dataItem.groupId);
        }
    }
    // Return to our original order
    matchedDataItemsWithOneFallbackPerGroup.reverse();
    return matchedDataItemsWithOneFallbackPerGroup;
}
/**
 * Sorts the given list of data items, and optionally restricts the list to a specified number of results.
 *
 * @param dataItems The data items to process.
 * @param allowUnmatchedFallbackResults Whether to allow fallback results to be included in the results. This will only be utilised in order to reach a preferredResultCount.
 * @param preferredResultCount? The preferrd number of results.
 * @param serverSideAppIdsOrdering List of ordered app ids from server side
 * @param diversify An optional flag that determines if we should diverse the personalized results on the basis of server side apps ranking
 * @returns The sorted list of dataItems, optionally restricted in length,
 */
function sortDataItems(objectGraph, dataItems, allowUnmatchedFallbackResults, serverSideAppIdsOrdering, preferredResultCount, diversify) {
    let sortResult;
    // Excluding fallback results is the preferred route, but if the number of results is less than our preferredResultCount, we will need to use the fallback results.
    const dataItemsWithoutFallback = dataItems.filter((data) => data.isExactMatch || data.isWildcardMatch || data.isUnpersonalizedMatch || serverData.isNull(data.groupId));
    if (serverData.isNull(preferredResultCount)) {
        // There is no preferred number of results, so simply perform our final sort and then return
        sortResult = sortAndDiversify(dataItemsWithoutFallback, serverSideAppIdsOrdering, diversify);
    }
    else if (dataItemsWithoutFallback.length >= preferredResultCount || !allowUnmatchedFallbackResults) {
        // There is a preferred number of results, but we either have enough items without needing to utilise
        // any fallback matches, or we don't allow fallback results.
        sortResult = sortAndDiversify(dataItemsWithoutFallback, serverSideAppIdsOrdering, diversify);
        sortResult.sortedDataItems = sortResult.sortedDataItems.slice(0, preferredResultCount);
    }
    else {
        // There is a preferred number of results, and we need to use fallback matches in order to
        // meet this number. We may still fall short, but this gets us as close as possible.
        sortResult = sortAndDiversify(dataItems, serverSideAppIdsOrdering, diversify);
        sortResult.sortedDataItems = sortResult.sortedDataItems.slice(0, preferredResultCount);
    }
    return sortResult;
}
/**
 * Rearranges a list of dataItems, so that any where there is an exact segment match are moved to the front of the list.
 *
 * @param dataItems The data items to process.
 * @param serverSideAppIdsOrdering List of ordered app ids from server side
 * @param diversify An optional flag that determines if we should diverse the personalized results on the basis of server side apps ranking
 * @returns The sorted list of data items.
 */
function sortAndDiversify(dataItems, serverSideAppIdsOrdering, diversify) {
    const exactMatchDataItems = dataItems.filter((value) => value.isExactMatch);
    let otherDataItems = dataItems.filter((value) => !value.isExactMatch);
    if (serverData.isDefinedNonNull(diversify) && diversify) {
        otherDataItems = diversifyDataItems(otherDataItems, serverSideAppIdsOrdering);
    }
    const sortedDataItems = exactMatchDataItems.concat(otherDataItems);
    const orderWasNotChanged = dataItems.every((dataItem, index) => {
        return dataItem === sortedDataItems[index];
    });
    return {
        sortedDataItems: sortedDataItems,
        processingType: orderWasNotChanged
            ? 0 /* onDevicePersonalization.ProcessingType.contentsNotChanged */
            : 2 /* onDevicePersonalization.ProcessingType.contentsSorted */,
    };
}
/**
 * Filters a list of raw data blobs into a list which only includes non-personalized data, or data that is set to "match all".
 *
 * @param dataItems The raw data blobs.
 * @param preferredResultCount The preferred number of items to be included in the results.
 * @returns The filtered set of data blobs. This will be a subset (or all) of the original dataItems.
 */
export function removePersonalizedDataItems(objectGraph, dataItems, preferredResultCount) {
    let filteredDataItems = [];
    const filledGroupIds = new Set();
    for (const data of dataItems) {
        // If the personalization data is invalid or empty, we keep this in our result set.
        const rawDataUserSegments = serverData.asString(data, "meta.personalizationData.segId");
        const appId = serverData.asString(data, "meta.personalizationData.appId");
        const groupId = serverData.asString(data, "meta.personalizationData.grpId");
        if (serverData.isNullOrEmpty(rawDataUserSegments) ||
            serverData.isNullOrEmpty(appId) ||
            serverData.isNullOrEmpty(groupId)) {
            filteredDataItems.push(data);
            continue;
        }
        // We already have a match for this group, so move onto the next item
        if (filledGroupIds.has(groupId)) {
            continue;
        }
        // If the data has a match all user segment, we keep this in our result set.
        const dataUserSegments = rawDataUserSegments.split(",");
        if (dataUserSegments.includes(alwaysMatchUserSegment)) {
            filteredDataItems.push(data);
            filledGroupIds.add(groupId);
        }
    }
    // Finally, if we have a preferredResultCount which is smaller than our result set, trim our results down to this count
    if (serverData.isDefinedNonNull(preferredResultCount) && filteredDataItems.length > preferredResultCount) {
        filteredDataItems = filteredDataItems.slice(0, preferredResultCount);
    }
    return {
        personalizedData: filteredDataItems,
        processingType: null,
    };
}
//# sourceMappingURL=on-device-personalization-processing.js.map