// 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) }