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

11
flow-typed/search.js vendored
View file

@ -28,7 +28,7 @@ declare type SearchOptions = {
declare type SearchState = { declare type SearchState = {
options: SearchOptions, options: SearchOptions,
resultsByQuery: {}, resultsByQuery: { [string]: { uris: Array<string>, recsys: string, uuid: string } },
results: Array<string>, results: Array<string>,
hasReachedMaxResultsLength: {}, hasReachedMaxResultsLength: {},
searching: boolean, searching: boolean,
@ -36,6 +36,12 @@ declare type SearchState = {
personalRecommendations: { gid: string, uris: Array<string>, fetched: boolean }, personalRecommendations: { gid: string, uris: Array<string>, fetched: boolean },
}; };
declare type SearchResults = {
body: Array<{ name: string, claimId: string}>,
poweredBy: string,
uuid: string,
};
declare type SearchSuccess = { declare type SearchSuccess = {
type: ACTIONS.SEARCH_SUCCESS, type: ACTIONS.SEARCH_SUCCESS,
data: { data: {
@ -43,7 +49,8 @@ declare type SearchSuccess = {
from: number, from: number,
size: number, size: number,
uris: Array<string>, 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 // 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 // the search param directly. Otherwise, the parent component would need to
// pass it. // pass it.
// @see https://www.notion.so/FYP-Design-Notes-727782dde2cb485290c530ae96a34285
const { search } = location; const { search } = location;
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const fypId = urlParams.get(FYP_ID); 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; const cmd = isSearchingRecommendations ? lighthouse.searchRecommendations : lighthouse.search;
cmd(queryWithOptions) cmd(queryWithOptions)
.then((data: { body: Array<{ name: string, claimId: string }>, poweredBy: string }) => { .then((data: SearchResults) => {
const { body: result, poweredBy } = data; const { body: result, poweredBy, uuid } = data;
const uris = processLighthouseResults(result); const uris = processLighthouseResults(result);
if (isSearchingRecommendations) { if (isSearchingRecommendations) {
// Temporarily resolve using `claim_search` until the SDK bug is fixed.
const claimIds = result.map((x) => x.claimId); const claimIds = result.map((x) => x.claimId);
dispatch(doResolveClaimIds(claimIds)).finally(() => { dispatch(doResolveClaimIds(claimIds)).finally(() => {
dispatch({ dispatch({
@ -185,7 +186,8 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
from: from, from: from,
size: size, size: size,
uris, uris,
recsys: poweredBy, poweredBy,
uuid,
}, },
}); });
}); });
@ -201,7 +203,8 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
from: from, from: from,
size: size, size: size,
uris, uris,
recsys: poweredBy, poweredBy,
uuid,
}, },
}); });

View file

@ -32,7 +32,7 @@ export default handleActions(
searching: true, searching: true,
}), }),
[ACTIONS.SEARCH_SUCCESS]: (state: SearchState, action: SearchSuccess): SearchState => { [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 normalizedQuery = createNormalizedSearchKey(query);
const urisForQuery = state.resultsByQuery[normalizedQuery] && state.resultsByQuery[normalizedQuery]['uris']; 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 // 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 noMoreResults = size && uris.length < size;
const results = { uris: newUris, recsys }; const results = { uris: newUris, recsys, uuid };
return { return {
...state, ...state,
searching: false, searching: false,

View file

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

View file

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