OpenAIのAPIをコネコネやるのが流行っているようです。それに変な対抗意識を持って、あえてGeminiAPIを使ってGolangコネコネしてみました。3Gということ(勝手に私が言ってるだけです)でGCPにデプロイしようかなと思っていたりいなかったり。
そんな備忘録として書いておきます。
なお、実装についてはこちらを参考にしました。
とてもわかりやすいです。
またしっかりチュートリアルも用意されているため、このチュートリアルも参考にしても良いかと思います。
Geminiって?
一応Geminiについて軽く説明させていただきます。
Geminiとは、Googleの生成AIサービスです。Geminiは2023年12月に高性能AIモデルとして発表され、現在では以下の3つのモデルが存在します。
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プロジェクトの実行
- モジュールの初期化
まずはGoのモジュールの初期化です。
go mod init 【モジュール名】
この呪文でgo.modファイルが生成されます。
詳しくはこちらの記事を参照ください。
- 使うパッケージ
GoでGeminiを用いる場合は、以下のパッケージが必要となります。
上記を見ると、使い方例が多くのっており初期設定の時に非常に重宝します。
- API KEYの取得
下記より取得が可能です。
今回は無料でやりたかったので、gemini-proを用いています。
取得したKEYはenvファイルに書きました。
GEMINI_APIKEY= ************
Goではenvファイルを読み込むために下記をgo getする必要があることを忘れないようにしましょう。
go get github.com/joho/godotenv
- それでは実装
実装したコードを下記に示しました。
実装後はモジュールの依存関係を整理するために、下記を実行しておきましょう。
go mod tidy
おそらく go.sumファイルが生成されると思います。
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の3つだけできていると思います。
- 実行
go run main.go
「質問をしてください」と表示された後に質問すると回答がきます。
今回は「長野県について教えてください」と質問しました。
ちゃんと返事が来ました。
たぶんあってます。
【おまけ】ベクトルデータベースに保存してみた
ベクトルデータベースとは、数学的表現として保存されたデータの集合体を意味します。ベクトルデータベースにより、機械学習モデルによる以前の入力の記憶が容易になり、機械学習の検索、推奨、テキスト生成でのユースケースへの活用が可能になるそうです。
とりあえず今回は質問した内容を保存するところまで・・・
pgvectorとPinecone
調べていると、ベクトルデータベースには有名なものの中にpgvectorとPineconeがあるそうです。この2つを戦わせている記事がたくさんありました。
簡単にまとめますと以下のような利点がそれぞれにあるようです。
象さんが好きですか?
松ぼっくりさんが好きですか?
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の導入についてはこちらを参照。
今回説明は割愛しますが、下記を実行すると上記と同様に質問ができ、質問した内容がデータベースへ書き込まれるようになっています。なお、上記コードを若干リファクタリングもしています。
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のサポートもしっかりしており色々できそうな気がしました。
また気が向いたらやってみようと思います。
以上備忘録でした!!