155 lines
5.2 KiB
Go
155 lines
5.2 KiB
Go
// File Path: monorepo/cloud/maplepress-backend/pkg/search/search.go
|
|
package search
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// SearchRequest represents a search request
|
|
type SearchRequest struct {
|
|
Query string
|
|
Limit int64
|
|
Offset int64
|
|
Filter string // e.g., "status = publish"
|
|
}
|
|
|
|
// SearchResult represents a search result
|
|
type SearchResult struct {
|
|
Hits []map[string]interface{} `json:"hits"`
|
|
Query string `json:"query"`
|
|
ProcessingTimeMs int64 `json:"processing_time_ms"`
|
|
TotalHits int64 `json:"total_hits"`
|
|
Limit int64 `json:"limit"`
|
|
Offset int64 `json:"offset"`
|
|
}
|
|
|
|
// Search performs a search query on the index
|
|
func (c *Client) Search(siteID string, req SearchRequest) (*SearchResult, error) {
|
|
indexName := c.GetIndexName(siteID)
|
|
|
|
c.logger.Info("initiating search",
|
|
zap.String("site_id", siteID),
|
|
zap.String("index_name", indexName),
|
|
zap.String("query", req.Query),
|
|
zap.Int64("limit", req.Limit),
|
|
zap.Int64("offset", req.Offset),
|
|
zap.String("filter", req.Filter))
|
|
|
|
// Build search request manually to avoid hybrid field
|
|
searchBody := map[string]interface{}{
|
|
"q": req.Query,
|
|
"limit": req.Limit,
|
|
"offset": req.Offset,
|
|
"attributesToHighlight": []string{"title", "excerpt", "content"},
|
|
}
|
|
|
|
// Add filter if provided
|
|
if req.Filter != "" {
|
|
searchBody["filter"] = req.Filter
|
|
}
|
|
|
|
// Marshal to JSON
|
|
jsonData, err := json.Marshal(searchBody)
|
|
if err != nil {
|
|
c.logger.Error("failed to marshal search request", zap.Error(err))
|
|
return nil, fmt.Errorf("failed to marshal search request: %w", err)
|
|
}
|
|
|
|
// Uncomment for debugging: shows exact JSON payload sent to Meilisearch
|
|
// c.logger.Debug("search request payload", zap.String("json", string(jsonData)))
|
|
|
|
// Build search URL
|
|
searchURL := fmt.Sprintf("%s/indexes/%s/search", c.config.Host, indexName)
|
|
// Uncomment for debugging: shows the Meilisearch API endpoint
|
|
// c.logger.Debug("search URL", zap.String("url", searchURL))
|
|
|
|
// Create HTTP request
|
|
httpReq, err := http.NewRequest("POST", searchURL, bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
c.logger.Error("failed to create HTTP request", zap.Error(err))
|
|
return nil, fmt.Errorf("failed to create search request: %w", err)
|
|
}
|
|
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.config.APIKey))
|
|
|
|
// Execute request
|
|
httpClient := &http.Client{}
|
|
resp, err := httpClient.Do(httpReq)
|
|
if err != nil {
|
|
c.logger.Error("failed to execute HTTP request", zap.Error(err))
|
|
return nil, fmt.Errorf("failed to execute search request: %w", err)
|
|
}
|
|
if resp == nil {
|
|
c.logger.Error("received nil response from search API")
|
|
return nil, fmt.Errorf("received nil response from search API")
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
c.logger.Info("received search response",
|
|
zap.Int("status_code", resp.StatusCode),
|
|
zap.String("status", resp.Status))
|
|
|
|
// Read response body for logging
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
c.logger.Error("failed to read response body", zap.Error(err))
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
|
|
// Uncomment for debugging: shows full Meilisearch response
|
|
// c.logger.Debug("search response body", zap.String("body", string(bodyBytes)))
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
c.logger.Error("search request failed",
|
|
zap.Int("status_code", resp.StatusCode),
|
|
zap.String("response_body", string(bodyBytes)))
|
|
return nil, fmt.Errorf("search request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
// Parse response
|
|
var searchResp struct {
|
|
Hits []map[string]interface{} `json:"hits"`
|
|
Query string `json:"query"`
|
|
ProcessingTimeMs int64 `json:"processingTimeMs"`
|
|
EstimatedTotalHits int `json:"estimatedTotalHits"`
|
|
Limit int64 `json:"limit"`
|
|
Offset int64 `json:"offset"`
|
|
}
|
|
|
|
if err := json.Unmarshal(bodyBytes, &searchResp); err != nil {
|
|
c.logger.Error("failed to decode search response", zap.Error(err))
|
|
return nil, fmt.Errorf("failed to decode search response: %w", err)
|
|
}
|
|
|
|
c.logger.Info("search completed successfully",
|
|
zap.Int("hits_count", len(searchResp.Hits)),
|
|
zap.Int("total_hits", searchResp.EstimatedTotalHits),
|
|
zap.Int64("processing_time_ms", searchResp.ProcessingTimeMs))
|
|
|
|
// Convert response
|
|
result := &SearchResult{
|
|
Hits: searchResp.Hits,
|
|
Query: searchResp.Query,
|
|
ProcessingTimeMs: searchResp.ProcessingTimeMs,
|
|
TotalHits: int64(searchResp.EstimatedTotalHits),
|
|
Limit: req.Limit,
|
|
Offset: req.Offset,
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// SearchWithHighlights performs a search with custom highlighting
|
|
// Note: Currently uses same implementation as Search()
|
|
func (c *Client) SearchWithHighlights(siteID string, req SearchRequest, highlightTags []string) (*SearchResult, error) {
|
|
// For now, just use the regular Search method
|
|
// TODO: Implement custom highlight tags if needed
|
|
return c.Search(siteID, req)
|
|
}
|