add score library item job

This commit is contained in:
Hongbo Wu 2024-05-27 11:37:59 +08:00
parent f6424c3bd2
commit 3cb93283b0
4 changed files with 130 additions and 41 deletions

View file

@ -0,0 +1,80 @@
import {
findLibraryItemById,
updateLibraryItem,
} from '../services/library_item'
import { Feature, getScores } from '../services/score'
import { lanaugeToCode } from '../utils/helpers'
import { logger } from '../utils/logger'
export const SCORE_LIBRARY_ITEM_JOB = 'SCORE_LIBRARY_ITEM_JOB'
export interface ScoreLibraryItemJobData {
userId: string
libraryItemId: string
}
export const scoreLibraryItem = async (
data: ScoreLibraryItemJobData
): Promise<void> => {
logger.info('Scoring library item', data)
const { userId, libraryItemId } = data
const libraryItem = await findLibraryItemById(libraryItemId, userId, {
select: [
'id',
'title',
'thumbnail',
'siteIcon',
'savedAt',
'siteName',
'directionality',
'folder',
'author',
'itemLanguage',
'wordCount',
],
})
if (!libraryItem) {
logger.error('Library item not found', data)
return
}
const itemFeatures = {
[libraryItem.id]: {
title: libraryItem.title,
has_thumbnail: !!libraryItem.thumbnail,
has_site_icon: !!libraryItem.siteIcon,
saved_at: libraryItem.savedAt,
site: libraryItem.siteName,
directionality: libraryItem.directionality,
folder: libraryItem.folder,
subscription_type: 'library',
author: libraryItem.author,
language: lanaugeToCode(libraryItem.itemLanguage || 'English'),
word_count: libraryItem.wordCount,
} as Feature,
}
const scores = await getScores({
user_id: userId,
item_features: itemFeatures,
})
logger.info('Scores', scores)
const score = scores[libraryItem.id]
if (!score) {
logger.error('Failed to score library item', data)
throw new Error('Failed to score library item')
}
await updateLibraryItem(
libraryItem.id,
{
score,
},
userId,
undefined,
true
)
}

View file

@ -1,4 +1,3 @@
import languages from '@cospired/i18n-iso-languages'
import { LibraryItem } from '../entity/library_item'
import { PublicItem } from '../entity/public_item'
import { User } from '../entity/user'
@ -6,7 +5,9 @@ import { JustReadFeedSection } from '../generated/graphql'
import { redisDataSource } from '../redis_data_source'
import { findUnseenPublicItems } from '../services/just_read_feed'
import { searchLibraryItems } from '../services/library_item'
import { Feature, getScores } from '../services/score'
import { findActiveUser } from '../services/user'
import { lanaugeToCode } from '../utils/helpers'
import { logger } from '../utils/logger'
export const UPDATE_JUST_READ_FEED_JOB = 'UPDATE_JUST_READ_FEED_JOB'
@ -15,25 +16,6 @@ export interface UpdateJustReadFeedJobData {
userId: string
}
interface Feature {
title: string
has_thumbnail: boolean
has_site_icon: boolean
saved_at: Date
site?: string
language?: string
author?: string
directionality: string
word_count?: number
subscription_type: string
folder: string
}
interface ScoreApiRequestBody {
user_id: string
item_features: Record<string, Feature> // item_id -> feature
}
interface FeedItem {
id: string
title: string
@ -60,9 +42,6 @@ interface FeedItem {
}
}
const lanaugeToCode = (language: string): string =>
languages.getAlpha2Code(language, 'en') || 'en'
const libraryItemToCandidate = (user: User, item: LibraryItem): FeedItem => ({
id: item.id,
title: item.title,
@ -111,8 +90,6 @@ const publicItemToCandidate = (item: PublicItem): FeedItem => ({
},
})
type ScoreApiResponse = Record<string, number> // item_id -> score
const selectCandidates = async (user: User): Promise<Array<FeedItem>> => {
const userId = user.id
// get last 100 library items saved and not seen by user
@ -163,9 +140,7 @@ const rankCandidates = async (
return candidates
}
// TODO: get score of candidates
const API_URL = 'http://127.0.0.1:5000/predictions'
const requestBody: ScoreApiRequestBody = {
const data = {
user_id: userId,
item_features: candidates.reduce((acc, item) => {
acc[item.id] = {
@ -185,19 +160,7 @@ const rankCandidates = async (
}, {} as Record<string, Feature>),
}
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
})
if (!response.ok) {
throw new Error(`Failed to score candidates: ${response.statusText}`)
}
const scores = (await response.json()) as ScoreApiResponse
const scores = await getScores(data)
// rank candidates by score in ascending order
candidates.sort((a, b) => {

View file

@ -0,0 +1,41 @@
export interface Feature {
title: string
has_thumbnail: boolean
has_site_icon: boolean
saved_at: Date
site?: string
language?: string
author?: string
directionality: string
word_count?: number
subscription_type: string
folder: string
}
export interface ScoreApiRequestBody {
user_id: string
item_features: Record<string, Feature> // item_id -> feature
}
export type ScoreApiResponse = Record<string, number> // item_id -> score
export const getScores = async (
data: ScoreApiRequestBody
): Promise<ScoreApiResponse> => {
const API_URL = 'http://127.0.0.1:5000/predictions'
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
if (!response.ok) {
throw new Error(`Failed to score candidates: ${response.statusText}`)
}
const scores = (await response.json()) as ScoreApiResponse
return scores
}

View file

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import languages from '@cospired/i18n-iso-languages'
import crypto from 'crypto'
import Redis from 'ioredis'
import normalizeUrl from 'normalize-url'
@ -31,6 +32,7 @@ import { validateUrl } from '../services/create_page_save_request'
import { updateLibraryItem } from '../services/library_item'
import { Merge } from '../util'
import { logger } from './logger'
interface InputObject {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
@ -423,3 +425,6 @@ export const getClientFromUserAgent = (userAgent: string): string => {
return 'other'
}
export const lanaugeToCode = (language: string): string =>
languages.getAlpha2Code(language, 'en') || 'en'