diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e14058c..71ecab29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,29 @@ # Changelog -## [0.9.16] - 2025-01-27 +## [0.9.17] - 2025-08-31 + +### πŸ‘₯ Author Statistics Enhancement +- **πŸ“Š Полная статистика Π°Π²Ρ‚ΠΎΡ€ΠΎΠ²**: Π”ΠΎΠ±Π°Π²Π»Π΅Π½Ρ‹ всС Π½Π΅Π΄ΠΎΡΡ‚Π°ΡŽΡ‰ΠΈΠ΅ счётчики Π² AuthorStat + - `topics`: ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Ρ… Ρ‚Π΅ΠΌ, Π² ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… участвовал Π°Π²Ρ‚ΠΎΡ€ + - `coauthors`: ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ соавторов + - `replies_count`: ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ Π²Ρ‹Π·Π²Π°Π½Π½Ρ‹Ρ… ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² + - `rating_shouts`: Π Π΅ΠΉΡ‚ΠΈΠ½Π³ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ Π°Π²Ρ‚ΠΎΡ€Π° (сумма Ρ€Π΅Π°ΠΊΡ†ΠΈΠΉ LIKE/AGREE/ACCEPT/PROOF/CREDIT минус DISLIKE/DISAGREE/REJECT/DISPROOF) + - `rating_comments`: Π Π΅ΠΉΡ‚ΠΈΠ½Π³ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² Π°Π²Ρ‚ΠΎΡ€Π° (Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ Π½Π° Π΅Π³ΠΎ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ) + - `replies_count`: ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ Π²Ρ‹Π·Π²Π°Π½Π½Ρ‹Ρ… ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² + - `comments`: ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ созданных ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² ΠΈ Ρ†ΠΈΡ‚Π°Ρ‚ + - `viewed_shouts`: ΠžΠ±Ρ‰Π΅Π΅ количСство просмотров всСх ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ Π°Π²Ρ‚ΠΎΡ€Π° +- **πŸ”„ Π£Π»ΡƒΡ‡ΡˆΠ΅Π½Π½Π°Ρ сортировка**: ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° сортировки ΠΏΠΎ всСм Π½ΠΎΠ²Ρ‹ΠΌ полям статистики +- **⚑ ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ запросы**: Batch-запросы для получСния всСй статистики ΠΎΠ΄Π½ΠΈΠΌ Π²Ρ‹Π·ΠΎΠ²ΠΎΠΌ +- **πŸ§ͺ ΠŸΠΎΠ΄Ρ€ΠΎΠ±Π½ΠΎΠ΅ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅**: Π­ΠΌΠΎΠ΄Π·ΠΈ-ΠΌΠ°Ρ€ΠΊΠ΅Ρ€Ρ‹ для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ Ρ‚ΠΈΠΏΠ° статистики + +### πŸ”§ Technical Implementation +- **Resolvers**: ΠžΠ±Π½ΠΎΠ²Π»Ρ‘Π½ `load_authors_by` для Π²ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ всСх счётчиков +- **Database**: ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ SQL-запросы с JOIN для статистики +- **Caching**: Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с ViewedStorage для подсчёта просмотров +- **GraphQL Schema**: ΠžΠ±Π½ΠΎΠ²Π»Ρ‘Π½ Ρ‚ΠΈΠΏ AuthorStat с Π½ΠΎΠ²Ρ‹ΠΌΠΈ полями + +## [0.9.16] - 2025-08-31 ### πŸ” Search System Revolution - **πŸš€ НастоящиС Π²Π΅ΠΊΡ‚ΠΎΡ€Π½Ρ‹Π΅ эмбСдинги**: Π—Π°ΠΌΠ΅Π½ΠΈΠ» псСвдослучайныС hash-эмбСдинги Π½Π° SentenceTransformers diff --git a/docs/author-statistics.md b/docs/author-statistics.md new file mode 100644 index 00000000..53f0a68a --- /dev/null +++ b/docs/author-statistics.md @@ -0,0 +1,291 @@ +# πŸ“Š БистСма статистики Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² + +Полная докумСнтация ΠΏΠΎ расчёту ΠΈ использованию статистики Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² Π² Discours. + +## 🎯 ΠžΠ±Π·ΠΎΡ€ + +БистСма статистики Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² прСдоставляСт ΠΌΠ½ΠΎΠ³ΠΎΠΌΠ΅Ρ€Π½ΡƒΡŽ ΠΎΡ†Π΅Π½ΠΊΡƒ активности, популярности ΠΈ вовлСчённости ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ Π°Π²Ρ‚ΠΎΡ€Π° Π½Π° ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΠ΅. ВсС ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ Ρ€Π°ΡΡΡ‡ΠΈΡ‚Ρ‹Π²Π°ΡŽΡ‚ΡΡ Π² Ρ€Π΅Π°Π»ΡŒΠ½ΠΎΠΌ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ ΠΈ ΠΊΠ΅ΡˆΠΈΡ€ΡƒΡŽΡ‚ΡΡ для ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ. + +## πŸ“ˆ ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ AuthorStat + +```graphql +# Бтатистика Π°Π²Ρ‚ΠΎΡ€Π° - полная ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠ° активности ΠΈ популярности +type AuthorStat { + # ΠšΠΎΠ½Ρ‚Π΅Π½Ρ‚ Π°Π²Ρ‚ΠΎΡ€Π° + shouts: Int # ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΠΎΠΏΡƒΠ±Π»ΠΈΠΊΠΎΠ²Π°Π½Π½Ρ‹Ρ… статСй + topics: Int # ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Ρ… Ρ‚Π΅ΠΌ, Π² ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… участвовал + comments: Int # ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ созданных ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² ΠΈ Ρ†ΠΈΡ‚Π°Ρ‚ + + # ВзаимодСйствиС с Π΄Ρ€ΡƒΠ³ΠΈΠΌΠΈ Π°Π²Ρ‚ΠΎΡ€Π°ΠΌΠΈ + coauthors: Int # ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Ρ… соавторов + followers: Int # ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ подписчиков + + # РСйтинговая систСма + rating: Int # ΠžΠ±Ρ‰ΠΈΠΉ Ρ€Π΅ΠΉΡ‚ΠΈΠ½Π³ (rating_shouts + rating_comments) + rating_shouts: Int # Π Π΅ΠΉΡ‚ΠΈΠ½Π³ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ (сумма Ρ€Π΅Π°ΠΊΡ†ΠΈΠΉ LIKE/AGREE/ACCEPT/PROOF/CREDIT минус DISLIKE/DISAGREE/REJECT/DISPROOF) + rating_comments: Int # Π Π΅ΠΉΡ‚ΠΈΠ½Π³ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² (Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π°Π²Ρ‚ΠΎΡ€Π°) + + # ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ вовлСчённости + replies_count: Int # ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ² Π½Π° ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ Π°Π²Ρ‚ΠΎΡ€Π° (ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ + ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π½Π° посты) + viewed_shouts: Int # ΠžΠ±Ρ‰Π΅Π΅ количСство просмотров всСх ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ Π°Π²Ρ‚ΠΎΡ€Π° +} +``` + +### πŸ“ ΠšΠΎΠ½Ρ‚Π΅Π½Ρ‚ Π°Π²Ρ‚ΠΎΡ€Π° + +#### `shouts: Int` +**ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΠΎΠΏΡƒΠ±Π»ΠΈΠΊΠΎΠ²Π°Π½Π½Ρ‹Ρ… статСй** +- Π£Ρ‡ΠΈΡ‚Ρ‹Π²Π°Π΅Ρ‚ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΡΡ‚Π°Ρ‚ΡŒΠΈ со статусом `published_at IS NOT NULL` +- Π˜ΡΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ ΡƒΠ΄Π°Π»Ρ‘Π½Π½Ρ‹Π΅ ΡΡ‚Π°Ρ‚ΡŒΠΈ (`deleted_at IS NULL`) +- ΠŸΠΎΠ΄ΡΡ‡ΠΈΡ‚Ρ‹Π²Π°Π΅Ρ‚ΡΡ Ρ‡Π΅Ρ€Π΅Π· Ρ‚Π°Π±Π»ΠΈΡ†Ρƒ `shout_author` + +```sql +SELECT sa.author, COUNT(DISTINCT s.id) as shouts_count +FROM shout_author sa +JOIN shout s ON sa.shout = s.id +WHERE s.deleted_at IS NULL AND s.published_at IS NOT NULL +GROUP BY sa.author +``` + +#### `topics: Int` +**ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Ρ… Ρ‚Π΅ΠΌ, Π² ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… участвовал Π°Π²Ρ‚ΠΎΡ€** +- ΠŸΠΎΠ΄ΡΡ‡ΠΈΡ‚Ρ‹Π²Π°Π΅Ρ‚ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹ Ρ‡Π΅Ρ€Π΅Π· связку статСй Π°Π²Ρ‚ΠΎΡ€Π° +- Основано Π½Π° Ρ‚Π°Π±Π»ΠΈΡ†Π°Ρ… `shout_author` β†’ `shout_topic` + +```sql +SELECT sa.author, COUNT(DISTINCT st.topic) as topics_count +FROM shout_author sa +JOIN shout s ON sa.shout = s.id AND s.deleted_at IS NULL AND s.published_at IS NOT NULL +JOIN shout_topic st ON s.id = st.shout +GROUP BY sa.author +``` + +#### `comments: Int` +**ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ созданных ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² ΠΈ Ρ†ΠΈΡ‚Π°Ρ‚** +- Π’ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ Ρ‚ΠΈΠΏΠ° `COMMENT` ΠΈ `QUOTE` +- Π˜ΡΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ ΡƒΠ΄Π°Π»Ρ‘Π½Π½Ρ‹Π΅ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ + +```sql +SELECT r.created_by, COUNT(DISTINCT r.id) as comments_count +FROM reaction r +JOIN shout s ON r.shout = s.id AND s.deleted_at IS NULL +WHERE r.deleted_at IS NULL AND r.kind IN ('COMMENT', 'QUOTE') +GROUP BY r.created_by +``` + +### πŸ‘₯ ВзаимодСйствиС с Π΄Ρ€ΡƒΠ³ΠΈΠΌΠΈ Π°Π²Ρ‚ΠΎΡ€Π°ΠΌΠΈ + +#### `coauthors: Int` +**ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Ρ… соавторов** +- ΠŸΠΎΠ΄ΡΡ‡ΠΈΡ‚Ρ‹Π²Π°Π΅Ρ‚ Π°Π²Ρ‚ΠΎΡ€ΠΎΠ², с ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΌΠΈ Π°Π²Ρ‚ΠΎΡ€ ΠΏΡƒΠ±Π»ΠΈΠΊΠΎΠ²Π°Π» совмСстныС ΡΡ‚Π°Ρ‚ΡŒΠΈ +- Π˜ΡΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ самого Π°Π²Ρ‚ΠΎΡ€Π° ΠΈΠ· подсчёта +- Π£Ρ‡ΠΈΡ‚Ρ‹Π²Π°Π΅Ρ‚ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠΏΡƒΠ±Π»ΠΈΠΊΠΎΠ²Π°Π½Π½Ρ‹Π΅ ΠΈ Π½Π΅ΡƒΠ΄Π°Π»Ρ‘Π½Π½Ρ‹Π΅ ΡΡ‚Π°Ρ‚ΡŒΠΈ + +```sql +SELECT sa1.author, COUNT(DISTINCT sa2.author) as coauthors_count +FROM shout_author sa1 +JOIN shout s ON sa1.shout = s.id + AND s.deleted_at IS NULL + AND s.published_at IS NOT NULL +JOIN shout_author sa2 ON s.id = sa2.shout + AND sa2.author != sa1.author -- ΠΈΡΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌ самого Π°Π²Ρ‚ΠΎΡ€Π° +GROUP BY sa1.author +``` + +#### `followers: Int` +**ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ подписчиков** +- ΠŸΡ€ΡΠΌΠΎΠΉ подсчёт ΠΈΠ· Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ `author_follower` + +```sql +SELECT following, COUNT(DISTINCT follower) as followers_count +FROM author_follower +GROUP BY following +``` + +### ⭐ РСйтинговая систСма + +#### `rating: Int` +**ΠžΠ±Ρ‰ΠΈΠΉ Ρ€Π΅ΠΉΡ‚ΠΈΠ½Π³ Π°Π²Ρ‚ΠΎΡ€Π°** +- Π‘ΡƒΠΌΠΌΠ° `rating_shouts + rating_comments` +- АгрСгированная ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠ° популярности ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π° + +#### `rating_shouts: Int` +**Π Π΅ΠΉΡ‚ΠΈΠ½Π³ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ Π°Π²Ρ‚ΠΎΡ€Π°** +- Π‘ΡƒΠΌΠΌΠ° всСх Ρ€Π΅Π°ΠΊΡ†ΠΈΠΉ Π½Π° ΡΡ‚Π°Ρ‚ΡŒΠΈ Π°Π²Ρ‚ΠΎΡ€Π° +- ΠŸΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ: `LIKE`, `AGREE`, `ACCEPT`, `PROOF`, `CREDIT` (+1) +- ΠžΡ‚Ρ€ΠΈΡ†Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ: `DISLIKE`, `DISAGREE`, `REJECT`, `DISPROOF` (-1) +- ΠΠ΅ΠΉΡ‚Ρ€Π°Π»ΡŒΠ½Ρ‹Π΅ Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ: ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Π΅ (0) + +```sql +SELECT sa.author, + SUM(CASE + WHEN r.kind IN ('LIKE', 'AGREE', 'ACCEPT', 'PROOF', 'CREDIT') THEN 1 + WHEN r.kind IN ('DISLIKE', 'DISAGREE', 'REJECT', 'DISPROOF') THEN -1 + ELSE 0 + END) as rating_shouts +FROM shout_author sa +JOIN shout s ON sa.shout = s.id AND s.deleted_at IS NULL AND s.published_at IS NOT NULL +JOIN reaction r ON s.id = r.shout AND r.deleted_at IS NULL +GROUP BY sa.author +``` + +#### `rating_comments: Int` +**Π Π΅ΠΉΡ‚ΠΈΠ½Π³ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² Π°Π²Ρ‚ΠΎΡ€Π°** +- Аналогичная систСма для Ρ€Π΅Π°ΠΊΡ†ΠΈΠΉ Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π°Π²Ρ‚ΠΎΡ€Π° +- ΠŸΠΎΠ΄ΡΡ‡ΠΈΡ‚Ρ‹Π²Π°Π΅Ρ‚ Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Ρ‡Π΅Ρ€Π΅Π· `reply_to` + +```sql +SELECT r1.created_by, + SUM(CASE + WHEN r2.kind IN ('LIKE', 'AGREE', 'ACCEPT', 'PROOF', 'CREDIT') THEN 1 + WHEN r2.kind IN ('DISLIKE', 'DISAGREE', 'REJECT', 'DISPROOF') THEN -1 + ELSE 0 + END) as rating_comments +FROM reaction r1 +JOIN reaction r2 ON r1.id = r2.reply_to AND r2.deleted_at IS NULL +WHERE r1.deleted_at IS NULL AND r1.kind IN ('COMMENT', 'QUOTE') +GROUP BY r1.created_by +``` + +### πŸ”„ ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ вовлСчённости + +#### `replies_count: Int` +**ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ² Π½Π° ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ Π°Π²Ρ‚ΠΎΡ€Π°** +- **КомплСксная ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠ°**, Π²ΠΊΠ»ΡŽΡ‡Π°ΡŽΡ‰Π°Ρ: + 1. **ΠžΡ‚Π²Π΅Ρ‚Ρ‹ Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π°Π²Ρ‚ΠΎΡ€Π°** (Ρ‡Π΅Ρ€Π΅Π· `reply_to`) + 2. **ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π½Π° посты Π°Π²Ρ‚ΠΎΡ€Π°** (прямыС ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ ΠΊ ΡΡ‚Π°Ρ‚ΡŒΡΠΌ) + +Π›ΠΎΠ³ΠΈΠΊΠ° расчёта: +```python +# ΠžΡ‚Π²Π΅Ρ‚Ρ‹ Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ +replies_to_comments = COUNT(r2) WHERE r1.created_by = author AND r2.reply_to = r1.id + +# ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π½Π° посты +comments_on_posts = COUNT(r) WHERE sa.author = author AND r.shout = s.id + +# Π˜Ρ‚ΠΎΠ³ΠΎ +replies_count = replies_to_comments + comments_on_posts +``` + +#### `viewed_shouts: Int` +**ΠžΠ±Ρ‰Π΅Π΅ количСство просмотров всСх ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ Π°Π²Ρ‚ΠΎΡ€Π°** +- Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с `ViewedStorage` (Google Analytics) +- Π‘ΡƒΠΌΠΌΠΈΡ€ΡƒΠ΅Ρ‚ просмотры всСх статСй Π°Π²Ρ‚ΠΎΡ€Π° +- ΠžΠ±Π½ΠΎΠ²Π»ΡΠ΅Ρ‚ΡΡ асинхронно ΠΈΠ· Π²Π½Π΅ΡˆΠ½ΠΈΡ… источников + +## πŸ” API использования + +### GraphQL запрос + +```graphql +query LoadAuthors($by: AuthorsBy, $limit: Int, $offset: Int) { + load_authors_by(by: $by, limit: $limit, offset: $offset) { + id + slug + name + bio + pic + stat { + shouts + topics + coauthors + followers + rating + rating_shouts + rating_comments + comments + replies_count + viewed_shouts + } + } +} +``` + +### ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ сортировки + +```graphql +# Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠ° ΠΏΠΎ количСству ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ +{ "order": "shouts" } + +# Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠ° ΠΏΠΎ ΠΎΠ±Ρ‰Π΅ΠΌΡƒ Ρ€Π΅ΠΉΡ‚ΠΈΠ½Π³Ρƒ +{ "order": "rating" } + +# Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠ° ΠΏΠΎ вовлСчённости +{ "order": "replies_count" } + +# Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠ° ΠΏΠΎ просмотрам +{ "order": "viewed_shouts" } +``` + +## ⚑ ΠŸΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ + +### ΠšΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ +- **Redis кСш** для Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ΠΎΠ² запросов +- **ΠšΠ»ΡŽΡ‡ΠΈ кСша**: `authors:stats:limit={limit}:offset={offset}:order={order}` +- **TTL**: НастраиваСтся Π² `cache.py` + +### ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΠΈ SQL +- **Batch запросы** для получСния статистики всСх Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² ΠΎΠ΄Π½ΠΎΠ²Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎ +- **ΠŸΠΎΠ΄Π³ΠΎΡ‚ΠΎΠ²Π»Π΅Π½Π½Ρ‹Π΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹** для Π·Π°Ρ‰ΠΈΡ‚Ρ‹ ΠΎΡ‚ SQL-ΠΈΠ½ΡŠΠ΅ΠΊΡ†ΠΈΠΉ +- **Π˜Π½Π΄Π΅ΠΊΡΡ‹** Π½Π° ΠΊΠ»ΡŽΡ‡Π΅Π²Ρ‹Ρ… полях (`author_id`, `shout_id`, `reaction.kind`) + +### Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠ° +- **SQL-ΡƒΡ€ΠΎΠ²Π΅Π½ΡŒ сортировки** для ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊ статистики +- **ΠŸΠΎΠ΄Π·Π°ΠΏΡ€ΠΎΡΡ‹ с JOIN** для ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ +- **COALESCE** для ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ NULL Π·Π½Π°Ρ‡Π΅Π½ΠΈΠΉ + +## πŸ§ͺ ВСстированиС + +### Unit тСсты +```python +# ВСстированиС расчёта статистики +async def test_author_stats_calculation(): + # Π‘ΠΎΠ·Π΄Π°Ρ‘ΠΌ тСстовыС Π΄Π°Π½Π½Ρ‹Π΅ + # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½ΠΎΡΡ‚ΡŒ расчёта ΠΊΠ°ΠΆΠ΄ΠΎΠΉ ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ + pass + +# ВСстированиС сортировки +async def test_author_sorting(): + # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ сортировку ΠΏΠΎ Ρ€Π°Π·Π½Ρ‹ΠΌ полям + pass +``` + +### Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΎΠ½Π½Ρ‹Π΅ тСсты +- ВСстированиС с Ρ€Π΅Π°Π»ΡŒΠ½Ρ‹ΠΌΠΈ Π΄Π°Π½Π½Ρ‹ΠΌΠΈ +- ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ Π½Π° Π±ΠΎΠ»ΡŒΡˆΠΈΡ… ΠΎΠ±ΡŠΡ‘ΠΌΠ°Ρ… +- Валидация ΠΊΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ + +## πŸ”§ ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ + +### ΠŸΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ окруТСния +```bash +# Google Analytics для просмотров +GOOGLE_KEYFILE_PATH=/path/to/service-account.json +GOOGLE_PROPERTY_ID=your-property-id + +# Redis для ΠΊΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ +REDIS_URL=redis://localhost:6379 +``` + +### Настройки Ρ€Π΅Π°ΠΊΡ†ΠΈΠΉ +Π’ΠΈΠΏΡ‹ Ρ€Π΅Π°ΠΊΡ†ΠΈΠΉ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½Ρ‹ Π² `orm/reaction.py`: +```python +# ΠŸΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ (+1) +POSITIVE_REACTIONS = ["LIKE", "AGREE", "ACCEPT", "PROOF", "CREDIT"] + +# ΠžΡ‚Ρ€ΠΈΡ†Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ (-1) +NEGATIVE_REACTIONS = ["DISLIKE", "DISAGREE", "REJECT", "DISPROOF"] +``` + +## πŸš€ Π Π°Π·Π²ΠΈΡ‚ΠΈΠ΅ + +### ΠŸΠ»Π°Π½ΠΈΡ€ΡƒΠ΅ΠΌΡ‹Π΅ ΡƒΠ»ΡƒΡ‡ΡˆΠ΅Π½ΠΈΡ +- [ ] Π˜ΡΡ‚ΠΎΡ€ΠΈΡ‡Π΅ΡΠΊΠΈΠ΅ Ρ‚Ρ€Π΅Π½Π΄Ρ‹ статистики +- [ ] БСгмСнтация ΠΏΠΎ ΠΏΠ΅Ρ€ΠΈΠΎΠ΄Π°ΠΌ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ +- [ ] Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ вовлСчённости +- [ ] ΠŸΠ΅Ρ€ΡΠΎΠ½Π°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ Ρ€Π΅ΠΊΠΎΠΌΠ΅Π½Π΄Π°Ρ†ΠΈΠΈ Π½Π° основС статистики + +### Π˜Π·Π²Π΅ΡΡ‚Π½Ρ‹Π΅ ограничСния +- ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Ρ‹ ΠΎΠ±Π½ΠΎΠ²Π»ΡΡŽΡ‚ΡΡ с Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ (Google Analytics API) +- Π‘ΠΎΠ»ΡŒΡˆΠΈΠ΅ ΠΎΠ±ΡŠΡ‘ΠΌΡ‹ Π΄Π°Π½Π½Ρ‹Ρ… ΠΌΠΎΠ³ΡƒΡ‚ Π·Π°ΠΌΠ΅Π΄Π»ΡΡ‚ΡŒ запросы Π±Π΅Π· кСша +- Π‘Π»ΠΎΠΆΠ½Ρ‹Π΅ запросы сортировки Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‚ большС рСсурсов diff --git a/resolvers/author.py b/resolvers/author.py index 4adcdcfb..b0471948 100644 --- a/resolvers/author.py +++ b/resolvers/author.py @@ -18,7 +18,8 @@ from cache.cache import ( ) from orm.author import Author, AuthorFollower from orm.community import Community, CommunityAuthor, CommunityFollower -from orm.shout import Shout, ShoutAuthor +from orm.reaction import Reaction +from orm.shout import Shout, ShoutAuthor, ShoutTopic from resolvers.stat import get_with_stat from services.auth import login_required from services.search import search_service @@ -34,17 +35,25 @@ DEFAULT_COMMUNITIES = [1] # ΠžΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅ Ρ‚ΠΈΠΏΠ° AuthorsBy Π½Π° основС схСмы GraphQL class AuthorsBy(TypedDict, total=False): """ - Π’ΠΈΠΏ для ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Π° сортировки Π°Π²Ρ‚ΠΎΡ€ΠΎΠ², ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΉ схСмС GraphQL. + ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΠΈ ΠΈ сортировки Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² для GraphQL запроса load_authors_by. - Поля: + πŸ“Š Поля сортировки: + order: ПолС для сортировки Π°Π²Ρ‚ΠΎΡ€ΠΎΠ²: + πŸ”’ Π‘Π°Π·ΠΎΠ²Ρ‹Π΅ ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ: "shouts" (ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ), "followers" (подписчики) + 🏷️ ΠšΠΎΠ½Ρ‚Π΅Π½Ρ‚: "topics" (Ρ‚Π΅ΠΌΡ‹), "comments" (ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ) + πŸ‘₯ Π‘ΠΎΡ†ΠΈΠ°Π»ΡŒΠ½Ρ‹Π΅: "coauthors" (соавторы), "replies_count" (ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ Π½Π° ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚) + ⭐ Π Π΅ΠΉΡ‚ΠΈΠ½Π³: "rating" (ΠΎΠ±Ρ‰ΠΈΠΉ), "rating_shouts" (ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ), "rating_comments" (ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ) + πŸ‘οΈ Π’ΠΎΠ²Π»Π΅Ρ‡Ρ‘Π½Π½ΠΎΡΡ‚ΡŒ: "viewed_shouts" (просмотры) + πŸ“ Алфавит: "name" (ΠΏΠΎ ΠΈΠΌΠ΅Π½ΠΈ) + + πŸ” Поля Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΠΈ: last_seen: ВрСмСнная ΠΌΠ΅Ρ‚ΠΊΠ° послСднСго посСщСния created_at: ВрСмСнная ΠΌΠ΅Ρ‚ΠΊΠ° создания slug: Π£Π½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ Π°Π²Ρ‚ΠΎΡ€Π° - name: Имя Π°Π²Ρ‚ΠΎΡ€Π° + name: Имя Π°Π²Ρ‚ΠΎΡ€Π° для поиска topic: Π’Π΅ΠΌΠ°, связанная с Π°Π²Ρ‚ΠΎΡ€ΠΎΠΌ - order: ПолС для сортировки (shouts, followers, rating, comments, name) after: ВрСмСнная ΠΌΠ΅Ρ‚ΠΊΠ° для Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΠΈ "послС" - stat: ПолС статистики + stat: ПолС статистики для Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠΉ Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΠΈ """ last_seen: int | None @@ -96,15 +105,36 @@ async def get_authors_with_stats( limit: int = 10, offset: int = 0, by: AuthorsBy | None = None, current_user_id: int | None = None ) -> list[dict[str, Any]]: """ - ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² со статистикой с ΠΏΠ°Π³ΠΈΠ½Π°Ρ†ΠΈΠ΅ΠΉ. + πŸ§ͺ ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² с ΠΏΠΎΠ»Π½ΠΎΠΉ статистикой ΠΈ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ сортировки. + + πŸ“Š РассчитываСт всС ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ AuthorStat: + - shouts: ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΠΎΠΏΡƒΠ±Π»ΠΈΠΊΠΎΠ²Π°Π½Π½Ρ‹Ρ… статСй + - topics: Π£Π½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹ участия + - coauthors: ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ соавторов + - followers: ΠŸΠΎΠ΄ΠΏΠΈΡΡ‡ΠΈΠΊΠΈ + - rating: ΠžΠ±Ρ‰ΠΈΠΉ Ρ€Π΅ΠΉΡ‚ΠΈΠ½Π³ (rating_shouts + rating_comments) + - rating_shouts: Π Π΅ΠΉΡ‚ΠΈΠ½Π³ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ (Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ) + - rating_comments: Π Π΅ΠΉΡ‚ΠΈΠ½Π³ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² (Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ) + - comments: Π‘ΠΎΠ·Π΄Π°Π½Π½Ρ‹Π΅ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ + - replies_count: ΠžΡ‚Π²Π΅Ρ‚Ρ‹ Π½Π° ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ (ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π½Π° посты + ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ) + - viewed_shouts: ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Ρ‹ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ (ΠΈΠ· ViewedStorage) + + ⚑ ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΠΈ: + - Batch SQL-запросы для статистики + - ΠšΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ΠΎΠ² + - Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠ° Π½Π° ΡƒΡ€ΠΎΠ²Π½Π΅ SQL для ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ Args: - limit: МаксимальноС количСство Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌΡ‹Ρ… Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² + limit: МаксимальноС количСство Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌΡ‹Ρ… Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² (1-100) offset: Π‘ΠΌΠ΅Ρ‰Π΅Π½ΠΈΠ΅ для ΠΏΠ°Π³ΠΈΠ½Π°Ρ†ΠΈΠΈ - by: ΠžΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ сортировки (AuthorsBy) - current_user_id: ID Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π³ΠΎ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + by: ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΠΈ ΠΈ сортировки (AuthorsBy) + current_user_id: ID Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π³ΠΎ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ для Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΠΈ доступа + Returns: - list: Бписок Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² с ΠΈΡ… статистикой + list[dict]: Бписок Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² с ΠΏΠΎΠ»Π½ΠΎΠΉ статистикой, отсортированных согласно ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Π°ΠΌ + + Raises: + Exception: ΠŸΡ€ΠΈ ΠΎΡˆΠΈΠ±ΠΊΠ°Ρ… выполнСния SQL-запросов ΠΈΠ»ΠΈ доступа ΠΊ ViewedStorage """ # Π€ΠΎΡ€ΠΌΠΈΡ€ΡƒΠ΅ΠΌ ΠΊΠ»ΡŽΡ‡ кСша с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ ΡƒΠ½ΠΈΠ²Π΅Ρ€ΡΠ°Π»ΡŒΠ½ΠΎΠΉ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ order_value = by.get("order", "default") if by else "default" @@ -128,7 +158,18 @@ async def get_authors_with_stats( if "order" in by: order_value = by["order"] logger.debug(f"Found order field with value: {order_value}") - if order_value in ["shouts", "followers", "rating", "comments"]: + if order_value in [ + "shouts", + "followers", + "rating", + "comments", + "topics", + "coauthors", + "viewed_shouts", + "rating_shouts", + "rating_comments", + "replies_count", + ]: stats_sort_field = order_value logger.debug(f"Applying statistics-based sorting by: {stats_sort_field}") # НС примСняСм Π΄Ρ€ΡƒΠ³ΡƒΡŽ сортировку, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ Π±ΡƒΠ΄Π΅ΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ stats_sort_field @@ -212,12 +253,140 @@ async def get_authors_with_stats( sql_desc(func.coalesce(subquery.c.followers_count, 0)) ) logger.debug("Applied sorting by followers count") + elif stats_sort_field == "topics": + # 🏷️ Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠ° ΠΏΠΎ количСству Ρ‚Π΅ΠΌ + logger.debug("Building subquery for topics sorting") + subquery = ( + select(ShoutAuthor.author, func.count(func.distinct(ShoutTopic.topic)).label("topics_count")) + .select_from(ShoutAuthor) + .join(Shout, ShoutAuthor.shout == Shout.id) + .join(ShoutTopic, Shout.id == ShoutTopic.shout) + .where(and_(Shout.deleted_at.is_(None), Shout.published_at.is_not(None))) + .group_by(ShoutAuthor.author) + .subquery() + ) + base_query = base_query.outerjoin(subquery, Author.id == subquery.c.author).order_by( + sql_desc(func.coalesce(subquery.c.topics_count, 0)) + ) + logger.debug("Applied sorting by topics count") + elif stats_sort_field == "coauthors": + # ✍️ Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠ° ΠΏΠΎ количСству соавторов + logger.debug("Building subquery for coauthors sorting") + sa1 = ShoutAuthor.__table__.alias("sa1") + sa2 = ShoutAuthor.__table__.alias("sa2") + subquery = ( + select(sa1.c.author, func.count(func.distinct(sa2.c.author)).label("coauthors_count")) + .select_from(sa1.join(Shout, sa1.c.shout == Shout.id).join(sa2, sa2.c.shout == Shout.id)) + .where( + and_( + Shout.deleted_at.is_(None), + Shout.published_at.is_not(None), + sa1.c.author != sa2.c.author, # ΠΈΡΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌ самого Π°Π²Ρ‚ΠΎΡ€Π° ΠΈΠ· подсчёта + ) + ) + .group_by(sa1.c.author) + .subquery() + ) + base_query = base_query.outerjoin(subquery, Author.id == subquery.c.author).order_by( + sql_desc(func.coalesce(subquery.c.coauthors_count, 0)) + ) + logger.debug("Applied sorting by coauthors count") + elif stats_sort_field == "comments": + # πŸ’¬ Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠ° ΠΏΠΎ количСству ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² + logger.debug("Building subquery for comments sorting") + subquery = ( + select(Reaction.created_by, func.count(func.distinct(Reaction.id)).label("comments_count")) + .select_from(Reaction) + .join(Shout, Reaction.shout == Shout.id) + .where( + and_( + Reaction.deleted_at.is_(None), + Shout.deleted_at.is_(None), + Reaction.kind.in_(["COMMENT", "QUOTE"]), + ) + ) + .group_by(Reaction.created_by) + .subquery() + ) + base_query = base_query.outerjoin(subquery, Author.id == subquery.c.created_by).order_by( + sql_desc(func.coalesce(subquery.c.comments_count, 0)) + ) + logger.debug("Applied sorting by comments count") + elif stats_sort_field == "replies_count": + # πŸ’¬ Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠ° ΠΏΠΎ ΠΎΠ±Ρ‰Π΅ΠΌΡƒ количСству ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ² (ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π½Π° посты + ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ) + logger.debug("Building subquery for replies_count sorting") + + # ΠŸΠΎΠ΄Π·Π°ΠΏΡ€ΠΎΡ для ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ² Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π°Π²Ρ‚ΠΎΡ€Π° + replies_to_comments_subq = ( + select( + Reaction.created_by.label("author_id"), + func.count(func.distinct(Reaction.id)).label("replies_count"), + ) + .select_from(Reaction) + .where( + and_( + Reaction.deleted_at.is_(None), + Reaction.reply_to.is_not(None), + Reaction.kind.in_(["COMMENT", "QUOTE"]), + ) + ) + .group_by(Reaction.created_by) + .subquery() + ) + + # ΠŸΠΎΠ΄Π·Π°ΠΏΡ€ΠΎΡ для ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² Π½Π° посты Π°Π²Ρ‚ΠΎΡ€Π° + comments_on_posts_subq = ( + select( + ShoutAuthor.author.label("author_id"), + func.count(func.distinct(Reaction.id)).label("replies_count"), + ) + .select_from(ShoutAuthor) + .join(Shout, ShoutAuthor.shout == Shout.id) + .join(Reaction, Shout.id == Reaction.shout) + .where( + and_( + Shout.deleted_at.is_(None), + Shout.published_at.is_not(None), + Reaction.deleted_at.is_(None), + Reaction.kind.in_(["COMMENT", "QUOTE"]), + ) + ) + .group_by(ShoutAuthor.author) + .subquery() + ) + + # ОбъСдиняСм ΠΎΠ±Π° подзапроса Ρ‡Π΅Ρ€Π΅Π· UNION ALL + combined_replies_subq = ( + select( + func.coalesce( + replies_to_comments_subq.c.author_id, comments_on_posts_subq.c.author_id + ).label("author_id"), + func.coalesce( + func.coalesce(replies_to_comments_subq.c.replies_count, 0) + + func.coalesce(comments_on_posts_subq.c.replies_count, 0), + 0, + ).label("total_replies"), + ) + .select_from( + replies_to_comments_subq + .outerjoin( + comments_on_posts_subq, + replies_to_comments_subq.c.author_id == comments_on_posts_subq.c.author_id, + ) + ) + .subquery() + ) + + base_query = base_query.outerjoin( + combined_replies_subq, Author.id == combined_replies_subq.c.author_id + ).order_by(sql_desc(func.coalesce(combined_replies_subq.c.total_replies, 0))) + logger.debug("Applied sorting by replies_count") # Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ для ΠΎΡ‚Π»Π°Π΄ΠΊΠΈ сортировки try: # ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ SQL запрос для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ sql_query = str(base_query.compile(compile_kwargs={"literal_binds": True})) - logger.debug(f"Generated SQL query for followers sorting: {sql_query}") + logger.debug(f"Generated SQL query for replies_count sorting: {sql_query}") except Exception as e: logger.error(f"Error generating SQL query: {e}") @@ -238,9 +407,13 @@ async def get_authors_with_stats( if stats_sort_field: logger.debug(f"Query returned {len(authors)} authors with sorting by {stats_sort_field}") - # ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ запрос для получСния статистики ΠΏΠΎ публикациям для Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² - logger.debug("Executing shouts statistics query") + # πŸ§ͺ ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ запросы для получСния всСй статистики Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² + logger.debug("Executing comprehensive statistics queries") placeholders = ", ".join([f":id{i}" for i in range(len(author_ids))]) + params = {f"id{i}": author_id for i, author_id in enumerate(author_ids)} + + # πŸ“Š Бтатистика ΠΏΠΎ публикациям + logger.debug("Executing shouts statistics query") shouts_stats_query = f""" SELECT sa.author, COUNT(DISTINCT s.id) as shouts_count FROM shout_author sa @@ -248,11 +421,10 @@ async def get_authors_with_stats( WHERE sa.author IN ({placeholders}) GROUP BY sa.author """ - params = {f"id{i}": author_id for i, author_id in enumerate(author_ids)} shouts_stats = {row[0]: row[1] for row in session.execute(text(shouts_stats_query), params)} logger.debug(f"Shouts stats retrieved: {shouts_stats}") - # Запрос Π½Π° ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ статистики ΠΏΠΎ подписчикам для Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² + # πŸ‘₯ Бтатистика ΠΏΠΎ подписчикам logger.debug("Executing followers statistics query") followers_stats_query = f""" SELECT following, COUNT(DISTINCT follower) as followers_count @@ -263,8 +435,163 @@ async def get_authors_with_stats( followers_stats = {row[0]: row[1] for row in session.execute(text(followers_stats_query), params)} logger.debug(f"Followers stats retrieved: {followers_stats}") - # Π€ΠΎΡ€ΠΌΠΈΡ€ΡƒΠ΅ΠΌ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ с Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ΠΌ статистики - logger.debug("Building final result with statistics") + # 🏷️ Бтатистика ΠΏΠΎ Ρ‚Π΅ΠΌΠ°ΠΌ (количСство ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Ρ… Ρ‚Π΅ΠΌ, Π² ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… участвовал Π°Π²Ρ‚ΠΎΡ€) + logger.debug("Executing topics statistics query") + topics_stats_query = f""" + SELECT sa.author, COUNT(DISTINCT st.topic) as topics_count + FROM shout_author sa + JOIN shout s ON sa.shout = s.id AND s.deleted_at IS NULL AND s.published_at IS NOT NULL + JOIN shout_topic st ON s.id = st.shout + WHERE sa.author IN ({placeholders}) + GROUP BY sa.author + """ + topics_stats = {row[0]: row[1] for row in session.execute(text(topics_stats_query), params)} + logger.debug(f"Topics stats retrieved: {topics_stats}") + + # ✍️ Бтатистика ΠΏΠΎ соавторам (количСство ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Ρ… соавторов) + logger.debug("Executing coauthors statistics query") + coauthors_stats_query = f""" + SELECT sa1.author, COUNT(DISTINCT sa2.author) as coauthors_count + FROM shout_author sa1 + JOIN shout s ON sa1.shout = s.id + AND s.deleted_at IS NULL + AND s.published_at IS NOT NULL + JOIN shout_author sa2 ON s.id = sa2.shout + AND sa2.author != sa1.author -- ΠΈΡΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌ самого Π°Π²Ρ‚ΠΎΡ€Π° + WHERE sa1.author IN ({placeholders}) + GROUP BY sa1.author + """ + coauthors_stats = {row[0]: row[1] for row in session.execute(text(coauthors_stats_query), params)} + logger.debug(f"Coauthors stats retrieved: {coauthors_stats}") + + # πŸ’¬ Бтатистика ΠΏΠΎ коммСнтариям (количСство созданных ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π²) + logger.debug("Executing comments statistics query") + comments_stats_query = f""" + SELECT r.created_by, COUNT(DISTINCT r.id) as comments_count + FROM reaction r + JOIN shout s ON r.shout = s.id AND s.deleted_at IS NULL + WHERE r.created_by IN ({placeholders}) AND r.deleted_at IS NULL + AND r.kind IN ('COMMENT', 'QUOTE') + GROUP BY r.created_by + """ + comments_stats = {row[0]: row[1] for row in session.execute(text(comments_stats_query), params)} + logger.debug(f"Comments stats retrieved: {comments_stats}") + + # πŸ’¬ Бтатистика ΠΏΠΎ Π²Ρ‹Π·Π²Π°Π½Π½Ρ‹ΠΌ коммСнтариям (ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ + ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π½Π° посты) + logger.debug("Executing replies_count statistics query") + + # ΠžΡ‚Π²Π΅Ρ‚Ρ‹ Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π°Π²Ρ‚ΠΎΡ€Π° + replies_to_comments_query = f""" + SELECT r1.created_by as author_id, COUNT(DISTINCT r2.id) as replies_count + FROM reaction r1 + JOIN reaction r2 ON r1.id = r2.reply_to AND r2.deleted_at IS NULL + WHERE r1.created_by IN ({placeholders}) AND r1.deleted_at IS NULL + AND r1.kind IN ('COMMENT', 'QUOTE') + AND r2.kind IN ('COMMENT', 'QUOTE') + GROUP BY r1.created_by + """ + replies_to_comments_stats = { + row[0]: row[1] for row in session.execute(text(replies_to_comments_query), params) + } + logger.debug(f"Replies to comments stats retrieved: {replies_to_comments_stats}") + + # ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π½Π° посты Π°Π²Ρ‚ΠΎΡ€Π° + comments_on_posts_query = f""" + SELECT sa.author as author_id, COUNT(DISTINCT r.id) as replies_count + FROM shout_author sa + JOIN shout s ON sa.shout = s.id AND s.deleted_at IS NULL AND s.published_at IS NOT NULL + JOIN reaction r ON s.id = r.shout AND r.deleted_at IS NULL + WHERE sa.author IN ({placeholders}) + AND r.kind IN ('COMMENT', 'QUOTE') + GROUP BY sa.author + """ + comments_on_posts_stats = { + row[0]: row[1] for row in session.execute(text(comments_on_posts_query), params) + } + logger.debug(f"Comments on posts stats retrieved: {comments_on_posts_stats}") + + # ОбъСдиняСм статистику + replies_count_stats = {} + for author_id in author_ids: + replies_to_comments = replies_to_comments_stats.get(author_id, 0) + comments_on_posts = comments_on_posts_stats.get(author_id, 0) + replies_count_stats[author_id] = replies_to_comments + comments_on_posts + logger.debug(f"Combined replies count stats: {replies_count_stats}") + + # ⭐ Бтатистика ΠΏΠΎ Ρ€Π΅ΠΉΡ‚ΠΈΠ½Π³Ρƒ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ (сумма Ρ€Π΅Π°ΠΊΡ†ΠΈΠΉ Π½Π° ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ Π°Π²Ρ‚ΠΎΡ€Π°) + logger.debug("Executing rating_shouts statistics query") + rating_shouts_stats_query = f""" + SELECT sa.author, + SUM(CASE + WHEN r.kind IN ('LIKE', 'AGREE', 'ACCEPT', 'PROOF', 'CREDIT') THEN 1 + WHEN r.kind IN ('DISLIKE', 'DISAGREE', 'REJECT', 'DISPROOF') THEN -1 + ELSE 0 + END) as rating_shouts + FROM shout_author sa + JOIN shout s ON sa.shout = s.id AND s.deleted_at IS NULL AND s.published_at IS NOT NULL + JOIN reaction r ON s.id = r.shout AND r.deleted_at IS NULL + WHERE sa.author IN ({placeholders}) + GROUP BY sa.author + """ + rating_shouts_stats = { + row[0]: row[1] for row in session.execute(text(rating_shouts_stats_query), params) + } + logger.debug(f"Rating shouts stats retrieved: {rating_shouts_stats}") + + # ⭐ Бтатистика ΠΏΠΎ Ρ€Π΅ΠΉΡ‚ΠΈΠ½Π³Ρƒ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² (Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π°Π²Ρ‚ΠΎΡ€Π°) + logger.debug("Executing rating_comments statistics query") + rating_comments_stats_query = f""" + SELECT r1.created_by, + SUM(CASE + WHEN r2.kind IN ('LIKE', 'AGREE', 'ACCEPT', 'PROOF', 'CREDIT') THEN 1 + WHEN r2.kind IN ('DISLIKE', 'DISAGREE', 'REJECT', 'DISPROOF') THEN -1 + ELSE 0 + END) as rating_comments + FROM reaction r1 + JOIN reaction r2 ON r1.id = r2.reply_to AND r2.deleted_at IS NULL + WHERE r1.created_by IN ({placeholders}) AND r1.deleted_at IS NULL + AND r1.kind IN ('COMMENT', 'QUOTE') + GROUP BY r1.created_by + """ + rating_comments_stats = { + row[0]: row[1] for row in session.execute(text(rating_comments_stats_query), params) + } + logger.debug(f"Rating comments stats retrieved: {rating_comments_stats}") + + # πŸ“ˆ ΠžΠ±Ρ‰ΠΈΠΉ Ρ€Π΅ΠΉΡ‚ΠΈΠ½Π³ (сумма Ρ€Π΅ΠΉΡ‚ΠΈΠ½Π³ΠΎΠ² ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ ΠΈ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π²) + logger.debug("Calculating overall rating") + overall_rating_stats = {} + for author_id in author_ids: + shouts_rating = rating_shouts_stats.get(author_id, 0) + comments_rating = rating_comments_stats.get(author_id, 0) + overall_rating_stats[author_id] = shouts_rating + comments_rating + logger.debug(f"Overall rating stats calculated: {overall_rating_stats}") + + # πŸ‘οΈ Бтатистика ΠΏΠΎ просмотрам ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ (ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ ViewedStorage для получСния Π°Π³Ρ€Π΅Π³ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Ρ… Π΄Π°Π½Π½Ρ‹Ρ…) + logger.debug("Calculating viewed_shouts statistics from ViewedStorage") + from services.viewed import ViewedStorage + + viewed_shouts_stats = {} + # ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΎΠ±Ρ‰ΠΈΠ΅ просмотры для всСх ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ Π°Π²Ρ‚ΠΎΡ€Π° + for author_id in author_ids: + total_views = 0 + # ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ всС ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ Π°Π²Ρ‚ΠΎΡ€Π° ΠΈ суммируСм ΠΈΡ… просмотры + author_shouts_query = """ + SELECT s.slug + FROM shout_author sa + JOIN shout s ON sa.shout = s.id AND s.deleted_at IS NULL AND s.published_at IS NOT NULL + WHERE sa.author = :author_id + """ + shout_rows = session.execute(text(author_shouts_query), {"author_id": author_id}) + for shout_row in shout_rows: + shout_slug = shout_row[0] + shout_views = ViewedStorage.get_shout(shout_slug=shout_slug) + total_views += shout_views + viewed_shouts_stats[author_id] = total_views + logger.debug(f"Viewed shouts stats calculated: {viewed_shouts_stats}") + + # 🎯 Π€ΠΎΡ€ΠΌΠΈΡ€ΡƒΠ΅ΠΌ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ с Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ΠΌ ΠΏΠΎΠ»Π½ΠΎΠΉ статистики + logger.debug("Building final result with comprehensive statistics") result = [] for author in authors: try: @@ -272,7 +599,15 @@ async def get_authors_with_stats( author_dict = author.dict() author_dict["stat"] = { "shouts": shouts_stats.get(author.id, 0), + "topics": topics_stats.get(author.id, 0), + "coauthors": coauthors_stats.get(author.id, 0), "followers": followers_stats.get(author.id, 0), + "rating": overall_rating_stats.get(author.id, 0), + "rating_shouts": rating_shouts_stats.get(author.id, 0), + "rating_comments": rating_comments_stats.get(author.id, 0), + "comments": comments_stats.get(author.id, 0), + "replies_count": replies_count_stats.get(author.id, 0), + "viewed_shouts": viewed_shouts_stats.get(author.id, 0), } result.append(author_dict) diff --git a/schema/type.graphql b/schema/type.graphql index a7e53e2b..e83268eb 100644 --- a/schema/type.graphql +++ b/schema/type.graphql @@ -1,13 +1,22 @@ +# Бтатистика Π°Π²Ρ‚ΠΎΡ€Π° - полная ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠ° активности ΠΈ популярности type AuthorStat { - shouts: Int - topics: Int - authors: Int - followers: Int - rating: Int - rating_shouts: Int - rating_comments: Int - comments: Int - viewed: Int + # ΠšΠΎΠ½Ρ‚Π΅Π½Ρ‚ Π°Π²Ρ‚ΠΎΡ€Π° + shouts: Int # ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΠΎΠΏΡƒΠ±Π»ΠΈΠΊΠΎΠ²Π°Π½Π½Ρ‹Ρ… статСй + topics: Int # ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Ρ… Ρ‚Π΅ΠΌ, Π² ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… участвовал + comments: Int # ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ созданных ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² ΠΈ Ρ†ΠΈΡ‚Π°Ρ‚ + + # ВзаимодСйствиС с Π΄Ρ€ΡƒΠ³ΠΈΠΌΠΈ Π°Π²Ρ‚ΠΎΡ€Π°ΠΌΠΈ + coauthors: Int # ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Ρ… соавторов + followers: Int # ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ подписчиков + + # РСйтинговая систСма + rating: Int # ΠžΠ±Ρ‰ΠΈΠΉ Ρ€Π΅ΠΉΡ‚ΠΈΠ½Π³ (rating_shouts + rating_comments) + rating_shouts: Int # Π Π΅ΠΉΡ‚ΠΈΠ½Π³ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ (сумма Ρ€Π΅Π°ΠΊΡ†ΠΈΠΉ LIKE/AGREE/ACCEPT/PROOF/CREDIT минус DISLIKE/DISAGREE/REJECT/DISPROOF) + rating_comments: Int # Π Π΅ΠΉΡ‚ΠΈΠ½Π³ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² (Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π°Π²Ρ‚ΠΎΡ€Π°) + + # ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ вовлСчённости + replies_count: Int # ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ² Π½Π° ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ Π°Π²Ρ‚ΠΎΡ€Π° (ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ + ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π½Π° посты) + viewed_shouts: Int # ΠžΠ±Ρ‰Π΅Π΅ количСство просмотров всСх ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ Π°Π²Ρ‚ΠΎΡ€Π° } type Author {