feat(api): search with Meilisearch (M2)
ci / api / api (push) Successful in 27s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m1s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 34s
ci / ts / ts (packages + web) (push) Successful in 1m1s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 25s

- artists, albums and tracks indexes are rebuilt from PostgreSQL next
  to the live ones and swapped in, so search keeps working; a rebuild
  runs on start, after every scan and when a source is removed
- transliteration fields (Кино ↔ kino) and a retry in the other
  keyboard layout (ghbdtn → привет) reported as corrected_query
- /search/suggest (3 artists, 3 albums, 5 tracks), /search by type
  with cursor pages, /admin/search/reindex; Meilisearch is part of
  /readyz
- found ids are hydrated from the catalog, so a lagging index never
  returns deleted entries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 06:51:50 +03:00
1 parent c583991d14
commit c1160cccd3
16 files changed
+1590 -124

No files matched your search

+142
View File
@@ -581,6 +581,65 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/search/suggest": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Выпадающий список поиска — до 3 исполнителей, 3 альбомов, 5 треков
* @description Если по запросу ничего не найдено, сервер повторяет его в другой раскладке клавиатуры
* («ghbdtn» → «привет») и возвращает исправленный запрос в `corrected_query`.
*/
get: operations["searchSuggest"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/search": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Страница поиска
* @description `type=all` — по несколько результатов каждого вида без пагинации;
* конкретный тип — страницы с `next_cursor`.
*/
get: operations["search"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/admin/search/reindex": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Пересобрать поисковые индексы (admin) */
post: operations["reindexSearch"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
@@ -961,6 +1020,20 @@ export interface components {
duration_ms: number;
tracks: components["schemas"]["Track"][];
};
SearchResults: {
artists: components["schemas"]["ArtistRef"][];
albums: components["schemas"]["AlbumSummary"][];
tracks: components["schemas"]["Track"][];
/** @description Оценка общего числа совпадений */
totals: {
artists: number;
albums: number;
tracks: number;
};
/** @description Запрос в другой раскладке, если по исходному ничего не нашлось */
corrected_query: string | null;
next_cursor: string | null;
};
Session: {
/** Format: uuid */
id: string;
@@ -1875,4 +1948,73 @@ export interface operations {
default: components["responses"]["Problem"];
};
};
searchSuggest: {
parameters: {
query: {
q: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Подсказки */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SearchResults"];
};
};
default: components["responses"]["Problem"];
};
};
search: {
parameters: {
query: {
q: string;
type?: "all" | "artists" | "albums" | "tracks";
limit?: components["parameters"]["Limit"];
/** @description Непрозрачный курсор из `next_cursor` предыдущей страницы */
cursor?: components["parameters"]["Cursor"];
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Результаты */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SearchResults"];
};
};
default: components["responses"]["Problem"];
};
};
reindexSearch: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Переиндексация запущена */
202: {
headers: {
[name: string]: unknown;
};
content?: never;
};
default: components["responses"]["Problem"];
};
};
}