Files
pipu-music/services/api/internal/search/translit.go
T
mrleo1nid 82f3eed9a0
ci / api / api (push) Successful in 56s
ci / db / db (push) Successful in 1m13s
Improve search relevance and artist transliteration
2026-10-30 12:09:24 +03:00

153 lines
4.6 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"
)
// Established spellings cannot always be derived letter by letter. Keep these
// as whole-word alternatives, retaining the mechanical transliteration too.
var pronunciationWords = map[string]string{"boulevard": "бульвар"}
func TranslitVariants(s string) []string {
out := []string{}
if mechanical := Translit(s); mechanical != "" {
out = append(out, mechanical)
}
words := strings.Fields(strings.ToLower(s))
changed := false
for i, word := range words {
if spoken, ok := pronunciationWords[word]; ok {
words[i], changed = spoken, true
} else if translated := Translit(word); translated != "" {
words[i] = translated
}
}
if changed {
out = append(out, strings.Join(words, " "))
}
return out
}
func searchKey(s string) string {
return strings.Join(strings.FieldsFunc(strings.ToLower(s), func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
}), " ")
}
// Кириллица → латиница (упрощённая ГОСТ-подобная схема: «Кино» → «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
}