send recsys powered-by #6875
9 changed files with 72 additions and 29 deletions
3
flow-typed/search.js
vendored
3
flow-typed/search.js
vendored
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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}`;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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);
|
||||||
|
|||||||
|
|
||||||
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}`;
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue
Maybe a rename would be good to reflect the actual returned object.