mirror of
https://github.com/karust/openserp.git
synced 2026-08-05 16:53:54 +08:00
162 lines
4.9 KiB
Go
162 lines
4.9 KiB
Go
package core
|
|
|
|
import "time"
|
|
|
|
// QueryEcho echoes the interpreted query parameters back to the client.
|
|
type QueryEcho struct {
|
|
Text string `json:"text"`
|
|
Lang string `json:"lang,omitempty"`
|
|
Region string `json:"region,omitempty"`
|
|
EnginesRequested []string `json:"engines_requested"`
|
|
}
|
|
|
|
// ResponseMeta carries request-level metadata for observability and debugging.
|
|
type ResponseMeta struct {
|
|
RequestID string `json:"request_id"`
|
|
RequestedAt string `json:"requested_at"`
|
|
TookMs int64 `json:"took_ms"`
|
|
EnginesResponded []string `json:"engines_responded,omitempty"`
|
|
EnginesFailed []string `json:"engines_failed"`
|
|
EngineErrors []EngineErrorDetail `json:"engine_errors,omitempty"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
// EngineErrorDetail is a client-facing, sanitized per-engine failure summary.
|
|
type EngineErrorDetail struct {
|
|
Engine string `json:"engine"`
|
|
Error string `json:"error"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// Pagination carries cursor information for client-side loop termination.
|
|
type Pagination struct {
|
|
Page int `json:"page"`
|
|
HasMore bool `json:"has_more"`
|
|
NextStart int `json:"next_start"`
|
|
}
|
|
|
|
// Envelope is the top-level v2 response wrapper for all search endpoints.
|
|
type Envelope struct {
|
|
Query QueryEcho `json:"query"`
|
|
Meta ResponseMeta `json:"meta"`
|
|
Results []Result `json:"results"`
|
|
SerpFeatures []SerpFeature `json:"serp_features"`
|
|
Pagination Pagination `json:"pagination"`
|
|
// Clusters is only populated by /mega/search (see clusters.go).
|
|
Clusters *[]Cluster `json:"clusters,omitempty"`
|
|
}
|
|
|
|
// Cluster groups results that refer to the same canonical URL across engines.
|
|
// Populated only by /mega/search. Full type defined in clusters.go.
|
|
type Cluster struct {
|
|
ID string `json:"id"`
|
|
CanonicalURL string `json:"canonical_url"`
|
|
Domain string `json:"domain"`
|
|
Title string `json:"title"`
|
|
Occurrences []ClusterOccurrence `json:"occurrences"`
|
|
EnginesCount int `json:"engines_count"`
|
|
BestRank int `json:"best_rank"`
|
|
Score float64 `json:"score"`
|
|
}
|
|
|
|
// ClusterOccurrence links one engine result back into the flat results list.
|
|
type ClusterOccurrence struct {
|
|
Engine string `json:"engine"`
|
|
Rank int `json:"rank"`
|
|
ResultID string `json:"result_id"`
|
|
}
|
|
|
|
// ImageEnvelope is the top-level v2 response wrapper for image search endpoints.
|
|
type ImageEnvelope struct {
|
|
Query QueryEcho `json:"query"`
|
|
Meta ResponseMeta `json:"meta"`
|
|
Results []ImageResult `json:"results"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
const apiVersion = "2.1"
|
|
|
|
// NewEnvelope builds a fresh Envelope pre-filled with query echo and an open
|
|
// meta block. Call Finalize before serializing.
|
|
func NewEnvelope(q Query, requestID string, startedAt time.Time, engines []string) *Envelope {
|
|
return &Envelope{
|
|
Query: QueryEcho{
|
|
Text: q.Text,
|
|
Lang: q.LangCode,
|
|
Region: q.Region,
|
|
EnginesRequested: engines,
|
|
},
|
|
Meta: ResponseMeta{
|
|
RequestID: requestID,
|
|
RequestedAt: startedAt.UTC().Format(time.RFC3339),
|
|
EnginesFailed: []string{},
|
|
Version: apiVersion,
|
|
},
|
|
Results: []Result{},
|
|
SerpFeatures: []SerpFeature{},
|
|
Pagination: Pagination{},
|
|
}
|
|
}
|
|
|
|
// NewImageEnvelope builds a fresh ImageEnvelope.
|
|
func NewImageEnvelope(q Query, requestID string, startedAt time.Time, engines []string) *ImageEnvelope {
|
|
return &ImageEnvelope{
|
|
Query: QueryEcho{
|
|
Text: q.Text,
|
|
Lang: q.LangCode,
|
|
Region: q.Region,
|
|
EnginesRequested: engines,
|
|
},
|
|
Meta: ResponseMeta{
|
|
RequestID: requestID,
|
|
RequestedAt: startedAt.UTC().Format(time.RFC3339),
|
|
EnginesFailed: []string{},
|
|
Version: apiVersion,
|
|
},
|
|
Results: []ImageResult{},
|
|
Pagination: Pagination{},
|
|
}
|
|
}
|
|
|
|
// Finalize stamps the elapsed time and computes pagination fields.
|
|
func (e *Envelope) Finalize(startedAt time.Time, q Query) {
|
|
e.Meta.TookMs = time.Since(startedAt).Milliseconds()
|
|
|
|
limit := q.Limit
|
|
if limit <= 0 {
|
|
limit = defaultQueryLimit
|
|
}
|
|
page := q.Start/limit + 1
|
|
e.Pagination = Pagination{
|
|
Page: page,
|
|
HasMore: countNonAdResults(e.Results) >= limit,
|
|
NextStart: q.Start + limit,
|
|
}
|
|
}
|
|
|
|
func countNonAdResults(results []Result) int {
|
|
count := 0
|
|
for _, result := range results {
|
|
if result.Type != ResultTypeAd {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// Finalize stamps the elapsed time and computes pagination fields.
|
|
func (e *ImageEnvelope) Finalize(startedAt time.Time, q Query) {
|
|
e.Meta.TookMs = time.Since(startedAt).Milliseconds()
|
|
|
|
limit := q.Limit
|
|
if limit <= 0 {
|
|
limit = defaultQueryLimit
|
|
}
|
|
page := q.Start/limit + 1
|
|
e.Pagination = Pagination{
|
|
Page: page,
|
|
HasMore: len(e.Results) >= limit,
|
|
NextStart: q.Start + limit,
|
|
}
|
|
}
|