181 lines
4.2 KiB
Go
181 lines
4.2 KiB
Go
package search
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/api/query"
|
|
)
|
|
|
|
type Service struct {
|
|
db *pgxpool.Pool
|
|
}
|
|
|
|
func NewService(db *pgxpool.Pool) *Service {
|
|
return &Service{db: db}
|
|
}
|
|
|
|
type Result struct {
|
|
Type string `json:"type"`
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Snippet string `json:"snippet"`
|
|
Date any `json:"date,omitempty"`
|
|
}
|
|
|
|
type SearchResponse struct {
|
|
Query string `json:"query"`
|
|
Results []Result `json:"results"`
|
|
Count int `json:"count"`
|
|
Pagination query.PaginationMeta `json:"pagination,omitempty"`
|
|
}
|
|
|
|
func (s *Service) Search(ctx context.Context, externalID, q, typesRaw string, params query.ListParams) (SearchResponse, error) {
|
|
typeList := parseTypes(typesRaw)
|
|
tsQuery := toTSQuery(q)
|
|
|
|
allResults := make([]Result, 0)
|
|
for _, t := range typeList {
|
|
switch t {
|
|
case "mail":
|
|
mailResults, err := s.searchMail(ctx, externalID, tsQuery, params)
|
|
if err != nil {
|
|
return SearchResponse{}, err
|
|
}
|
|
allResults = append(allResults, mailResults...)
|
|
case "contacts":
|
|
contactResults, err := s.searchContacts(ctx, q, params)
|
|
if err != nil {
|
|
return SearchResponse{}, err
|
|
}
|
|
allResults = append(allResults, contactResults...)
|
|
case "events":
|
|
// Events are in Nextcloud CalDAV, not in PG - skip for now.
|
|
}
|
|
}
|
|
|
|
total := int64(len(allResults))
|
|
page, _ := paginateResults(allResults, params.Offset(), params.Limit())
|
|
|
|
return SearchResponse{
|
|
Query: q,
|
|
Results: page,
|
|
Count: len(page),
|
|
Pagination: params.Meta(&total),
|
|
}, nil
|
|
}
|
|
|
|
func parseTypes(typesRaw string) []string {
|
|
if strings.TrimSpace(typesRaw) == "" {
|
|
return []string{"mail", "contacts", "events"}
|
|
}
|
|
parts := strings.Split(typesRaw, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
if t := strings.TrimSpace(p); t != "" {
|
|
out = append(out, t)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func paginateResults(results []Result, offset, limit int) ([]Result, int64) {
|
|
total := int64(len(results))
|
|
if offset >= len(results) {
|
|
return []Result{}, total
|
|
}
|
|
end := offset + limit
|
|
if end > len(results) {
|
|
end = len(results)
|
|
}
|
|
return results[offset:end], total
|
|
}
|
|
|
|
func (s *Service) searchMail(ctx context.Context, externalID, tsQuery string, params query.ListParams) ([]Result, error) {
|
|
limit := params.Offset() + params.Limit()
|
|
if limit < 20 {
|
|
limit = 20
|
|
}
|
|
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT m.id, m.subject, m.snippet, m.date,
|
|
ts_rank(m.search_vector, to_tsquery('simple', $1)) as rank
|
|
FROM messages m
|
|
JOIN mail_accounts ma ON m.account_id = ma.id
|
|
WHERE ma.user_id = (SELECT id FROM users WHERE external_id = $2)
|
|
AND m.search_vector @@ to_tsquery('simple', $1)
|
|
ORDER BY rank DESC
|
|
LIMIT $3
|
|
`, tsQuery, externalID, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
results := make([]Result, 0)
|
|
for rows.Next() {
|
|
var id, subject, snippet string
|
|
var date any
|
|
var rank float64
|
|
if err := rows.Scan(&id, &subject, &snippet, &date, &rank); err != nil {
|
|
return nil, err
|
|
}
|
|
results = append(results, Result{
|
|
Type: "mail",
|
|
ID: id,
|
|
Title: subject,
|
|
Snippet: snippet,
|
|
Date: date,
|
|
})
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func (s *Service) searchContacts(ctx context.Context, queryText string, params query.ListParams) ([]Result, error) {
|
|
limit := params.Offset() + params.Limit()
|
|
if limit < 10 {
|
|
limit = 10
|
|
}
|
|
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id, name, email FROM users
|
|
WHERE (name ILIKE '%' || $1 || '%' OR email ILIKE '%' || $1 || '%')
|
|
LIMIT $2
|
|
`, queryText, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
results := make([]Result, 0)
|
|
for rows.Next() {
|
|
var id, name, email string
|
|
if err := rows.Scan(&id, &name, &email); err != nil {
|
|
return nil, err
|
|
}
|
|
results = append(results, Result{
|
|
Type: "contact",
|
|
ID: id,
|
|
Title: name,
|
|
Snippet: email,
|
|
})
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func toTSQuery(input string) string {
|
|
words := strings.Fields(input)
|
|
for i, w := range words {
|
|
words[i] = w + ":*"
|
|
}
|
|
return strings.Join(words, " & ")
|
|
}
|