147 lines
4.3 KiB
Go
147 lines
4.3 KiB
Go
package storage
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
|
||
"dana/internal/domain"
|
||
)
|
||
|
||
var likeEscaper = strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`)
|
||
|
||
// likePattern готовит подстроку для ILIKE ... ESCAPE '\': символы % и _ из запроса
|
||
// ищутся буквально, а не работают как шаблон.
|
||
func likePattern(q string) string {
|
||
return "%" + likeEscaper.Replace(q) + "%"
|
||
}
|
||
|
||
func prefixPattern(q string) string {
|
||
return likeEscaper.Replace(q) + "%"
|
||
}
|
||
|
||
// Search ищет по услугам, врачам, прайсу и чек-апам. types ограничивает, где искать
|
||
// (пустая карта — везде); limit — сколько находок на каждый тип.
|
||
func (s *Storage) Search(ctx context.Context, q string, types map[string]bool, limit int) (*domain.SearchResult, error) {
|
||
res := &domain.SearchResult{
|
||
Query: q,
|
||
Services: []domain.SearchHit{},
|
||
Doctors: []domain.SearchHit{},
|
||
Prices: []domain.SearchHit{},
|
||
Checkups: []domain.SearchHit{},
|
||
}
|
||
want := func(t string) bool { return len(types) == 0 || types[t] }
|
||
like, prefix := likePattern(q), prefixPattern(q)
|
||
|
||
// Совпадения с начала названия показываются выше совпадений в середине.
|
||
if want("services") {
|
||
rows, err := s.pool.Query(ctx, `
|
||
SELECT s.name, COALESCE(p.name, ''), s.path, COALESCE(m.path, '')
|
||
FROM services s
|
||
LEFT JOIN services p ON p.id = s.parent_id
|
||
LEFT JOIN media m ON m.id = s.image_id
|
||
WHERE s.published AND s.deleted_at IS NULL
|
||
AND (s.name ILIKE $1 ESCAPE '\' OR s.short_name ILIKE $1 ESCAPE '\')
|
||
ORDER BY (s.name ILIKE $2 ESCAPE '\') DESC, s.parent_id NULLS FIRST, s.name
|
||
LIMIT $3`, like, prefix, limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for rows.Next() {
|
||
h := domain.SearchHit{Type: "service"}
|
||
if err := rows.Scan(&h.Title, &h.Subtitle, &h.Path, &h.Image); err != nil {
|
||
rows.Close()
|
||
return nil, err
|
||
}
|
||
res.Services = append(res.Services, h)
|
||
}
|
||
rows.Close()
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
if want("doctors") {
|
||
rows, err := s.pool.Query(ctx, `
|
||
SELECT concat_ws(' ', d.last_name, d.first_name, d.middle_name),
|
||
d.position, d.slug, COALESCE(m.path, '')
|
||
FROM doctors d
|
||
LEFT JOIN media m ON m.id = d.photo_id
|
||
WHERE d.published AND d.deleted_at IS NULL
|
||
AND (concat_ws(' ', d.last_name, d.first_name, d.middle_name) ILIKE $1 ESCAPE '\'
|
||
OR d.position ILIKE $1 ESCAPE '\')
|
||
ORDER BY (d.last_name ILIKE $2 ESCAPE '\') DESC, d.sort, d.last_name
|
||
LIMIT $3`, like, prefix, limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for rows.Next() {
|
||
h := domain.SearchHit{Type: "doctor"}
|
||
if err := rows.Scan(&h.Title, &h.Subtitle, &h.Slug, &h.Image); err != nil {
|
||
rows.Close()
|
||
return nil, err
|
||
}
|
||
res.Doctors = append(res.Doctors, h)
|
||
}
|
||
rows.Close()
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
if want("prices") {
|
||
rows, err := s.pool.Query(ctx, `
|
||
SELECT pi.name, ps.name, pi.price
|
||
FROM price_items pi
|
||
JOIN price_sections ps ON ps.id = pi.section_id
|
||
WHERE pi.published AND ps.published
|
||
AND pi.name ILIKE $1 ESCAPE '\'
|
||
ORDER BY (pi.name ILIKE $2 ESCAPE '\') DESC, pi.name
|
||
LIMIT $3`, like, prefix, limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for rows.Next() {
|
||
h := domain.SearchHit{Type: "price"}
|
||
var price float64
|
||
if err := rows.Scan(&h.Title, &h.Subtitle, &price); err != nil {
|
||
rows.Close()
|
||
return nil, err
|
||
}
|
||
h.Price = &price
|
||
res.Prices = append(res.Prices, h)
|
||
}
|
||
rows.Close()
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
if want("checkups") {
|
||
rows, err := s.pool.Query(ctx, `
|
||
SELECT c.name, c.lead, c.slug, COALESCE(m.path, '')
|
||
FROM checkups c
|
||
LEFT JOIN media m ON m.id = c.image_id
|
||
WHERE c.published AND c.deleted_at IS NULL
|
||
AND c.name ILIKE $1 ESCAPE '\'
|
||
ORDER BY (c.name ILIKE $2 ESCAPE '\') DESC, c.sort, c.name
|
||
LIMIT $3`, like, prefix, limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for rows.Next() {
|
||
h := domain.SearchHit{Type: "checkup"}
|
||
if err := rows.Scan(&h.Title, &h.Subtitle, &h.Slug, &h.Image); err != nil {
|
||
rows.Close()
|
||
return nil, err
|
||
}
|
||
res.Checkups = append(res.Checkups, h)
|
||
}
|
||
rows.Close()
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
return res, nil
|
||
}
|