【Golang入門】Gemini APIをGoで呼んでみた

OpenAIのAPIをコネコネやるのが流行っているようです。それに変な対抗意識を持って、あえてGeminiAPIを使ってGolangコネコネしてみました。３Gということ（勝手に私が言ってるだけです）でGCPにデプロイしようかなと思っていたりいなかったり。

そんな備忘録として書いておきます。

なお、実装についてはこちらを参考にしました。
とてもわかりやすいです。

https://www.youtube.com/watch?v=3ZomyM0expM

またしっかりチュートリアルも用意されているため、このチュートリアルも参考にしても良いかと思います。

https://ai.google.dev/gemini-api/docs/get-started/tutorial?lang=go&hl=ja#set-up-api-key

## Geminiって？

一応Geminiについて軽く説明させていただきます。

Geminiとは、Googleの生成AIサービスです。Geminiは2023年12月に高性能AIモデルとして発表され、現在では以下の３つのモデルが存在します。

### Gemini Nano
高度な処理や重い処理は求められないものの、軽量な処理を並行で進めるのに適しています。

### Gemini Pro
Gemini ProはGemini（旧Bard）に搭載されているモデルです。幅広い入出力の対応には優れているということです。

### Gemini Ultra
Gemini UltraはGeminiの中でも性能がもっとも高いモデルです。Gemini Advancedに搭載されています。Gemini AdvancedはGeminiの上位版で、Geminiの有料版とも言えます。


## Goを使って実装

ここからがメインになります。
前述した通り、今回はGolangを用いて実装しました。
Golangも、生成AIも素人なため、指摘箇所あればご意見いただけると幸いです。
今回は、初めてGoを触る人でも分かる粒度で書いております。
そこまで説明は詳しくないのですが、基本コピペで実行できると思います。
Goへの興味の入り口になれば幸いです。


### Goプロジェクトの実行

1. モジュールの初期化
まずはGoのモジュールの初期化です。

```
go mod init 【モジュール名】
```
この呪文で**go.mod**ファイルが生成されます。
詳しくはこちらの記事を参照ください。

https://qiita.com/TakanoriVega/items/6d7210147c289b45298a

2. 使うパッケージ
GoでGeminiを用いる場合は、以下のパッケージが必要となります。

https://pkg.go.dev/github.com/google/generative-ai-go/genai#pkg-examples

上記を見ると、使い方例が多くのっており初期設定の時に非常に重宝します。

3. API KEYの取得
下記より取得が可能です。
今回は無料でやりたかったので、**gemini-pro**を用いています。

https://aistudio.google.com/app/apikey

取得したKEYはenvファイルに書きました。

```.env
GEMINI_APIKEY= ************
```

Goではenvファイルを読み込むために下記をgo getする必要があることを忘れないようにしましょう。

```
go get github.com/joho/godotenv
```

3. それでは実装
実装したコードを下記に示しました。
実装後はモジュールの依存関係を整理するために、下記を実行しておきましょう。

```
go mod tidy
```
おそらく **go.sum**ファイルが生成されると思います。

https://zenn.dev/boogaloo/articles/2de3b24e7460a1

```main.go
package main

import (
	"bufio"
	"context"
	"fmt"
	"os"

	"github.com/google/generative-ai-go/genai"
	"github.com/joho/godotenv"
	"google.golang.org/api/option"
)

func main() {
	// .envファイルを読み込む
	err := godotenv.Load()
	if err != nil {
		fmt.Println("Error loading .env file")
		return
	}

	ctx := context.Background()

	if err := run(ctx); err != nil {
		fmt.Fprintln(os.Stderr, "Error:", err)
		os.Exit(1)
	}
}

func run(ctx context.Context) error {
	// コンソールから質問を入力する
	fmt.Print("質問を入力してください: ")
	reader := bufio.NewReader(os.Stdin)
	question, err := reader.ReadString('\n')
	if err != nil {
		return fmt.Errorf("質問の読み取りに失敗しました: %v", err)
	}

	// Gemini APIクライアントを作成する
	client, err := genai.NewClient(ctx, option.WithAPIKey(os.Getenv("GEMINI_APIKEY")))
	if err != nil {
		return fmt.Errorf("Geminiクライアントの作成に失敗しました: %v", err)
	}
	defer client.Close()

	// 質問を送信して回答を取得する
	model := client.GenerativeModel("gemini-pro")
	prompt := genai.Text(question)
	resp, err := model.GenerateContent(ctx, prompt)
	if err != nil {
		return fmt.Errorf("Gemini APIの呼び出しに失敗しました: %v", err)
	}

	// 回答を表示する
	printCandidates(resp.Candidates)

	return nil
}

func printCandidates(cs []*genai.Candidate) {
	for _, c := range cs {
		for _, p := range c.Content.Parts {
			fmt.Println(p)
		}
	}
}
```

さてこれで完成です。
ファイルは **go.sum** , **go.mod** , **main.go**の３つだけできていると思います。

4. 実行

```
go run main.go
```
「質問をしてください」と表示された後に質問すると回答がきます。
今回は「長野県について教えてください」と質問しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287130/fd75b884-0119-8de3-4f16-472d5644ede3.png)


ちゃんと返事が来ました。
たぶんあってます。

## 【おまけ】ベクトルデータベースに保存してみた

ベクトルデータベースとは、数学的表現として保存されたデータの集合体を意味します。ベクトルデータベースにより、機械学習モデルによる以前の入力の記憶が容易になり、機械学習の検索、推奨、テキスト生成でのユースケースへの活用が可能になるそうです。

とりあえず今回は質問した内容を保存するところまで・・・


## pgvectorとPinecone
調べていると、ベクトルデータベースには有名なものの中にpgvectorとPineconeがあるそうです。この２つを戦わせている記事がたくさんありました。




https://www.pinecone.io/blog/pinecone-vs-pgvector/

https://www.timescale.com/blog/pgvector-is-now-as-fast-as-pinecone-at-75-less-cost/

簡単にまとめますと以下のような利点がそれぞれにあるようです。

象さんが好きですか？

松ぼっくりさんが好きですか？

### Pineconeの利点
**1. 高品質なベクター検索:**
Pineconeはベクター検索専用に設計されており、高品質な検索結果を提供します。

**2. スケーラビリティ:**
Pineconeはサーバーレスで、データセットの規模に関係なくスムーズにスケールします。

**3.シンプルな操作:**
PineconeではインデックスのサイズやRAMのプロビジョニングを気にする必要がなく、開発者は本来のアプリケーション開発に集中できます。

**4.コスト効率:**
Pineconeは多くのワークロードでpgvectorよりもコスト効率が良く、特に月々の運用コストが低く抑えられます。

**5.メタデータフィルタリングのサポート:**
Pineconeは効率的なメタデータフィルタリングを提供し、一貫した高品質の検索結果を実現します。

### pgvectorの問題点

**1.品質よりも簡便さを優先:**
ユーザーはPostgreSQL（pgvector）を使うことでデータベースの管理を簡単にしようとしますが、すぐに高品質な検索が難しいことに気付きます。

**2.複雑な調整が必要:**
pgvectorを使ったベクター検索は、特にデータセットが大きくなると、パフォーマンスを維持するために多くの調整が必要です。これは開発者にとって大きな負担です。

**3.RAMの制限:**
pgvectorでは、インデックスがRAMに収まりきらないと、ディスクにスワップアウトされてパフォーマンスが大幅に低下します。大規模なデータセットではRAMの大規模なオーバープロビジョニングが必要になります。

**4.メタデータフィルタリングの限界:**
pgvectorはメタデータフィルタリングを効率的に行えず、予測できないクエリパフォーマンスを引き起こします。

**5.IVF-Flatインデックスの問題:**
データセットが変化すると、IVF-Flatインデックスの検索品質が劣化します。

### 以下実装

とりあえず社内でよく耳をするpgvectorを用いて実装してみました。
pgVectorの導入についてはこちらを参照。

https://qiita.com/kanaza-s/items/b46214ba8543e34c5003

今回説明は割愛しますが、下記を実行すると上記と同様に質問ができ、質問した内容がデータベースへ書き込まれるようになっています。なお、上記コードを若干リファクタリングもしています。


```main.go
package main

import (
    "bufio"
    "context"
    "database/sql"
    "fmt"
    "log"
    "math/rand"
    "os"
    "strings"
    "time"

    "github.com/google/generative-ai-go/genai"
    "github.com/joho/godotenv"
    _ "github.com/lib/pq"
    "google.golang.org/api/option"
)

func main() {
    if err := loadEnv(); err != nil {
        log.Fatalf("error loading .env file: %v", err)
    }

    db, err := setupDatabase()
    if err != nil {
        log.Fatalf("failed to connect to the database: %v", err)
    }
    defer db.Close()

    ctx := context.Background()
    if err := runApp(ctx, db); err != nil {
        log.Fatalf("error: %v", err)
    }
}

func loadEnv() error {
    if err := godotenv.Load(); err != nil {
        return fmt.Errorf("error loading .env file: %v", err)
    }
    return nil
}

func setupDatabase() (*sql.DB, error) {
    connStr := fmt.Sprintf("user=%s password=%s dbname=%s host=%s port=%s sslmode=disable",
        os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME"), os.Getenv("DB_HOST"), os.Getenv("DB_PORT"))
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        return nil, fmt.Errorf("failed to connect to the database: %v", err)
    }
    return db, nil
}

func runApp(ctx context.Context, db *sql.DB) error {
    question, err := promptForQuestion()
    if err != nil {
        return fmt.Errorf("failed to read question: %v", err)
    }

    client, err := genai.NewClient(ctx, option.WithAPIKey(os.Getenv("GEMINI_APIKEY")))
    if err != nil {
        return fmt.Errorf("failed to create Gemini client: %v", err)
    }
    defer client.Close()

    answer, err := getAnswerFromAPI(ctx, client, question)
    if err != nil {
        return err
    }

    if err := saveQA(ctx, db, question, answer); err != nil {
        return err
    }
    fmt.Println("回答:", answer)
    log.Println("データベースに保存されました。")
    return nil
}

func promptForQuestion() (string, error) {
    fmt.Print("質問を入力してください: ")
    reader := bufio.NewReader(os.Stdin)
    return reader.ReadString('\n')
}

func getAnswerFromAPI(ctx context.Context, client *genai.Client, question string) (string, error) {
    model := client.GenerativeModel("gemini-pro")
    resp, err := model.GenerateContent(ctx, genai.Text(question))
    if err != nil {
        return "", fmt.Errorf("failed to call Gemini API: %v", err)
    }
    return extractAnswer(resp.Candidates), nil
}

func extractAnswer(cs []*genai.Candidate) string {
    var answer string
    for _, c := range cs {
        for _, p := range c.Content.Parts {
            partDetails := fmt.Sprintf("%v", p)
            answer += partDetails
        }
    }
    return answer
}

func saveQA(ctx context.Context, db *sql.DB, question, answer string) error {
    questionVector := generateRandomVector(768)
    answerVector := generateRandomVector(768)

    questionVectorStr := vectorToString(questionVector)
    answerVectorStr := vectorToString(answerVector)

    _, err := db.ExecContext(ctx, `
        INSERT INTO qa_data (question, answer, question_vector, answer_vector)
        VALUES ($1, $2, $3::vector, $4::vector)`,
        question, answer, questionVectorStr, answerVectorStr)
    if err != nil {
        return fmt.Errorf("failed to save Q&A to database: %v", err)
    }
    return nil
}

func generateRandomVector(dim int) []float64 {
    rand.Seed(time.Now().UnixNano())
    vector := make([]float64, dim)
    for i := range vector {
        vector[i] = rand.Float64()
    }
    return vector
}

func vectorToString(vector []float64) string {
    strValues := make([]string, len(vector))
    for i, v := range vector {
        strValues[i] = fmt.Sprintf("%f", v)
    }
    return fmt.Sprintf("[%s]", strings.Join(strValues, ","))
}
```

pgVectorですが、Goのサポートもしっかりしており色々できそうな気がしました。
また気が向いたらやってみようと思います。

https://github.com/pgvector/pgvector-go

以上備忘録でした！！