send recsys powered-by #6875

Merged
jessopb merged 4 commits from recsys-sendPoweredBy into master 2021-08-17 16:03:25 +02:00
9 changed files with 72 additions and 29 deletions

View file

@ -28,7 +28,7 @@ declare type SearchOptions = {
declare type SearchState = { declare type SearchState = {
options: SearchOptions, options: SearchOptions,
urisByQuery: {}, resultsByQuery: {},
hasReachedMaxResultsLength: {}, hasReachedMaxResultsLength: {},
searching: boolean, searching: boolean,
}; };
@ -40,6 +40,7 @@ declare type SearchSuccess = {
from: number, from: number,
size: number, size: number,
uris: Array<string>, uris: Array<string>,
recsys: string,
}, },
}; };

View file

@ -81,7 +81,7 @@ function ChannelContent(props: Props) {
!showMature ? '&nsfw=false&size=50&from=0' : '' !showMature ? '&nsfw=false&size=50&from=0' : ''
}` }`
) )
.then((results) => { .then(({ body: results }) => {
const urls = results.map(({ name, claimId }) => { const urls = results.map(({ name, claimId }) => {
return `lbry://${name}#${claimId}`; return `lbry://${name}#${claimId}`;
}); });

View file

@ -6,6 +6,7 @@ import {
makeSelectRecommendedClaimIds, makeSelectRecommendedClaimIds,
makeSelectRecommendationClicks, makeSelectRecommendationClicks,
} from 'redux/selectors/content'; } from 'redux/selectors/content';
import { makeSelectRecommendedRecsysIdForClaimId } from 'redux/selectors/search';
const VERSION = '0.0.1'; const VERSION = '0.0.1';
@ -36,7 +37,7 @@ function createRecsys(claimId, userId, events, loadedAt, isEmbed) {
claimId: claimId, claimId: claimId,
pageLoadedAt: pageLoadedAt, pageLoadedAt: pageLoadedAt,
pageExitedAt: pageExitedAt, pageExitedAt: pageExitedAt,
recsysId: recsysId, recsysId: makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId,
recClaimIds: makeSelectRecommendedClaimIds(claimId)(state), recClaimIds: makeSelectRecommendedClaimIds(claimId)(state),
recClickedVideoIdx: makeSelectRecommendationClicks(claimId)(state), recClickedVideoIdx: makeSelectRecommendationClicks(claimId)(state),
events: events, events: events,

View file

@ -25,7 +25,7 @@ export default function useLighthouse(
let isSubscribed = true; let isSubscribed = true;
lighthouse lighthouse
.search(throttledQuery) .search(throttledQuery)
.then((results) => { .then(({ body: results }) => {
if (isSubscribed) { if (isSubscribed) {
setResults( setResults(
results.map((result) => `lbry://${result.name}#${result.claimId}`).filter((uri) => isURIValid(uri)) results.map((result) => `lbry://${result.name}#${result.claimId}`).filter((uri) => isURIValid(uri))

View file

@ -3,7 +3,7 @@ import { withRouter } from 'react-router';
import { doSearch } from 'redux/actions/search'; import { doSearch } from 'redux/actions/search';
import { import {
selectIsSearching, selectIsSearching,
makeSelectSearchUris, makeSelectSearchUrisForQuery,
selectSearchOptions, selectSearchOptions,
makeSelectHasReachedMaxResultsLength, makeSelectHasReachedMaxResultsLength,
} from 'redux/selectors/search'; } from 'redux/selectors/search';
@ -28,7 +28,7 @@ const select = (state, props) => {
}; };
const query = getSearchQueryString(urlQuery, searchOptions); const query = getSearchQueryString(urlQuery, searchOptions);
const uris = makeSelectSearchUris(query)(state); const uris = makeSelectSearchUrisForQuery(query)(state);
const hasReachedMaxResultsLength = makeSelectHasReachedMaxResultsLength(query)(state); const hasReachedMaxResultsLength = makeSelectHasReachedMaxResultsLength(query)(state);
return { return {

View file

@ -2,7 +2,7 @@
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import { SEARCH_OPTIONS } from 'constants/search'; import { SEARCH_OPTIONS } from 'constants/search';
import { buildURI, doResolveUris, batchActions, isURIValid, makeSelectClaimForUri } from 'lbry-redux'; import { buildURI, doResolveUris, batchActions, isURIValid, makeSelectClaimForUri } from 'lbry-redux';
import { makeSelectSearchUris, selectSearchValue } from 'redux/selectors/search'; import { makeSelectSearchUrisForQuery, selectSearchValue } from 'redux/selectors/search';
import handleFetchResponse from 'util/handle-fetch'; import handleFetchResponse from 'util/handle-fetch';
import { getSearchQueryString } from 'util/query-params'; import { getSearchQueryString } from 'util/query-params';
import { SIMPLE_SITE, SEARCH_SERVER_API } from 'config'; import { SIMPLE_SITE, SEARCH_SERVER_API } from 'config';
@ -48,7 +48,7 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
const from = searchOptions.from; const from = searchOptions.from;
// If we have already searched for something, we don't need to do anything // If we have already searched for something, we don't need to do anything
const urisForQuery = makeSelectSearchUris(queryWithOptions)(state); const urisForQuery = makeSelectSearchUrisForQuery(queryWithOptions)(state);
if (urisForQuery && !!urisForQuery.length) { if (urisForQuery && !!urisForQuery.length) {
if (!size || !from || from + size < urisForQuery.length) { if (!size || !from || from + size < urisForQuery.length) {
return; return;
@ -61,13 +61,14 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
lighthouse lighthouse
.search(queryWithOptions) .search(queryWithOptions)
.then((data: Array<{ name: string, claimId: string }>) => { .then((data: { body: Array<{ name: string, claimId: string }>, poweredBy: string }) => {
const { body: result, poweredBy } = data;
const uris = []; const uris = [];
const actions = []; const actions = [];
data.forEach((result) => { result.forEach((item) => {
if (result) { if (item) {
const { name, claimId } = result; const { name, claimId } = item;
const urlObj: LbryUrlObj = {}; const urlObj: LbryUrlObj = {};
if (name.startsWith('@')) { if (name.startsWith('@')) {
@ -94,6 +95,7 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
from: from, from: from,
size: size, size: size,
uris, uris,
recsys: poweredBy,
}, },
}); });
dispatch(batchActions(...actions)); dispatch(batchActions(...actions));

View file

@ -17,7 +17,7 @@ const defaultState: SearchState = {
[SEARCH_OPTIONS.MEDIA_IMAGE]: defaultSearchTypes.includes(SEARCH_OPTIONS.MEDIA_IMAGE), [SEARCH_OPTIONS.MEDIA_IMAGE]: defaultSearchTypes.includes(SEARCH_OPTIONS.MEDIA_IMAGE),
[SEARCH_OPTIONS.MEDIA_APPLICATION]: defaultSearchTypes.includes(SEARCH_OPTIONS.MEDIA_APPLICATION), [SEARCH_OPTIONS.MEDIA_APPLICATION]: defaultSearchTypes.includes(SEARCH_OPTIONS.MEDIA_APPLICATION),
}, },
urisByQuery: {}, resultsByQuery: {},
hasReachedMaxResultsLength: {}, hasReachedMaxResultsLength: {},
searching: false, searching: false,
}; };
@ -29,21 +29,23 @@ 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 } = action.data; const { query, uris, from, size, recsys } = action.data;
const normalizedQuery = createNormalizedSearchKey(query); const normalizedQuery = createNormalizedSearchKey(query);
const urisForQuery = state.resultsByQuery[normalizedQuery] && state.resultsByQuery[normalizedQuery]['uris'];
let newUris = uris; let newUris = uris;
if (from !== 0 && state.urisByQuery[normalizedQuery]) { if (from !== 0 && urisForQuery) {
newUris = Array.from(new Set(state.urisByQuery[normalizedQuery].concat(uris))); newUris = Array.from(new Set(urisForQuery.concat(uris)));
} }
// 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 };
return { return {
...state, ...state,
searching: false, searching: false,
urisByQuery: Object.assign({}, state.urisByQuery, { [normalizedQuery]: newUris }), resultsByQuery: Object.assign({}, state.resultsByQuery, { [normalizedQuery]: results }),
hasReachedMaxResultsLength: Object.assign({}, state.hasReachedMaxResultsLength, { hasReachedMaxResultsLength: Object.assign({}, state.hasReachedMaxResultsLength, {
[normalizedQuery]: noMoreResults, [normalizedQuery]: noMoreResults,
}), }),

View file

@ -4,6 +4,7 @@ import { selectShowMatureContent } from 'redux/selectors/settings';
import { import {
parseURI, parseURI,
makeSelectClaimForUri, makeSelectClaimForUri,
makeSelectClaimForClaimId,
makeSelectClaimIsNsfw, makeSelectClaimIsNsfw,
buildURI, buildURI,
isClaimNsfw, isClaimNsfw,
@ -26,9 +27,9 @@ export const selectSearchOptions: (state: State) => SearchOptions = createSelect
export const selectIsSearching: (state: State) => boolean = createSelector(selectState, (state) => state.searching); export const selectIsSearching: (state: State) => boolean = createSelector(selectState, (state) => state.searching);
infinite-persistence commented 2021-08-13 04:33:45 +02:00 (Migrated from github.com)
Review

Maybe a rename would be good to reflect the actual returned object.

Maybe a rename would be good to reflect the actual returned object.
export const selectSearchUrisByQuery: (state: State) => { [string]: Array<string> } = createSelector( export const selectSearchResultByQuery: (state: State) => { [string]: Array<string> } = createSelector(
selectState, selectState,
(state) => state.urisByQuery (state) => state.resultsByQuery
); );
export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = createSelector( export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = createSelector(
@ -36,15 +37,15 @@ export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Ar
(state) => state.hasReachedMaxResultsLength (state) => state.hasReachedMaxResultsLength
); );
export const makeSelectSearchUris = (query: string): ((state: State) => Array<string>) => export const makeSelectSearchUrisForQuery = (query: string): ((state: State) => Array<string>) =>
// replace statement below is kind of ugly, and repeated in doSearch action // replace statement below is kind of ugly, and repeated in doSearch action
createSelector(selectSearchUrisByQuery, (byQuery) => { createSelector(selectSearchResultByQuery, (byQuery) => {
if (query) { if (query) {
query = query.replace(/^lbry:\/\//i, '').replace(/\//, ' '); query = query.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
const normalizedQuery = createNormalizedSearchKey(query); const normalizedQuery = createNormalizedSearchKey(query);
return byQuery[normalizedQuery]; return byQuery[normalizedQuery] && byQuery[normalizedQuery]['uris'];
} }
return byQuery[query]; return byQuery[query] && byQuery[query]['uris'];
}); });
export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: State) => boolean) => export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: State) => boolean) =>
@ -60,7 +61,7 @@ export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: St
export const makeSelectRecommendedContentForUri = (uri: string) => export const makeSelectRecommendedContentForUri = (uri: string) =>
createSelector( createSelector(
makeSelectClaimForUri(uri), makeSelectClaimForUri(uri),
selectSearchUrisByQuery, selectSearchResultByQuery,
makeSelectClaimIsNsfw(uri), makeSelectClaimIsNsfw(uri),
(claim, searchUrisByQuery, isMature) => { (claim, searchUrisByQuery, isMature) => {
let recommendedContent; let recommendedContent;
@ -84,16 +85,47 @@ export const makeSelectRecommendedContentForUri = (uri: string) =>
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options); const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
const normalizedSearchQuery = createNormalizedSearchKey(searchQuery); const normalizedSearchQuery = createNormalizedSearchKey(searchQuery);
let searchUris = searchUrisByQuery[normalizedSearchQuery]; let searchResult = searchUrisByQuery[normalizedSearchQuery];
if (searchUris) { if (searchResult) {
searchUris = searchUris.filter((searchUri) => searchUri !== currentUri); recommendedContent = searchResult['uris'].filter((searchUri) => searchUri !== currentUri);
recommendedContent = searchUris;
} }
} }
return recommendedContent; return recommendedContent;
} }
); );
export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) =>
createSelector(makeSelectClaimForClaimId(claimId), selectSearchResultByQuery, (claim, searchUrisByQuery) => {
// TODO: DRY this out.
let poweredBy;
if (claim) {
const isMature = isClaimNsfw(claim);
const { title } = claim.value;
if (!title) {
return;
}
const options: {
related_to?: string,
nsfw?: boolean,
isBackgroundSearch?: boolean,
} = { related_to: claim.claim_id, isBackgroundSearch: true };
options['nsfw'] = isMature;
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
const normalizedSearchQuery = createNormalizedSearchKey(searchQuery);
let searchResult = searchUrisByQuery[normalizedSearchQuery];
if (searchResult) {
poweredBy = searchResult.recsys;
} else {
return normalizedSearchQuery;
}
}
return poweredBy;
});
export const makeSelectWinningUriForQuery = (query: string) => { export const makeSelectWinningUriForQuery = (query: string) => {
const uriFromQuery = `lbry://${query}`; const uriFromQuery = `lbry://${query}`;

View file

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