add just added section to home

This commit is contained in:
Hongbo Wu 2024-05-30 13:43:12 +08:00
parent 93f1076a1c
commit 4bb33da27f
4 changed files with 110 additions and 56 deletions

View file

@ -1324,7 +1324,7 @@ export type HomeItemSource = {
__typename?: 'HomeItemSource';
icon?: Maybe<Scalars['String']>;
id?: Maybe<Scalars['ID']>;
name: Scalars['String'];
name?: Maybe<Scalars['String']>;
type: HomeItemSourceType;
url?: Maybe<Scalars['String']>;
};
@ -6117,7 +6117,7 @@ export type HomeItemResolvers<ContextType = ResolverContext, ParentType extends
export type HomeItemSourceResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['HomeItemSource'] = ResolversParentTypes['HomeItemSource']> = {
icon?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
id?: Resolver<Maybe<ResolversTypes['ID']>, ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
type?: Resolver<ResolversTypes['HomeItemSourceType'], ParentType, ContextType>;
url?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;

View file

@ -1192,7 +1192,7 @@ type HomeItem {
type HomeItemSource {
icon: String
id: ID
name: String!
name: String
type: HomeItemSourceType!
url: String
}

View file

@ -104,19 +104,55 @@ const publicItemToCandidate = (item: PublicItem): Candidate => ({
score: 0,
})
const selectCandidates = async (user: User): Promise<Array<Candidate>> => {
const userId = user.id
// get last 100 library items saved and not seen by user
const getJustAddedCandidates = async (
userId: string,
limit = 100
): Promise<Array<Candidate>> => {
const libraryItems = await searchLibraryItems(
{
size: 100,
size: limit,
includeContent: false,
query: `-is:seen wordsCount:>0`,
query: `in:inbox saved:last24hrs -is:seen`,
},
userId
)
logger.info(`Found ${libraryItems.length} library items`)
logger.info(`Found ${libraryItems.length} just added library items`)
// get subscriptions for the library items
const subscriptionNames = libraryItems
.filter((item) => !!item.subscription)
.map((item) => item.subscription as string)
const subscriptions = await findSubscriptionsByNames(
userId,
subscriptionNames
)
// map library items to candidates
const justAddedCandidates: Array<Candidate> = libraryItems.map((item) =>
libraryItemToCandidate(item, subscriptions)
)
return justAddedCandidates
}
const selectCandidates = async (
user: User,
limit = 100
): Promise<Array<Candidate>> => {
const userId = user.id
// get last 100 library items saved and not seen by user
const libraryItems = await searchLibraryItems(
{
size: limit,
includeContent: false,
query: `in:inbox -saved:last24hrs -is:seen`,
},
userId
)
logger.info(`Found ${libraryItems.length} not just added library items`)
// get subscriptions for the library items
const subscriptionNames = libraryItems
@ -290,27 +326,10 @@ const appendSectionsToHome = async (
await pipeline.exec()
}
const mixHomeItems = (rankedHomeItems: Array<Candidate>): Array<Section> => {
// find the median word count
const wordCounts = rankedHomeItems.map((item) => item.wordCount)
wordCounts.sort((a, b) => a - b)
const medianWordCount = wordCounts[Math.floor(wordCounts.length / 2)]
// separate items into two groups based on word count
const shortItems: Array<Candidate> = []
const longItems: Array<Candidate> = []
for (const item of rankedHomeItems) {
if (item.wordCount < medianWordCount) {
shortItems.push(item)
} else {
longItems.push(item)
}
}
// initialize empty batches
const batches: Array<Array<Candidate>> = Array.from(
{ length: Math.floor(rankedHomeItems.length / 10) },
() => []
)
const mixHomeItems = (
justAddedCandidates: Array<Candidate>,
rankedHomeItems: Array<Candidate>
): Array<Section> => {
const checkConstraints = (batch: Array<Candidate>, item: Candidate) => {
const titleCount = batch.filter((i) => i.title === item.title).length
const authorCount = batch.filter((i) => i.author === item.author).length
@ -328,14 +347,22 @@ const mixHomeItems = (rankedHomeItems: Array<Candidate>): Array<Section> => {
)
}
const candidateToItem = (candidate: Candidate): Item => ({
id: candidate.id,
type: candidate.type,
score: candidate.score,
})
const distributeItems = (
items: Array<Candidate>,
batches: Array<Array<Candidate>>
) => {
const batchSize = Math.ceil(items.length / batches.length)
for (const item of items) {
let added = false
for (const batch of batches) {
if (batch.length < 5 && checkConstraints(batch, item)) {
if (batch.length < batchSize && checkConstraints(batch, item)) {
batch.push(item)
added = true
break
@ -344,7 +371,7 @@ const mixHomeItems = (rankedHomeItems: Array<Candidate>): Array<Section> => {
if (!added) {
for (const batch of batches) {
if (batch.length < 10) {
if (batch.length < batchSize) {
batch.push(item)
break
}
@ -353,31 +380,51 @@ const mixHomeItems = (rankedHomeItems: Array<Candidate>): Array<Section> => {
}
}
// distribute quick link items first
distributeItems(shortItems, batches)
distributeItems(longItems, batches)
// find the median word count
const wordCounts = rankedHomeItems.map((item) => item.wordCount)
wordCounts.sort((a, b) => a - b)
const medianWordCount = wordCounts[Math.floor(wordCounts.length / 2)]
// separate items into two groups based on word count
const shortItems: Array<Candidate> = []
const longItems: Array<Candidate> = []
for (const item of rankedHomeItems) {
if (item.wordCount < medianWordCount) {
shortItems.push(item)
} else {
longItems.push(item)
}
}
// initialize empty batches
const numOfBatches = 10
const batches = {
short: Array.from({ length: numOfBatches }, () => []) as Array<
Array<Candidate>
>,
long: Array.from({ length: numOfBatches }, () => []) as Array<
Array<Candidate>
>,
}
distributeItems(shortItems, batches.short)
distributeItems(longItems, batches.long)
// convert batches to sections
const sections = []
for (const batch of batches) {
// create a section for all quick links
sections.push({
items: batch.slice(0, 5).map((item) => ({
id: item.id,
type: item.type,
score: item.score,
})),
layout: 'quick links',
})
sections.push({
items: batches.short.flat().map(candidateToItem),
layout: 'quick links',
})
// create a section for each long item
sections.push(
...batch.slice(5).map((item) => ({
items: [{ id: item.id, type: item.type, score: item.score }],
layout: 'long',
}))
)
}
sections.push({
items: batches.long.flat().map(candidateToItem),
layout: 'long',
})
sections.push({
items: justAddedCandidates.map(candidateToItem),
layout: 'just added',
})
return sections
}
@ -395,6 +442,13 @@ export const updateHome = async (data: UpdateHomeJobData) => {
logger.info(`Updating home for user ${userId}`)
logger.profile('justAdded')
const justAddedCandidates = await getJustAddedCandidates(userId)
logger.profile('justAdded', {
level: 'info',
message: `Found ${justAddedCandidates.length} just added candidates`,
})
logger.profile('selecting')
const candidates = await selectCandidates(user)
logger.profile('selecting', {
@ -402,7 +456,7 @@ export const updateHome = async (data: UpdateHomeJobData) => {
message: `Found ${candidates.length} candidates`,
})
if (candidates.length === 0) {
if (!justAddedCandidates.length && !candidates.length) {
logger.info('No candidates found')
return
}
@ -419,7 +473,7 @@ export const updateHome = async (data: UpdateHomeJobData) => {
// TODO: filter candidates
logger.profile('mixing')
const rankedSections = mixHomeItems(rankedCandidates)
const rankedSections = mixHomeItems(justAddedCandidates, rankedCandidates)
logger.profile('mixing', {
level: 'info',
message: `Created ${rankedSections.length} sections`,

View file

@ -3121,7 +3121,7 @@ const schema = gql`
type HomeItemSource {
id: ID
name: String!
name: String
url: String
icon: String
type: HomeItemSourceType!