dana-medical-site/internal/storage/search.go
2026-10-14 13:33:13 +00:00

147 lines
4.3 KiB
Go
Raw Permalink 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 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
}