GoでGoogle Custom Search APIを使用する

# はじめに
3ヶ月ほど前に使う機会があった`Google Custom Search JSON API`の使い方をまとめた記事です。


# 手順
#### 前提条件: 
Googleアカウントを所持していること

### 1. 検索エンジンIDの作成
https://programmablesearchengine.google.com/controlpanel/all?hl=ja


右上の追加ボタンをクリックします
![スクリーンショット 2024-12-14 9.25.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759397/909ca2d9-d337-1781-7646-d21cebf32ede.png)

検索エンジン名を任意のものにします。
今回は特定のサイトのみの検索結果にし、`https://qiita.com`にして追加します。

:::note info
特定のサイトは、複数サイトを選択することができ、論文のみの検索にするときためには`https://scholar.google.com`や`https://www.openaire.eu`などを複数追加するといいと思います。
:::

![スクリーンショット 2024-12-14 9.27.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759397/b9563860-2232-eeed-25a2-9739204392bd.png)

エンジン名と検索対象が追加できると同じ画面下の作成ボタンをクリックして作成！
作成が完了し、画面が変わるとカスタマイズボタンをクリック


概要ページで基本的な情報や、検索対象などを確認することができます。
**検索エンジンID**は後でコード内で使用します。
![スクリーンショット 2024-12-14 9.46.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759397/cab7750b-646b-b44d-f7d1-22394363009d.png)



### 2. APIキーの取得

https://developers.google.com/custom-search/v1/overview?hl=ja

APIキーを作成します。上記のリンク内にある`キーを取得`のボタンをクリックし

![スクリーンショット 2024-12-14 10.05.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759397/0ab1277d-eb60-1614-ad98-dda5ced6822e.png)

`Create a new project`でAPIキーを作成します。
![スクリーンショット 2024-12-14 10.07.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759397/7c0a1405-0701-91f3-731f-a8953b0c8701.png)

任意のプロジェクト名を入力し`Next`ボタンをクリック
![スクリーンショット 2024-12-14 10.08.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759397/58a49974-b98a-2504-9a7f-435143beb53d.png)

作成が完了すると以下の画面が表示されます。
`SHOW KEY`をクリックして`APIキー`を表示することができます。
この**APIキー**は後でコード内で使用します。
![スクリーンショット 2024-12-14 10.10.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759397/cc4d4c0d-4a07-462b-2a35-50c4264b6c02.png)


# 実装
まず、プロジェクトのディレクトリを作成し、VSCodeで開きます。
```bash
mkdir custom_search_demo
cd custom_search_demo
code .
```

`rootディレクトリ`に`main.go`を作成します。
`main.go`ファイルは、`Google Custom Search API`を利用するために、主に以下の機能を実装しています。

- Ginフレームワークを使用したウェブサーバーの設定
- 検索リクエストを処理するエンドポイントの作成
- APIから取得した検索結果を返すロジック
```go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"

	"github.com/gin-gonic/gin"
	"github.com/joho/godotenv"
)

// SearchRequest は検索リクエストを表す構造体です
type SearchRequest struct {
	Query string `json:"query" binding:"required"`
}

// SearchResult は検索結果を表す構造体です
type SearchResult struct {
	Items []SearchItem `json:"items"`
}

// SearchItem は検索結果の各アイテムを表す構造体です
type SearchItem struct {
	Title   string `json:"title"`
	Link    string `json:"link"`
	Snippet string `json:"snippet"`
}

func main() {
	r := gin.Default()

	// エンドポイントの設定
	r.POST("/search", handleSearch)

	// ポートの設定
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	// サーバーの起動
	r.Run(":" + port)
}

// handleSearch は検索リクエストを処理するハンドラーです
func handleSearch(c *gin.Context) {
	// request body から検索クエリを取得
	var req SearchRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"リクエストが正しくありません": err.Error()})
		return
	}

	// Google Custom Search API を使って検索を実行
	results, err := performGoogleSearch(c, req.Query)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"ハンドラー内でエラーが発生しました": err.Error()})
		return
	}

	// レスポンスを返す
	c.JSON(http.StatusOK, results)
}

// performGoogleSearch は Google Custom Search API を使って検索を実行します
func performGoogleSearch(ctx context.Context, query string) (*SearchResult, error) {
	// env ファイルの読み込み
	err := godotenv.Load()
	if err != nil {
		fmt.Println(".envファイルの読み込みに失敗しました")
	}

	// Google Custom Search API のエンドポイントのsetup
	baseURL := "https://www.googleapis.com/customsearch/v1"
	apiKey := os.Getenv("GOOGLE_API_KEY")
	searchEngineID := os.Getenv("SEARCH_ENGINE_ID")
	if apiKey == "" || searchEngineID == "" {
		return nil, fmt.Errorf("APIキーまたは検索エンジンIDが設定されていません")
	}

	// エンドポイントを作成
	req, err := http.NewRequestWithContext(ctx, "GET", baseURL, nil)
	if err != nil {
		return nil, fmt.Errorf("エンドポイントの作成に失敗しました: %v", err)
	}

	// クエリパラメータを設定
	q := req.URL.Query()
	q.Set("key", apiKey)
	q.Set("cx", searchEngineID)
	q.Set("q", query) // 検索クエリ

	// URL にクエリパラメータをセット
	req.URL.RawQuery = q.Encode()

	// クライアントの作成
	client := &http.Client{}

	// Google Custom Search API にリクエストを送信
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("”リクエストの送信に失敗しました: %v", err)
	}
	defer resp.Body.Close()

	// Google Custom Search API からのレスポンスをチェック
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("API からのレスポンスがエラーです: %s", body)
	}

	// レスポンスのデコード
	var searchResponse struct {
		Items []SearchItem `json:"items"`
	}

	if err := json.NewDecoder(resp.Body).Decode(&searchResponse); err != nil {
		return nil, fmt.Errorf("デコードに失敗しました: %v", err)
	}

	// 検索結果を返す
	return &SearchResult{
		Items: searchResponse.Items,
	}, nil
}

```
:::note info
### コード補足

今回のデモでは使用していませんが、検索するものをソート(日付順)や言語などを細かく設定できます。
以下のリンクにそれらのリファレンスがあるので参考にしてください。

https://developers.google.com/custom-search/v1/reference/rest/v1/cse/list?hl=ja

:::

プロジェクトの依存関係を初期化します。

- `go mod init demo`でGoモジュールを作成
- `go mod tidy`で必要な依存パッケージを自動的にダウンロードし、`go.mod`ファイルを更新
```bash
go mod init demo
go mod tidy
```
環境変数ファイル(.env)では、セキュリティを考慮して以下の情報を設定します。
- GOOGLE_API_KEY: Google Custom Search APIにアクセスするための認証キー
- SEARCH_ENGINE_ID: 特定の検索エンジン（この場合はQiitaサイト）用の識別子
```.env
GOOGLE_API_KEY=api_key
SEARCH_ENGINE_ID=engin_id
```
# 動作確認
今回はPostmanで動作確認をします。

**エンドポイント:`POST`**
```
http://localhost:8080/search
```

**リクエストボディー**
```
{
    "query": "きーたん"
}
```

![スクリーンショット 2024-12-14 23.09.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759397/24dc5158-14f6-23b4-bca4-ee2ddafbd281.png)

こんな感じで`qiita`内で「きーたん」が出てくる検索結果を取り出すことができます。

# おわりに
Google Custom Search JSON APIは、検索したいドメインなどを絞ることによってwebサービスやスクレイピングなどに活用できるとても使い勝手がいいAPIです。是非皆さんも試してみてはいかがでしょうか？