Compare commits
32 Commits
4502709205
...
944afc977d
Author | SHA1 | Date |
---|---|---|
Chocobozzz | 944afc977d | |
Chocobozzz | 06890d094e | |
Chocobozzz | b8db45dca1 | |
Chocobozzz | 6a5563ed5b | |
Chocobozzz | c7edcd1839 | |
Chocobozzz | 535b2b6594 | |
Chocobozzz | 3a180c1b3c | |
Chocobozzz | 8590598a78 | |
Chocobozzz | c74d6aa671 | |
josé m | 8af619a214 | |
nexi | 84177ed99a | |
0que | a0a49a4ee8 | |
Chocobozzz | 6787428cd7 | |
Chocobozzz | a04dea2dd1 | |
Chocobozzz | aaad2ea75d | |
Chocobozzz | bf63503e23 | |
Chocobozzz | 477fd984cf | |
Chocobozzz | 65295a804a | |
Chocobozzz | 491f936906 | |
Chocobozzz | 9c43989b59 | |
Chocobozzz | 69e17e2777 | |
Chocobozzz | fde7007788 | |
Chocobozzz | c9f901d6a3 | |
Chocobozzz | b98aa43a77 | |
Chocobozzz | 603b3b31a6 | |
Chocobozzz | 71e310f639 | |
Chocobozzz | 6d96cfab43 | |
Chocobozzz | 2edc24cae3 | |
Chocobozzz | 1a2d9ea921 | |
Chocobozzz | 1c81808bdd | |
Chocobozzz | d50775f88a | |
Chocobozzz | 7fb1b791ae |
|
@ -0,0 +1,10 @@
|
|||
# v1.0.0
|
||||
|
||||
* Use [Meilisearch](https://www.meilisearch.com) instead of Elastic Search
|
||||
* See https://framagit.org/framasoft/peertube/search-index/-/issues/50 for more information
|
||||
* Migration from Elastic Search is not implemented, just run another PeerTube search index to index videos/channels/playlists and when it's done you can switch from the old index and the new one
|
||||
* Better global search relevancy
|
||||
* Fix search with accentuated characters
|
||||
* Improve search relevancy with non latin search
|
||||
* Support verbatim search
|
||||
|
28
README.md
28
README.md
|
@ -7,12 +7,12 @@ $ git submodule update --init --recursive
|
|||
$ yarn install --pure-lockfile
|
||||
```
|
||||
|
||||
The database (Elastic Search) is automatically created by PeerTube at startup.
|
||||
Indexes in Meilisearch are automatically created by PeerTube at startup.
|
||||
|
||||
Run simultaneously (for example with 3 terminals):
|
||||
|
||||
```terminal
|
||||
$ tsc -w
|
||||
$ npm run tsc -- -w
|
||||
```
|
||||
|
||||
```terminal
|
||||
|
@ -31,9 +31,11 @@ Add the locale in `client/src/main.ts` and `client/gettext.config.js` and run `n
|
|||
|
||||
## Production
|
||||
|
||||
### Installation
|
||||
|
||||
Install dependencies:
|
||||
* NodeJS (v16)
|
||||
* Elastic Search
|
||||
* NodeJS (v18)
|
||||
* MeiliSearch
|
||||
|
||||
```terminal
|
||||
$ git clone https://framagit.org/framasoft/peertube/search-index.git /var/www/peertube-search-index
|
||||
|
@ -45,18 +47,12 @@ $ vim config/production.yaml
|
|||
$ NODE_ENV=production NODE_CONFIG_DIR=/var/www/peertube-search-index/config node dist/server.js
|
||||
```
|
||||
|
||||
### Mapping migration
|
||||
|
||||
To update Elastic Search index mappings without downtime, run another instance of the search indexer
|
||||
using the same configuration that the main node. You just have to update `elastic-search.indexes.*` to use new index names.
|
||||
### Upgrade
|
||||
|
||||
```
|
||||
$ cd /var/www/peertube-search-index
|
||||
$ cp config/production.yaml config/production-1.yaml
|
||||
$ vim config/production-1.yaml
|
||||
$ NODE_ENV=production NODE_APP_INSTANCE=1 NODE_CONFIG_DIR=/var/www/peertube-search-index/config node dist/server.js
|
||||
cd /var/www/peertube-search-index && \
|
||||
git pull origin master && \
|
||||
yarn install --pure-lockfile && \
|
||||
npm run build && \
|
||||
sudo systemctl restart peertube-search-index.service
|
||||
```
|
||||
|
||||
After a while the new indexes will be filled. You can then stop the second indexer, update `config/production.yaml` to use
|
||||
the new index names and restart the main index.
|
||||
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"PO-Revision-Date: 2023-01-06 09:38+0000\n"
|
||||
"Last-Translator: josé m. <correoxm@disroot.org>\n"
|
||||
"Language-Team: Galician <https://weblate.framasoft.org/projects/peertube-"
|
||||
"search-index/client/gl/>\n"
|
||||
"PO-Revision-Date: 2023-12-06 10:37+0000\n"
|
||||
"Last-Translator: \"josé m.\" <correoxm@disroot.org>\n"
|
||||
"Language-Team: Galician <https://weblate.framasoft.org/projects/"
|
||||
"peertube-search-index/client/gl/>\n"
|
||||
"Language: gl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.15\n"
|
||||
"X-Generator: Weblate 5.2.1\n"
|
||||
"Generated-By: easygettext\n"
|
||||
|
||||
#: src/views/Search.vue:64 src/views/Search.vue:65 src/views/Search.vue:68
|
||||
|
@ -43,8 +43,8 @@ msgstr[1] "%{totalVideos} vídeos"
|
|||
#: src/components/ChannelResult.vue:29
|
||||
msgid "%{videosCount} video"
|
||||
msgid_plural "%{videosCount} videos"
|
||||
msgstr[0] "%{videosLength} vídeo"
|
||||
msgstr[1] "%{videosLength} vídeos"
|
||||
msgstr[0] "%{videosCount} vídeo"
|
||||
msgstr[1] "%{videosCount} vídeos"
|
||||
|
||||
#: src/components/PlaylistResult.vue:118
|
||||
msgid "%{videosLength} video"
|
||||
|
|
|
@ -1,45 +1,43 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"PO-Revision-Date: 2022-12-15 07:03+0000\n"
|
||||
"Last-Translator: Дмитрий Кузнецов <dk65536@gmail.com>\n"
|
||||
"Language-Team: Russian <https://weblate.framasoft.org/projects/peertube-"
|
||||
"search-index/client/ru/>\n"
|
||||
"PO-Revision-Date: 2023-10-30 11:37+0000\n"
|
||||
"Last-Translator: 0que <0que@users.noreply.weblate.framasoft.org>\n"
|
||||
"Language-Team: Russian <https://weblate.framasoft.org/projects/"
|
||||
"peertube-search-index/client/ru/>\n"
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 4.14.1\n"
|
||||
"X-Generator: Weblate 5.1.1\n"
|
||||
"Generated-By: easygettext\n"
|
||||
|
||||
#: src/views/Search.vue:64 src/views/Search.vue:65 src/views/Search.vue:68
|
||||
#: src/views/Search.vue:72
|
||||
msgid "%{totalChannels} channel"
|
||||
msgid_plural "%{totalChannels} channels"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
msgstr[2] ""
|
||||
msgstr[0] "%{totalChannels} канал"
|
||||
msgstr[1] "%{totalChannels} канала"
|
||||
msgstr[2] "%{totalChannels} каналов"
|
||||
|
||||
#: src/views/Search.vue:84 src/views/Search.vue:85 src/views/Search.vue:88
|
||||
#: src/views/Search.vue:96
|
||||
msgid "%{totalPlaylists} playlist"
|
||||
msgid_plural "%{totalPlaylists} playlists"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
msgstr[2] ""
|
||||
msgstr[0] "%{totalChannels} плейлист"
|
||||
msgstr[1] "%{totalChannels} плейлиста"
|
||||
msgstr[2] "%{totalChannels} плейлистов"
|
||||
|
||||
#: src/views/Search.vue:38 src/views/Search.vue:39 src/views/Search.vue:42
|
||||
#, fuzzy
|
||||
msgid "%{totalResults} result found:"
|
||||
msgid_plural "%{totalResults} results found:"
|
||||
msgstr[0] "%{resultsCount} результат найден для"
|
||||
msgstr[1] "%{resultsCount} результата найдено для"
|
||||
msgstr[2] "%{resultsCount} результатов найдено для"
|
||||
msgstr[0] "Найден %{resultsCount} результат:"
|
||||
msgstr[1] "Найдено %{resultsCount} результата:"
|
||||
msgstr[2] "Найдено %{resultsCount} результатов:"
|
||||
|
||||
#: src/views/Search.vue:44 src/views/Search.vue:45 src/views/Search.vue:48
|
||||
#, fuzzy
|
||||
msgid "%{totalVideos} video"
|
||||
msgid_plural "%{totalVideos} videos"
|
||||
msgstr[0] "%{videosLength} видео"
|
||||
|
@ -48,7 +46,6 @@ msgstr[2] "%{videosLength} видео"
|
|||
|
||||
#: src/components/ChannelResult.vue:25 src/components/ChannelResult.vue:26
|
||||
#: src/components/ChannelResult.vue:29
|
||||
#, fuzzy
|
||||
msgid "%{videosCount} video"
|
||||
msgid_plural "%{videosCount} videos"
|
||||
msgstr[0] "%{videosLength} видео"
|
||||
|
@ -56,7 +53,6 @@ msgstr[1] "%{videosLength} видео"
|
|||
msgstr[2] "%{videosLength} видео"
|
||||
|
||||
#: src/components/PlaylistResult.vue:118
|
||||
#, fuzzy
|
||||
msgid "%{videosLength} video"
|
||||
msgid_plural "%{videosLength} videos"
|
||||
msgstr[0] "%{videosLength} видео"
|
||||
|
@ -64,28 +60,24 @@ msgstr[1] "%{videosLength} видео"
|
|||
msgstr[2] "%{videosLength} видео"
|
||||
|
||||
#: src/components/SearchWarning.vue:7 src/components/SearchWarning.vue:8
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"<strong>%{indexName}</strong> displays videos and channels that match your "
|
||||
"search but is not the publisher, nor the owner."
|
||||
msgstr ""
|
||||
"<strong>%{indexName}</strong> показывает ролики и каналы, удовлетворяющие "
|
||||
"вашему запросу. Поиск по издателям и владельцам не производится. Если вы "
|
||||
"столкнулись с какими-то проблемами, уведомите администрацию PeerTube сайта, "
|
||||
"на котором располагается видео."
|
||||
"<strong>%{indexName}</strong> показывает ролики и каналы, соответствующие "
|
||||
"вашему запросу, но не является их издателем или правообладателем."
|
||||
|
||||
#: src/components/Footer.vue:6 src/components/Footer.vue:7
|
||||
msgid "A free software to take back control of your videos"
|
||||
msgstr "Свободный софт позволяющий вам контролировать ваши видео"
|
||||
msgstr "Свободное программное обеспечение, возвращающее вам контроль над видео"
|
||||
|
||||
#: src/components/Header.vue:17 src/components/Header.vue:18
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"A search engine of <a class=\"peertube-link\" href=\"https://joinpeertube."
|
||||
"org\" target=\"_blank\">PeerTube</a> videos, channels and playlists"
|
||||
msgstr ""
|
||||
"Поисковый движок для видео, каналов и плейлистов в <a href=\"https://"
|
||||
"joinpeertube.org\" target=\"_blank\">PeerTube</a>"
|
||||
"Поисковый движок для видео, каналов и плейлистов в <a class=\"peertube-link\""
|
||||
" href=\"https://joinpeertube.org\" target=\"_blank\">PeerTube</a>"
|
||||
|
||||
#: src/components/Filters.vue:296
|
||||
msgid "Activism"
|
||||
|
@ -93,17 +85,16 @@ msgstr "Активизм"
|
|||
|
||||
#: src/components/Filters.vue:232
|
||||
msgid "Add tag"
|
||||
msgstr "Добавить тег"
|
||||
msgstr "Введите тег здесь"
|
||||
|
||||
#: src/components/Filters.vue:151
|
||||
msgid "All of these tags"
|
||||
msgstr "Все эти теги"
|
||||
msgstr "Содержит следующие теги"
|
||||
|
||||
#: src/components/PlaylistResult.vue:42 src/components/PlaylistResult.vue:43
|
||||
#: src/components/PlaylistResult.vue:46
|
||||
#, fuzzy
|
||||
msgid "and updated on"
|
||||
msgstr "Обновлено"
|
||||
msgstr "и обновлено"
|
||||
|
||||
#: src/components/Filters.vue:298
|
||||
msgid "Animals"
|
||||
|
@ -111,7 +102,7 @@ msgstr "Животные"
|
|||
|
||||
#: src/components/Filters.vue:239 src/components/Filters.vue:264
|
||||
msgid "Any"
|
||||
msgstr "Любой"
|
||||
msgstr "Любая"
|
||||
|
||||
#: src/components/Filters.vue:224
|
||||
msgid "Apply filters"
|
||||
|
@ -123,31 +114,31 @@ msgstr "Исскуство"
|
|||
|
||||
#: src/components/Filters.vue:306
|
||||
msgid "Attribution"
|
||||
msgstr "Атрибуция"
|
||||
msgstr "Указание автора"
|
||||
|
||||
#: src/components/Filters.vue:308
|
||||
msgid "Attribution - No Derivatives"
|
||||
msgstr "Атрибуция - Без производных произведений"
|
||||
msgstr "Указание автора - Без производных"
|
||||
|
||||
#: src/components/Filters.vue:309
|
||||
msgid "Attribution - Non Commercial"
|
||||
msgstr "Атрибуция - Некоммерческая"
|
||||
msgstr "Указание автора - Некоммерческая"
|
||||
|
||||
#: src/components/Filters.vue:311
|
||||
msgid "Attribution - Non Commercial - No Derivatives"
|
||||
msgstr "Атрибуция - Некоммерческая - Без производных произведений"
|
||||
msgstr "Указание автора - Некоммерческая - Без производных произведений"
|
||||
|
||||
#: src/components/Filters.vue:310
|
||||
msgid "Attribution - Non Commercial - Share Alike"
|
||||
msgstr "Атрибуция - Некоммерческая - Сохраняя условия"
|
||||
msgstr "Указание автора - Некоммерческая - Сохраняя условия"
|
||||
|
||||
#: src/components/Filters.vue:307
|
||||
msgid "Attribution - Share Alike"
|
||||
msgstr "Атрибуция - Сохраняя условия"
|
||||
msgstr "Указание автора - Сохраняя условия"
|
||||
|
||||
#: src/components/SortButton.vue:6 src/components/SortButton.vue:7
|
||||
msgid "Best match"
|
||||
msgstr "Лучшие совпадения"
|
||||
msgstr "Лучшему совпадению"
|
||||
|
||||
#: src/components/Filters.vue:322
|
||||
msgid "Català"
|
||||
|
@ -168,11 +159,11 @@ msgstr "Изменить язык интерфейса"
|
|||
#: src/components/ChannelResult.vue:29 src/components/ChannelResult.vue:30
|
||||
#: src/components/ChannelResult.vue:33
|
||||
msgid "Channel created on platform"
|
||||
msgstr ""
|
||||
msgstr "Канал создан на платформе"
|
||||
|
||||
#: src/components/Header.vue:7
|
||||
msgid "Come back to homepage"
|
||||
msgstr ""
|
||||
msgstr "Вернуться на главную"
|
||||
|
||||
#: src/components/Filters.vue:291
|
||||
msgid "Comedy"
|
||||
|
@ -188,12 +179,12 @@ msgid "Deutsch"
|
|||
msgstr "Немецкий"
|
||||
|
||||
#: src/components/Header.vue:25 src/components/Header.vue:26
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Developed by <a class=\"peertube-link\" href=\"https://framasoft.org\" "
|
||||
"target=\"_blank\">Framasoft</a>"
|
||||
msgstr ""
|
||||
"Разработано <a href=\"https://framasoft.org\" target=\"_blank\">Framasoft</a>"
|
||||
"Разработано <a class=\"peertube-link\" href=\"https://framasoft.org\" target="
|
||||
"\"_blank\">Framasoft</a>"
|
||||
|
||||
#: src/components/ChannelResult.vue:83
|
||||
msgid "Discover this channel on %{host}"
|
||||
|
@ -201,32 +192,29 @@ msgstr "Посмотреть этот канал на %{host}"
|
|||
|
||||
#: src/components/Filters.vue:101 src/components/Filters.vue:102
|
||||
msgid "Display all categories"
|
||||
msgstr "Показывать все категории"
|
||||
msgstr "Любая"
|
||||
|
||||
#: src/components/Filters.vue:139 src/components/Filters.vue:140
|
||||
msgid "Display all languages"
|
||||
msgstr "Показывать все языки"
|
||||
msgstr "Любой"
|
||||
|
||||
#: src/components/Filters.vue:120 src/components/Filters.vue:121
|
||||
msgid "Display all licenses"
|
||||
msgstr "Показывать все лицензии"
|
||||
msgstr "Любая"
|
||||
|
||||
#: src/views/Search.vue:75 src/views/Search.vue:76 src/views/Search.vue:79
|
||||
#: src/views/Search.vue:83
|
||||
#, fuzzy
|
||||
msgid "Display more channels"
|
||||
msgstr "Показывать только"
|
||||
msgstr "Показать больше каналов"
|
||||
|
||||
#: src/views/Search.vue:107 src/views/Search.vue:95 src/views/Search.vue:96
|
||||
#: src/views/Search.vue:99
|
||||
#, fuzzy
|
||||
msgid "Display more playlists"
|
||||
msgstr "Показывать только"
|
||||
msgstr "Показать большей плейлистов"
|
||||
|
||||
#: src/views/Search.vue:55 src/views/Search.vue:56 src/views/Search.vue:59
|
||||
#, fuzzy
|
||||
msgid "Display more videos"
|
||||
msgstr "Показывать все категории"
|
||||
msgstr "Показать больше видео"
|
||||
|
||||
#: src/components/Filters.vue:47 src/components/Filters.vue:48
|
||||
msgid "Display only"
|
||||
|
@ -234,7 +222,7 @@ msgstr "Показывать только"
|
|||
|
||||
#: src/components/Filters.vue:4 src/components/Filters.vue:5
|
||||
msgid "Display sensitive content"
|
||||
msgstr "Показывать чувствительный контент"
|
||||
msgstr "Показывать небезопасные материалы"
|
||||
|
||||
#: src/components/Filters.vue:80 src/components/Filters.vue:81
|
||||
msgid "Duration"
|
||||
|
@ -275,7 +263,7 @@ msgstr "Фильмы"
|
|||
|
||||
#: src/components/Filters.vue:24 src/components/Filters.vue:25
|
||||
msgid "Filter by result type"
|
||||
msgstr ""
|
||||
msgstr "Фильтровать по типу"
|
||||
|
||||
#: src/views/Search.vue:22 src/views/Search.vue:23
|
||||
msgid "Filters"
|
||||
|
@ -310,21 +298,18 @@ msgid "How To"
|
|||
msgstr "Учебники"
|
||||
|
||||
#: src/components/SearchWarning.vue:15 src/components/SearchWarning.vue:16
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"If you notice any problems with a video, report it to the administrators on "
|
||||
"the PeerTube website where the video is published."
|
||||
msgstr ""
|
||||
"<strong>%{indexName}</strong> показывает ролики и каналы, удовлетворяющие "
|
||||
"вашему запросу. Поиск по издателям и владельцам не производится. Если вы "
|
||||
"столкнулись с какими-то проблемами, уведомите администрацию PeerTube сайта, "
|
||||
"на котором располагается видео."
|
||||
"Если с видео что-то не так, уведомите об этом администрацию сервера "
|
||||
"PeerTube, на котором оно расположено."
|
||||
|
||||
#: src/components/PlaylistResult.vue:48 src/components/PlaylistResult.vue:49
|
||||
#: src/components/PlaylistResult.vue:52 src/components/VideoResult.vue:37
|
||||
#: src/components/VideoResult.vue:38 src/components/VideoResult.vue:41
|
||||
msgid "In channel"
|
||||
msgstr ""
|
||||
msgstr "На канале"
|
||||
|
||||
#: src/components/Filters.vue:327
|
||||
msgid "Italiano"
|
||||
|
@ -332,7 +317,7 @@ msgstr "Итальянский"
|
|||
|
||||
#: src/components/Filters.vue:228 src/components/SearchInput.vue:59
|
||||
msgid "Keyword, channel, video, playlist, etc."
|
||||
msgstr "Ключевое слово, канал, видео, плейлист и т.д."
|
||||
msgstr "Ключевое слово, канал, видео, плейлист и т.п."
|
||||
|
||||
#: src/components/Filters.vue:299
|
||||
msgid "Kids"
|
||||
|
@ -344,9 +329,8 @@ msgstr "Язык"
|
|||
|
||||
#: src/components/VideoResult.vue:49 src/components/VideoResult.vue:50
|
||||
#: src/components/VideoResult.vue:53
|
||||
#, fuzzy
|
||||
msgid "Language:"
|
||||
msgstr "Язык"
|
||||
msgstr "Язык:"
|
||||
|
||||
#: src/components/Filters.vue:251
|
||||
msgid "Last 30 days"
|
||||
|
@ -362,15 +346,15 @@ msgstr "Последние 7 дней"
|
|||
|
||||
#: src/components/Footer.vue:11 src/components/Footer.vue:12
|
||||
msgid "Learn more about PeerTube"
|
||||
msgstr ""
|
||||
msgstr "Узнайте больше про PeerTube"
|
||||
|
||||
#: src/components/SortButton.vue:10 src/components/SortButton.vue:11
|
||||
msgid "Least recent"
|
||||
msgstr "Наиболее свежие"
|
||||
msgstr "Самые старые"
|
||||
|
||||
#: src/components/Footer.vue:15 src/components/Footer.vue:16
|
||||
msgid "Legal notices"
|
||||
msgstr "Юридическое уведомление"
|
||||
msgstr "Юридическая информация"
|
||||
|
||||
#: src/components/Filters.vue:113
|
||||
msgid "Licence"
|
||||
|
@ -378,7 +362,7 @@ msgstr "Лицензия"
|
|||
|
||||
#: src/components/VideoResult.vue:6 src/components/VideoResult.vue:7
|
||||
msgid "LIVE"
|
||||
msgstr "ТРАНСЛЯЦИЯ"
|
||||
msgstr "В ЭФИРЕ"
|
||||
|
||||
#: src/components/Filters.vue:55
|
||||
msgid "Live videos"
|
||||
|
@ -386,15 +370,15 @@ msgstr "Трансляции"
|
|||
|
||||
#: src/components/Filters.vue:276
|
||||
msgid "Long (> 10 min)"
|
||||
msgstr "Долго (> 10 мин)"
|
||||
msgstr "Долгие (> 10 мин.)"
|
||||
|
||||
#: src/components/Filters.vue:272
|
||||
msgid "Medium (4-10 min)"
|
||||
msgstr "Средне (4-10 мин)"
|
||||
msgstr "Средние (4-10 мин.)"
|
||||
|
||||
#: src/components/SortButton.vue:8 src/components/SortButton.vue:9
|
||||
msgid "Most recent"
|
||||
msgstr "Недавние"
|
||||
msgstr "Новизне"
|
||||
|
||||
#: src/components/Filters.vue:283
|
||||
msgid "Music"
|
||||
|
@ -402,7 +386,7 @@ msgstr "Музыка"
|
|||
|
||||
#: src/components/SearchInput.vue:2 src/components/SearchInput.vue:3
|
||||
msgid "My search"
|
||||
msgstr ""
|
||||
msgstr "Мой поиск"
|
||||
|
||||
#: src/components/Filters.vue:328
|
||||
msgid "Nederlands"
|
||||
|
@ -410,7 +394,7 @@ msgstr "Нидерландский"
|
|||
|
||||
#: src/components/Filters.vue:293
|
||||
msgid "News & Politics"
|
||||
msgstr "Новости & Политика"
|
||||
msgstr "Новости и Политика"
|
||||
|
||||
#: src/components/Pagination.vue:19 src/components/Pagination.vue:20
|
||||
msgid "Next page"
|
||||
|
@ -421,9 +405,8 @@ msgid "No"
|
|||
msgstr "Нет"
|
||||
|
||||
#: src/views/Search.vue:36 src/views/Search.vue:37 src/views/Search.vue:40
|
||||
#, fuzzy
|
||||
msgid "No results found."
|
||||
msgstr "Результатов не найдено"
|
||||
msgstr "Ничего не нашлось."
|
||||
|
||||
#: src/components/Filters.vue:330
|
||||
msgid "Occitan"
|
||||
|
@ -433,25 +416,23 @@ msgstr "Окситанский"
|
|||
#: src/components/PlaylistResult.vue:58 src/components/VideoResult.vue:43
|
||||
#: src/components/VideoResult.vue:44 src/components/VideoResult.vue:47
|
||||
msgid "On platform"
|
||||
msgstr ""
|
||||
msgstr "На сервере"
|
||||
|
||||
#: src/components/Filters.vue:163
|
||||
msgid "One of these tags"
|
||||
msgstr "Один из тегов"
|
||||
msgstr "Содержит хотя бы один из тегов"
|
||||
|
||||
#: src/components/Filters.vue:37
|
||||
msgid "Only channels"
|
||||
msgstr ""
|
||||
msgstr "Только каналы"
|
||||
|
||||
#: src/components/Filters.vue:42
|
||||
#, fuzzy
|
||||
msgid "Only playlists"
|
||||
msgstr "Создать плейлист"
|
||||
msgstr "Только плейлисты"
|
||||
|
||||
#: src/components/Filters.vue:32
|
||||
#, fuzzy
|
||||
msgid "Only videos"
|
||||
msgstr "Видео по запросу"
|
||||
msgstr "Только видео"
|
||||
|
||||
#: src/components/Filters.vue:175
|
||||
msgid "PeerTube instance"
|
||||
|
@ -475,13 +456,12 @@ msgstr "Предыдущая страница"
|
|||
|
||||
#: src/components/Filters.vue:312
|
||||
msgid "Public Domain Dedication"
|
||||
msgstr "Достояние общественности"
|
||||
msgstr "Общественное достояние"
|
||||
|
||||
#: src/components/VideoResult.vue:28 src/components/VideoResult.vue:29
|
||||
#: src/components/VideoResult.vue:32
|
||||
#, fuzzy
|
||||
msgid "Published by"
|
||||
msgstr "Дата публикации"
|
||||
msgstr "Опубликовано"
|
||||
|
||||
#: src/components/Filters.vue:65 src/components/Filters.vue:66
|
||||
msgid "Published date"
|
||||
|
@ -506,26 +486,24 @@ msgid "Science & Technology"
|
|||
msgstr "Наука & Технология"
|
||||
|
||||
#: src/views/Home.vue:15 src/views/Home.vue:16
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Search for your favorite videos, channels and playlists on <a "
|
||||
"class=\"peertube-link\" href=\"%{indexedInstancesUrl}\" "
|
||||
"target=\"_blank\">%{instancesCount} PeerTube websites</a> indexed by "
|
||||
"%{indexName}!"
|
||||
msgstr ""
|
||||
"Ищите ваши любимые видео, каналы и плейлисты на <a "
|
||||
"href=\"%{indexedInstancesUrl}\" target=\"_blank\">%{instancesCount} серверах "
|
||||
"PeerTube</a>, проиндексированных %{indexName}!"
|
||||
"Ищите ваши любимые видео, каналы и плейлисты на <a class=\"peertube-link\" "
|
||||
"href=\"%{indexedInstancesUrl}\" target=\"_blank\">серверах PeerTube</a>, "
|
||||
"проиндексированных %{indexName}!"
|
||||
|
||||
#: src/components/Footer.vue:13 src/components/Footer.vue:14
|
||||
#, fuzzy
|
||||
msgid "Search Index source code"
|
||||
msgstr "Исходный код"
|
||||
msgstr "Исходный код поисковика"
|
||||
|
||||
#: src/components/SearchInput.vue:13 src/components/SearchInput.vue:14
|
||||
#: src/components/SearchInput.vue:25 src/components/SearchInput.vue:35
|
||||
msgid "Search!"
|
||||
msgstr ""
|
||||
msgstr "Искать!"
|
||||
|
||||
#: src/components/Filters.vue:268
|
||||
msgid "Short (< 4 min)"
|
||||
|
@ -533,7 +511,7 @@ msgstr "Коротко (< 4 мин)"
|
|||
|
||||
#: src/components/SortButton.vue:3
|
||||
msgid "Sort by:"
|
||||
msgstr "Сортировать по:"
|
||||
msgstr "Упорядочить по:"
|
||||
|
||||
#: src/components/Filters.vue:287
|
||||
msgid "Sports"
|
||||
|
@ -553,7 +531,7 @@ msgstr "Сегодня"
|
|||
|
||||
#: src/views/Search.vue:10 src/views/Search.vue:9
|
||||
msgid "Toggle warning information"
|
||||
msgstr ""
|
||||
msgstr "Переключить предупреждение"
|
||||
|
||||
#: src/components/Filters.vue:288
|
||||
msgid "Travels"
|
||||
|
@ -569,7 +547,7 @@ msgstr "Видео по запросу"
|
|||
|
||||
#: src/components/PlaylistResult.vue:114
|
||||
msgid "Watch the playlist on %{host}"
|
||||
msgstr "Смотреть плейлист на %{host}"
|
||||
msgstr "Открыть плейлист на %{host}"
|
||||
|
||||
#: src/components/VideoResult.vue:123
|
||||
msgid "Watch the video on %{host}"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2023-10-10 06:12+0000\n"
|
||||
"PO-Revision-Date: 2023-11-15 01:52+0000\n"
|
||||
"Last-Translator: nexi <nexiphotographer@gmail.com>\n"
|
||||
"Language-Team: Serbian (cyrillic) <https://weblate.framasoft.org/projects/"
|
||||
"peertube-search-index/client/sr_Cyrl/>\n"
|
||||
|
@ -9,7 +9,7 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 5.0.1\n"
|
||||
"X-Generator: Weblate 5.1.1\n"
|
||||
|
||||
#: src/views/Search.vue:64
|
||||
#: src/views/Search.vue:65
|
||||
|
@ -182,7 +182,7 @@ msgstr "Комедија"
|
|||
#: src/components/PlaylistResult.vue:40
|
||||
#: src/components/PlaylistResult.vue:43
|
||||
msgid "Created by"
|
||||
msgstr "Направљено од стране"
|
||||
msgstr "Направио:"
|
||||
|
||||
#: src/components/Filters.vue:326
|
||||
msgid "Deutsch"
|
||||
|
@ -192,8 +192,8 @@ msgstr "немачки"
|
|||
#: src/components/Header.vue:26
|
||||
msgid "Developed by <a class=\"peertube-link\" href=\"https://framasoft.org\" target=\"_blank\">Framasoft</a>"
|
||||
msgstr ""
|
||||
"Развијено од стране <a class=\"peertube-link\" href=\"https://framasoft.org\""
|
||||
" target=\"_blank\">Framasoft</a>"
|
||||
"Развио: <a class=\"peertube-link\" href=\"https://framasoft.org\" target="
|
||||
"\"_blank\">Framasoft</a>"
|
||||
|
||||
#: src/components/ChannelResult.vue:83
|
||||
msgid "Discover this channel on %{host}"
|
||||
|
@ -504,7 +504,7 @@ msgstr "Посвета у јавном власништву"
|
|||
#: src/components/VideoResult.vue:29
|
||||
#: src/components/VideoResult.vue:32
|
||||
msgid "Published by"
|
||||
msgstr "Објављено од стране"
|
||||
msgstr "Објавио:"
|
||||
|
||||
#: src/components/Filters.vue:65
|
||||
#: src/components/Filters.vue:66
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import axios from 'axios'
|
||||
import { ResultList, VideoChannelsSearchQuery, VideoPlaylistsSearchQuery, VideosSearchQuery } from '@peertube/peertube-types'
|
||||
import { EnhancedVideoChannel } from '../../../server/types/channel.model'
|
||||
import { EnhancedPlaylist } from '../../../server/types/playlist.model'
|
||||
import { EnhancedVideo } from '../../../server/types/video.model'
|
||||
import { APIVideoChannel } from '../../../server/types/channel.model'
|
||||
import { APIPlaylist } from '../../../server/types/playlist.model'
|
||||
import { APIVideo } from '../../../server/types/video.model'
|
||||
import { buildApiUrl } from './utils'
|
||||
|
||||
const baseVideosPath = '/api/v1/search/videos'
|
||||
|
@ -18,7 +18,7 @@ function searchVideos (options: VideosSearchQuery) {
|
|||
}
|
||||
}
|
||||
|
||||
return axios.get<ResultList<EnhancedVideo>>(buildApiUrl(baseVideosPath), axiosOptions)
|
||||
return axios.get<ResultList<APIVideo>>(buildApiUrl(baseVideosPath), axiosOptions)
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ function searchVideoChannels (options: VideoChannelsSearchQuery) {
|
|||
}
|
||||
}
|
||||
|
||||
return axios.get<ResultList<EnhancedVideoChannel>>(buildApiUrl(baseVideoChannelsPath), axiosOptions)
|
||||
return axios.get<ResultList<APIVideoChannel>>(buildApiUrl(baseVideoChannelsPath), axiosOptions)
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@ function searchVideoPlaylists (options: VideoPlaylistsSearchQuery) {
|
|||
}
|
||||
}
|
||||
|
||||
return axios.get<ResultList<EnhancedPlaylist>>(buildApiUrl(baseVideoPlaylistsPath), axiosOptions)
|
||||
return axios.get<ResultList<APIPlaylist>>(buildApiUrl(baseVideoPlaylistsPath), axiosOptions)
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -133,14 +133,14 @@
|
|||
import { getConfig } from '../shared/config'
|
||||
import { pageToAPIParams, durationRangeToAPIParams, publishedDateRangeToAPIParams, extractTagsFromQuery, extractQueryToIntArray, extractQueryToStringArray, extractQueryToInt, extractQueryToBoolean } from '../shared/utils'
|
||||
import { SearchUrl } from '../models'
|
||||
import { EnhancedVideo } from '../../../server/types/video.model'
|
||||
import { EnhancedVideoChannel } from '../../../server/types/channel.model'
|
||||
import { APIVideo } from '../../../server/types/video.model'
|
||||
import { APIVideoChannel } from '../../../server/types/channel.model'
|
||||
import Pagination from '../components/Pagination.vue'
|
||||
import SearchInput from '../components/SearchInput.vue'
|
||||
import SortButton from '../components/SortButton.vue'
|
||||
import { VideoChannelsSearchQuery, ResultList, VideosSearchQuery } from '@peertube/peertube-types'
|
||||
import type { VideoChannelsSearchQuery, ResultList, VideosSearchQuery } from '@peertube/peertube-types'
|
||||
import Nprogress from 'nprogress'
|
||||
import { EnhancedPlaylist } from '../../../server/types/playlist.model'
|
||||
import { APIPlaylist } from '../../../server/types/playlist.model'
|
||||
import { PlaylistsSearchQuery } from '../../../server/types/search-query/playlist-search.model'
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -166,13 +166,13 @@
|
|||
totalResults: null as number,
|
||||
|
||||
totalVideos: null as number,
|
||||
videos: null as EnhancedVideo[],
|
||||
videos: null as APIVideo[],
|
||||
|
||||
totalChannels: null as number,
|
||||
channels: null as EnhancedVideoChannel[],
|
||||
channels: null as APIVideoChannel[],
|
||||
|
||||
totalPlaylists: null as number,
|
||||
playlists: null as EnhancedPlaylist[],
|
||||
playlists: null as APIPlaylist[],
|
||||
|
||||
currentPage: 1,
|
||||
pages: [],
|
||||
|
@ -295,8 +295,8 @@
|
|||
if (query.languageOneOf) count++
|
||||
if (query.isLive) count++
|
||||
if (query.resultType) count++
|
||||
if (Array.isArray(query.tagsAllOf) && query.tagsAllOf.length !== 0) count++
|
||||
if (Array.isArray(query.tagsOneOf) && query.tagsOneOf.length !== 0) count++
|
||||
if (extractQueryToStringArray(query.tagsAllOf) && query.tagsAllOf.length !== 0) count++
|
||||
if (extractQueryToStringArray(query.tagsOneOf) && query.tagsOneOf.length !== 0) count++
|
||||
|
||||
return count
|
||||
},
|
||||
|
@ -385,7 +385,7 @@
|
|||
} as PlaylistsSearchQuery
|
||||
},
|
||||
|
||||
searchVideos (): Promise<ResultList<EnhancedVideo>> {
|
||||
searchVideos (): Promise<ResultList<APIVideo>> {
|
||||
if (this.isVideoSearchDisabled()) {
|
||||
return Promise.resolve({ total: 0, data: [] })
|
||||
}
|
||||
|
@ -395,7 +395,7 @@
|
|||
return searchVideos(query)
|
||||
},
|
||||
|
||||
searchChannels (): Promise<ResultList<EnhancedVideoChannel>> {
|
||||
searchChannels (): Promise<ResultList<APIVideoChannel>> {
|
||||
if (this.isChannelSearchDisabled()) {
|
||||
return Promise.resolve({ data: [], total: 0 })
|
||||
}
|
||||
|
@ -405,7 +405,7 @@
|
|||
return searchVideoChannels(query)
|
||||
},
|
||||
|
||||
searchPlaylists (): Promise<ResultList<EnhancedPlaylist>> {
|
||||
searchPlaylists (): Promise<ResultList<APIPlaylist>> {
|
||||
if (this.isPlaylistSearchDisabled()) {
|
||||
return Promise.resolve({ data: [], total: 0 })
|
||||
}
|
||||
|
|
|
@ -7,22 +7,20 @@ webserver:
|
|||
hostname: 'localhost'
|
||||
port: 3234
|
||||
|
||||
elastic-search:
|
||||
# https or http
|
||||
http: 'http'
|
||||
auth:
|
||||
username: null
|
||||
password: null
|
||||
ssl:
|
||||
# Specificy a custom CA
|
||||
ca: null
|
||||
hostname: 'localhost'
|
||||
port: 9200
|
||||
meilisearch:
|
||||
host: 'http://127.0.0.1:7700'
|
||||
|
||||
api_key: null
|
||||
|
||||
indexes:
|
||||
videos: 'peertube-index-videos'
|
||||
channels: 'peertube-index-channels'
|
||||
playlists: 'peertube-index-playlists'
|
||||
|
||||
# Avoid updating index settings at startup
|
||||
# Set to true when a new release of the search index updates index settings
|
||||
force_settings_update_at_startup: false
|
||||
|
||||
log:
|
||||
level: 'debug' # debug/info/warning/error
|
||||
|
||||
|
@ -54,91 +52,16 @@ instances-index:
|
|||
enabled: false
|
||||
hosts: null
|
||||
|
||||
videos-search:
|
||||
# Allow client to send browser language to boost results score that are in these languages
|
||||
boost-languages:
|
||||
enabled: true
|
||||
|
||||
# Add ability to change videos search fields boost and match value
|
||||
# See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html for more information
|
||||
#
|
||||
# If boost == 0, the field will not be part of the search
|
||||
#
|
||||
# match_type could be 'default' or 'phrase'
|
||||
# * default: use default Elastic Search match query, including fuzziness
|
||||
# * phrase: use Elastic Search phrase match query
|
||||
search-fields:
|
||||
uuid:
|
||||
boost: 100
|
||||
match_type: 'default'
|
||||
short-uuid:
|
||||
boost: 100
|
||||
match_type: 'default'
|
||||
name:
|
||||
boost: 5
|
||||
match_type: 'default'
|
||||
description:
|
||||
boost: 1
|
||||
match_type: 'phrase'
|
||||
tags:
|
||||
boost: 2
|
||||
match_type: 'default'
|
||||
account-display-name:
|
||||
boost: 2
|
||||
match_type: 'default'
|
||||
channel-display-name:
|
||||
boost: 2
|
||||
match_type: 'default'
|
||||
|
||||
channels-search:
|
||||
# Add ability to change channels search fields boost and match value
|
||||
# See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html for more information
|
||||
#
|
||||
# If boost == 0, the field will not be part of the search
|
||||
#
|
||||
# match_type could be 'default' or 'phrase'
|
||||
# * default: use default Elastic Search match query, including fuzziness
|
||||
# * phrase: use Elastic Search phrase match query
|
||||
search-fields:
|
||||
name:
|
||||
boost: 5
|
||||
match_type: 'default'
|
||||
description:
|
||||
boost: 1
|
||||
match_type: 'phrase'
|
||||
display-name:
|
||||
boost: 3
|
||||
match_type: 'default'
|
||||
account-display-name:
|
||||
boost: 2
|
||||
match_type: 'default'
|
||||
|
||||
playlists-search:
|
||||
# Add ability to change playlists search fields boost and match value
|
||||
# See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html for more information
|
||||
#
|
||||
# If boost == 0, the field will not be part of the search
|
||||
#
|
||||
# match_type could be 'default' or 'phrase'
|
||||
# * default: use default Elastic Search match query, including fuzziness
|
||||
# * phrase: use Elastic Search phrase match query
|
||||
search-fields:
|
||||
uuid:
|
||||
boost: 100
|
||||
match_type: 'default'
|
||||
short-uuid:
|
||||
boost: 100
|
||||
match_type: 'default'
|
||||
display-name:
|
||||
boost: 5
|
||||
match_type: 'default'
|
||||
description:
|
||||
boost: 1
|
||||
match_type: 'phrase'
|
||||
|
||||
api:
|
||||
# Blacklist hosts that will not be returned by the search API
|
||||
blacklist:
|
||||
enabled: false
|
||||
# Array of hosts
|
||||
hosts: null
|
||||
|
||||
indexer:
|
||||
# How many hosts in parallel to index
|
||||
host_concurrency: 10
|
||||
|
||||
# How much time to wait before bulk indexing in Meilisearch data
|
||||
bulk_indexation_interval_ms: 10000
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
elastic-search:
|
||||
meilisearch:
|
||||
# You can put this key as master key when starting meilisearch
|
||||
api_key: '4kMeZtP0QsgE3QCDSEMYUt_WFusGjq5JgOc9atujpKw'
|
||||
|
||||
indexes:
|
||||
videos: 'peertube-index-videos-test1'
|
||||
channels: 'peertube-index-channels-test1'
|
||||
playlists: 'peertube-index-playlists-test1'
|
||||
videos: 'peertube-index-videos-test2'
|
||||
channels: 'peertube-index-channels-test2'
|
||||
playlists: 'peertube-index-playlists-test2'
|
||||
|
||||
force_settings_update_at_startup: true
|
||||
|
||||
search-instance:
|
||||
name_image: '/theme/framasoft/img/title.svg'
|
||||
|
@ -25,9 +30,9 @@ instances-index:
|
|||
- 'thinkerview.video'
|
||||
- 'replay.jres.org'
|
||||
- 'tube.nah.re'
|
||||
- 'peertube.parleur.net'
|
||||
- 'video.passageenseine.fr'
|
||||
- 'exode.me'
|
||||
- 'peertube.luga.at'
|
||||
- 'peertube.ch'
|
||||
|
||||
api:
|
||||
blacklist:
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
"i18n:update": "cd client && git fetch weblate && git merge weblate/master && npm run gettext:extract && npm run gettext:compile"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "^8.2.1",
|
||||
"async": "^3.2.3",
|
||||
"bluebird": "^3.5.3",
|
||||
"body-parser": "^1.20.0",
|
||||
|
@ -33,6 +32,7 @@
|
|||
"fs-extra": "^11.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.15",
|
||||
"meilisearch": "^0.35.0",
|
||||
"mkdirp": "^1.0.4",
|
||||
"morgan": "^1.9.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
|
@ -59,7 +59,6 @@
|
|||
"@types/node": "^18.11.15",
|
||||
"@types/pino": "^7.0.5",
|
||||
"@types/request": "^2.48.8",
|
||||
"@types/sequelize": "^4.28.13",
|
||||
"@types/validator": "^13.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||
"@typescript-eslint/parser": "^5.27.0",
|
||||
|
|
|
@ -149,15 +149,15 @@ app.use(function (err, req, res, next) {
|
|||
app.listen(CONFIG.LISTEN.PORT, async () => {
|
||||
logger.info('Server listening on port %d', CONFIG.LISTEN.PORT)
|
||||
|
||||
IndexationScheduler.Instance.enable()
|
||||
|
||||
try {
|
||||
await IndexationScheduler.Instance.initIndexes()
|
||||
} catch (err) {
|
||||
logger.error(err, 'Cannot init videos index.')
|
||||
logger.error(err, 'Cannot init indexes.')
|
||||
process.exit(-1)
|
||||
}
|
||||
|
||||
IndexationScheduler.Instance.enable()
|
||||
|
||||
IndexationScheduler.Instance.execute()
|
||||
.catch(err => logger.error(err, 'Cannot run video indexer'))
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import express from 'express'
|
||||
import { Searcher } from '../../lib/controllers/searcher'
|
||||
import { formatChannelForAPI, queryChannels } from '../../lib/elastic-search/elastic-search-channels'
|
||||
import { formatChannelForAPI, queryChannels } from '../../lib/meilisearch/meilisearch-channels'
|
||||
import { asyncMiddleware } from '../../middlewares/async'
|
||||
import { setDefaultPagination } from '../../middlewares/pagination'
|
||||
import { setDefaultSearchSort } from '../../middlewares/sort'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import express from 'express'
|
||||
import { Searcher } from '../../lib/controllers/searcher'
|
||||
import { formatPlaylistForAPI, queryPlaylists } from '../../lib/elastic-search/elastic-search-playlists'
|
||||
import { formatPlaylistForAPI, queryPlaylists } from '../../lib/meilisearch/meilisearch-playlists'
|
||||
import { asyncMiddleware } from '../../middlewares/async'
|
||||
import { setDefaultPagination } from '../../middlewares/pagination'
|
||||
import { setDefaultSearchSort } from '../../middlewares/sort'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import express from 'express'
|
||||
import { Searcher } from '../../lib/controllers/searcher'
|
||||
import { formatVideoForAPI, queryVideos } from '../../lib/elastic-search/elastic-search-videos'
|
||||
import { formatVideoForAPI, queryVideos } from '../../lib/meilisearch/meilisearch-videos'
|
||||
import { asyncMiddleware } from '../../middlewares/async'
|
||||
import { setDefaultPagination } from '../../middlewares/pagination'
|
||||
import { setDefaultSearchSort } from '../../middlewares/sort'
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import { readFileSync } from 'fs-extra'
|
||||
import { Client } from '@elastic/elasticsearch'
|
||||
import { CONFIG } from '../initializers/constants'
|
||||
|
||||
const elasticOptions = {
|
||||
node: CONFIG.ELASTIC_SEARCH.HTTP + '://' + CONFIG.ELASTIC_SEARCH.HOSTNAME + ':' + CONFIG.ELASTIC_SEARCH.PORT
|
||||
}
|
||||
|
||||
if (CONFIG.ELASTIC_SEARCH.SSL.CA) {
|
||||
Object.assign(elasticOptions, {
|
||||
tls: {
|
||||
ca: readFileSync(CONFIG.ELASTIC_SEARCH.SSL.CA)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (CONFIG.ELASTIC_SEARCH.AUTH.USERNAME) {
|
||||
Object.assign(elasticOptions, {
|
||||
auth: {
|
||||
username: CONFIG.ELASTIC_SEARCH.AUTH.USERNAME,
|
||||
password: CONFIG.ELASTIC_SEARCH.AUTH.PASSWORD
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const elasticSearch = new Client(elasticOptions)
|
||||
|
||||
export {
|
||||
elasticSearch
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { MeiliSearch } from 'meilisearch'
|
||||
import { CONFIG } from '../initializers/constants'
|
||||
|
||||
const client = new MeiliSearch({
|
||||
host: CONFIG.MEILISEARCH.HOST,
|
||||
apiKey: CONFIG.MEILISEARCH.API_KEY
|
||||
})
|
||||
|
||||
export { client }
|
|
@ -12,22 +12,15 @@ const CONFIG = {
|
|||
HOSTNAME: config.get<string>('webserver.hostname'),
|
||||
PORT: config.get<number>('webserver.port')
|
||||
},
|
||||
ELASTIC_SEARCH: {
|
||||
HTTP: config.get<string>('elastic-search.http'),
|
||||
AUTH: {
|
||||
USERNAME: config.get<string>('elastic-search.auth.username'),
|
||||
PASSWORD: config.get<string>('elastic-search.auth.password')
|
||||
},
|
||||
SSL: {
|
||||
CA: config.get<string>('elastic-search.ssl.ca')
|
||||
},
|
||||
HOSTNAME: config.get<string>('elastic-search.hostname'),
|
||||
PORT: config.get<number>('elastic-search.port'),
|
||||
MEILISEARCH: {
|
||||
HOST: config.get<string>('meilisearch.host'),
|
||||
API_KEY: config.get<string>('meilisearch.api_key'),
|
||||
INDEXES: {
|
||||
VIDEOS: config.get<string>('elastic-search.indexes.videos'),
|
||||
CHANNELS: config.get<string>('elastic-search.indexes.channels'),
|
||||
PLAYLISTS: config.get<string>('elastic-search.indexes.playlists')
|
||||
}
|
||||
VIDEOS: config.get<string>('meilisearch.indexes.videos'),
|
||||
CHANNELS: config.get<string>('meilisearch.indexes.channels'),
|
||||
PLAYLISTS: config.get<string>('meilisearch.indexes.playlists')
|
||||
},
|
||||
FORCE_SETTINGS_UPDATE_AT_STARTUP: config.get<boolean>('meilisearch.force_settings_update_at_startup')
|
||||
},
|
||||
LOG: {
|
||||
LEVEL: config.get<string>('log.level')
|
||||
|
@ -40,96 +33,6 @@ const CONFIG = {
|
|||
LEGAL_NOTICES_URL: config.get<string>('search-instance.legal_notices_url'),
|
||||
THEME: config.get<string>('search-instance.theme')
|
||||
},
|
||||
VIDEOS_SEARCH: {
|
||||
BOOST_LANGUAGES: {
|
||||
ENABLED: config.get<boolean>('videos-search.boost-languages.enabled')
|
||||
},
|
||||
SEARCH_FIELDS: {
|
||||
UUID: {
|
||||
FIELD_NAME: 'uuid',
|
||||
BOOST: config.get<number>('videos-search.search-fields.uuid.boost'),
|
||||
MATCH_TYPE: config.get<string>('videos-search.search-fields.uuid.match_type')
|
||||
},
|
||||
SHORT_UUID: {
|
||||
FIELD_NAME: 'shortUUID',
|
||||
BOOST: config.get<number>('videos-search.search-fields.short-uuid.boost'),
|
||||
MATCH_TYPE: config.get<string>('videos-search.search-fields.short-uuid.match_type')
|
||||
},
|
||||
NAME: {
|
||||
FIELD_NAME: 'name',
|
||||
BOOST: config.get<number>('videos-search.search-fields.name.boost'),
|
||||
MATCH_TYPE: config.get<string>('videos-search.search-fields.name.match_type')
|
||||
},
|
||||
DESCRIPTION: {
|
||||
FIELD_NAME: 'description',
|
||||
BOOST: config.get<number>('videos-search.search-fields.description.boost'),
|
||||
MATCH_TYPE: config.get<string>('videos-search.search-fields.description.match_type')
|
||||
},
|
||||
TAGS: {
|
||||
FIELD_NAME: 'tags',
|
||||
BOOST: config.get<number>('videos-search.search-fields.tags.boost'),
|
||||
MATCH_TYPE: config.get<string>('videos-search.search-fields.tags.match_type')
|
||||
},
|
||||
ACCOUNT_DISPLAY_NAME: {
|
||||
FIELD_NAME: 'account.displayName',
|
||||
BOOST: config.get<number>('videos-search.search-fields.account-display-name.boost'),
|
||||
MATCH_TYPE: config.get<string>('videos-search.search-fields.account-display-name.match_type')
|
||||
},
|
||||
CHANNEL_DISPLAY_NAME: {
|
||||
FIELD_NAME: 'channel.displayName',
|
||||
BOOST: config.get<number>('videos-search.search-fields.channel-display-name.boost'),
|
||||
MATCH_TYPE: config.get<string>('videos-search.search-fields.channel-display-name.match_type')
|
||||
}
|
||||
}
|
||||
},
|
||||
CHANNELS_SEARCH: {
|
||||
SEARCH_FIELDS: {
|
||||
NAME: {
|
||||
FIELD_NAME: 'name',
|
||||
BOOST: config.get<number>('channels-search.search-fields.name.boost'),
|
||||
MATCH_TYPE: config.get<string>('channels-search.search-fields.name.match_type')
|
||||
},
|
||||
DESCRIPTION: {
|
||||
FIELD_NAME: 'description',
|
||||
BOOST: config.get<number>('channels-search.search-fields.description.boost'),
|
||||
MATCH_TYPE: config.get<string>('channels-search.search-fields.description.match_type')
|
||||
},
|
||||
DISPLAY_NAME: {
|
||||
FIELD_NAME: 'displayName',
|
||||
BOOST: config.get<number>('channels-search.search-fields.display-name.boost'),
|
||||
MATCH_TYPE: config.get<string>('channels-search.search-fields.display-name.match_type')
|
||||
},
|
||||
ACCOUNT_DISPLAY_NAME: {
|
||||
FIELD_NAME: 'ownerAccount.displayName',
|
||||
BOOST: config.get<number>('channels-search.search-fields.account-display-name.boost'),
|
||||
MATCH_TYPE: config.get<string>('channels-search.search-fields.account-display-name.match_type')
|
||||
}
|
||||
}
|
||||
},
|
||||
PLAYLISTS_SEARCH: {
|
||||
SEARCH_FIELDS: {
|
||||
UUID: {
|
||||
FIELD_NAME: 'uuid',
|
||||
BOOST: config.get<number>('playlists-search.search-fields.uuid.boost'),
|
||||
MATCH_TYPE: config.get<string>('playlists-search.search-fields.uuid.match_type')
|
||||
},
|
||||
SHORT_UUID: {
|
||||
FIELD_NAME: 'shortUUID',
|
||||
BOOST: config.get<number>('playlists-search.search-fields.short-uuid.boost'),
|
||||
MATCH_TYPE: config.get<string>('playlists-search.search-fields.short-uuid.match_type')
|
||||
},
|
||||
DISPLAY_NAME: {
|
||||
FIELD_NAME: 'displayName',
|
||||
BOOST: config.get<number>('playlists-search.search-fields.display-name.boost'),
|
||||
MATCH_TYPE: config.get<string>('playlists-search.search-fields.display-name.match_type')
|
||||
},
|
||||
DESCRIPTION: {
|
||||
FIELD_NAME: 'description',
|
||||
BOOST: config.get<number>('playlists-search.search-fields.description.boost'),
|
||||
MATCH_TYPE: config.get<string>('playlists-search.search-fields.description.match_type')
|
||||
}
|
||||
}
|
||||
},
|
||||
INSTANCES_INDEX: {
|
||||
URL: config.get<string>('instances-index.url'),
|
||||
PUBLIC_URL: config.get<string>('instances-index.public_url'),
|
||||
|
@ -143,13 +46,17 @@ const CONFIG = {
|
|||
ENABLED: config.get<boolean>('api.blacklist.enabled'),
|
||||
HOSTS: config.get<string[]>('api.blacklist.hosts')
|
||||
}
|
||||
},
|
||||
INDEXER: {
|
||||
HOST_CONCURRENCY: config.get<number>('indexer.host_concurrency'),
|
||||
BULK_INDEXATION_INTERVAL_MS: config.get<number>('indexer.bulk_indexation_interval_ms')
|
||||
}
|
||||
}
|
||||
|
||||
const SORTABLE_COLUMNS = {
|
||||
VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ],
|
||||
CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
|
||||
PLAYLISTS_SEARCH: [ 'match', 'displayName', 'createdAt' ]
|
||||
VIDEOS_SEARCH: [ '_rankingScore', 'match', 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes' ],
|
||||
CHANNELS_SEARCH: [ '_rankingScore', 'match', 'displayName', 'createdAt' ],
|
||||
PLAYLISTS_SEARCH: [ '_rankingScore', 'match', 'displayName', 'createdAt' ]
|
||||
}
|
||||
|
||||
const PAGINATION_START = {
|
||||
|
@ -165,10 +72,9 @@ const SCHEDULER_INTERVALS_MS = {
|
|||
indexation: 60000 * 60 * 24 // 24 hours
|
||||
}
|
||||
|
||||
const INDEXER_COUNT = 10
|
||||
const INDEXER_COUNT = 20
|
||||
const INDEXER_LIMIT = 500000
|
||||
|
||||
const INDEXER_HOST_CONCURRENCY = 3
|
||||
const INDEXER_QUEUE_CONCURRENCY = 3
|
||||
|
||||
const REQUESTS = {
|
||||
|
@ -176,17 +82,6 @@ const REQUESTS = {
|
|||
WAIT: 10000 // 10 seconds
|
||||
}
|
||||
|
||||
const ELASTIC_SEARCH_QUERY = {
|
||||
FUZZINESS: 'AUTO:4,7',
|
||||
OPERATOR: 'OR',
|
||||
MINIMUM_SHOULD_MATCH: '3<75%',
|
||||
BOOST_LANGUAGE_VALUE: 1,
|
||||
MALUS_LANGUAGE_VALUE: 0.5,
|
||||
VIDEOS_MULTI_MATCH_FIELDS: buildMatchFieldConfig(CONFIG.VIDEOS_SEARCH.SEARCH_FIELDS),
|
||||
CHANNELS_MULTI_MATCH_FIELDS: buildMatchFieldConfig(CONFIG.CHANNELS_SEARCH.SEARCH_FIELDS),
|
||||
PLAYLISTS_MULTI_MATCH_FIELDS: buildMatchFieldConfig(CONFIG.PLAYLISTS_SEARCH.SEARCH_FIELDS)
|
||||
}
|
||||
|
||||
function getWebserverUrl () {
|
||||
if (CONFIG.WEBSERVER.PORT === 80 || CONFIG.WEBSERVER.PORT === 443) {
|
||||
return CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME
|
||||
|
@ -195,28 +90,6 @@ function getWebserverUrl () {
|
|||
return CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
|
||||
}
|
||||
|
||||
function buildMatchFieldConfig (fields: { [name: string]: { BOOST: number, FIELD_NAME: string, MATCH_TYPE: string } }) {
|
||||
const selectFields = (matchType: 'phrase' | 'default') => {
|
||||
return Object.keys(fields)
|
||||
.filter(fieldName => fields[fieldName].MATCH_TYPE === matchType)
|
||||
.map(fieldName => fields[fieldName])
|
||||
}
|
||||
|
||||
const buildMultiMatch = (fields: { BOOST: number, FIELD_NAME: string }[]) => {
|
||||
return fields.map(fieldObj => {
|
||||
if (fieldObj.BOOST <= 0) return ''
|
||||
|
||||
return `${fieldObj.FIELD_NAME}^${fieldObj.BOOST}`
|
||||
})
|
||||
.filter(v => !!v)
|
||||
}
|
||||
|
||||
return {
|
||||
default: buildMultiMatch(selectFields('default')),
|
||||
phrase: buildMultiMatch(selectFields('phrase'))
|
||||
}
|
||||
}
|
||||
|
||||
if (isTestInstance()) {
|
||||
SCHEDULER_INTERVALS_MS.indexation = 1000 * 60 * 5 // 5 minutes
|
||||
}
|
||||
|
@ -231,9 +104,7 @@ export {
|
|||
SORTABLE_COLUMNS,
|
||||
INDEXER_QUEUE_CONCURRENCY,
|
||||
SCHEDULER_INTERVALS_MS,
|
||||
INDEXER_HOST_CONCURRENCY,
|
||||
INDEXER_COUNT,
|
||||
INDEXER_LIMIT,
|
||||
REQUESTS,
|
||||
ELASTIC_SEARCH_QUERY
|
||||
REQUESTS
|
||||
}
|
||||
|
|
|
@ -1,209 +0,0 @@
|
|||
import { MappingProperty, PropertyName } from '@elastic/elasticsearch/lib/api/types'
|
||||
import { elasticSearch } from '../../helpers/elastic-search'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { CONFIG, ELASTIC_SEARCH_QUERY } from '../../initializers/constants'
|
||||
import { DBChannel, EnhancedVideoChannel, IndexableChannel } from '../../types/channel.model'
|
||||
import { ChannelsSearchQuery } from '../../types/search-query/channel-search.model'
|
||||
import { buildSort, extractSearchQueryResult } from './elastic-search-queries'
|
||||
import { buildChannelOrAccountCommonMapping, buildMultiMatchBool } from './shared'
|
||||
import {
|
||||
formatActorImageForAPI,
|
||||
formatActorImageForDB,
|
||||
formatActorImagesForAPI,
|
||||
formatActorImagesForDB
|
||||
} from './shared/elastic-search-avatar'
|
||||
|
||||
async function queryChannels (search: ChannelsSearchQuery) {
|
||||
const bool: any = {}
|
||||
const mustNot: any[] = []
|
||||
const filter: any[] = []
|
||||
|
||||
if (search.search) {
|
||||
Object.assign(bool, buildMultiMatchBool(search.search, ELASTIC_SEARCH_QUERY.CHANNELS_MULTI_MATCH_FIELDS))
|
||||
}
|
||||
|
||||
if (search.blockedAccounts) {
|
||||
mustNot.push({
|
||||
terms: {
|
||||
'ownerAccount.handle': search.blockedAccounts
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.blockedHosts) {
|
||||
mustNot.push({
|
||||
terms: {
|
||||
host: search.blockedHosts
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
mustNot.push({
|
||||
term: {
|
||||
videosCount: 0
|
||||
}
|
||||
})
|
||||
|
||||
if (search.host) {
|
||||
filter.push({
|
||||
term: {
|
||||
host: search.host
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.handles) {
|
||||
filter.push({
|
||||
terms: {
|
||||
handle: search.handles
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (filter.length !== 0) {
|
||||
Object.assign(bool, { filter })
|
||||
}
|
||||
|
||||
if (mustNot.length !== 0) {
|
||||
Object.assign(bool, { must_not: mustNot })
|
||||
}
|
||||
|
||||
const body = {
|
||||
from: search.start,
|
||||
size: search.count,
|
||||
sort: buildSort(search.sort),
|
||||
query: { bool }
|
||||
}
|
||||
|
||||
logger.debug({ body }, 'Will query Elastic Search for channels.')
|
||||
|
||||
const res = await elasticSearch.search({
|
||||
index: CONFIG.ELASTIC_SEARCH.INDEXES.CHANNELS,
|
||||
body
|
||||
})
|
||||
|
||||
return extractSearchQueryResult(res)
|
||||
}
|
||||
|
||||
function formatChannelForAPI (c: DBChannel, fromHost?: string): EnhancedVideoChannel {
|
||||
return {
|
||||
id: c.id,
|
||||
|
||||
score: c.score,
|
||||
|
||||
url: c.url,
|
||||
name: c.name,
|
||||
host: c.host,
|
||||
followingCount: c.followingCount,
|
||||
followersCount: c.followersCount,
|
||||
createdAt: c.createdAt,
|
||||
updatedAt: c.updatedAt,
|
||||
|
||||
avatar: formatActorImageForAPI(c.avatar),
|
||||
avatars: formatActorImagesForAPI(c.avatars, c.avatar),
|
||||
|
||||
banner: formatActorImageForAPI(c.banner),
|
||||
banners: formatActorImagesForAPI(c.banners, c.banner),
|
||||
|
||||
displayName: c.displayName,
|
||||
description: c.description,
|
||||
support: c.support,
|
||||
isLocal: fromHost === c.host,
|
||||
|
||||
videosCount: c.videosCount || 0,
|
||||
|
||||
ownerAccount: {
|
||||
id: c.ownerAccount.id,
|
||||
url: c.ownerAccount.url,
|
||||
|
||||
displayName: c.ownerAccount.displayName,
|
||||
description: c.ownerAccount.description,
|
||||
name: c.ownerAccount.name,
|
||||
host: c.ownerAccount.host,
|
||||
followingCount: c.ownerAccount.followingCount,
|
||||
followersCount: c.ownerAccount.followersCount,
|
||||
createdAt: c.ownerAccount.createdAt,
|
||||
updatedAt: c.ownerAccount.updatedAt,
|
||||
|
||||
avatar: formatActorImageForAPI(c.ownerAccount.avatar),
|
||||
avatars: formatActorImagesForAPI(c.ownerAccount.avatars, c.ownerAccount.avatar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatChannelForDB (c: IndexableChannel): DBChannel {
|
||||
return {
|
||||
id: c.id,
|
||||
|
||||
name: c.name,
|
||||
host: c.host,
|
||||
url: c.url,
|
||||
|
||||
avatar: formatActorImageForDB(c.avatar, c.host),
|
||||
avatars: formatActorImagesForDB(c.avatars, c.host),
|
||||
|
||||
banner: formatActorImageForDB(c.banner, c.host),
|
||||
banners: formatActorImagesForDB(c.banners, c.host),
|
||||
|
||||
displayName: c.displayName,
|
||||
|
||||
indexedAt: new Date(),
|
||||
|
||||
followingCount: c.followingCount,
|
||||
followersCount: c.followersCount,
|
||||
createdAt: c.createdAt,
|
||||
updatedAt: c.updatedAt,
|
||||
|
||||
description: c.description,
|
||||
support: c.support,
|
||||
videosCount: c.videosCount,
|
||||
|
||||
handle: `${c.name}@${c.host}`,
|
||||
|
||||
ownerAccount: {
|
||||
id: c.ownerAccount.id,
|
||||
url: c.ownerAccount.url,
|
||||
|
||||
displayName: c.ownerAccount.displayName,
|
||||
description: c.ownerAccount.description,
|
||||
name: c.ownerAccount.name,
|
||||
host: c.ownerAccount.host,
|
||||
followingCount: c.ownerAccount.followingCount,
|
||||
followersCount: c.ownerAccount.followersCount,
|
||||
createdAt: c.ownerAccount.createdAt,
|
||||
updatedAt: c.ownerAccount.updatedAt,
|
||||
|
||||
handle: `${c.ownerAccount.name}@${c.ownerAccount.host}`,
|
||||
|
||||
avatar: formatActorImageForDB(c.ownerAccount.avatar, c.ownerAccount.host),
|
||||
avatars: formatActorImagesForDB(c.ownerAccount.avatars, c.ownerAccount.host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildChannelsMapping () {
|
||||
const base = buildChannelOrAccountCommonMapping()
|
||||
|
||||
Object.assign(base, {
|
||||
videosCount: {
|
||||
type: 'long'
|
||||
},
|
||||
|
||||
support: {
|
||||
type: 'keyword'
|
||||
},
|
||||
|
||||
ownerAccount: {
|
||||
properties: buildChannelOrAccountCommonMapping()
|
||||
}
|
||||
} as Record<PropertyName, MappingProperty>)
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
export {
|
||||
buildChannelsMapping,
|
||||
formatChannelForDB,
|
||||
formatChannelForAPI,
|
||||
queryChannels
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
import { flatMap } from 'lodash'
|
||||
import { MappingProperty, PropertyName } from '@elastic/elasticsearch/lib/api/types'
|
||||
import { elasticSearch } from '../../helpers/elastic-search'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { IndexableDoc } from '../../types/indexable-doc.model'
|
||||
|
||||
function buildIndex (name: string, mapping: Record<PropertyName, MappingProperty>) {
|
||||
logger.info('Initialize %s Elastic Search index.', name)
|
||||
|
||||
return elasticSearch.indices.create({
|
||||
index: name,
|
||||
body: {
|
||||
settings: {
|
||||
number_of_shards: 1,
|
||||
number_of_replicas: 1
|
||||
},
|
||||
mappings: {
|
||||
properties: mapping
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
if (err.name === 'ResponseError' && err.meta?.body?.error.root_cause[0]?.type === 'resource_already_exists_exception') return
|
||||
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
async function indexDocuments <T extends IndexableDoc> (options: {
|
||||
objects: T[]
|
||||
formatter: (o: T) => any
|
||||
replace: boolean
|
||||
index: string
|
||||
}) {
|
||||
const { objects, formatter, replace, index } = options
|
||||
|
||||
const elIdIndex: { [elId: string]: T } = {}
|
||||
|
||||
for (const object of objects) {
|
||||
elIdIndex[object.elasticSearchId] = object
|
||||
}
|
||||
|
||||
const method = replace ? 'index' : 'update'
|
||||
|
||||
const body = flatMap(objects, v => {
|
||||
const doc = formatter(v)
|
||||
|
||||
const options = replace
|
||||
? doc
|
||||
: { doc, doc_as_upsert: true }
|
||||
|
||||
return [
|
||||
{
|
||||
[method]: {
|
||||
_id: v.elasticSearchId,
|
||||
_index: index
|
||||
}
|
||||
},
|
||||
options
|
||||
]
|
||||
})
|
||||
|
||||
const result = await elasticSearch.bulk({
|
||||
index,
|
||||
body
|
||||
})
|
||||
|
||||
if (result.errors === true) {
|
||||
const msg = 'Cannot insert data in elastic search.'
|
||||
logger.error({ err: result }, msg)
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
const created: T[] = result.items
|
||||
.map(i => i[method])
|
||||
.filter(i => i.result === 'created')
|
||||
.map(i => elIdIndex[i._id])
|
||||
|
||||
return { created }
|
||||
}
|
||||
|
||||
function refreshIndex (indexName: string) {
|
||||
logger.info('Refreshing %s index.', indexName)
|
||||
|
||||
return elasticSearch.indices.refresh({ index: indexName })
|
||||
}
|
||||
|
||||
export {
|
||||
buildIndex,
|
||||
indexDocuments,
|
||||
refreshIndex
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
import { AggregationsStringTermsAggregate } from '@elastic/elasticsearch/lib/api/types'
|
||||
import { elasticSearch } from '../../helpers/elastic-search'
|
||||
import { CONFIG } from '../../initializers/constants'
|
||||
import { getMajorInstanceVersion, listIndexInstancesHost } from '../requests/instances-index'
|
||||
import { extractBucketsFromAggregation } from './elastic-search-queries'
|
||||
import { logger } from '../../helpers/logger'
|
||||
|
||||
async function buildInstanceHosts () {
|
||||
let indexHosts = await listIndexInstancesHost()
|
||||
|
||||
if (CONFIG.INSTANCES_INDEX.WHITELIST.ENABLED) {
|
||||
const whitelistHosts = Array.isArray(CONFIG.INSTANCES_INDEX.WHITELIST.HOSTS)
|
||||
? CONFIG.INSTANCES_INDEX.WHITELIST.HOSTS
|
||||
: []
|
||||
|
||||
indexHosts = indexHosts.filter(h => whitelistHosts.includes(h))
|
||||
}
|
||||
|
||||
for (const indexHost of indexHosts) {
|
||||
const instanceVersion = await getMajorInstanceVersion(indexHost)
|
||||
|
||||
if (instanceVersion < 4) {
|
||||
logger.info(`Do not index ${indexHost} because the major version is too low (v${instanceVersion} < v4)`)
|
||||
|
||||
indexHosts = indexHosts.filter(h => h !== indexHost)
|
||||
}
|
||||
}
|
||||
|
||||
const dbHosts = await listDBInstances()
|
||||
const removedHosts = getRemovedHosts(dbHosts, indexHosts)
|
||||
|
||||
return { indexHosts, removedHosts }
|
||||
}
|
||||
|
||||
export {
|
||||
buildInstanceHosts
|
||||
}
|
||||
|
||||
// ##################################################
|
||||
|
||||
async function listDBInstances () {
|
||||
const setResult = new Set<string>()
|
||||
const indexes = [
|
||||
CONFIG.ELASTIC_SEARCH.INDEXES.VIDEOS,
|
||||
CONFIG.ELASTIC_SEARCH.INDEXES.CHANNELS
|
||||
]
|
||||
|
||||
for (const index of indexes) {
|
||||
const res = await elasticSearch.search<unknown, Record<'hosts', AggregationsStringTermsAggregate>>({
|
||||
index,
|
||||
body: {
|
||||
size: 0,
|
||||
aggs: {
|
||||
hosts: {
|
||||
terms: {
|
||||
size: 5000,
|
||||
field: 'host'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for (const b of extractBucketsFromAggregation<string>(res.aggregations.hosts.buckets)) {
|
||||
setResult.add(b.key)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(setResult)
|
||||
}
|
||||
|
||||
function getRemovedHosts (dbHosts: string[], indexHosts: string[]) {
|
||||
return dbHosts.filter(dbHost => indexHosts.includes(dbHost) === false)
|
||||
}
|
|
@ -1,234 +0,0 @@
|
|||
import { MappingProperty, PropertyName } from '@elastic/elasticsearch/lib/api/types'
|
||||
import { elasticSearch } from '../../helpers/elastic-search'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { buildUrl } from '../../helpers/utils'
|
||||
import { CONFIG, ELASTIC_SEARCH_QUERY } from '../../initializers/constants'
|
||||
import { DBPlaylist, EnhancedPlaylist, IndexablePlaylist } from '../../types/playlist.model'
|
||||
import { PlaylistsSearchQuery } from '../../types/search-query/playlist-search.model'
|
||||
import { buildSort, extractSearchQueryResult } from './elastic-search-queries'
|
||||
import { addUUIDFilters, buildMultiMatchBool } from './shared'
|
||||
import { buildChannelOrAccountSummaryMapping, formatActorForDB, formatActorSummaryForAPI } from './shared/elastic-search-actor'
|
||||
|
||||
async function queryPlaylists (search: PlaylistsSearchQuery) {
|
||||
const bool: any = {}
|
||||
const mustNot: any[] = []
|
||||
const filter: any[] = []
|
||||
|
||||
if (search.search) {
|
||||
Object.assign(bool, buildMultiMatchBool(search.search, ELASTIC_SEARCH_QUERY.PLAYLISTS_MULTI_MATCH_FIELDS))
|
||||
}
|
||||
|
||||
if (search.blockedAccounts) {
|
||||
mustNot.push({
|
||||
terms: {
|
||||
'ownerAccount.handle': search.blockedAccounts
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.blockedHosts) {
|
||||
mustNot.push({
|
||||
terms: {
|
||||
host: search.blockedHosts
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
mustNot.push({
|
||||
term: {
|
||||
videosLength: 0
|
||||
}
|
||||
})
|
||||
|
||||
if (search.host) {
|
||||
filter.push({
|
||||
term: {
|
||||
'ownerAccount.host': search.host
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.uuids) {
|
||||
addUUIDFilters(filter, search.uuids)
|
||||
}
|
||||
|
||||
if (filter.length !== 0) {
|
||||
Object.assign(bool, { filter })
|
||||
}
|
||||
|
||||
if (mustNot.length !== 0) {
|
||||
Object.assign(bool, { must_not: mustNot })
|
||||
}
|
||||
|
||||
const body = {
|
||||
from: search.start,
|
||||
size: search.count,
|
||||
sort: buildSort(search.sort),
|
||||
query: { bool }
|
||||
}
|
||||
|
||||
logger.debug({ body }, 'Will query Elastic Search for playlists.')
|
||||
|
||||
const res = await elasticSearch.search({
|
||||
index: CONFIG.ELASTIC_SEARCH.INDEXES.PLAYLISTS,
|
||||
body
|
||||
})
|
||||
|
||||
return extractSearchQueryResult(res)
|
||||
}
|
||||
|
||||
function formatPlaylistForAPI (p: DBPlaylist, fromHost?: string): EnhancedPlaylist {
|
||||
return {
|
||||
id: p.id,
|
||||
uuid: p.uuid,
|
||||
shortUUID: p.shortUUID,
|
||||
|
||||
score: p.score,
|
||||
|
||||
isLocal: fromHost === p.host,
|
||||
|
||||
url: p.url,
|
||||
|
||||
displayName: p.displayName,
|
||||
description: p.description,
|
||||
|
||||
privacy: {
|
||||
id: p.privacy.id,
|
||||
label: p.privacy.label
|
||||
},
|
||||
|
||||
videosLength: p.videosLength,
|
||||
|
||||
type: {
|
||||
id: p.type.id,
|
||||
label: p.type.label
|
||||
},
|
||||
|
||||
thumbnailPath: p.thumbnailPath,
|
||||
thumbnailUrl: buildUrl(p.host, p.thumbnailPath),
|
||||
|
||||
embedPath: p.embedPath,
|
||||
embedUrl: buildUrl(p.host, p.embedPath),
|
||||
|
||||
createdAt: p.createdAt,
|
||||
updatedAt: p.updatedAt,
|
||||
|
||||
ownerAccount: formatActorSummaryForAPI(p.ownerAccount),
|
||||
videoChannel: formatActorSummaryForAPI(p.videoChannel)
|
||||
}
|
||||
}
|
||||
|
||||
function formatPlaylistForDB (p: IndexablePlaylist): DBPlaylist {
|
||||
return {
|
||||
id: p.id,
|
||||
uuid: p.uuid,
|
||||
shortUUID: p.shortUUID,
|
||||
|
||||
indexedAt: new Date(),
|
||||
createdAt: p.createdAt,
|
||||
updatedAt: p.updatedAt,
|
||||
|
||||
host: p.host,
|
||||
url: p.url,
|
||||
|
||||
displayName: p.displayName,
|
||||
description: p.description,
|
||||
|
||||
thumbnailPath: p.thumbnailPath,
|
||||
embedPath: p.embedPath,
|
||||
|
||||
type: {
|
||||
id: p.type.id,
|
||||
label: p.type.label
|
||||
},
|
||||
privacy: {
|
||||
id: p.privacy.id,
|
||||
label: p.privacy.label
|
||||
},
|
||||
|
||||
videosLength: p.videosLength,
|
||||
|
||||
ownerAccount: formatActorForDB(p.ownerAccount),
|
||||
videoChannel: formatActorForDB(p.videoChannel)
|
||||
}
|
||||
}
|
||||
|
||||
function buildPlaylistsMapping () {
|
||||
return {
|
||||
id: {
|
||||
type: 'long'
|
||||
},
|
||||
|
||||
uuid: {
|
||||
type: 'keyword'
|
||||
},
|
||||
shortUUID: {
|
||||
type: 'keyword'
|
||||
},
|
||||
createdAt: {
|
||||
type: 'date',
|
||||
format: 'date_optional_time'
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'date',
|
||||
format: 'date_optional_time'
|
||||
},
|
||||
indexedAt: {
|
||||
type: 'date',
|
||||
format: 'date_optional_time'
|
||||
},
|
||||
|
||||
privacy: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword'
|
||||
},
|
||||
label: {
|
||||
type: 'text'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
displayName: {
|
||||
type: 'text'
|
||||
},
|
||||
|
||||
description: {
|
||||
type: 'text'
|
||||
},
|
||||
|
||||
thumbnailPath: {
|
||||
type: 'keyword'
|
||||
},
|
||||
embedPath: {
|
||||
type: 'keyword'
|
||||
},
|
||||
|
||||
url: {
|
||||
type: 'keyword'
|
||||
},
|
||||
|
||||
host: {
|
||||
type: 'keyword'
|
||||
},
|
||||
|
||||
videosLength: {
|
||||
type: 'long'
|
||||
},
|
||||
|
||||
ownerAccount: {
|
||||
properties: buildChannelOrAccountSummaryMapping()
|
||||
},
|
||||
|
||||
videoChannel: {
|
||||
properties: buildChannelOrAccountSummaryMapping()
|
||||
}
|
||||
} as Record<PropertyName, MappingProperty>
|
||||
}
|
||||
|
||||
export {
|
||||
formatPlaylistForAPI,
|
||||
buildPlaylistsMapping,
|
||||
formatPlaylistForDB,
|
||||
queryPlaylists
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
import { difference } from 'lodash'
|
||||
import { estypes } from '@elastic/elasticsearch'
|
||||
import { AggregationsBuckets, AggregationsStringTermsAggregate, AggregationsStringTermsBucket } from '@elastic/elasticsearch/lib/api/types'
|
||||
import { elasticSearch } from '../../helpers/elastic-search'
|
||||
import { logger } from '../../helpers/logger'
|
||||
|
||||
async function removeNotExistingIdsFromHost (indexName: string, host: string, existingIds: Set<number>) {
|
||||
const idsFromDB = await getIdsOf(indexName, host)
|
||||
|
||||
// Elastic Search limits max terms amount
|
||||
const idsToRemove = difference(idsFromDB, Array.from(existingIds)).splice(0, 50000)
|
||||
|
||||
logger.info({ idsToRemove }, 'Will remove %d entries from %s of host %s.', idsToRemove.length, indexName, host)
|
||||
|
||||
return elasticSearch.deleteByQuery({
|
||||
index: indexName,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
terms: {
|
||||
id: idsToRemove
|
||||
}
|
||||
},
|
||||
{
|
||||
term: {
|
||||
host
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function removeFromHosts (indexName: string, hosts: string[]) {
|
||||
if (hosts.length === 0) return
|
||||
|
||||
logger.info({ hosts }, 'Will remove entries of index %s from hosts.', indexName)
|
||||
|
||||
return elasticSearch.deleteByQuery({
|
||||
index: indexName,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: {
|
||||
terms: {
|
||||
host: hosts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function getIdsOf (indexName: string, host: string) {
|
||||
const res = await elasticSearch.search<unknown, Record<'ids', AggregationsStringTermsAggregate>>({
|
||||
index: indexName,
|
||||
body: {
|
||||
size: 0,
|
||||
aggs: {
|
||||
ids: {
|
||||
terms: {
|
||||
size: 500000,
|
||||
field: 'id'
|
||||
}
|
||||
}
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
host
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return extractBucketsFromAggregation<number>(res.aggregations.ids.buckets).map(b => b.key)
|
||||
}
|
||||
|
||||
function extractSearchQueryResult (result: estypes.SearchResponse<any, any>) {
|
||||
const hits = result.hits
|
||||
|
||||
return {
|
||||
total: (hits.total as estypes.SearchTotalHits).value,
|
||||
data: hits.hits.map(h => Object.assign(h._source, { score: h._score }))
|
||||
}
|
||||
}
|
||||
|
||||
function extractBucketsFromAggregation <T extends string | number> (buckets: AggregationsBuckets<AggregationsStringTermsBucket>) {
|
||||
// FIXME: key returned by elastic search can also be a number
|
||||
return buckets as unknown as { key: T }[]
|
||||
}
|
||||
|
||||
function buildSort (value: string) {
|
||||
let sortField: string
|
||||
let direction: 'asc' | 'desc'
|
||||
|
||||
if (value.substring(0, 1) === '-') {
|
||||
direction = 'desc'
|
||||
sortField = value.substring(1)
|
||||
} else {
|
||||
direction = 'asc'
|
||||
sortField = value
|
||||
}
|
||||
|
||||
const field = sortField === 'match'
|
||||
? '_score'
|
||||
: sortField
|
||||
|
||||
return [
|
||||
{
|
||||
[field]: { order: direction }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export {
|
||||
elasticSearch,
|
||||
removeNotExistingIdsFromHost,
|
||||
getIdsOf,
|
||||
extractSearchQueryResult,
|
||||
removeFromHosts,
|
||||
buildSort,
|
||||
extractBucketsFromAggregation
|
||||
}
|
|
@ -1,528 +0,0 @@
|
|||
import { MappingProperty, PropertyName } from '@elastic/elasticsearch/lib/api/types'
|
||||
import { exists } from '../../helpers/custom-validators/misc'
|
||||
import { elasticSearch } from '../../helpers/elastic-search'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { buildUrl } from '../../helpers/utils'
|
||||
import { CONFIG, ELASTIC_SEARCH_QUERY } from '../../initializers/constants'
|
||||
import { VideosSearchQuery } from '../../types/search-query/video-search.model'
|
||||
import { DBVideo, DBVideoDetails, EnhancedVideo, IndexableVideo, IndexableVideoDetails } from '../../types/video.model'
|
||||
import { buildSort, extractSearchQueryResult } from './elastic-search-queries'
|
||||
import { addUUIDFilters, buildMultiMatchBool } from './shared'
|
||||
import { buildChannelOrAccountSummaryMapping, formatActorForDB, formatActorSummaryForAPI } from './shared/elastic-search-actor'
|
||||
|
||||
async function queryVideos (search: VideosSearchQuery) {
|
||||
const bool: any = {}
|
||||
const filter: any[] = []
|
||||
const mustNot: any[] = []
|
||||
|
||||
if (search.search) {
|
||||
Object.assign(bool, buildMultiMatchBool(search.search, ELASTIC_SEARCH_QUERY.VIDEOS_MULTI_MATCH_FIELDS))
|
||||
}
|
||||
|
||||
if (search.blockedAccounts) {
|
||||
mustNot.push({
|
||||
terms: {
|
||||
'account.handle': search.blockedAccounts
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.blockedHosts) {
|
||||
mustNot.push({
|
||||
terms: {
|
||||
host: search.blockedHosts
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.startDate) {
|
||||
filter.push({
|
||||
range: {
|
||||
publishedAt: {
|
||||
gte: search.startDate
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.endDate) {
|
||||
filter.push({
|
||||
range: {
|
||||
publishedAt: {
|
||||
lte: search.endDate
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.originallyPublishedStartDate) {
|
||||
filter.push({
|
||||
range: {
|
||||
originallyPublishedAt: {
|
||||
gte: search.originallyPublishedStartDate
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.originallyPublishedEndDate) {
|
||||
filter.push({
|
||||
range: {
|
||||
originallyPublishedAt: {
|
||||
lte: search.originallyPublishedEndDate
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.nsfw && search.nsfw !== 'both') {
|
||||
filter.push({
|
||||
term: {
|
||||
nsfw: (search.nsfw + '') === 'true'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.categoryOneOf) {
|
||||
filter.push({
|
||||
terms: {
|
||||
'category.id': search.categoryOneOf
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.licenceOneOf) {
|
||||
filter.push({
|
||||
terms: {
|
||||
'licence.id': search.licenceOneOf
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.languageOneOf) {
|
||||
filter.push({
|
||||
terms: {
|
||||
'language.id': search.languageOneOf
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.tagsOneOf) {
|
||||
filter.push({
|
||||
terms: {
|
||||
tags: search.tagsOneOf.map(t => t.toLowerCase())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.tagsAllOf) {
|
||||
for (const t of search.tagsAllOf) {
|
||||
filter.push({
|
||||
term: {
|
||||
'tags.raw': {
|
||||
value: t,
|
||||
case_insensitive: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (search.durationMin) {
|
||||
filter.push({
|
||||
range: {
|
||||
duration: {
|
||||
gte: search.durationMin
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.durationMax) {
|
||||
filter.push({
|
||||
range: {
|
||||
duration: {
|
||||
lte: search.durationMax
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (exists(search.isLive)) {
|
||||
filter.push({
|
||||
term: {
|
||||
isLive: search.isLive
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.host) {
|
||||
filter.push({
|
||||
term: {
|
||||
'account.host': search.host
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search.uuids) {
|
||||
addUUIDFilters(filter, search.uuids)
|
||||
}
|
||||
|
||||
Object.assign(bool, { filter })
|
||||
|
||||
if (mustNot.length !== 0) {
|
||||
Object.assign(bool, { must_not: mustNot })
|
||||
}
|
||||
|
||||
const body = {
|
||||
from: search.start,
|
||||
size: search.count,
|
||||
sort: buildSort(search.sort)
|
||||
}
|
||||
|
||||
// Allow to boost results depending on query languages
|
||||
if (
|
||||
CONFIG.VIDEOS_SEARCH.BOOST_LANGUAGES.ENABLED &&
|
||||
Array.isArray(search.boostLanguages) &&
|
||||
search.boostLanguages.length !== 0
|
||||
) {
|
||||
const boostScript = `
|
||||
if (doc['language.id'].size() == 0) {
|
||||
return _score;
|
||||
}
|
||||
|
||||
String language = doc['language.id'].value;
|
||||
|
||||
for (String docLang: params.boostLanguages) {
|
||||
if (docLang == language) return _score * params.boost;
|
||||
}
|
||||
|
||||
return _score * params.malus;
|
||||
`
|
||||
|
||||
Object.assign(body, {
|
||||
query: {
|
||||
script_score: {
|
||||
query: { bool },
|
||||
script: {
|
||||
source: boostScript,
|
||||
params: {
|
||||
boostLanguages: search.boostLanguages,
|
||||
boost: ELASTIC_SEARCH_QUERY.BOOST_LANGUAGE_VALUE,
|
||||
malus: ELASTIC_SEARCH_QUERY.MALUS_LANGUAGE_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Object.assign(body, { query: { bool } })
|
||||
}
|
||||
|
||||
logger.debug({ body }, 'Will query Elastic Search for videos.')
|
||||
|
||||
const res = await elasticSearch.search({
|
||||
index: CONFIG.ELASTIC_SEARCH.INDEXES.VIDEOS,
|
||||
body
|
||||
})
|
||||
|
||||
return extractSearchQueryResult(res)
|
||||
}
|
||||
|
||||
function buildVideosMapping () {
|
||||
return {
|
||||
id: {
|
||||
type: 'long'
|
||||
},
|
||||
|
||||
uuid: {
|
||||
type: 'keyword'
|
||||
},
|
||||
shortUUID: {
|
||||
type: 'keyword'
|
||||
},
|
||||
createdAt: {
|
||||
type: 'date',
|
||||
format: 'date_optional_time'
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'date',
|
||||
format: 'date_optional_time'
|
||||
},
|
||||
publishedAt: {
|
||||
type: 'date',
|
||||
format: 'date_optional_time'
|
||||
},
|
||||
originallyPublishedAt: {
|
||||
type: 'date',
|
||||
format: 'date_optional_time'
|
||||
},
|
||||
indexedAt: {
|
||||
type: 'date',
|
||||
format: 'date_optional_time'
|
||||
},
|
||||
|
||||
category: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword'
|
||||
},
|
||||
label: {
|
||||
type: 'text'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
licence: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword'
|
||||
},
|
||||
label: {
|
||||
type: 'text'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
language: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword'
|
||||
},
|
||||
label: {
|
||||
type: 'text'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
privacy: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword'
|
||||
},
|
||||
label: {
|
||||
type: 'text'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
name: {
|
||||
type: 'text'
|
||||
},
|
||||
|
||||
description: {
|
||||
type: 'text'
|
||||
},
|
||||
|
||||
tags: {
|
||||
type: 'text',
|
||||
|
||||
fields: {
|
||||
raw: {
|
||||
type: 'keyword'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
duration: {
|
||||
type: 'long'
|
||||
},
|
||||
|
||||
thumbnailPath: {
|
||||
type: 'keyword'
|
||||
},
|
||||
previewPath: {
|
||||
type: 'keyword'
|
||||
},
|
||||
embedPath: {
|
||||
type: 'keyword'
|
||||
},
|
||||
|
||||
url: {
|
||||
type: 'keyword'
|
||||
},
|
||||
|
||||
views: {
|
||||
type: 'long'
|
||||
},
|
||||
likes: {
|
||||
type: 'long'
|
||||
},
|
||||
dislikes: {
|
||||
type: 'long'
|
||||
},
|
||||
nsfw: {
|
||||
type: 'boolean'
|
||||
},
|
||||
isLive: {
|
||||
type: 'boolean'
|
||||
},
|
||||
|
||||
host: {
|
||||
type: 'keyword'
|
||||
},
|
||||
|
||||
account: {
|
||||
properties: buildChannelOrAccountSummaryMapping()
|
||||
},
|
||||
|
||||
channel: {
|
||||
properties: buildChannelOrAccountSummaryMapping()
|
||||
}
|
||||
} as Record<PropertyName, MappingProperty>
|
||||
}
|
||||
|
||||
function formatVideoForDB (v: IndexableVideo | IndexableVideoDetails): DBVideo | DBVideoDetails {
|
||||
const video = {
|
||||
id: v.id,
|
||||
uuid: v.uuid,
|
||||
shortUUID: v.shortUUID,
|
||||
|
||||
indexedAt: new Date(),
|
||||
createdAt: v.createdAt,
|
||||
updatedAt: v.updatedAt,
|
||||
publishedAt: v.publishedAt,
|
||||
originallyPublishedAt: v.originallyPublishedAt,
|
||||
|
||||
category: {
|
||||
id: v.category.id,
|
||||
label: v.category.label
|
||||
},
|
||||
licence: {
|
||||
id: v.licence.id,
|
||||
label: v.licence.label
|
||||
},
|
||||
language: {
|
||||
id: v.language.id,
|
||||
label: v.language.label
|
||||
},
|
||||
privacy: {
|
||||
id: v.privacy.id,
|
||||
label: v.privacy.label
|
||||
},
|
||||
|
||||
name: v.name,
|
||||
|
||||
truncatedDescription: v.truncatedDescription,
|
||||
description: v.description,
|
||||
|
||||
waitTranscoding: v.waitTranscoding,
|
||||
|
||||
duration: v.duration,
|
||||
|
||||
thumbnailPath: v.thumbnailPath,
|
||||
previewPath: v.previewPath,
|
||||
embedPath: v.embedPath,
|
||||
|
||||
views: v.views,
|
||||
viewers: v.viewers,
|
||||
likes: v.likes,
|
||||
dislikes: v.dislikes,
|
||||
|
||||
isLive: v.isLive || false,
|
||||
nsfw: v.nsfw,
|
||||
|
||||
host: v.host,
|
||||
url: v.url,
|
||||
|
||||
files: v.files,
|
||||
streamingPlaylists: v.streamingPlaylists,
|
||||
|
||||
tags: (v as IndexableVideoDetails).tags ? (v as IndexableVideoDetails).tags : undefined,
|
||||
|
||||
account: formatActorForDB(v.account),
|
||||
channel: formatActorForDB(v.channel)
|
||||
}
|
||||
|
||||
if (isVideoDetails(v)) {
|
||||
return {
|
||||
...video,
|
||||
trackerUrls: v.trackerUrls,
|
||||
|
||||
descriptionPath: v.descriptionPath,
|
||||
|
||||
support: v.support,
|
||||
|
||||
commentsEnabled: v.commentsEnabled,
|
||||
downloadEnabled: v.downloadEnabled
|
||||
}
|
||||
}
|
||||
|
||||
return video
|
||||
}
|
||||
|
||||
function formatVideoForAPI (v: DBVideoDetails, fromHost?: string): EnhancedVideo {
|
||||
return {
|
||||
id: v.id,
|
||||
uuid: v.uuid,
|
||||
shortUUID: v.shortUUID,
|
||||
|
||||
score: v.score,
|
||||
|
||||
createdAt: new Date(v.createdAt),
|
||||
updatedAt: new Date(v.updatedAt),
|
||||
publishedAt: new Date(v.publishedAt),
|
||||
originallyPublishedAt: v.originallyPublishedAt,
|
||||
|
||||
category: {
|
||||
id: v.category.id,
|
||||
label: v.category.label
|
||||
},
|
||||
licence: {
|
||||
id: v.licence.id,
|
||||
label: v.licence.label
|
||||
},
|
||||
language: {
|
||||
id: v.language.id,
|
||||
label: v.language.label
|
||||
},
|
||||
privacy: {
|
||||
id: v.privacy.id,
|
||||
label: v.privacy.label
|
||||
},
|
||||
|
||||
name: v.name,
|
||||
description: v.description,
|
||||
truncatedDescription: v.truncatedDescription,
|
||||
duration: v.duration,
|
||||
|
||||
tags: v.tags,
|
||||
|
||||
thumbnailPath: v.thumbnailPath,
|
||||
thumbnailUrl: buildUrl(v.host, v.thumbnailPath),
|
||||
|
||||
previewPath: v.previewPath,
|
||||
previewUrl: buildUrl(v.host, v.previewPath),
|
||||
|
||||
embedPath: v.embedPath,
|
||||
embedUrl: buildUrl(v.host, v.embedPath),
|
||||
|
||||
url: v.url,
|
||||
|
||||
isLocal: fromHost && fromHost === v.host,
|
||||
|
||||
views: v.views,
|
||||
viewers: v.viewers,
|
||||
likes: v.likes,
|
||||
dislikes: v.dislikes,
|
||||
|
||||
isLive: v.isLive,
|
||||
nsfw: v.nsfw,
|
||||
|
||||
account: formatActorSummaryForAPI(v.account),
|
||||
channel: formatActorSummaryForAPI(v.channel)
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
queryVideos,
|
||||
formatVideoForDB,
|
||||
formatVideoForAPI,
|
||||
buildVideosMapping
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isVideoDetails (video: IndexableVideo | IndexableVideoDetails): video is IndexableVideoDetails {
|
||||
return (video as IndexableVideoDetails).commentsEnabled !== undefined
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
import { MappingProperty, PropertyName } from '@elastic/elasticsearch/lib/api/types'
|
||||
import { AccountSummary, VideoChannelSummary } from '@peertube/peertube-types'
|
||||
import { AdditionalActorAttributes } from '../../../types/actor.model'
|
||||
import { formatActorImageForDB } from './'
|
||||
import { buildActorImageMapping, formatActorImageForAPI, formatActorImagesForAPI, formatActorImagesForDB } from './elastic-search-avatar'
|
||||
|
||||
function buildChannelOrAccountSummaryMapping () {
|
||||
return {
|
||||
id: {
|
||||
type: 'long'
|
||||
},
|
||||
|
||||
name: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
raw: {
|
||||
type: 'keyword'
|
||||
}
|
||||
}
|
||||
},
|
||||
displayName: {
|
||||
type: 'text'
|
||||
},
|
||||
url: {
|
||||
type: 'keyword'
|
||||
},
|
||||
host: {
|
||||
type: 'keyword'
|
||||
},
|
||||
handle: {
|
||||
type: 'keyword'
|
||||
},
|
||||
|
||||
avatar: {
|
||||
properties: buildActorImageMapping()
|
||||
},
|
||||
|
||||
// Introduced in 4.2
|
||||
avatars: {
|
||||
properties: buildActorImageMapping()
|
||||
}
|
||||
} as Record<PropertyName, MappingProperty>
|
||||
}
|
||||
|
||||
function buildChannelOrAccountCommonMapping () {
|
||||
return {
|
||||
...buildChannelOrAccountSummaryMapping(),
|
||||
|
||||
followingCount: {
|
||||
type: 'long'
|
||||
},
|
||||
followersCount: {
|
||||
type: 'long'
|
||||
},
|
||||
|
||||
createdAt: {
|
||||
type: 'date',
|
||||
format: 'date_optional_time'
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'date',
|
||||
format: 'date_optional_time'
|
||||
},
|
||||
|
||||
description: {
|
||||
type: 'text'
|
||||
}
|
||||
} as Record<PropertyName, MappingProperty>
|
||||
}
|
||||
|
||||
function formatActorSummaryForAPI (actor: (AccountSummary | VideoChannelSummary) & AdditionalActorAttributes) {
|
||||
return {
|
||||
id: actor.id,
|
||||
name: actor.name,
|
||||
displayName: actor.displayName,
|
||||
url: actor.url,
|
||||
host: actor.host,
|
||||
|
||||
avatar: formatActorImageForAPI(actor.avatar),
|
||||
avatars: formatActorImagesForAPI(actor.avatars, actor.avatar)
|
||||
}
|
||||
}
|
||||
|
||||
function formatActorForDB (actor: AccountSummary | VideoChannelSummary) {
|
||||
return {
|
||||
id: actor.id,
|
||||
name: actor.name,
|
||||
displayName: actor.displayName,
|
||||
url: actor.url,
|
||||
host: actor.host,
|
||||
|
||||
handle: `${actor.name}@${actor.host}`,
|
||||
|
||||
avatar: formatActorImageForDB(actor.avatar, actor.host),
|
||||
avatars: formatActorImagesForDB(actor.avatars, actor.host)
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
buildChannelOrAccountCommonMapping,
|
||||
buildChannelOrAccountSummaryMapping,
|
||||
formatActorSummaryForAPI,
|
||||
formatActorForDB
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
import { MappingProperty, PropertyName } from '@elastic/elasticsearch/lib/api/types'
|
||||
import { ActorImage } from '@peertube/peertube-types'
|
||||
import { buildUrl } from '../../../helpers/utils'
|
||||
|
||||
function formatActorImageForAPI (image?: ActorImage) {
|
||||
if (!image) return null
|
||||
|
||||
return {
|
||||
url: image.url,
|
||||
path: image.path,
|
||||
width: image.width,
|
||||
createdAt: image.createdAt,
|
||||
updatedAt: image.updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
function formatActorImagesForAPI (images?: ActorImage[], image?: ActorImage) {
|
||||
// Does not exist in PeerTube < 4.2
|
||||
if (!images) {
|
||||
if (!image) return []
|
||||
|
||||
return [ formatActorImageForAPI(image) ]
|
||||
}
|
||||
|
||||
return images.map(a => formatActorImageForAPI(a))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function formatActorImageForDB (image: ActorImage, host: string) {
|
||||
if (!image) return null
|
||||
|
||||
return {
|
||||
url: buildUrl(host, image.path),
|
||||
path: image.path,
|
||||
width: image.width,
|
||||
createdAt: image.createdAt,
|
||||
updatedAt: image.updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
function formatActorImagesForDB (images: ActorImage[], host: string) {
|
||||
if (!images) return null
|
||||
|
||||
return images.map(image => formatActorImageForDB(image, host))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildActorImageMapping () {
|
||||
return {
|
||||
path: {
|
||||
type: 'keyword'
|
||||
},
|
||||
width: {
|
||||
type: 'long'
|
||||
},
|
||||
createdAt: {
|
||||
type: 'date',
|
||||
format: 'date_optional_time'
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'date',
|
||||
format: 'date_optional_time'
|
||||
}
|
||||
} as Record<PropertyName, MappingProperty>
|
||||
}
|
||||
|
||||
export {
|
||||
formatActorImageForAPI,
|
||||
formatActorImagesForAPI,
|
||||
|
||||
formatActorImageForDB,
|
||||
formatActorImagesForDB,
|
||||
|
||||
buildActorImageMapping
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export * from './elastic-search-actor'
|
||||
export * from './elastic-search-avatar'
|
||||
export * from './query-helpers'
|
|
@ -1,88 +0,0 @@
|
|||
import validator from 'validator'
|
||||
|
||||
import { ELASTIC_SEARCH_QUERY } from '../../../initializers/constants'
|
||||
|
||||
function addUUIDFilters (filters: any[], uuids: string[]) {
|
||||
if (!filters) return
|
||||
|
||||
const result = {
|
||||
shortUUIDs: [] as string[],
|
||||
uuids: [] as string[]
|
||||
}
|
||||
|
||||
for (const uuid of uuids) {
|
||||
if (validator.isUUID(uuid)) result.uuids.push(uuid)
|
||||
else result.shortUUIDs.push(uuid)
|
||||
}
|
||||
|
||||
filters.push({
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
terms: {
|
||||
uuid: result.uuids
|
||||
}
|
||||
},
|
||||
{
|
||||
terms: {
|
||||
shortUUID: result.shortUUIDs
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function buildMultiMatchBool (search: string, fieldsObject: { default: string[], phrase: string[] }) {
|
||||
return {
|
||||
must: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
multi_match: {
|
||||
query: search,
|
||||
fields: fieldsObject.default,
|
||||
fuzziness: ELASTIC_SEARCH_QUERY.FUZZINESS,
|
||||
operator: ELASTIC_SEARCH_QUERY.OPERATOR,
|
||||
minimum_should_match: ELASTIC_SEARCH_QUERY.MINIMUM_SHOULD_MATCH
|
||||
}
|
||||
},
|
||||
{
|
||||
multi_match: {
|
||||
query: search,
|
||||
fields: fieldsObject.default,
|
||||
operator: ELASTIC_SEARCH_QUERY.OPERATOR,
|
||||
minimum_should_match: ELASTIC_SEARCH_QUERY.MINIMUM_SHOULD_MATCH,
|
||||
type: 'cross_fields'
|
||||
}
|
||||
},
|
||||
{
|
||||
multi_match: {
|
||||
query: search,
|
||||
fields: fieldsObject.phrase,
|
||||
type: 'phrase'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
should: [
|
||||
// Better score for exact search
|
||||
{
|
||||
multi_match: {
|
||||
query: search,
|
||||
fields: [ ...fieldsObject.default, ...fieldsObject.phrase ],
|
||||
operator: ELASTIC_SEARCH_QUERY.OPERATOR,
|
||||
minimum_should_match: ELASTIC_SEARCH_QUERY.MINIMUM_SHOULD_MATCH
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
addUUIDFilters,
|
||||
buildMultiMatchBool
|
||||
}
|
|
@ -1,31 +1,38 @@
|
|||
import { logger } from '../../helpers/logger'
|
||||
import { CONFIG } from '../../initializers/constants'
|
||||
import { CONFIG, SORTABLE_COLUMNS } from '../../initializers/constants'
|
||||
import { DBChannel, IndexableChannel } from '../../types/channel.model'
|
||||
import { buildChannelsMapping, formatChannelForDB } from '../elastic-search/elastic-search-channels'
|
||||
import { formatChannelForDB } from '../meilisearch/meilisearch-channels'
|
||||
import { getChannel } from '../requests/peertube-instance'
|
||||
import { AbstractIndexer } from './shared'
|
||||
|
||||
export class ChannelIndexer extends AbstractIndexer <IndexableChannel, DBChannel> {
|
||||
protected readonly primaryKey = 'primaryKey'
|
||||
protected readonly filterableAttributes = [ 'url', 'host', 'videosCount', 'ownerAccount.handle', 'handle' ]
|
||||
protected readonly sortableAttributes = SORTABLE_COLUMNS.CHANNELS_SEARCH
|
||||
// Keep the order, most important first
|
||||
protected readonly searchableAttributes = [ 'name', 'displayName', 'ownerAccount.name', 'ownerAccount.displayName', 'description' ]
|
||||
|
||||
protected readonly rankingRules = [
|
||||
'words',
|
||||
'typo',
|
||||
'proximity',
|
||||
'attribute',
|
||||
'sort',
|
||||
'exactness',
|
||||
'followersCount:desc'
|
||||
]
|
||||
|
||||
constructor () {
|
||||
super(CONFIG.ELASTIC_SEARCH.INDEXES.CHANNELS, formatChannelForDB)
|
||||
|
||||
this.indexQueue.drain(async () => {
|
||||
logger.info('Refresh channels index.')
|
||||
|
||||
await this.refreshIndex()
|
||||
})
|
||||
super(CONFIG.MEILISEARCH.INDEXES.CHANNELS, formatChannelForDB)
|
||||
}
|
||||
|
||||
async indexSpecificElement (host: string, name: string) {
|
||||
await this.waitForBulkIndexation()
|
||||
|
||||
const channel = await getChannel(host, name)
|
||||
|
||||
logger.info('Indexing specific channel %s@%s.', name, host)
|
||||
|
||||
return this.indexElements([ channel ], true)
|
||||
}
|
||||
|
||||
buildMapping () {
|
||||
return buildChannelsMapping()
|
||||
this.addElementsToBulkIndex([ channel ])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import { CONFIG } from '../../initializers/constants'
|
||||
import { CONFIG, SORTABLE_COLUMNS } from '../../initializers/constants'
|
||||
import { DBPlaylist, IndexablePlaylist } from '../../types/playlist.model'
|
||||
import { buildPlaylistsMapping, formatPlaylistForDB } from '../elastic-search/elastic-search-playlists'
|
||||
import { formatPlaylistForDB } from '../meilisearch/meilisearch-playlists'
|
||||
import { AbstractIndexer } from './shared'
|
||||
|
||||
export class PlaylistIndexer extends AbstractIndexer <IndexablePlaylist, DBPlaylist> {
|
||||
protected readonly primaryKey = 'uuid'
|
||||
protected readonly filterableAttributes = [ 'uuid', 'host', 'ownerAccount.handle', 'ownerAccount.host', 'videosLength' ]
|
||||
protected readonly sortableAttributes = SORTABLE_COLUMNS.PLAYLISTS_SEARCH
|
||||
// Keep the order, most important first
|
||||
protected readonly searchableAttributes = [ 'displayName', 'videoChannel.displayName', 'ownerAccount.displayName', 'description' ]
|
||||
|
||||
constructor () {
|
||||
super(CONFIG.ELASTIC_SEARCH.INDEXES.PLAYLISTS, formatPlaylistForDB)
|
||||
super(CONFIG.MEILISEARCH.INDEXES.PLAYLISTS, formatPlaylistForDB)
|
||||
}
|
||||
|
||||
async indexSpecificElement (host: string, uuid: string) {
|
||||
async indexSpecificElement (host: string, uuid: string): Promise<any> {
|
||||
// We don't need to index a specific element yet, since we have all playlist information in the list endpoint
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
buildMapping () {
|
||||
return buildPlaylistsMapping()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,65 +1,137 @@
|
|||
import { AsyncQueue, queue } from 'async'
|
||||
import { QueueObject, queue } from 'async'
|
||||
import { inspect } from 'util'
|
||||
import { MappingProperty, PropertyName } from '@elastic/elasticsearch/lib/api/types'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { INDEXER_QUEUE_CONCURRENCY } from '../../../initializers/constants'
|
||||
import { buildIndex, indexDocuments, refreshIndex } from '../../../lib/elastic-search/elastic-search-index'
|
||||
import { removeFromHosts, removeNotExistingIdsFromHost } from '../../../lib/elastic-search/elastic-search-queries'
|
||||
import { CONFIG, INDEXER_QUEUE_CONCURRENCY } from '../../../initializers/constants'
|
||||
import { IndexableDoc } from '../../../types/indexable-doc.model'
|
||||
import { client } from '../../../helpers/meilisearch'
|
||||
import { buildInValuesArray } from '../../meilisearch/meilisearch-queries'
|
||||
import { EnqueuedTask } from 'meilisearch'
|
||||
|
||||
// identifier could be an uuid, an handle or a url for example
|
||||
export type QueueParam = { host: string, identifier: string }
|
||||
|
||||
export abstract class AbstractIndexer <T extends IndexableDoc, DB> {
|
||||
protected readonly indexQueue: AsyncQueue<QueueParam>
|
||||
protected readonly indexQueue: QueueObject<QueueParam>
|
||||
|
||||
abstract indexSpecificElement (host: string, uuid: string): Promise<any>
|
||||
abstract buildMapping (): Record<PropertyName, MappingProperty>
|
||||
protected abstract readonly primaryKey: keyof DB
|
||||
protected abstract readonly filterableAttributes: string[]
|
||||
protected abstract readonly sortableAttributes: string[]
|
||||
protected abstract readonly searchableAttributes: string[]
|
||||
|
||||
protected readonly rankingRules: string[]
|
||||
|
||||
private elementsToBulkIndex: T[] = []
|
||||
private bulkIndexationTimer: any
|
||||
private bulkProcessEnqueuedTask: EnqueuedTask
|
||||
|
||||
abstract indexSpecificElement (host: string, uuid: string): Promise<void>
|
||||
|
||||
constructor (
|
||||
protected readonly indexName: string,
|
||||
protected readonly formatterFn: (o: T) => DB
|
||||
) {
|
||||
this.indexQueue = queue<QueueParam, Error>((task, cb) => {
|
||||
this.indexSpecificElement(task.host, task.identifier)
|
||||
.then(() => cb())
|
||||
.catch(err => {
|
||||
logger.error(
|
||||
{ err: inspect(err) },
|
||||
'Error in index specific element %s of %s in index %s.', task.identifier, task.host, this.indexName
|
||||
)
|
||||
cb()
|
||||
})
|
||||
this.indexQueue = queue<QueueParam, Error>(async (task, cb) => {
|
||||
try {
|
||||
await this.indexSpecificElement(task.host, task.identifier)
|
||||
|
||||
return cb()
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err: inspect(err) },
|
||||
'Error in index specific element %s of %s in index %s.', task.identifier, task.host, this.indexName
|
||||
)
|
||||
cb()
|
||||
}
|
||||
}, INDEXER_QUEUE_CONCURRENCY)
|
||||
}
|
||||
|
||||
initIndex () {
|
||||
return buildIndex(this.indexName, this.buildMapping())
|
||||
async initIndex () {
|
||||
const { results } = await client.getIndexes()
|
||||
|
||||
if (!CONFIG.MEILISEARCH.FORCE_SETTINGS_UPDATE_AT_STARTUP && results.some(r => r.uid === this.indexName)) {
|
||||
logger.info(this.indexName + ' already exists, skipping configuration')
|
||||
return
|
||||
}
|
||||
|
||||
logger.info(`Creating/updating "${this.indexName}" index settings`)
|
||||
|
||||
await client.index(this.indexName).updateSearchableAttributes(this.searchableAttributes)
|
||||
await client.index(this.indexName).updateFilterableAttributes(this.filterableAttributes)
|
||||
await client.index(this.indexName).updateSortableAttributes(this.sortableAttributes)
|
||||
await client.index(this.indexName).updateFaceting({ maxValuesPerFacet: 10_000 })
|
||||
await client.index(this.indexName).updatePagination({ maxTotalHits: 10_000 })
|
||||
|
||||
if (this.rankingRules) {
|
||||
await client.index(this.indexName).updateRankingRules(this.rankingRules)
|
||||
}
|
||||
}
|
||||
|
||||
scheduleIndexation (host: string, identifier: string) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
removeNotExisting (host: string, existingPrimaryKeys: Set<string>) {
|
||||
return client.index(this.indexName).deleteDocuments({
|
||||
filter: `${this.primaryKey.toString()} NOT IN ${buildInValuesArray(Array.from(existingPrimaryKeys))} AND host = ${host}`
|
||||
})
|
||||
}
|
||||
|
||||
removeFromHosts (existingHosts: string[]) {
|
||||
return client.index(this.indexName).deleteDocuments({
|
||||
filter: 'host NOT IN ' + buildInValuesArray(Array.from(existingHosts))
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
scheduleParallelIndexation (host: string, identifier: string) {
|
||||
this.indexQueue.push({ identifier, host })
|
||||
.catch(err => logger.error({ err: inspect(err) }, 'Cannot schedule indexation of %s for %s', identifier, host))
|
||||
}
|
||||
|
||||
refreshIndex () {
|
||||
return refreshIndex(this.indexName)
|
||||
async waitForBulkIndexation () {
|
||||
if (!this.bulkProcessEnqueuedTask) return
|
||||
|
||||
await this.waitForTask(this.bulkProcessEnqueuedTask.taskUid, 1000)
|
||||
|
||||
this.bulkProcessEnqueuedTask = undefined
|
||||
}
|
||||
|
||||
removeNotExisting (host: string, existingIds: Set<number>) {
|
||||
return removeNotExistingIdsFromHost(this.indexName, host, existingIds)
|
||||
addElementsToBulkIndex (elements: T[]) {
|
||||
this.elementsToBulkIndex = this.elementsToBulkIndex.concat(elements)
|
||||
|
||||
this.scheduleBulkIndexationProcess()
|
||||
}
|
||||
|
||||
removeFromHosts (hosts: string[]) {
|
||||
return removeFromHosts(this.indexName, hosts)
|
||||
private scheduleBulkIndexationProcess () {
|
||||
if (this.bulkIndexationTimer) return
|
||||
|
||||
this.bulkIndexationTimer = setTimeout(async () => {
|
||||
try {
|
||||
const elements = this.elementsToBulkIndex
|
||||
this.elementsToBulkIndex = []
|
||||
|
||||
logger.info(`Bulk indexing ${elements.length} elements in ${this.indexName}`)
|
||||
|
||||
this.bulkProcessEnqueuedTask = await this.indexElements(elements)
|
||||
|
||||
this.bulkIndexationTimer = undefined
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'Cannot schedule bulk indexation')
|
||||
}
|
||||
}, CONFIG.INDEXER.BULK_INDEXATION_INTERVAL_MS)
|
||||
}
|
||||
|
||||
indexElements (elements: T[], replace = false) {
|
||||
return indexDocuments({
|
||||
objects: elements,
|
||||
formatter: v => this.formatterFn(v),
|
||||
replace,
|
||||
index: this.indexName
|
||||
})
|
||||
private async indexElements (elements: T[]) {
|
||||
const documents = elements.map(e => this.formatterFn(e))
|
||||
|
||||
const result = await client.index(this.indexName).updateDocuments(documents, { primaryKey: this.primaryKey.toString() })
|
||||
logger.debug(result, 'Indexed ' + documents.length + ' documents in ' + this.indexName)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private waitForTask (taskId: number, intervalMs?: number) {
|
||||
return client.index(this.indexName).waitForTask(taskId, { timeOutMs: 1000 * 60 * 5, intervalMs })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,63 @@
|
|||
import { QueueObject } from 'async'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { CONFIG } from '../../initializers/constants'
|
||||
import { DBVideo, IndexableVideo } from '../../types/video.model'
|
||||
import { buildVideosMapping, formatVideoForDB } from '../elastic-search/elastic-search-videos'
|
||||
import { AbstractIndexer, QueueParam } from './shared'
|
||||
import { CONFIG, SORTABLE_COLUMNS } from '../../initializers/constants'
|
||||
import { formatVideoForDB } from '../meilisearch/meilisearch-videos'
|
||||
import { getVideo } from '../requests/peertube-instance'
|
||||
import { AbstractIndexer } from './shared'
|
||||
import { DBVideo, IndexableVideo } from '../../types/video.model'
|
||||
|
||||
export class VideoIndexer extends AbstractIndexer <IndexableVideo, DBVideo> {
|
||||
protected readonly indexQueue: QueueObject<QueueParam>
|
||||
protected readonly primaryKey = 'uuid'
|
||||
protected readonly filterableAttributes = [
|
||||
'uuid',
|
||||
'host',
|
||||
'account.handle',
|
||||
'account.host',
|
||||
'publishedAt',
|
||||
'originallyPublishedAt',
|
||||
'nsfw',
|
||||
'category.id',
|
||||
'licence.id',
|
||||
'language.id',
|
||||
'tags',
|
||||
'duration',
|
||||
'isLive'
|
||||
]
|
||||
|
||||
protected readonly sortableAttributes = SORTABLE_COLUMNS.VIDEOS_SEARCH
|
||||
|
||||
// Keep the order, most important first
|
||||
protected readonly searchableAttributes = [
|
||||
'name',
|
||||
'tags',
|
||||
'account.displayName',
|
||||
'channel.displayName',
|
||||
'searchableDescription'
|
||||
]
|
||||
|
||||
protected readonly rankingRules = [
|
||||
'words',
|
||||
'typo',
|
||||
'proximity',
|
||||
'attribute',
|
||||
'sort',
|
||||
'exactness',
|
||||
'language:asc',
|
||||
'views:desc'
|
||||
]
|
||||
|
||||
constructor () {
|
||||
super(CONFIG.ELASTIC_SEARCH.INDEXES.VIDEOS, formatVideoForDB)
|
||||
super(CONFIG.MEILISEARCH.INDEXES.VIDEOS, formatVideoForDB)
|
||||
}
|
||||
|
||||
async indexSpecificElement (host: string, uuid: string) {
|
||||
await this.waitForBulkIndexation()
|
||||
|
||||
const video = await getVideo(host, uuid)
|
||||
|
||||
logger.info('Indexing specific video %s of %s.', uuid, host)
|
||||
|
||||
return this.indexElements([ video ], true)
|
||||
}
|
||||
|
||||
buildMapping () {
|
||||
return buildVideosMapping()
|
||||
this.addElementsToBulkIndex([ video ])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
import { createHash } from 'crypto'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { client } from '../../helpers/meilisearch'
|
||||
import { CONFIG } from '../../initializers/constants'
|
||||
import { DBChannel, APIVideoChannel, IndexableChannel } from '../../types/channel.model'
|
||||
import { ChannelsSearchQuery } from '../../types/search-query/channel-search.model'
|
||||
import { buildInValuesArray, buildSort, extractSearchQueryResult } from './meilisearch-queries'
|
||||
import {
|
||||
formatActorImageForAPI,
|
||||
formatActorImageForDB,
|
||||
formatActorImagesForAPI,
|
||||
formatActorImagesForDB
|
||||
} from './shared/meilisearch-avatar'
|
||||
|
||||
export async function queryChannels (search: ChannelsSearchQuery) {
|
||||
const filter: string[] = [ 'videosCount != 0' ]
|
||||
|
||||
if (search.host) filter.push(`host = '${search.host}'`)
|
||||
|
||||
if (search.blockedAccounts && search.blockedAccounts.length !== 0) {
|
||||
filter.push(`ownerAccount.handle NOT IN ${buildInValuesArray(search.blockedAccounts)}`)
|
||||
}
|
||||
|
||||
if (search.handles && search.handles.length !== 0) {
|
||||
filter.push(`handle IN ${buildInValuesArray(search.handles)}`)
|
||||
}
|
||||
|
||||
if (search.blockedHosts && search.blockedHosts.length !== 0) {
|
||||
filter.push(`host NOT IN ${buildInValuesArray(search.blockedHosts)}`)
|
||||
}
|
||||
|
||||
logger.debug({ filter }, 'Will query Meilisearch for channels.')
|
||||
|
||||
const result = await client.index(CONFIG.MEILISEARCH.INDEXES.CHANNELS).search(search.search, {
|
||||
offset: search.start,
|
||||
limit: search.count,
|
||||
sort: buildSort(search.sort),
|
||||
showRankingScore: true,
|
||||
filter
|
||||
})
|
||||
|
||||
return extractSearchQueryResult(result)
|
||||
}
|
||||
|
||||
export function formatChannelForAPI (c: DBChannel, fromHost?: string): APIVideoChannel {
|
||||
return {
|
||||
id: c.id,
|
||||
|
||||
score: c._rankingScore,
|
||||
|
||||
url: c.url,
|
||||
name: c.name,
|
||||
host: c.host,
|
||||
followingCount: c.followingCount,
|
||||
followersCount: c.followersCount,
|
||||
createdAt: new Date(c.createdAt),
|
||||
updatedAt: new Date(c.updatedAt),
|
||||
|
||||
avatar: formatActorImageForAPI(c.avatar),
|
||||
avatars: formatActorImagesForAPI(c.avatars, c.avatar),
|
||||
|
||||
banner: formatActorImageForAPI(c.banner),
|
||||
banners: formatActorImagesForAPI(c.banners, c.banner),
|
||||
|
||||
displayName: c.displayName,
|
||||
description: c.description,
|
||||
support: c.support,
|
||||
isLocal: fromHost === c.host,
|
||||
|
||||
videosCount: c.videosCount || 0,
|
||||
|
||||
ownerAccount: {
|
||||
id: c.ownerAccount.id,
|
||||
url: c.ownerAccount.url,
|
||||
|
||||
displayName: c.ownerAccount.displayName,
|
||||
description: c.ownerAccount.description,
|
||||
name: c.ownerAccount.name,
|
||||
host: c.ownerAccount.host,
|
||||
followingCount: c.ownerAccount.followingCount,
|
||||
followersCount: c.ownerAccount.followersCount,
|
||||
createdAt: new Date(c.ownerAccount.createdAt),
|
||||
updatedAt: new Date(c.ownerAccount.updatedAt),
|
||||
|
||||
avatar: formatActorImageForAPI(c.ownerAccount.avatar),
|
||||
avatars: formatActorImagesForAPI(c.ownerAccount.avatars, c.ownerAccount.avatar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function formatChannelForDB (c: IndexableChannel): DBChannel {
|
||||
return {
|
||||
primaryKey: buildDBChannelPrimaryKey(c),
|
||||
|
||||
id: c.id,
|
||||
|
||||
name: c.name,
|
||||
host: c.host,
|
||||
url: c.url,
|
||||
|
||||
avatar: formatActorImageForDB(c.avatar, c.host),
|
||||
avatars: formatActorImagesForDB(c.avatars, c.host),
|
||||
|
||||
banner: formatActorImageForDB(c.banner, c.host),
|
||||
banners: formatActorImagesForDB(c.banners, c.host),
|
||||
|
||||
displayName: c.displayName,
|
||||
|
||||
indexedAt: new Date().getTime(),
|
||||
createdAt: new Date(c.createdAt).getTime(),
|
||||
updatedAt: new Date(c.updatedAt).getTime(),
|
||||
|
||||
followingCount: c.followingCount,
|
||||
followersCount: c.followersCount,
|
||||
|
||||
description: c.description,
|
||||
support: c.support,
|
||||
videosCount: c.videosCount,
|
||||
|
||||
handle: `${c.name}@${c.host}`,
|
||||
|
||||
ownerAccount: {
|
||||
id: c.ownerAccount.id,
|
||||
url: c.ownerAccount.url,
|
||||
|
||||
displayName: c.ownerAccount.displayName,
|
||||
description: c.ownerAccount.description,
|
||||
name: c.ownerAccount.name,
|
||||
host: c.ownerAccount.host,
|
||||
followingCount: c.ownerAccount.followingCount,
|
||||
followersCount: c.ownerAccount.followersCount,
|
||||
createdAt: new Date(c.ownerAccount.createdAt).getTime(),
|
||||
updatedAt: new Date(c.ownerAccount.updatedAt).getTime(),
|
||||
|
||||
handle: `${c.ownerAccount.name}@${c.ownerAccount.host}`,
|
||||
|
||||
avatar: formatActorImageForDB(c.ownerAccount.avatar, c.ownerAccount.host),
|
||||
avatars: formatActorImagesForDB(c.ownerAccount.avatars, c.ownerAccount.host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDBChannelPrimaryKey (c: { id: number, host: string }) {
|
||||
return `${c.id}_${md5(c.host)}`
|
||||
}
|
||||
|
||||
// Collisions are fine, we just want to generate a primary key in an efficient way
|
||||
function md5 (value: string) {
|
||||
return createHash('md5').update(value).digest('hex')
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { CONFIG } from '../../initializers/constants'
|
||||
import { listIndexInstancesHost, getMajorInstanceVersion } from '../requests/instances-index'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import Bluebird from 'bluebird'
|
||||
|
||||
export async function buildInstanceHosts () {
|
||||
let indexHosts = await listIndexInstancesHost()
|
||||
|
||||
if (CONFIG.INSTANCES_INDEX.WHITELIST.ENABLED) {
|
||||
const whitelistHosts = Array.isArray(CONFIG.INSTANCES_INDEX.WHITELIST.HOSTS)
|
||||
? CONFIG.INSTANCES_INDEX.WHITELIST.HOSTS
|
||||
: []
|
||||
|
||||
indexHosts = indexHosts.filter(h => whitelistHosts.includes(h))
|
||||
}
|
||||
|
||||
indexHosts = await Bluebird.filter(indexHosts, async indexHost => {
|
||||
const instanceVersion = await getMajorInstanceVersion(indexHost)
|
||||
|
||||
if (instanceVersion < 4) {
|
||||
logger.info(`Do not index ${indexHost} because the major version is too low (v${instanceVersion} < v4)`)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}, { concurrency: 10 })
|
||||
|
||||
return indexHosts
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
import { logger } from '../../helpers/logger'
|
||||
import { client } from '../../helpers/meilisearch'
|
||||
import { buildUrl } from '../../helpers/utils'
|
||||
import { CONFIG } from '../../initializers/constants'
|
||||
import { DBPlaylist, APIPlaylist, IndexablePlaylist } from '../../types/playlist.model'
|
||||
import { PlaylistsSearchQuery } from '../../types/search-query/playlist-search.model'
|
||||
import { buildInValuesArray, buildSort, extractSearchQueryResult } from './meilisearch-queries'
|
||||
import { addUUIDFilters } from './shared'
|
||||
import { formatActorForDB, formatActorSummaryForAPI } from './shared/meilisearch-actor'
|
||||
|
||||
export async function queryPlaylists (search: PlaylistsSearchQuery) {
|
||||
const filter: string[] = [
|
||||
'videosLength != 0'
|
||||
]
|
||||
|
||||
if (search.host) filter.push(`ownerAccount.host = '${search.host}'`)
|
||||
|
||||
if (search.blockedAccounts && search.blockedAccounts.length !== 0) {
|
||||
filter.push(`ownerAccount.handle NOT IN ${buildInValuesArray(search.blockedAccounts)}`)
|
||||
}
|
||||
|
||||
if (search.blockedHosts && search.blockedHosts.length !== 0) {
|
||||
filter.push(`host NOT IN ${buildInValuesArray(search.blockedHosts)}`)
|
||||
}
|
||||
|
||||
if (search.uuids) addUUIDFilters(filter, search.uuids)
|
||||
|
||||
logger.debug({ filter }, 'Will query Meilisearch for playlists.')
|
||||
|
||||
const result = await client.index(CONFIG.MEILISEARCH.INDEXES.PLAYLISTS).search(search.search, {
|
||||
offset: search.start,
|
||||
limit: search.count,
|
||||
sort: buildSort(search.sort),
|
||||
showRankingScore: true,
|
||||
filter
|
||||
})
|
||||
|
||||
return extractSearchQueryResult(result)
|
||||
}
|
||||
|
||||
export function formatPlaylistForAPI (p: DBPlaylist, fromHost?: string): APIPlaylist {
|
||||
return {
|
||||
id: p.id,
|
||||
uuid: p.uuid,
|
||||
shortUUID: p.shortUUID,
|
||||
|
||||
score: p._rankingScore,
|
||||
|
||||
isLocal: fromHost === p.host,
|
||||
|
||||
url: p.url,
|
||||
|
||||
displayName: p.displayName,
|
||||
description: p.description,
|
||||
|
||||
privacy: {
|
||||
id: p.privacy.id,
|
||||
label: p.privacy.label
|
||||
},
|
||||
|
||||
videosLength: p.videosLength,
|
||||
|
||||
type: {
|
||||
id: p.type.id,
|
||||
label: p.type.label
|
||||
},
|
||||
|
||||
thumbnailPath: p.thumbnailPath,
|
||||
thumbnailUrl: buildUrl(p.host, p.thumbnailPath),
|
||||
|
||||
embedPath: p.embedPath,
|
||||
embedUrl: buildUrl(p.host, p.embedPath),
|
||||
|
||||
createdAt: new Date(p.createdAt),
|
||||
updatedAt: new Date(p.updatedAt),
|
||||
|
||||
ownerAccount: formatActorSummaryForAPI(p.ownerAccount),
|
||||
videoChannel: formatActorSummaryForAPI(p.videoChannel)
|
||||
}
|
||||
}
|
||||
|
||||
export function formatPlaylistForDB (p: IndexablePlaylist): DBPlaylist {
|
||||
return {
|
||||
id: p.id,
|
||||
uuid: p.uuid,
|
||||
shortUUID: p.shortUUID,
|
||||
|
||||
indexedAt: new Date().getTime(),
|
||||
createdAt: new Date(p.createdAt).getTime(),
|
||||
updatedAt: new Date(p.updatedAt).getTime(),
|
||||
|
||||
host: p.host,
|
||||
url: p.url,
|
||||
|
||||
displayName: p.displayName,
|
||||
description: p.description,
|
||||
|
||||
thumbnailPath: p.thumbnailPath,
|
||||
embedPath: p.embedPath,
|
||||
|
||||
type: {
|
||||
id: p.type.id,
|
||||
label: p.type.label
|
||||
},
|
||||
privacy: {
|
||||
id: p.privacy.id,
|
||||
label: p.privacy.label
|
||||
},
|
||||
|
||||
videosLength: p.videosLength,
|
||||
|
||||
ownerAccount: formatActorForDB(p.ownerAccount),
|
||||
videoChannel: formatActorForDB(p.videoChannel)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { SearchResponse } from 'meilisearch'
|
||||
|
||||
export function extractSearchQueryResult (result: SearchResponse<any, any>) {
|
||||
return {
|
||||
total: result.estimatedTotalHits,
|
||||
data: result.hits
|
||||
}
|
||||
}
|
||||
|
||||
export function buildSort (value: string) {
|
||||
if (value === '-match') return [ '_rankingScore:desc' ]
|
||||
if (value === 'match') return [ '_rankingScore:asc' ]
|
||||
|
||||
if (value.substring(0, 1) === '-') {
|
||||
return [ `${value.substring(1)}:desc`, '_rankingScore:desc' ]
|
||||
}
|
||||
|
||||
return [ `${value}:asc`, '_rankingScore:desc' ]
|
||||
}
|
||||
|
||||
export function buildInQuery (term: string, values: string[] | number[]) {
|
||||
return `${term} IN ${buildInValuesArray(values)}`
|
||||
}
|
||||
|
||||
export function buildInValuesArray (values: string[] | number[]) {
|
||||
return '[' + values.map(v => `'${v.replace(/'/g, '\\\'')}'`).join(',') + ']'
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
import { exists } from '../../helpers/custom-validators/misc'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { client } from '../../helpers/meilisearch'
|
||||
import { buildUrl } from '../../helpers/utils'
|
||||
import { CONFIG } from '../../initializers/constants'
|
||||
import { VideosSearchQuery } from '../../types/search-query/video-search.model'
|
||||
import { DBVideo, DBVideoDetails, APIVideo, IndexableVideo, IndexableVideoDetails } from '../../types/video.model'
|
||||
import { buildInQuery, buildInValuesArray, buildSort, extractSearchQueryResult } from './meilisearch-queries'
|
||||
import { addUUIDFilters } from './shared'
|
||||
import { formatActorForDB, formatActorSummaryForAPI } from './shared/meilisearch-actor'
|
||||
|
||||
export async function queryVideos (search: VideosSearchQuery) {
|
||||
const filter: string[] = []
|
||||
|
||||
if (search.blockedAccounts && search.blockedAccounts.length !== 0) {
|
||||
filter.push(`account.handle NOT IN ${buildInValuesArray(search.blockedAccounts)}`)
|
||||
}
|
||||
|
||||
if (search.blockedHosts && search.blockedHosts.length !== 0) {
|
||||
filter.push(`host NOT IN ${buildInValuesArray(search.blockedHosts)}`)
|
||||
}
|
||||
|
||||
if (search.startDate) filter.push(`publishedAt >= ${new Date(search.startDate).getTime()}`)
|
||||
if (search.endDate) filter.push(`publishedAt <= ${new Date(search.endDate).getTime()}`)
|
||||
|
||||
if (search.originallyPublishedStartDate) {
|
||||
filter.push(`originallyPublishedAt >= ${new Date(search.originallyPublishedStartDate).getTime()}`)
|
||||
}
|
||||
if (search.originallyPublishedEndDate) {
|
||||
filter.push(`originallyPublishedAt <= ${new Date(search.originallyPublishedEndDate).getTime()}`)
|
||||
}
|
||||
|
||||
if (search.categoryOneOf && search.categoryOneOf.length !== 0) {
|
||||
filter.push(`category.id IN ${buildInValuesArray(search.categoryOneOf)}`)
|
||||
}
|
||||
|
||||
if (search.licenceOneOf && search.licenceOneOf.length !== 0) {
|
||||
filter.push(`licence.id IN ${buildInValuesArray(search.licenceOneOf)}`)
|
||||
}
|
||||
|
||||
if (search.languageOneOf && search.languageOneOf.length !== 0) {
|
||||
filter.push(`language.id IN ${buildInValuesArray(search.languageOneOf)}`)
|
||||
}
|
||||
|
||||
if (search.durationMin) filter.push(`duration >= ${search.durationMin}`)
|
||||
if (search.durationMax) filter.push(`duration <= ${search.durationMax}`)
|
||||
|
||||
if (search.nsfw && search.nsfw !== 'both') filter.push(`nsfw = ${search.nsfw}`)
|
||||
|
||||
if (exists(search.isLive)) filter.push(`isLive = ${search.isLive}`)
|
||||
if (search.host) filter.push(`account.host = '${search.host}'`)
|
||||
if (search.uuids) addUUIDFilters(filter, search.uuids)
|
||||
|
||||
if (search.tagsOneOf && search.tagsOneOf.length !== 0) {
|
||||
const tagsOneOf = search.tagsOneOf.map(t => t.toLowerCase())
|
||||
filter.push(`tags IN ${buildInValuesArray(tagsOneOf)}`)
|
||||
}
|
||||
|
||||
if (search.tagsAllOf && search.tagsAllOf.length !== 0) {
|
||||
const tagsAllOf = search.tagsAllOf.map(t => t.toLowerCase())
|
||||
const clause = tagsAllOf.map(tag => `tags = '${tag}'`).join(' AND ')
|
||||
|
||||
filter.push(clause)
|
||||
}
|
||||
|
||||
logger.debug({ filter }, 'Will query Meilisearch for videos.')
|
||||
|
||||
if (search.boostLanguages && search.boostLanguages.length !== 0) {
|
||||
// No linguistic content
|
||||
filter.push(`language.id IN ${buildInValuesArray([ ...search.boostLanguages, 'zxx' ])} OR language.id IS NULL`)
|
||||
}
|
||||
|
||||
const result = await client.index(CONFIG.MEILISEARCH.INDEXES.VIDEOS).search(search.search, {
|
||||
offset: search.start,
|
||||
limit: search.count,
|
||||
sort: buildSort(search.sort),
|
||||
showRankingScore: true,
|
||||
filter
|
||||
})
|
||||
|
||||
return extractSearchQueryResult(result)
|
||||
}
|
||||
|
||||
export async function getVideosUpdatedAt (uuids: string[]): Promise<{ updatedAt: number, uuid: string }[]> {
|
||||
const result = await client.index(CONFIG.MEILISEARCH.INDEXES.VIDEOS).getDocuments({
|
||||
fields: [ 'updatedAt', 'uuid' ],
|
||||
filter: [ buildInQuery('uuid', uuids) ]
|
||||
})
|
||||
|
||||
return result.results
|
||||
}
|
||||
|
||||
export function formatVideoForDB (v: IndexableVideo | IndexableVideoDetails): DBVideo | DBVideoDetails {
|
||||
const video = {
|
||||
id: v.id,
|
||||
uuid: v.uuid,
|
||||
shortUUID: v.shortUUID,
|
||||
|
||||
indexedAt: new Date().getTime(),
|
||||
createdAt: new Date(v.createdAt).getTime(),
|
||||
updatedAt: new Date(v.updatedAt).getTime(),
|
||||
publishedAt: new Date(v.publishedAt).getTime(),
|
||||
originallyPublishedAt: new Date(v.originallyPublishedAt).getTime(),
|
||||
|
||||
category: {
|
||||
id: v.category.id,
|
||||
label: v.category.label
|
||||
},
|
||||
licence: {
|
||||
id: v.licence.id,
|
||||
label: v.licence.label
|
||||
},
|
||||
language: {
|
||||
id: v.language.id,
|
||||
label: v.language.label
|
||||
},
|
||||
privacy: {
|
||||
id: v.privacy.id,
|
||||
label: v.privacy.label
|
||||
},
|
||||
|
||||
name: v.name,
|
||||
|
||||
truncatedDescription: v.truncatedDescription,
|
||||
description: v.description,
|
||||
searchableDescription: (v.description || v.truncatedDescription || '').slice(0, 250),
|
||||
|
||||
waitTranscoding: v.waitTranscoding,
|
||||
|
||||
duration: v.duration,
|
||||
|
||||
thumbnailPath: v.thumbnailPath,
|
||||
previewPath: v.previewPath,
|
||||
embedPath: v.embedPath,
|
||||
|
||||
views: v.views,
|
||||
viewers: v.viewers,
|
||||
likes: v.likes,
|
||||
dislikes: v.dislikes,
|
||||
|
||||
isLive: v.isLive || false,
|
||||
nsfw: v.nsfw,
|
||||
|
||||
host: v.host,
|
||||
url: v.url,
|
||||
|
||||
files: v.files,
|
||||
streamingPlaylists: v.streamingPlaylists,
|
||||
|
||||
tags: (v as IndexableVideoDetails).tags ? (v as IndexableVideoDetails).tags : undefined,
|
||||
|
||||
account: formatActorForDB(v.account),
|
||||
channel: formatActorForDB(v.channel)
|
||||
}
|
||||
|
||||
if (isVideoDetails(v)) {
|
||||
return {
|
||||
...video,
|
||||
|
||||
trackerUrls: v.trackerUrls,
|
||||
|
||||
descriptionPath: v.descriptionPath,
|
||||
|
||||
support: v.support,
|
||||
|
||||
commentsEnabled: v.commentsEnabled,
|
||||
downloadEnabled: v.downloadEnabled
|
||||
}
|
||||
}
|
||||
|
||||
return video
|
||||
}
|
||||
|
||||
export function formatVideoForAPI (v: DBVideoDetails, fromHost?: string): APIVideo {
|
||||
return {
|
||||
id: v.id,
|
||||
uuid: v.uuid,
|
||||
shortUUID: v.shortUUID,
|
||||
|
||||
score: v._rankingScore,
|
||||
|
||||
createdAt: new Date(v.createdAt),
|
||||
updatedAt: new Date(v.updatedAt),
|
||||
publishedAt: new Date(v.publishedAt),
|
||||
originallyPublishedAt: new Date(v.originallyPublishedAt),
|
||||
|
||||
category: {
|
||||
id: v.category.id,
|
||||
label: v.category.label
|
||||
},
|
||||
licence: {
|
||||
id: v.licence.id,
|
||||
label: v.licence.label
|
||||
},
|
||||
language: {
|
||||
id: v.language.id,
|
||||
label: v.language.label
|
||||
},
|
||||
privacy: {
|
||||
id: v.privacy.id,
|
||||
label: v.privacy.label
|
||||
},
|
||||
|
||||
name: v.name,
|
||||
description: v.description,
|
||||
truncatedDescription: v.truncatedDescription,
|
||||
duration: v.duration,
|
||||
|
||||
tags: v.tags,
|
||||
|
||||
thumbnailPath: v.thumbnailPath,
|
||||
thumbnailUrl: buildUrl(v.host, v.thumbnailPath),
|
||||
|
||||
previewPath: v.previewPath,
|
||||
previewUrl: buildUrl(v.host, v.previewPath),
|
||||
|
||||
embedPath: v.embedPath,
|
||||
embedUrl: buildUrl(v.host, v.embedPath),
|
||||
|
||||
url: v.url,
|
||||
|
||||
isLocal: fromHost && fromHost === v.host,
|
||||
|
||||
views: v.views,
|
||||
viewers: v.viewers,
|
||||
likes: v.likes,
|
||||
dislikes: v.dislikes,
|
||||
|
||||
isLive: v.isLive,
|
||||
nsfw: v.nsfw,
|
||||
|
||||
account: formatActorSummaryForAPI(v.account),
|
||||
channel: formatActorSummaryForAPI(v.channel)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function isVideoDetails (video: IndexableVideo | IndexableVideoDetails): video is IndexableVideoDetails {
|
||||
return (video as IndexableVideoDetails).commentsEnabled !== undefined
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './meilisearch-actor'
|
||||
export * from './meilisearch-avatar'
|
||||
export * from './query-helpers'
|
|
@ -0,0 +1,32 @@
|
|||
import { AccountSummary, VideoChannelSummary } from '@peertube/peertube-types'
|
||||
import { formatActorImageForAPI, formatActorImageForDB, formatActorImagesForAPI, formatActorImagesForDB } from './meilisearch-avatar'
|
||||
import { DBAccountSummary } from '../../../types/account.model'
|
||||
import { DBChannelSummary } from '../../../types/channel.model'
|
||||
|
||||
export function formatActorSummaryForAPI (actor: DBAccountSummary | DBChannelSummary) {
|
||||
return {
|
||||
id: actor.id,
|
||||
name: actor.name,
|
||||
displayName: actor.displayName,
|
||||
url: actor.url,
|
||||
host: actor.host,
|
||||
|
||||
avatar: formatActorImageForAPI(actor.avatar),
|
||||
avatars: formatActorImagesForAPI(actor.avatars, actor.avatar)
|
||||
}
|
||||
}
|
||||
|
||||
export function formatActorForDB (actor: AccountSummary | VideoChannelSummary) {
|
||||
return {
|
||||
id: actor.id,
|
||||
name: actor.name,
|
||||
displayName: actor.displayName,
|
||||
url: actor.url,
|
||||
host: actor.host,
|
||||
|
||||
handle: `${actor.name}@${actor.host}`,
|
||||
|
||||
avatar: formatActorImageForDB(actor.avatar, actor.host),
|
||||
avatars: formatActorImagesForDB(actor.avatars, actor.host)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { ActorImage } from '@peertube/peertube-types'
|
||||
import { buildUrl } from '../../../helpers/utils'
|
||||
import { DBActorImage } from '../../../types/actor.model'
|
||||
|
||||
export function formatActorImageForAPI (image?: DBActorImage) {
|
||||
if (!image) return null
|
||||
|
||||
return {
|
||||
url: image.url,
|
||||
path: image.path,
|
||||
width: image.width,
|
||||
createdAt: new Date(image.createdAt),
|
||||
updatedAt: new Date(image.updatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
export function formatActorImagesForAPI (images?: DBActorImage[], image?: DBActorImage) {
|
||||
// Does not exist in PeerTube < 4.2
|
||||
if (!images) {
|
||||
if (!image) return []
|
||||
|
||||
return [ formatActorImageForAPI(image) ]
|
||||
}
|
||||
|
||||
return images.map(a => formatActorImageForAPI(a))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function formatActorImageForDB (image: ActorImage, host: string) {
|
||||
if (!image) return null
|
||||
|
||||
return {
|
||||
url: buildUrl(host, image.path),
|
||||
path: image.path,
|
||||
width: image.width,
|
||||
createdAt: new Date(image.createdAt).getTime(),
|
||||
updatedAt: new Date(image.updatedAt).getTime()
|
||||
}
|
||||
}
|
||||
|
||||
export function formatActorImagesForDB (images: ActorImage[], host: string) {
|
||||
if (!images) return null
|
||||
|
||||
return images.map(image => formatActorImageForDB(image, host))
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import validator from 'validator'
|
||||
import { buildInQuery } from '../meilisearch-queries'
|
||||
|
||||
export function addUUIDFilters (filters: string[], uuids: string[]) {
|
||||
if (!filters || filters.length === 0) return
|
||||
|
||||
const result = {
|
||||
shortUUIDs: [] as string[],
|
||||
uuids: [] as string[]
|
||||
}
|
||||
|
||||
for (const uuid of uuids) {
|
||||
if (validator.isUUID(uuid)) result.uuids.push(uuid)
|
||||
else result.shortUUIDs.push(uuid)
|
||||
}
|
||||
|
||||
const parts: string[] = []
|
||||
if (result.uuids.length !== 0) parts.push(buildInQuery('uuid', result.uuids))
|
||||
if (result.shortUUIDs.length !== 0) parts.push(buildInQuery('shortUUID', result.shortUUIDs))
|
||||
|
||||
filters.push(parts.join(' OR '))
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
import { IndexablePlaylist } from 'server/types/playlist.model'
|
||||
import { ResultList, Video, VideoChannel, VideoDetails, VideoPlaylist, VideosCommonQuery } from '@peertube/peertube-types'
|
||||
import { doRequestWithRetries } from '../../helpers/requests'
|
||||
import { INDEXER_COUNT, REQUESTS } from '../../initializers/constants'
|
||||
import { IndexableChannel } from '../../types/channel.model'
|
||||
import { IndexableDoc } from '../../types/indexable-doc.model'
|
||||
import { IndexableVideo } from '../../types/video.model'
|
||||
import { IndexablePlaylist } from '../../types/playlist.model'
|
||||
|
||||
async function getVideo (host: string, uuid: string): Promise<IndexableVideo> {
|
||||
const url = 'https://' + host + '/api/v1/videos/' + uuid
|
||||
|
@ -91,7 +91,6 @@ async function getPlaylistsOf (host: string, handle: string, start: number): Pro
|
|||
|
||||
function prepareVideoForDB <T extends Video> (video: T, host: string): T & IndexableDoc {
|
||||
return Object.assign(video, {
|
||||
elasticSearchId: host + video.id,
|
||||
host,
|
||||
url: 'https://' + host + '/videos/watch/' + video.uuid
|
||||
})
|
||||
|
@ -99,7 +98,6 @@ function prepareVideoForDB <T extends Video> (video: T, host: string): T & Index
|
|||
|
||||
function prepareChannelForDB (channel: VideoChannel, host: string, videosCount: number): IndexableChannel {
|
||||
return Object.assign(channel, {
|
||||
elasticSearchId: host + channel.id,
|
||||
host,
|
||||
videosCount,
|
||||
url: 'https://' + host + '/video-channels/' + channel.name
|
||||
|
@ -108,7 +106,6 @@ function prepareChannelForDB (channel: VideoChannel, host: string, videosCount:
|
|||
|
||||
function preparePlaylistForDB (playlist: VideoPlaylist, host: string): IndexablePlaylist {
|
||||
return Object.assign(playlist, {
|
||||
elasticSearchId: host + playlist.id,
|
||||
host,
|
||||
url: 'https://' + host + '/videos/watch/playlist/' + playlist.uuid
|
||||
})
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import Bluebird from 'bluebird'
|
||||
import { IndexablePlaylist } from 'server/types/playlist.model'
|
||||
import { inspect } from 'util'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { INDEXER_HOST_CONCURRENCY, INDEXER_COUNT, INDEXER_LIMIT, SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
|
||||
import { INDEXER_COUNT, INDEXER_LIMIT, SCHEDULER_INTERVALS_MS, CONFIG } from '../../initializers/constants'
|
||||
import { IndexableVideo } from '../../types/video.model'
|
||||
import { buildInstanceHosts } from '../elastic-search/elastic-search-instances'
|
||||
import { buildInstanceHosts } from '../meilisearch/meilisearch-instances'
|
||||
import { ChannelIndexer } from '../indexers/channel-indexer'
|
||||
import { PlaylistIndexer } from '../indexers/playlist-indexer'
|
||||
import { VideoIndexer } from '../indexers/video-indexer'
|
||||
import { getPlaylistsOf, getVideos } from '../requests/peertube-instance'
|
||||
import { AbstractScheduler } from './abstract-scheduler'
|
||||
import { IndexablePlaylist } from '../../types/playlist.model'
|
||||
import { buildDBChannelPrimaryKey } from '../meilisearch/meilisearch-channels'
|
||||
import { getVideosUpdatedAt } from '../meilisearch/meilisearch-videos'
|
||||
|
||||
export class IndexationScheduler extends AbstractScheduler {
|
||||
|
||||
|
@ -54,35 +56,30 @@ export class IndexationScheduler extends AbstractScheduler {
|
|||
private async runIndexer () {
|
||||
logger.info('Running indexer.')
|
||||
|
||||
const { indexHosts, removedHosts } = await buildInstanceHosts()
|
||||
this.indexedHosts = indexHosts
|
||||
this.indexedHosts = await buildInstanceHosts()
|
||||
|
||||
logger.info({ indexHosts: this.indexedHosts }, `Will index ${this.indexedHosts.length} hosts and remove non existing hosts`)
|
||||
|
||||
for (const o of this.indexers) {
|
||||
await o.removeFromHosts(removedHosts)
|
||||
await o.removeFromHosts(this.indexedHosts)
|
||||
}
|
||||
|
||||
logger.info({ indexHosts }, 'Will index %s hosts', indexHosts.length)
|
||||
|
||||
await Bluebird.map(indexHosts, async host => {
|
||||
await Bluebird.map(this.indexedHosts, async host => {
|
||||
try {
|
||||
await this.indexHost(host)
|
||||
} catch (err) {
|
||||
console.error(inspect(err, { depth: 10 }))
|
||||
logger.warn({ err: inspect(err) }, 'Cannot index videos from %s.', host)
|
||||
}
|
||||
}, { concurrency: INDEXER_HOST_CONCURRENCY })
|
||||
|
||||
for (const o of this.indexers) {
|
||||
await o.refreshIndex()
|
||||
}
|
||||
}, { concurrency: CONFIG.INDEXER.HOST_CONCURRENCY })
|
||||
|
||||
logger.info('Indexer ended.')
|
||||
}
|
||||
|
||||
private async indexHost (host: string) {
|
||||
const channelsToSync = new Set<string>()
|
||||
const existingChannelsId = new Set<number>()
|
||||
const existingVideosId = new Set<number>()
|
||||
const existingChannelsId = new Set<string>()
|
||||
const existingVideosId = new Set<string>()
|
||||
|
||||
let videos: IndexableVideo[] = []
|
||||
let start = 0
|
||||
|
@ -90,6 +87,8 @@ export class IndexationScheduler extends AbstractScheduler {
|
|||
logger.info('Adding video data from %s.', host)
|
||||
|
||||
do {
|
||||
await this.videoIndexer.waitForBulkIndexation()
|
||||
|
||||
logger.debug('Getting video results from %s (from = %d).', host, start)
|
||||
|
||||
videos = await getVideos(host, start)
|
||||
|
@ -98,30 +97,36 @@ export class IndexationScheduler extends AbstractScheduler {
|
|||
start += videos.length
|
||||
|
||||
if (videos.length !== 0) {
|
||||
const { created } = await this.videoIndexer.indexElements(videos)
|
||||
const videosFromDB = await getVideosUpdatedAt(videos.map(v => v.uuid))
|
||||
|
||||
logger.debug('Indexed %d videos from %s.', videos.length, host)
|
||||
logger.debug('Indexing %d videos from %s.', videos.length, host)
|
||||
this.videoIndexer.addElementsToBulkIndex(videos)
|
||||
|
||||
// Fetch complete video foreach created video (to get tags)
|
||||
for (const c of created) {
|
||||
this.videoIndexer.scheduleIndexation(host, c.uuid)
|
||||
// Fetch complete video foreach created video (to get tags) if needed
|
||||
for (const video of videos) {
|
||||
const videoDB = videosFromDB.find(v => v.uuid === video.uuid)
|
||||
|
||||
if (!videoDB || videoDB.updatedAt !== new Date(video.updatedAt).getTime()) {
|
||||
this.videoIndexer.scheduleParallelIndexation(host, video.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const video of videos) {
|
||||
channelsToSync.add(video.channel.name)
|
||||
|
||||
existingChannelsId.add(video.channel.id)
|
||||
existingVideosId.add(video.id)
|
||||
existingChannelsId.add(buildDBChannelPrimaryKey(video.channel))
|
||||
existingVideosId.add(video.uuid)
|
||||
}
|
||||
} while (videos.length === INDEXER_COUNT && start < INDEXER_LIMIT)
|
||||
|
||||
logger.info('Added video data from %s.', host)
|
||||
|
||||
for (const c of channelsToSync) {
|
||||
this.channelIndexer.scheduleIndexation(host, c)
|
||||
this.channelIndexer.scheduleParallelIndexation(host, c)
|
||||
}
|
||||
|
||||
logger.info('Removing non-existing channels and videos from ' + host)
|
||||
await this.channelIndexer.removeNotExisting(host, existingChannelsId)
|
||||
await this.videoIndexer.removeNotExisting(host, existingVideosId)
|
||||
|
||||
|
@ -129,7 +134,7 @@ export class IndexationScheduler extends AbstractScheduler {
|
|||
}
|
||||
|
||||
private async indexPlaylists (host: string, channelHandles: string[]) {
|
||||
const existingPlaylistsId = new Set<number>()
|
||||
const existingPlaylistsId = new Set<string>()
|
||||
|
||||
logger.info('Adding playlist data from %s.', host)
|
||||
|
||||
|
@ -138,6 +143,8 @@ export class IndexationScheduler extends AbstractScheduler {
|
|||
let start = 0
|
||||
|
||||
do {
|
||||
await this.playlistIndexer.waitForBulkIndexation()
|
||||
|
||||
logger.debug('Getting playlist results from %s (from = %d, channelHandle = %s).', host, start, channelHandle)
|
||||
|
||||
playlists = await getPlaylistsOf(host, channelHandle, start)
|
||||
|
@ -146,13 +153,13 @@ export class IndexationScheduler extends AbstractScheduler {
|
|||
start += playlists.length
|
||||
|
||||
if (playlists.length !== 0) {
|
||||
await this.playlistIndexer.indexElements(playlists)
|
||||
this.playlistIndexer.addElementsToBulkIndex(playlists)
|
||||
|
||||
logger.debug('Indexed %d playlists from %s.', playlists.length, host)
|
||||
}
|
||||
|
||||
for (const playlist of playlists) {
|
||||
existingPlaylistsId.add(playlist.id)
|
||||
existingPlaylistsId.add(playlist.uuid)
|
||||
}
|
||||
} while (playlists.length === INDEXER_COUNT && start < INDEXER_LIMIT)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { Account, AccountSummary } from '@peertube/peertube-types'
|
||||
import { DBActorImage } from './actor.model'
|
||||
|
||||
type IgnoredAccountFields = 'createdAt' | 'updatedAt' | 'avatar' | 'avatars'
|
||||
|
||||
interface DBAccountShared {
|
||||
handle: string
|
||||
|
||||
avatar: DBActorImage
|
||||
avatars: DBActorImage[]
|
||||
}
|
||||
|
||||
export interface DBAccount extends Omit<Account, IgnoredAccountFields> {
|
||||
handle: string
|
||||
|
||||
avatar: DBActorImage
|
||||
avatars: DBActorImage[]
|
||||
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export interface DBAccountSummary extends DBAccountShared, Omit<AccountSummary, 'avatar' | 'avatars'> {
|
||||
|
||||
}
|
|
@ -1,11 +1,7 @@
|
|||
import { ActorImage } from '@peertube/peertube-types'
|
||||
|
||||
export type AdditionalActorAttributes = {
|
||||
handle: string
|
||||
export type DBActorImage = Omit<ActorImage, 'createdAt' | 'updatedAt'> & {
|
||||
url: string
|
||||
|
||||
avatar: ActorImageExtended
|
||||
avatars: ActorImageExtended[]
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export type ActorImageExtended = ActorImage & { url: string }
|
||||
|
|
|
@ -1,33 +1,42 @@
|
|||
import { Account, VideoChannel, VideoChannelSummary } from '@peertube/peertube-types'
|
||||
import { ActorImageExtended, AdditionalActorAttributes } from './actor.model'
|
||||
import { VideoChannel, VideoChannelSummary } from '@peertube/peertube-types'
|
||||
import { DBActorImage } from './actor.model'
|
||||
import { IndexableDoc } from './indexable-doc.model'
|
||||
import { DBAccount } from './account.model'
|
||||
|
||||
export interface IndexableChannel extends VideoChannel, IndexableDoc {
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface DBChannel extends Omit<VideoChannel, 'isLocal'> {
|
||||
indexedAt: Date
|
||||
interface DBChannelShared {
|
||||
handle: string
|
||||
url: string
|
||||
|
||||
ownerAccount?: Account & AdditionalActorAttributes
|
||||
|
||||
avatar: ActorImageExtended
|
||||
avatars: ActorImageExtended[]
|
||||
|
||||
banner: ActorImageExtended
|
||||
banners: ActorImageExtended[]
|
||||
|
||||
score?: number
|
||||
avatar: DBActorImage
|
||||
avatars: DBActorImage[]
|
||||
}
|
||||
|
||||
export interface DBChannelSummary extends VideoChannelSummary {
|
||||
indexedAt: Date
|
||||
type IgnoredChannelFields = 'ownerAccount' | 'isLocal' | 'createdAt' | 'updatedAt' | 'avatar' | 'avatars' | 'banner' | 'banners'
|
||||
|
||||
export interface DBChannel extends DBChannelShared, Omit<VideoChannel, IgnoredChannelFields> {
|
||||
indexedAt: number
|
||||
|
||||
primaryKey: string
|
||||
|
||||
ownerAccount?: DBAccount
|
||||
|
||||
banner: DBActorImage
|
||||
banners: DBActorImage[]
|
||||
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
|
||||
_rankingScore?: number
|
||||
}
|
||||
|
||||
export interface DBChannelSummary extends DBChannelShared, Omit<VideoChannelSummary, 'avatar' | 'avatars'> {
|
||||
|
||||
}
|
||||
|
||||
// Results from the search API
|
||||
export interface EnhancedVideoChannel extends VideoChannel {
|
||||
export interface APIVideoChannel extends VideoChannel {
|
||||
videosCount: number
|
||||
|
||||
score: number
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
export interface IndexableDoc {
|
||||
elasticSearchId: string
|
||||
host: string
|
||||
url: string
|
||||
}
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
import { AccountSummary, VideoChannelSummary, VideoPlaylist } from '@peertube/peertube-types'
|
||||
import { AdditionalActorAttributes } from './actor.model'
|
||||
import { VideoPlaylist } from '@peertube/peertube-types'
|
||||
import { IndexableDoc } from './indexable-doc.model'
|
||||
import { DBAccountSummary } from './account.model'
|
||||
import { DBChannelSummary } from './channel.model'
|
||||
|
||||
export interface IndexablePlaylist extends VideoPlaylist, IndexableDoc {
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface DBPlaylist extends Omit<VideoPlaylist, 'isLocal'> {
|
||||
indexedAt: Date
|
||||
export interface DBPlaylist extends Omit<VideoPlaylist, 'isLocal' | 'createdAt' | 'updatedAt' | 'ownerAccount' | 'videoChannel'> {
|
||||
indexedAt: number
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
|
||||
host: string
|
||||
|
||||
// Added by the query
|
||||
score?: number
|
||||
_rankingScore?: number
|
||||
|
||||
ownerAccount: AccountSummary & AdditionalActorAttributes
|
||||
videoChannel: VideoChannelSummary & AdditionalActorAttributes
|
||||
ownerAccount: DBAccountSummary
|
||||
videoChannel: DBChannelSummary
|
||||
}
|
||||
|
||||
// Results from the search API
|
||||
export interface EnhancedPlaylist extends VideoPlaylist {
|
||||
export interface APIPlaylist extends VideoPlaylist {
|
||||
score: number
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Account, AccountSummary, Video, VideoChannel, VideoChannelSummary, VideoDetails } from '@peertube/peertube-types'
|
||||
import { AdditionalActorAttributes } from './actor.model'
|
||||
import { Video, VideoDetails } from '@peertube/peertube-types'
|
||||
import { IndexableDoc } from './indexable-doc.model'
|
||||
import { DBChannel, DBChannelSummary } from './channel.model'
|
||||
import { DBAccount, DBAccountSummary } from './account.model'
|
||||
|
||||
export interface IndexableVideo extends Video, IndexableDoc {
|
||||
}
|
||||
|
@ -8,28 +9,36 @@ export interface IndexableVideo extends Video, IndexableDoc {
|
|||
export interface IndexableVideoDetails extends VideoDetails, IndexableDoc {
|
||||
}
|
||||
|
||||
export interface DBVideoDetails extends Omit<VideoDetails, 'isLocal'> {
|
||||
indexedAt: Date
|
||||
type IgnoredVideoFields = 'isLocal' | 'createdAt' | 'updatedAt' | 'publishedAt' | 'originallyPublishedAt' | 'channel' | 'account'
|
||||
|
||||
interface DBVideoShared {
|
||||
indexedAt: number
|
||||
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
publishedAt: number
|
||||
originallyPublishedAt: number
|
||||
|
||||
host: string
|
||||
url: string
|
||||
|
||||
account: Account & AdditionalActorAttributes
|
||||
channel: VideoChannel & AdditionalActorAttributes
|
||||
|
||||
score?: number
|
||||
searchableDescription: string
|
||||
}
|
||||
|
||||
export interface DBVideo extends Omit<Video, 'isLocal'> {
|
||||
indexedAt: Date
|
||||
host: string
|
||||
url: string
|
||||
export interface DBVideo extends Omit<Video, IgnoredVideoFields>, DBVideoShared {
|
||||
account: DBAccountSummary
|
||||
channel: DBChannelSummary
|
||||
}
|
||||
|
||||
account: AccountSummary & AdditionalActorAttributes
|
||||
channel: VideoChannelSummary & AdditionalActorAttributes
|
||||
export interface DBVideoDetails extends Omit<VideoDetails, IgnoredVideoFields>, DBVideoShared {
|
||||
account: DBAccount
|
||||
channel: DBChannel
|
||||
|
||||
_rankingScore?: number
|
||||
}
|
||||
|
||||
// Results from the search API
|
||||
export interface EnhancedVideo extends Video {
|
||||
export interface APIVideo extends Video {
|
||||
tags: VideoDetails['tags']
|
||||
|
||||
score: number
|
||||
|
|
|
@ -16,13 +16,11 @@
|
|||
],
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"baseUrl": "."
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"client",
|
||||
"PeerTube"
|
||||
"client"
|
||||
]
|
||||
}
|
||||
|
|
93
yarn.lock
93
yarn.lock
|
@ -980,26 +980,6 @@
|
|||
enabled "2.0.x"
|
||||
kuler "^2.0.0"
|
||||
|
||||
"@elastic/elasticsearch@^8.2.1":
|
||||
version "8.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-8.5.0.tgz#407aee0950a082ee76735a567f2571cf4301d4ea"
|
||||
integrity sha512-iOgr/3zQi84WmPhAplnK2W13R89VXD2oc6WhlQmH3bARQwmI+De23ZJKBEn7bvuG/AHMAqasPXX7uJIiJa2MqQ==
|
||||
dependencies:
|
||||
"@elastic/transport" "^8.2.0"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@elastic/transport@^8.2.0":
|
||||
version "8.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.3.1.tgz#e7569d7df35b03108ea7aa886113800245faa17f"
|
||||
integrity sha512-jv/Yp2VLvv5tSMEOF8iGrtL2YsYHbpf4s+nDsItxUTLFTzuJGpnsB/xBlfsoT2kAYEnWHiSJuqrbRcpXEI/SEQ==
|
||||
dependencies:
|
||||
debug "^4.3.4"
|
||||
hpagent "^1.0.0"
|
||||
ms "^2.1.3"
|
||||
secure-json-parse "^2.4.0"
|
||||
tslib "^2.4.0"
|
||||
undici "^5.5.1"
|
||||
|
||||
"@eslint/eslintrc@^1.3.3":
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95"
|
||||
|
@ -1202,7 +1182,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/async/-/async-3.2.16.tgz#7473aa015cf8a60584a94dc79b9203d465c32b41"
|
||||
integrity sha512-jnlGp5Z/cAZ7JVYyLnSDuYJ+YyYm0o2yzL8Odv6ckWmGMow3j/P/wgfziybB044cXXA93lEuymJyxVR8Iz2amQ==
|
||||
|
||||
"@types/bluebird@*", "@types/bluebird@^3.5.33":
|
||||
"@types/bluebird@^3.5.33":
|
||||
version "3.5.38"
|
||||
resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.38.tgz#7a671e66750ccd21c9fc9d264d0e1e5330bc9908"
|
||||
integrity sha512-yR/Kxc0dd4FfwtEoLZMoqJbM/VE/W7hXn/MIjb+axcwag0iFmSPK7OBUZq1YWLynJUoWQkfUrI7T0HDqGApNSg==
|
||||
|
@ -1242,13 +1222,6 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/continuation-local-storage@*":
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/continuation-local-storage/-/continuation-local-storage-3.2.4.tgz#655c8ffd9327aa60fb8ae773a5f2efbc973a7cbb"
|
||||
integrity sha512-OT32vCVMymU1JMPKDeY0lX3cduAr0Pm/VwIbxygMeDS4lRcv57qYXn9bMwBRcRnEpXKBb/r4xYaZCARTZllP0A==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/cookie@^0.4.1":
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
|
||||
|
@ -1333,7 +1306,7 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/lodash@*", "@types/lodash@^4.14.182":
|
||||
"@types/lodash@^4.14.182":
|
||||
version "4.14.191"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
|
||||
integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
|
||||
|
@ -1428,16 +1401,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
|
||||
integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==
|
||||
|
||||
"@types/sequelize@^4.28.13":
|
||||
version "4.28.14"
|
||||
resolved "https://registry.yarnpkg.com/@types/sequelize/-/sequelize-4.28.14.tgz#70302689ddef09f5d472b548a14e5a150e690aff"
|
||||
integrity sha512-O8lTJ8YPVVaoY9xjduchDlo0MOS3w262pro2H1QMuFIo/kc/p1elP/UxLOTP2wcVO2cFd6Gvghg9ZSAiJi0GLA==
|
||||
dependencies:
|
||||
"@types/bluebird" "*"
|
||||
"@types/continuation-local-storage" "*"
|
||||
"@types/lodash" "*"
|
||||
"@types/validator" "*"
|
||||
|
||||
"@types/serve-static@*":
|
||||
version "1.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155"
|
||||
|
@ -1451,7 +1414,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
|
||||
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
|
||||
|
||||
"@types/validator@*", "@types/validator@^13.0.0", "@types/validator@^13.7.1", "@types/validator@^13.7.2":
|
||||
"@types/validator@^13.0.0", "@types/validator@^13.7.1", "@types/validator@^13.7.2":
|
||||
version "13.7.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.10.tgz#f9763dc0933f8324920afa9c0790308eedf55ca7"
|
||||
integrity sha512-t1yxFAR2n0+VO6hd/FJ9F2uezAZVWHLmpmlJzm1eX03+H7+HsuTAp7L8QJs+2pQCfWkP1+EXsGK9Z9v7o/qPVQ==
|
||||
|
@ -1824,7 +1787,7 @@ bullmq@^1.87.0:
|
|||
tslib "^2.0.0"
|
||||
uuid "^9.0.0"
|
||||
|
||||
busboy@^1.0.0, busboy@^1.6.0:
|
||||
busboy@^1.0.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
|
||||
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
|
||||
|
@ -2039,6 +2002,13 @@ cron-parser@^4.6.0:
|
|||
dependencies:
|
||||
luxon "^3.1.0"
|
||||
|
||||
cross-fetch@^3.1.6:
|
||||
version "3.1.8"
|
||||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82"
|
||||
integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==
|
||||
dependencies:
|
||||
node-fetch "^2.6.12"
|
||||
|
||||
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
|
@ -3496,6 +3466,13 @@ media-typer@0.3.0:
|
|||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
|
||||
|
||||
meilisearch@^0.35.0:
|
||||
version "0.35.0"
|
||||
resolved "https://registry.yarnpkg.com/meilisearch/-/meilisearch-0.35.0.tgz#c94d5f266e39ad5ed5a666782e729805a4e8a956"
|
||||
integrity sha512-gF1I6K5/Wpe7BWfjBnG+o19y/FIpJ9HbN+byON6CB9U3uE7qc6GvwUbjKOllh7LKXQVVxH/kCu7Jn0ODCUwqbQ==
|
||||
dependencies:
|
||||
cross-fetch "^3.1.6"
|
||||
|
||||
memoizee@^0.4.14:
|
||||
version "0.4.15"
|
||||
resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72"
|
||||
|
@ -3634,7 +3611,7 @@ ms@2.1.2:
|
|||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
ms@2.1.3, ms@^2.1.1, ms@^2.1.3:
|
||||
ms@2.1.3, ms@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
@ -3702,6 +3679,13 @@ next-tick@1, next-tick@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
|
||||
integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
|
||||
|
||||
node-fetch@^2.6.12:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-gyp-build-optional-packages@5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17"
|
||||
|
@ -4686,6 +4670,11 @@ tough-cookie@~2.5.0:
|
|||
psl "^1.1.28"
|
||||
punycode "^2.1.1"
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
triple-beam@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
|
||||
|
@ -4787,13 +4776,6 @@ unbox-primitive@^1.0.2:
|
|||
has-symbols "^1.0.3"
|
||||
which-boxed-primitive "^1.0.2"
|
||||
|
||||
undici@^5.5.1:
|
||||
version "5.14.0"
|
||||
resolved "https://registry.yarnpkg.com/undici/-/undici-5.14.0.tgz#1169d0cdee06a4ffdd30810f6228d57998884d00"
|
||||
integrity sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==
|
||||
dependencies:
|
||||
busboy "^1.6.0"
|
||||
|
||||
universalify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||
|
@ -4855,6 +4837,19 @@ verror@1.10.0:
|
|||
core-util-is "1.0.2"
|
||||
extsprintf "^1.2.0"
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
which-boxed-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
|
||||
|
|
Loading…
Reference in New Issue