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>
123 lines
3.7 KiB
Go
123 lines
3.7 KiB
Go
// Package search — поиск по каталогу через Meilisearch (docs/library-and-storage.md#поиск).
|
||
package search
|
||
|
||
import (
|
||
"strings"
|
||
"unicode"
|
||
)
|
||
|
||
// Кириллица → латиница (упрощённая ГОСТ-подобная схема: «Кино» → «kino», «Щедрин» → «shchedrin»).
|
||
var cyrToLat = map[rune]string{
|
||
'а': "a", 'б': "b", 'в': "v", 'г': "g", 'д': "d", 'е': "e", 'ё': "e", 'ж': "zh", 'з': "z", 'и': "i",
|
||
'й': "y", 'к': "k", 'л': "l", 'м': "m", 'н': "n", 'о': "o", 'п': "p", 'р': "r", 'с': "s", 'т': "t",
|
||
'у': "u", 'ф': "f", 'х': "kh", 'ц': "ts", 'ч': "ch", 'ш': "sh", 'щ': "shch", 'ъ': "", 'ы': "y", 'ь': "",
|
||
'э': "e", 'ю': "yu", 'я': "ya", 'і': "i", 'ї': "yi", 'є': "ye", 'ґ': "g",
|
||
}
|
||
|
||
// Латиница → кириллица: сначала длинные сочетания.
|
||
var latToCyr = []struct{ lat, cyr string }{
|
||
{"shch", "щ"}, {"zh", "ж"}, {"kh", "х"}, {"ts", "ц"}, {"ch", "ч"}, {"sh", "ш"}, {"yu", "ю"}, {"ya", "я"},
|
||
{"yo", "ё"}, {"a", "а"}, {"b", "б"}, {"v", "в"}, {"g", "г"}, {"d", "д"}, {"e", "е"}, {"z", "з"},
|
||
{"i", "и"}, {"y", "й"}, {"k", "к"}, {"l", "л"}, {"m", "м"}, {"n", "н"}, {"o", "о"}, {"p", "п"},
|
||
{"r", "р"}, {"s", "с"}, {"t", "т"}, {"u", "у"}, {"f", "ф"}, {"h", "х"}, {"c", "к"}, {"w", "в"},
|
||
{"x", "кс"}, {"q", "к"}, {"j", "дж"},
|
||
}
|
||
|
||
// Translit — вариант строки в другой письменности для поля *_translit: кириллица → латиница,
|
||
// латиница → кириллица. Смешанные строки переводятся посимвольно. Пустая строка — перевод не нужен.
|
||
func Translit(s string) string {
|
||
s = strings.ToLower(s)
|
||
hasCyr, hasLat := false, false
|
||
for _, r := range s {
|
||
switch {
|
||
case unicode.Is(unicode.Cyrillic, r):
|
||
hasCyr = true
|
||
case r < unicode.MaxASCII && unicode.IsLetter(r):
|
||
hasLat = true
|
||
}
|
||
}
|
||
var b strings.Builder
|
||
switch {
|
||
case hasCyr:
|
||
for _, r := range s {
|
||
if v, ok := cyrToLat[r]; ok {
|
||
b.WriteString(v)
|
||
} else {
|
||
b.WriteRune(r)
|
||
}
|
||
}
|
||
case hasLat:
|
||
for i := 0; i < len(s); {
|
||
matched := false
|
||
for _, p := range latToCyr {
|
||
if strings.HasPrefix(s[i:], p.lat) {
|
||
b.WriteString(p.cyr)
|
||
i += len(p.lat)
|
||
matched = true
|
||
break
|
||
}
|
||
}
|
||
if !matched {
|
||
b.WriteByte(s[i])
|
||
i++
|
||
}
|
||
}
|
||
default:
|
||
return ""
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
// Раскладки ЙЦУКЕН ↔ QWERTY: та же клавиша.
|
||
const (
|
||
qwerty = "`qwertyuiop[]asdfghjkl;'zxcvbnm,./~QWERTYUIOP{}ASDFGHJKL:\"ZXCVBNM<>?"
|
||
jcuken = "ёйцукенгшщзхъфывапролджэячсмитьбю.ЁЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,"
|
||
)
|
||
|
||
var (
|
||
toRu = map[rune]rune{}
|
||
toEn = map[rune]rune{}
|
||
)
|
||
|
||
func init() {
|
||
q, j := []rune(qwerty), []rune(jcuken)
|
||
for i := range q {
|
||
toRu[q[i]] = j[i]
|
||
toEn[j[i]] = q[i]
|
||
}
|
||
}
|
||
|
||
// SwitchLayout — запрос, набранный не в той раскладке: «ghbdtn» → «привет», «руддщ» → «hello».
|
||
// Направление выбирается по большинству букв. Пустая строка — менять нечего.
|
||
func SwitchLayout(s string) string {
|
||
lat, cyr := 0, 0
|
||
for _, r := range s {
|
||
switch {
|
||
case unicode.Is(unicode.Cyrillic, r):
|
||
cyr++
|
||
case r < unicode.MaxASCII && unicode.IsLetter(r):
|
||
lat++
|
||
}
|
||
}
|
||
if lat == 0 && cyr == 0 {
|
||
return ""
|
||
}
|
||
m := toRu
|
||
if cyr > lat {
|
||
m = toEn
|
||
}
|
||
var b strings.Builder
|
||
for _, r := range s {
|
||
if v, ok := m[r]; ok {
|
||
b.WriteRune(v)
|
||
} else {
|
||
b.WriteRune(r)
|
||
}
|
||
}
|
||
out := b.String()
|
||
if out == s {
|
||
return ""
|
||
}
|
||
return out
|
||
}
|