package indexer import ( "bytes" "context" "encoding/json" "fmt" "time" "github.com/elastic/go-elasticsearch/v8" "github.com/elastic/go-elasticsearch/v8/esapi" ) // SearchIndexer handles indexing documents to Elasticsearch/OpenSearch type SearchIndexer struct { client *elasticsearch.Client indexPrefix string } // NewSearchIndexer creates a new search indexer func NewSearchIndexer(client *elasticsearch.Client, indexPrefix string) *SearchIndexer { return &SearchIndexer{ client: client, indexPrefix: indexPrefix, } } // IndexBlock indexes a block document func (s *SearchIndexer) IndexBlock(ctx context.Context, chainID int, block *BlockDocument) error { indexName := fmt.Sprintf("%s-blocks-%d", s.indexPrefix, chainID) return s.indexDocument(ctx, indexName, block.Hash, block) } // IndexTransaction indexes a transaction document func (s *SearchIndexer) IndexTransaction(ctx context.Context, chainID int, tx *TransactionDocument) error { indexName := fmt.Sprintf("%s-transactions-%d", s.indexPrefix, chainID) return s.indexDocument(ctx, indexName, tx.Hash, tx) } // IndexAddress indexes an address document func (s *SearchIndexer) IndexAddress(ctx context.Context, chainID int, addr *AddressDocument) error { indexName := fmt.Sprintf("%s-addresses-%d", s.indexPrefix, chainID) return s.indexDocument(ctx, indexName, addr.Address, addr) } // indexDocument indexes a document to Elasticsearch func (s *SearchIndexer) indexDocument(ctx context.Context, indexName, docID string, doc interface{}) error { body, err := json.Marshal(doc) if err != nil { return fmt.Errorf("failed to marshal document: %w", err) } req := esapi.IndexRequest{ Index: indexName, DocumentID: docID, Body: bytes.NewReader(body), Refresh: "false", } res, err := req.Do(ctx, s.client) if err != nil { return fmt.Errorf("failed to index document: %w", err) } defer res.Body.Close() if res.IsError() { return fmt.Errorf("elasticsearch error: %s", res.String()) } return nil } // BlockDocument represents a block in the search index type BlockDocument struct { BlockNumber int64 `json:"block_number"` Hash string `json:"hash"` Timestamp time.Time `json:"timestamp"` Miner string `json:"miner"` TransactionCount int `json:"transaction_count"` GasUsed int64 `json:"gas_used"` GasLimit int64 `json:"gas_limit"` ChainID int `json:"chain_id"` ParentHash string `json:"parent_hash"` Size int64 `json:"size"` } // TransactionDocument represents a transaction in the search index type TransactionDocument struct { Hash string `json:"hash"` BlockNumber int64 `json:"block_number"` TransactionIndex int `json:"transaction_index"` FromAddress string `json:"from_address"` ToAddress string `json:"to_address"` Value string `json:"value"` ValueNumeric int64 `json:"value_numeric"` GasPrice int64 `json:"gas_price"` GasUsed int64 `json:"gas_used"` Status string `json:"status"` Timestamp time.Time `json:"timestamp"` ChainID int `json:"chain_id"` InputDataLength int `json:"input_data_length"` IsContractCreation bool `json:"is_contract_creation"` ContractAddress string `json:"contract_address,omitempty"` } // AddressDocument represents an address in the search index type AddressDocument struct { Address string `json:"address"` ChainID int `json:"chain_id"` Label string `json:"label,omitempty"` Tags []string `json:"tags,omitempty"` TokenCount int `json:"token_count"` TransactionCount int64 `json:"transaction_count"` FirstSeen time.Time `json:"first_seen"` LastSeen time.Time `json:"last_seen"` IsContract bool `json:"is_contract"` ContractName string `json:"contract_name,omitempty"` BalanceETH string `json:"balance_eth"` BalanceUSD float64 `json:"balance_usd,omitempty"` }