Files
pipu-music/services/api/internal/search/translit.go
T
mrleo1nidandClaude Opus 5.5 c1160cccd3
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
feat(api): search with Meilisearch (M2)
- 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>
2026-10-20 06:51:50 +03:00

123 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}