Добро пожаловать в третью часть этой серии!
Если вы ищете мою заявку на участие в хакатоне, перейдите к части 2.
РЕДАКТИРОВАНИЕ: С тех пор я перевел сервисы приема и генерации с Vercel ($20/мес) на Railway ($5-10/мес), чтобы снизить расходы. Обновленное после хакатона руководство по установке находится в процессе разработки.
- JOBS SERVER services-migration branch
- репо SERVICES SERVER
Теперь, когда прием заявок на участие в хакатоне DEV x Redis завершен и началось судейство, я завершу(?) эту серию статей своего рода вскрытием, сравнивая коллекции meatballs.live (MB) с главной страницей Hacker News (HN).
Передовая страница Hacker News все еще меняется, и ранговые позиции, указанные в следующей таблице, могут быть другими после даты публикации. Я буду редактировать по мере необходимости.
EDIT: Таблица обновлена сравнением оригинального ранга и обновленного ранга.
MB | HN | Звание | Создано (CT) |
---|---|---|---|
1 | 57/58 | Наблюдения из нашего опыта Joe Rogan Experience | 6:41 AM |
2 | 65/66 | Telegram спрашивает немецких пользователей, когда делиться информацией с правоохранительными органами | 5:51 AM |
3 | 80/81 | Covid «Fudge Factor» — карта коррупции данных Covid и подход, который сработал | 7:13 AM |
4 | 79/80 | Трекеры проблем, считающиеся вредными | 3:13 AM |
5 | 5/16 | Почему WebMD такой ужасный? | 8:03 AM |
6 | 85/85 | Спросите HN: Должен ли я сообщать инвесторам, что работодатель моего стартапа — мошенник? | 3:33 AM |
7 | 60/61 | Использование вашего телефона в качестве платформы для разработки программного обеспечения | 3:15 AM |
8 | 42/43 | Спросите HN: Как вы получаете подработки? | 6:19 AM |
9 | 28/35 | Дистанционно управляемый газонный трактор | 7:08 AM |
На момент публикации только одна история из коллекции meatballs.live, созданной за 30 августа, находится в топ-9 на Hacker News.
EDIT: Обновленный топ-9 от Hacker News. Алгоритм meatballs.live предвзято относится к историям, опубликованным в начале дня, поскольку процессор для генерации коллекций ограничен 24-часовым диапазоном (00:00-23:59). Имеет смысл расширить этот диапазон еще на 12 или около того часов с потолка. К счастью, данные уже есть, так что это просто вопрос повторной генерации прошлых дат.
Наши алгоритмы совершенно разные.
Для контекста, вот проницательный пост прошлого года о том, как ранжируется Hacker News.
HN: rankingScore = pow(upvotes, 0.8) / pow(ageHours + 2, 1.8)
.
С другой стороны, meatballs.live измеряет производительность временного ряда, делая упор на комментарии, а не на рейтинг. Давайте рассмотрим, как достигаются эти результаты.
Алгоритм генерации коллекций
Через конечную точку /api/services/generate/new-collections/
можно сгенерировать коллекции за предыдущий день (или за любой прошлый день с данными о входе) в 00:00 UTC. API требует только параметр dateKey
в формате YYYY:M:D, например, 2022:8:30
. Затем выполняется processNewCollections
.
Что происходит дальше?
- Возвращается
success: false
, если запрошенныйdateKey
находится раньше значения переменной окруженияMEATBALLS_COLLECTIONS_START_DATE_KEY
или позже вчерашнего дня. - Возвращает
success: false
, если данные коллекции уже существуют для запрошенногоdateKey
.
export const getCollectionsByDate = async ({
repository,
date: { year, month, day }
}: {
repository: Repository<Collection>
date: CollectionDate
}) =>
await repository
.search()
.where('year')
.eq(year)
.and('month')
.eq(month)
.and('day')
.eq(day)
.sortBy('position')
.return.all()
- Возврат
success: false
, если данные временного ряда не найдены для запрашиваемогоdateKey
await redisClient.ts.mRange(
startOfRequestedDayInMilliseconds,
endOfRequestedDayInMilliseconds,
['type=weighted', 'compacted=day'],
{
GROUPBY: { label: 'story', reducer: TimeSeriesReducers.MAXIMUM }
}
)
- Сортировка по наибольшему значению выборки, по убыванию и возвращает первые 20; это значение вычисляется и сохраняется во временном ряду истории через
getStoryActivityTimeSeriesSampleValue
в активности истории - Возвращает
success: false
, если конкретные данные истории не найдены в графике
const findStoriesTransaction = redisClient.multi()
// prepare transaction calls
timeSeriesWithSamples.map((series) => {
const storyId = series.key.replace('story=', `${DATA_SOURCE.HN}:`)
findStoriesTransaction.graph.query(
`${MEATBALLS_DB_KEY.GRAPH}`,
`
MATCH (s:Story)
WHERE s.name = "${storyId}"
return s.name, s.score, s.comment_total, s.created
`
)
})
const foundStories = await findStoriesTransaction.exec()
- Ранжирование найденных историй, выводя комментарии на самый верх и возвращая первые 9 из них
- Создавайте коллекции; находите рекомендации по историям, комментариям и изображениям обложек.
Подробнее о рекомендациях после перерыва.
- Сохранение и кэширование страницы каждой коллекции
- Кэширование страницы коллекций
- Возврат
success: true
Подробнее о рекомендациях
Моя любимая функция meatballs.live — это рекомендации по комментариям к коллекциям и историям.
Когда вы открываете коллекцию, вас приветствуют максимум 5 комментариев, а справа — блок из максимум 5 рекомендаций, основанных на названии истории. Для Observations from our Joe Rogan Experience рекомендации выглядят следующим образом:
- Марк Цукерберг на Joe Rogan Experience
- Спросите HN: Что вы думаете об интервью Цука с Joe Rogan Experience?
- Спросите HN: Дизайн опыта для AR. Кто делает интересную работу?
- AlphaCX Lifetime Deal — лучшая платформа для унифицированного клиентского опыта 2022 года
- Об опыте разработчиков Ethereum
Процесс получения рекомендаций в настоящее время прост, но достаточно эффективен на данном этапе:
const queryTitle = storyContent.title
? removeSpecialCharacters(storyContent.title).replace(/ /g, '|')
: undefined
const recommendedStories: { id: string; title: string }[] = []
const foundDocuments = (
await redisClient.ft.search(`Story:index`, queryTitle, {
LIMIT: { from: 0, size: 5 }
})
).documents,
docTitles = foundDocuments.map(({ value }) => value.title)
foundDocuments
.filter(
({ id, value }, index) =>
value.title &&
!docTitles.includes(value.title, index + 1) &&
id.replace('Story:', '') !== story.id &&
value.title !== storyContent.title
)
.map(({ id, value }) => {
if (value.title)
recommendedStories.push({
id: id.replace('Story:hn:', ''),
title: value.title as string
})
})
Поиск рекомендованных комментариев более сложен:
redisClient.graph.query(
`${MEATBALLS_DB_KEY.GRAPH}`,
`
MATCH (:Story { name: "${story.id}" })-[:PROVOKED]->(topComment)<-[:REACTION_TO*1..]-(childComment)
WITH topComment, collect(childComment) as childComments
RETURN topComment.name, topComment.created, SIZE(childComments)
ORDER BY SIZE(childComments) DESC LIMIT 5
`
)
Здесь запрос находит максимум 5 topComment
от корня, основываясь на активности и глубине реакций с помощью функции Cypher’s collect aggregation.
Является ли этот комментарий верхнего уровня просто одноразовым или он вызвал оживленную беседу?
Визуальное представление части запроса MATCH
будет выглядеть примерно так; выполняется в RedisInsight:
GRAPH.QUERY _meatballs 'MATCH graph=(:Story { name: "hn:32567147" })-[:PROVOKED]->(topComment)<-[:REACTION_TO*1..]-(childComment)
RETURN graph'
Первые мысли по поводу улучшений?
Мне интересны ваши отзывы. Вот некоторые из моих ближайших мыслей:
- Взвешивание по времени, чтобы лучшие результаты не создавались чаще всего утром, т.е. вместо агрегирования по всему дню, просматривайте выборки, основанные на переменных временных диапазонах в течение дня, а затем сравнивайте.
- Поддержка параметра перезаписи в API
new-collections
. - Удаление частично созданных данных коллекции, если
processNewCollections
завершится неудачей до завершения усложнения - В дополнение к названиям, поиск комментариев и текста о пользователе как часть выбора рекомендуемых историй
И это все для этой записи!
Заходите на meatballs.live и дайте мне знать, что вы думаете. Я с нетерпением жду развития этого проекта и приглашаю всех желающих.
Спасибо за чтение и удачи всем участникам! 😎