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:
parent
63a2430a7c
commit
486a557d75
7 changed files with 60 additions and 42 deletions
|
@ -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
11
flow-typed/search.js
vendored
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}`;
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue