Recsys: capture and use x-uuid from search results (#1727)

* Recsys/FYP: add documentation.

* Recsys: capture and use `x-uuid` from search results

Ticket: 1717
This commit is contained in:
infinite-persistence 2022-06-22 21:43:54 +08:00 committed by GitHub
parent 63a2430a7c
commit 486a557d75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 60 additions and 42 deletions

View file

@ -1,8 +1,7 @@
// @flow
import { RECSYS_ENDPOINT } from 'config';
import { selectUser } from 'redux/selectors/user';
import { makeSelectRecommendedRecsysIdForClaimId } from 'redux/selectors/search';
import { v4 as Uuidv4 } from 'uuid';
import { selectRecommendedMetaForClaimId } from 'redux/selectors/search';
import { parseURI } from 'util/lbryURI';
import { getAuthToken } from 'util/saved-passwords';
import * as ACTIONS from 'constants/action_types';
@ -16,7 +15,7 @@ import { selectIsSubscribedForClaimId } from 'redux/selectors/subscriptions';
import { history } from 'ui/store';
const recsysEndpoint = RECSYS_ENDPOINT;
const recsysId = 'lighthouse-v0';
const DEFAULT_RECSYS_ID = 'lighthouse-v0';
const getClaimIdsFromUris = (uris) => {
return uris
@ -81,15 +80,23 @@ const recsys: Recsys = {
* Page was loaded. Get or Create entry and populate it with default data,
* plus recommended content, recsysId, etc.
* Called from recommendedContent component
*
* @param claimId The ID of the content the recommendations are for.
* @param uris The recommended uris for `claimId`.
* @param uuid Specific uuid to use (e.g. for FYP); uses the recommendation's
* uuid otherwise.
*/
onRecsLoaded: function (claimId, uris, uuid = '') {
if (window && window.store) {
const state = window.store.getState();
const recommendedMeta = selectRecommendedMetaForClaimId(state, claimId);
if (!recsys.entries[claimId]) {
recsys.createRecsysEntry(claimId, null, uuid);
recsys.createRecsysEntry(claimId, null, uuid || recommendedMeta.uuid);
}
const claimIds = getClaimIdsFromUris(uris);
recsys.entries[claimId]['recsysId'] = makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId;
recsys.entries[claimId]['recsysId'] = recommendedMeta.poweredBy || DEFAULT_RECSYS_ID;
recsys.entries[claimId]['pageLoadedAt'] = Date.now();
// It is possible that `claimIds` include `null | undefined` entries
@ -107,18 +114,20 @@ const recsys: Recsys = {
* Creates an Entry with optional parentUuid
* @param: claimId: string
* @param: parentUuid: string (optional)
* @param: uuid: string Specific uuid to use.
* @param uuid Specific uuid to use (e.g. for FYP); uses the recommendation's
* uuid otherwise.
*/
createRecsysEntry: function (claimId, parentUuid, uuid = '') {
if (window && window.store && claimId) {
const state = window.store.getState();
const recommendedMeta = selectRecommendedMetaForClaimId(state, claimId);
const user = selectUser(state);
const userId = user ? user.id : null;
// Make a stub entry that will be filled out on page load
// $FlowIgnore: not everything is defined since this is a stub
recsys.entries[claimId] = {
uuid: uuid || Uuidv4(),
uuid: uuid || recommendedMeta.uuid,
claimId: claimId,
recClickedVideoIdx: [],
pageLoadedAt: Date.now(),
@ -154,6 +163,7 @@ const recsys: Recsys = {
IS_WEB || (window && window.store && selectDaemonSettings(window.store.getState()).share_usage_data);
if (recsys.entries[claimId] && shareTelemetry) {
// Exclude `events` in the submission https://github.com/OdyseeTeam/odysee-frontend/issues/1317
const { events, ...entryData } = recsys.entries[claimId];
const data = JSON.stringify(entryData);

11
flow-typed/search.js vendored
View file

@ -28,7 +28,7 @@ declare type SearchOptions = {
declare type SearchState = {
options: SearchOptions,
resultsByQuery: {},
resultsByQuery: { [string]: { uris: Array<string>, recsys: string, uuid: string } },
results: Array<string>,
hasReachedMaxResultsLength: {},
searching: boolean,
@ -36,6 +36,12 @@ declare type SearchState = {
personalRecommendations: { gid: string, uris: Array<string>, fetched: boolean },
};
declare type SearchResults = {
body: Array<{ name: string, claimId: string}>,
poweredBy: string,
uuid: string,
};
declare type SearchSuccess = {
type: ACTIONS.SEARCH_SUCCESS,
data: {
@ -43,7 +49,8 @@ declare type SearchSuccess = {
from: number,
size: number,
uris: Array<string>,
recsys: string,
poweredBy: string,
uuid: string,
},
};

View file

@ -88,6 +88,7 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
// e.g. never in a floating popup. With that, we can grab the FYP ID from
// the search param directly. Otherwise, the parent component would need to
// pass it.
// @see https://www.notion.so/FYP-Design-Notes-727782dde2cb485290c530ae96a34285
const { search } = location;
const urlParams = new URLSearchParams(search);
const fypId = urlParams.get(FYP_ID);

View file

@ -171,11 +171,12 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
const cmd = isSearchingRecommendations ? lighthouse.searchRecommendations : lighthouse.search;
cmd(queryWithOptions)
.then((data: { body: Array<{ name: string, claimId: string }>, poweredBy: string }) => {
const { body: result, poweredBy } = data;
.then((data: SearchResults) => {
const { body: result, poweredBy, uuid } = data;
const uris = processLighthouseResults(result);
if (isSearchingRecommendations) {
// Temporarily resolve using `claim_search` until the SDK bug is fixed.
const claimIds = result.map((x) => x.claimId);
dispatch(doResolveClaimIds(claimIds)).finally(() => {
dispatch({
@ -185,7 +186,8 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
from: from,
size: size,
uris,
recsys: poweredBy,
poweredBy,
uuid,
},
});
});
@ -201,7 +203,8 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
from: from,
size: size,
uris,
recsys: poweredBy,
poweredBy,
uuid,
},
});

View file

@ -32,7 +32,7 @@ export default handleActions(
searching: true,
}),
[ACTIONS.SEARCH_SUCCESS]: (state: SearchState, action: SearchSuccess): SearchState => {
const { query, uris, from, size, recsys } = action.data;
const { query, uris, from, size, poweredBy: recsys, uuid } = action.data;
const normalizedQuery = createNormalizedSearchKey(query);
const urisForQuery = state.resultsByQuery[normalizedQuery] && state.resultsByQuery[normalizedQuery]['uris'];
@ -44,7 +44,7 @@ export default handleActions(
// The returned number of urls is less than the page size, so we're on the last page
const noMoreResults = size && uris.length < size;
const results = { uris: newUris, recsys };
const results = { uris: newUris, recsys, uuid };
return {
...state,
searching: false,

View file

@ -2,6 +2,7 @@
import { selectShowMatureContent } from 'redux/selectors/settings';
import {
selectClaimsByUri,
selectClaimForClaimId,
makeSelectClaimForUri,
makeSelectClaimForClaimId,
selectClaimIsNsfwForUri,
@ -25,8 +26,7 @@ export const selectState = (state: State): SearchState => state.search;
export const selectSearchValue: (state: State) => string = (state) => selectState(state).searchQuery;
export const selectSearchOptions: (state: State) => SearchOptions = (state) => selectState(state).options;
export const selectIsSearching: (state: State) => boolean = (state) => selectState(state).searching;
export const selectSearchResultByQuery: (state: State) => { [string]: Array<string> } = (state) =>
selectState(state).resultsByQuery;
export const selectSearchResultByQuery = (state: State) => selectState(state).resultsByQuery;
export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = (state) =>
selectState(state).hasReachedMaxResultsLength;
export const selectMentionSearchResults: (state: State) => Array<string> = (state) => selectState(state).results;
@ -142,34 +142,30 @@ export const selectRecommendedContentForUri = createCachedSelector(
}
)((state, uri) => String(uri));
export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) =>
createSelector(
makeSelectClaimForClaimId(claimId),
export const selectRecommendedMetaForClaimId = createCachedSelector(
selectClaimForClaimId,
selectShowMatureContent,
selectSearchResultByQuery,
(claim, matureEnabled, searchUrisByQuery) => {
// TODO: DRY this out.
let poweredBy;
if (claim && claimId) {
if (claim && claim?.value?.title && claim.claim_id) {
const isMature = isClaimNsfw(claim);
const { title } = claim.value;
if (!title) {
return;
}
const title = claim.value.title;
const options = getRecommendationSearchOptions(matureEnabled, isMature, claimId);
const options = getRecommendationSearchOptions(matureEnabled, isMature, claim.claim_id);
const normalizedSearchQuery = getRecommendationSearchKey(title, options);
const searchResult = searchUrisByQuery[normalizedSearchQuery];
if (searchResult) {
poweredBy = searchResult.recsys;
return {
poweredBy: searchResult.recsys,
uuid: searchResult.uuid,
};
} else {
return normalizedSearchQuery;
}
}
return poweredBy;
}
);
)((state, claimId) => String(claimId));
export const makeSelectWinningUriForQuery = (query: string) => {
const uriFromQuery = `lbry://${query}`;

View file

@ -2,8 +2,9 @@
export default function handleFetchResponse(response: Response): Promise<any> {
const headers = response.headers;
const poweredBy = headers.get('x-powered-by');
const uuid = headers.get('x-uuid');
return response.status === 200
? response.json().then((body) => ({ body, poweredBy }))
? response.json().then((body) => ({ body, poweredBy, uuid }))
: Promise.reject(new Error(response.statusText));
}