Update user feed each hour and get feed from cache doc.

This commit is contained in:
James Grugett 2022-04-29 15:25:38 -04:00
parent cd5689a72f
commit 4b3bc71cbb
9 changed files with 97 additions and 88 deletions

View File

@ -34,6 +34,10 @@ service cloud.firestore {
allow create: if userId == request.auth.uid; allow create: if userId == request.auth.uid;
} }
match /private-users/{userId}/cache/feed {
allow read: if userId == request.auth.uid || isAdmin();
}
match /contracts/{contractId} { match /contracts/{contractId} {
allow read; allow read;
allow update: if request.resource.data.diff(resource.data).affectedKeys() allow update: if request.resource.data.diff(resource.data).affectedKeys()

View File

@ -21,8 +21,8 @@ export * from './unsubscribe'
export * from './update-contract-metrics' export * from './update-contract-metrics'
export * from './update-user-metrics' export * from './update-user-metrics'
export * from './update-recommendations' export * from './update-recommendations'
export * from './update-user-feed'
export * from './backup-db' export * from './backup-db'
export * from './change-user-info' export * from './change-user-info'
export * from './market-close-emails' export * from './market-close-emails'
export * from './add-liquidity' export * from './add-liquidity'
export * from './get-feed'

View File

@ -12,13 +12,13 @@ export const onView = functions.firestore
const { contractId, timestamp } = snapshot.data() as View const { contractId, timestamp } = snapshot.data() as View
await firestore await firestore
.doc(`private-users/${userId}/cached/viewCounts`) .doc(`private-users/${userId}/cache/viewCounts`)
.set( .set(
{ [contractId]: admin.firestore.FieldValue.increment(1) }, { [contractId]: admin.firestore.FieldValue.increment(1) },
{ merge: true } { merge: true }
) )
await firestore await firestore
.doc(`private-users/${userId}/cached/lastViewTime`) .doc(`private-users/${userId}/cache/lastViewTime`)
.set({ [contractId]: timestamp }, { merge: true }) .set({ [contractId]: timestamp }, { merge: true })
}) })

View File

@ -39,7 +39,7 @@ export const updateUserRecommendations = async (
), ),
getValue<{ [contractId: string]: number }>( getValue<{ [contractId: string]: number }>(
firestore.doc(`private-users/${user.id}/cached/viewCounts`) firestore.doc(`private-users/${user.id}/cache/viewCounts`)
), ),
getValues<ClickEvent>( getValues<ClickEvent>(
@ -53,7 +53,7 @@ export const updateUserRecommendations = async (
const contractScores = getContractScores(contracts, wordScores) const contractScores = getContractScores(contracts, wordScores)
const cachedCollection = firestore.collection( const cachedCollection = firestore.collection(
`private-users/${user.id}/cached` `private-users/${user.id}/cache`
) )
await cachedCollection.doc('wordScores').set(wordScores) await cachedCollection.doc('wordScores').set(wordScores)
await cachedCollection.doc('contractScores').set(contractScores) await cachedCollection.doc('contractScores').set(contractScores)

View File

@ -13,19 +13,18 @@ import {
} from '../../common/calculate' } from '../../common/calculate'
import { Bet } from '../../common/bet' import { Bet } from '../../common/bet'
import { Comment } from '../../common/comment' import { Comment } from '../../common/comment'
import { User } from '../../common/user'
import { batchedWaitAll } from '../../common/util/promise'
const firestore = admin.firestore() const firestore = admin.firestore()
const MAX_FEED_CONTRACTS = 60 const MAX_FEED_CONTRACTS = 60
export const getFeed = functions export const updateUserFeed = functions.pubsub
.runWith({ minInstances: 1 }) .schedule('every 60 minutes')
.https.onCall(async (_data, context) => { .onRun(async () => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
// Get contracts bet on or created in last week. // Get contracts bet on or created in last week.
const contractsPromise = Promise.all([ const contracts = await Promise.all([
getValues<Contract>( getValues<Contract>(
firestore firestore
.collection('contracts') .collection('contracts')
@ -46,59 +45,59 @@ export const getFeed = functions
return combined.filter((c) => (c.closeTime ?? Infinity) > Date.now()) return combined.filter((c) => (c.closeTime ?? Infinity) > Date.now())
}) })
const userCacheCollection = firestore.collection( const users = await getValues<User>(firestore.collection('users'))
`private-users/${userId}/cached`
)
const [recommendationScores, lastViewedTime] = await Promise.all([
getValue<{ [contractId: string]: number }>(
userCacheCollection.doc('contractScores')
),
getValue<{ [contractId: string]: number }>(
userCacheCollection.doc('lastViewTime')
),
]).then((dicts) => dicts.map((dict) => dict ?? {}))
const contracts = await contractsPromise await batchedWaitAll(users.map((user) => () => updateFeed(user, contracts)))
const averageRecScore =
1 +
_.sumBy(
contracts.filter((c) => recommendationScores[c.id] !== undefined),
(c) => recommendationScores[c.id]
) /
(contracts.length + 1)
console.log({ recommendationScores, averageRecScore, lastViewedTime })
const scoredContracts = contracts.map((contract) => {
const score = scoreContract(
contract,
recommendationScores[contract.id] ?? averageRecScore,
lastViewedTime[contract.id]
)
return [contract, score] as [Contract, number]
})
const sortedContracts = _.sortBy(
scoredContracts,
([_, score]) => score
).reverse()
console.log(sortedContracts.map(([c, score]) => c.question + ': ' + score))
const feedContracts = sortedContracts
.slice(0, MAX_FEED_CONTRACTS)
.map(([c]) => c)
const feed = await Promise.all(
feedContracts.map((contract) => getRecentBetsAndComments(contract))
)
console.log('feed', feed)
return { status: 'success', feed }
}) })
const updateFeed = async (user: User, contracts: Contract[]) => {
const userCacheCollection = firestore.collection(
`private-users/${user.id}/cache`
)
const [recommendationScores, lastViewedTime] = await Promise.all([
getValue<{ [contractId: string]: number }>(
userCacheCollection.doc('contractScores')
),
getValue<{ [contractId: string]: number }>(
userCacheCollection.doc('lastViewTime')
),
]).then((dicts) => dicts.map((dict) => dict ?? {}))
const averageRecScore =
1 +
_.sumBy(
contracts.filter((c) => recommendationScores[c.id] !== undefined),
(c) => recommendationScores[c.id]
) /
(contracts.length + 1)
const scoredContracts = contracts.map((contract) => {
const score = scoreContract(
contract,
recommendationScores[contract.id] ?? averageRecScore,
lastViewedTime[contract.id]
)
return [contract, score] as [Contract, number]
})
const sortedContracts = _.sortBy(
scoredContracts,
([_, score]) => score
).reverse()
console.log(sortedContracts.map(([c, score]) => c.question + ': ' + score))
const feedContracts = sortedContracts
.slice(0, MAX_FEED_CONTRACTS)
.map(([c]) => c)
const feed = await Promise.all(
feedContracts.map((contract) => getRecentBetsAndComments(contract))
)
await userCacheCollection.doc('feed').set(feed)
}
function scoreContract( function scoreContract(
contract: Contract, contract: Contract,
recommendationScore: number, recommendationScore: number,
@ -171,12 +170,16 @@ async function getRecentBetsAndComments(contract: Contract) {
contractDoc contractDoc
.collection('bets') .collection('bets')
.where('createdTime', '>', Date.now() - DAY_MS) .where('createdTime', '>', Date.now() - DAY_MS)
.orderBy('createdTime', 'desc')
.limit(1)
), ),
getValues<Comment>( getValues<Comment>(
contractDoc contractDoc
.collection('comments') .collection('comments')
.where('createdTime', '>', Date.now() - 3 * DAY_MS) .where('createdTime', '>', Date.now() - 3 * DAY_MS)
.orderBy('createdTime', 'desc')
.limit(3)
), ),
]) ])

View File

@ -5,9 +5,10 @@ import { Comment } from '../../common/comment'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { useTimeSinceFirstRender } from './use-time-since-first-render' import { useTimeSinceFirstRender } from './use-time-since-first-render'
import { trackLatency } from '../lib/firebase/tracking' import { trackLatency } from '../lib/firebase/tracking'
import { getFeed } from '../lib/firebase/api-call' import { User } from '../../common/user'
import { getUserFeed } from '../lib/firebase/users'
export const useAlgoFeed = () => { export const useAlgoFeed = (user: User | null | undefined) => {
const [feed, setFeed] = useState< const [feed, setFeed] = useState<
{ {
contract: Contract contract: Contract
@ -19,14 +20,15 @@ export const useAlgoFeed = () => {
const getTime = useTimeSinceFirstRender() const getTime = useTimeSinceFirstRender()
useEffect(() => { useEffect(() => {
getFeed().then(({ data }) => { if (user) {
console.log('got data', data) getUserFeed(user.id).then((feed) => {
setFeed(data.feed) setFeed(feed)
trackLatency('feed', getTime()) trackLatency('feed', getTime())
console.log('feed load time', getTime()) console.log('feed load time', getTime())
}) })
}, [getTime]) }
}, [user, getTime])
return feed return feed
} }

View File

@ -1,7 +1,4 @@
import { httpsCallable } from 'firebase/functions' import { httpsCallable } from 'firebase/functions'
import { Bet } from '../../../common/bet'
import { Comment } from '../../../common/comment'
import { Contract } from '../../../common/contract'
import { Fold } from '../../../common/fold' import { Fold } from '../../../common/fold'
import { User } from '../../../common/user' import { User } from '../../../common/user'
import { randomString } from '../../../common/util/random' import { randomString } from '../../../common/util/random'
@ -74,15 +71,3 @@ export const addLiquidity = (data: { amount: number; contractId: string }) => {
.then((r) => r.data as { status: string }) .then((r) => r.data as { status: string })
.catch((e) => ({ status: 'error', message: e.message })) .catch((e) => ({ status: 'error', message: e.message }))
} }
export const getFeed = cloudFunction<
undefined,
{
status: 'error' | 'success'
feed: {
contract: Contract
recentBets: Bet[]
recentComments: Comment[]
}[]
}
>('getFeed')

View File

@ -23,8 +23,11 @@ import _ from 'lodash'
import { app } from './init' import { app } from './init'
import { PrivateUser, User } from '../../../common/user' import { PrivateUser, User } from '../../../common/user'
import { createUser } from './api-call' import { createUser } from './api-call'
import { getValues, listenForValue, listenForValues } from './utils' import { getValue, getValues, listenForValue, listenForValues } from './utils'
import { DAY_MS } from '../../../common/util/time' import { DAY_MS } from '../../../common/util/time'
import { Contract } from './contracts'
import { Bet } from './bets'
import { Comment } from './comments'
export type { User } export type { User }
@ -207,3 +210,15 @@ export async function getDailyNewUsers(
return usersByDay return usersByDay
} }
export async function getUserFeed(userId: string) {
const feedDoc = doc(db, 'private-users', userId, 'cache', 'feed')
const userFeed = await getValue<{
feed: {
contract: Contract
recentBets: Bet[]
recentComments: Comment[]
}[]
}>(feedDoc)
return userFeed?.feed ?? []
}

View File

@ -15,7 +15,7 @@ import { ContractPageContent } from './[username]/[contractSlug]'
const Home = () => { const Home = () => {
const user = useUser() const user = useUser()
const feed = useAlgoFeed() const feed = useAlgoFeed(user)
const router = useRouter() const router = useRouter()
const { u: username, s: slug } = router.query const { u: username, s: slug } = router.query