Skip to content

Commit

Permalink
Randomize suggestions (#1844)
Browse files Browse the repository at this point in the history
* randomize suggestions

* fix snap

* cursor fix

* pr feedback
  • Loading branch information
dholms authored Nov 15, 2023
1 parent 697f5d3 commit 74b7fdf
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 29 deletions.
66 changes: 45 additions & 21 deletions packages/bsky/src/api/app/bsky/actor/getSuggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ const skeleton = async (
ctx: Context,
): Promise<SkeletonState> => {
const { db } = ctx
const { limit, cursor, viewer } = params
const { viewer } = params
const alreadyIncluded = parseCursor(params.cursor)
const { ref } = db.db.dynamic
let suggestionsQb = db.db
const suggestions = await db.db
.selectFrom('suggested_follow')
.innerJoin('actor', 'actor.did', 'suggested_follow.did')
.innerJoin('profile_agg', 'profile_agg.did', 'actor.did')
.where(notSoftDeletedClause(ref('actor')))
.where('suggested_follow.did', '!=', viewer ?? '')
.whereNotExists((qb) =>
Expand All @@ -57,27 +57,30 @@ const skeleton = async (
.where('creator', '=', viewer ?? '')
.whereRef('subjectDid', '=', ref('actor.did')),
)
.if(alreadyIncluded.length > 0, (qb) =>
qb.where('suggested_follow.order', 'not in', alreadyIncluded),
)
.selectAll()
.select('profile_agg.postsCount as postsCount')
.limit(limit)
.orderBy('suggested_follow.order', 'asc')
.execute()

if (cursor) {
const cursorRow = await db.db
.selectFrom('suggested_follow')
.where('did', '=', cursor)
.selectAll()
.executeTakeFirst()
if (cursorRow) {
suggestionsQb = suggestionsQb.where(
'suggested_follow.order',
'>',
cursorRow.order,
)
}
}
const suggestions = await suggestionsQb.execute()
return { params, suggestions, cursor: suggestions.at(-1)?.did }
// always include first two
const firstTwo = suggestions.filter(
(row) => row.order === 1 || row.order === 2,
)
const rest = suggestions.filter((row) => row.order !== 1 && row.order !== 2)
const limited = firstTwo.concat(shuffle(rest)).slice(0, params.limit)

// if the result set ends up getting larger, consider using a seed included in the cursor for for the randomized shuffle
const cursor =
limited.length > 0
? limited
.map((row) => row.order.toString())
.concat(alreadyIncluded.map((id) => id.toString()))
.join(':')
: undefined

return { params, suggestions: limited, cursor }
}

const hydration = async (state: SkeletonState, ctx: Context) => {
Expand Down Expand Up @@ -110,6 +113,27 @@ const presentation = (state: HydrationState) => {
return { actors: suggestedActors, cursor }
}

const parseCursor = (cursor?: string): number[] => {
if (!cursor) {
return []
}
try {
return cursor
.split(':')
.map((id) => parseInt(id, 10))
.filter((id) => !isNaN(id))
} catch {
return []
}
}

const shuffle = <T>(arr: T[]): T[] => {
return arr
.map((value) => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value)
}

type Context = {
db: Database
actorService: ActorService
Expand Down
22 changes: 15 additions & 7 deletions packages/bsky/tests/views/suggestions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ describe('pds user search views', () => {
await network.bsky.processAll()

const suggestions = [
{ did: sc.dids.bob, order: 1 },
{ did: sc.dids.carol, order: 2 },
{ did: sc.dids.dan, order: 3 },
{ did: sc.dids.alice, order: 1 },
{ did: sc.dids.bob, order: 2 },
{ did: sc.dids.carol, order: 3 },
{ did: sc.dids.dan, order: 4 },
]

await network.bsky.ctx.db
.getPrimary()
.db.insertInto('suggested_follow')
Expand Down Expand Up @@ -63,16 +65,22 @@ describe('pds user search views', () => {
{ limit: 1 },
{ headers: await network.serviceHeaders(sc.dids.carol) },
)
expect(result1.data.actors.length).toBe(1)
expect(result1.data.actors[0].handle).toEqual('bob.test')

const result2 = await agent.api.app.bsky.actor.getSuggestions(
{ limit: 1, cursor: result1.data.cursor },
{ headers: await network.serviceHeaders(sc.dids.carol) },
)

expect(result1.data.actors.length).toBe(1)
expect(result1.data.actors[0].handle).toEqual('bob.test')

expect(result2.data.actors.length).toBe(1)
expect(result2.data.actors[0].handle).toEqual('dan.test')

const result3 = await agent.api.app.bsky.actor.getSuggestions(
{ limit: 1, cursor: result2.data.cursor },
{ headers: await network.serviceHeaders(sc.dids.carol) },
)
expect(result3.data.actors.length).toBe(0)
expect(result3.data.cursor).toBeUndefined()
})

it('fetches suggestions unauthed', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Object {
},
},
],
"cursor": "user(2)",
"cursor": "1:3",
}
`;

Expand Down

0 comments on commit 74b7fdf

Please sign in to comment.