package search import ( "context" "encoding/json" "fmt" "net/http" "strconv" "strings" "github.com/elastic/go-elasticsearch/v8" "github.com/elastic/go-elasticsearch/v8/esapi" httperrors "github.com/explorer/backend/libs/go-http-errors" ) // SearchService handles unified search type SearchService struct { client *elasticsearch.Client indexPrefix string } // NewSearchService creates a new search service func NewSearchService(client *elasticsearch.Client, indexPrefix string) *SearchService { return &SearchService{ client: client, indexPrefix: indexPrefix, } } // Search performs unified search across all indices func (s *SearchService) Search(ctx context.Context, query string, chainID *int, limit int) ([]SearchResult, error) { // Build search query var indices []string if chainID != nil { indices = []string{ fmt.Sprintf("%s-blocks-%d", s.indexPrefix, *chainID), fmt.Sprintf("%s-transactions-%d", s.indexPrefix, *chainID), fmt.Sprintf("%s-addresses-%d", s.indexPrefix, *chainID), } } else { // Search all chains (simplified - would need to enumerate) indices = []string{ fmt.Sprintf("%s-blocks-*", s.indexPrefix), fmt.Sprintf("%s-transactions-*", s.indexPrefix), fmt.Sprintf("%s-addresses-*", s.indexPrefix), } } searchQuery := map[string]interface{}{ "query": map[string]interface{}{ "multi_match": map[string]interface{}{ "query": query, "fields": []string{"hash", "address", "from_address", "to_address"}, "type": "best_fields", }, }, "size": limit, } queryJSON, _ := json.Marshal(searchQuery) queryString := string(queryJSON) // Execute search req := esapi.SearchRequest{ Index: indices, Body: strings.NewReader(queryString), Pretty: true, } res, err := req.Do(ctx, s.client) if err != nil { return nil, fmt.Errorf("search failed: %w", err) } defer res.Body.Close() if res.IsError() { return nil, fmt.Errorf("elasticsearch error: %s", res.String()) } var result map[string]interface{} if err := json.NewDecoder(res.Body).Decode(&result); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } // Parse results results := []SearchResult{} if hits, ok := result["hits"].(map[string]interface{}); ok { if hitsList, ok := hits["hits"].([]interface{}); ok { for _, hit := range hitsList { if hitMap, ok := hit.(map[string]interface{}); ok { if source, ok := hitMap["_source"].(map[string]interface{}); ok { result := s.parseResult(source) results = append(results, result) } } } } } return results, nil } // SearchResult represents a search result type SearchResult struct { Type string `json:"type"` ChainID int `json:"chain_id"` Data map[string]interface{} `json:"data"` Score float64 `json:"score"` } func (s *SearchService) parseResult(source map[string]interface{}) SearchResult { result := SearchResult{ Data: source, } if chainID, ok := source["chain_id"].(float64); ok { result.ChainID = int(chainID) } // Determine type based on fields if _, ok := source["block_number"]; ok { result.Type = "block" } else if _, ok := source["transaction_index"]; ok { result.Type = "transaction" } else if _, ok := source["address"]; ok { result.Type = "address" } return result } // HandleSearch handles HTTP search requests func (s *SearchService) HandleSearch(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { httperrors.WriteJSON(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Method not allowed") return } query := r.URL.Query().Get("q") if query == "" { httperrors.WriteJSON(w, http.StatusBadRequest, "BAD_REQUEST", "Query parameter 'q' is required") return } var chainID *int if chainIDStr := r.URL.Query().Get("chain_id"); chainIDStr != "" { if id, err := strconv.Atoi(chainIDStr); err == nil { chainID = &id } } limit := 50 if limitStr := r.URL.Query().Get("limit"); limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil { limit = l } } results, err := s.Search(r.Context(), query, chainID, limit) if err != nil { httperrors.WriteJSON(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Search failed") return } response := map[string]interface{}{ "query": query, "results": results, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) }