LoginSignup
18
11

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プロジェクトの実行

  1. モジュールの初期化
    まずはGoのモジュールの初期化です。
go mod init 【モジュール名】

この呪文でgo.modファイルが生成されます。
詳しくはこちらの記事を参照ください。

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

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

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

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

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

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

go get github.com/joho/godotenv
  1. それでは実装
    実装したコードを下記に示しました。
    実装後はモジュールの依存関係を整理するために、下記を実行しておきましょう。
go mod tidy

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

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の3つだけできていると思います。

  1. 実行
go run main.go

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

image.png

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

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

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

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

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の導入についてはこちらを参照。

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

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のサポートもしっかりしており色々できそうな気がしました。
また気が向いたらやってみようと思います。

以上備忘録でした!!

18
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
11