GoでOllama Web Search APIを使用する

GoとOllamaを使ってAI検索エージェントを構築する

目次

OllamaのWeb検索APIは、ローカルLLMにリアルタイムのウェブ情報を補完する機能を提供します。このガイドでは、GoでのWeb検索の実装について、単純なAPI呼び出しからフル機能の検索エージェントまでの実装方法を示します。

gopher biking

はじめに

Ollamaには公式のGoライブラリでWeb検索が提供されていますか? Ollamaは、任意のGo HTTPクライアントで動作するWeb検索用のREST APIを提供しています。公式のGo SDKはまだありませんが、標準ライブラリパッケージを使用してAPI呼び出しを簡単に実装できます。

まず、OllamaアカウントからAPIキーを作成してください。Ollamaのコマンドや使用法に関する詳しいリファレンスについては、Ollamaチートシートをご覧ください。

APIキーを環境変数に設定してください:

export OLLAMA_API_KEY="your_api_key"

Windows PowerShellの場合:

$env:OLLAMA_API_KEY = "your_api_key"

プロジェクトの設定

新しいGoモジュールを作成します:

mkdir ollama-search
cd ollama-search
go mod init ollama-search

基本的なWeb検索

GoでOllamaのWeb検索APIに認証するにはどうすればよいですか? AuthorizationヘッダーにAPIキーをベアラートークンとして設定してください。OllamaアカウントからAPIキーを作成し、リクエストヘッダーに渡してください。

以下はWeb検索APIの完全な実装です:

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
)

// Web_search用のリクエスト/レスポンス型
type WebSearchRequest struct {
	Query      string `json:"query"`
	MaxResults int    `json:"max_results,omitempty"`
}

type WebSearchResult struct {
	Title   string `json:"title"`
	URL     string `json:"url"`
	Content string `json:"content"`
}

type WebSearchResponse struct {
	Results []WebSearchResult `json:"results"`
}

func webSearch(query string, maxResults int) (*WebSearchResponse, error) {
	apiKey := os.Getenv("OLLAMA_API_KEY")
	if apiKey == "" {
		return nil, fmt.Errorf("OLLAMA_API_KEY環境変数が設定されていません")
	}

	reqBody := WebSearchRequest{
		Query:      query,
		MaxResults: maxResults,
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("リクエストのマーシャリングに失敗しました: %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("リクエストの作成に失敗しました: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("リクエストに失敗しました: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("APIエラー (ステータス %d): %s", resp.StatusCode, string(body))
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("レスポンスの読み取りに失敗しました: %w", err)
	}

	var searchResp WebSearchResponse
	if err := json.Unmarshal(body, &searchResp); err != nil {
		return nil, fmt.Errorf("レスポンスのアンマーシャリングに失敗しました: %w", err)
	}

	return &searchResp, nil
}

func main() {
	results, err := webSearch("Ollamaとは?", 5)
	if err != nil {
		fmt.Printf("エラー: %v\n", err)
		return
	}

	fmt.Println("検索結果:")
	fmt.Println("===============")
	for i, result := range results.Results {
		fmt.Printf("\n%d. %s\n", i+1, result.Title)
		fmt.Printf("   URL: %s\n", result.URL)
		fmt.Printf("   %s\n", truncate(result.Content, 150))
	}
}

func truncate(s string, maxLen int) string {
	if len(s) <= maxLen {
		return s
	}
	return s[:maxLen] + ". .."
}

Web Fetchの実装

web_searchとweb_fetchエンドポイントの違いは何ですか? web_searchエンドポイントはインターネットをクエリし、タイトル、URL、スニペットを含む複数の検索結果を返します。web_fetchエンドポイントは特定のURLのフルコンテンツを取得し、ページタイトル、コンテンツ、リンクを返します。

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
)

// Web_fetch用のリクエスト/レスポンス型
type WebFetchRequest struct {
	URL string `json:"url"`
}

type WebFetchResponse struct {
	Title   string   `json:"title"`
	Content string   `json:"content"`
	Links   []string `json:"links"`
}

func webFetch(url string) (*WebFetchResponse, error) {
	apiKey := os.Getenv("OLLAMA_API_KEY")
	if apiKey == "" {
		return nil, fmt.Errorf("OLLAMA_API_KEY環境変数が設定されていません")
	}

	reqBody := WebFetchRequest{URL: url}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("リクエストのマーシャリングに失敗しました: %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_fetch", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("リクエストの作成に失敗しました: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("リクエストに失敗しました: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("APIエラー (ステータス %d): %s", resp.StatusCode, string(body))
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("レスポンスの読み取りに失敗しました: %w", err)
	}

	var fetchResp WebFetchResponse
	if err := json.Unmarshal(body, &fetchResp); err != nil {
		return nil, fmt.Errorf("レスポンスのアンマーシャリングに失敗しました: %w", err)
	}

	return &fetchResp, nil
}

func main() {
	result, err := webFetch("https://ollama.com")
	if err != nil {
		fmt.Printf("エラー: %v\n", err)
		return
	}

	fmt.Printf("タイトル: %s\n\n", result.Title)
	fmt.Printf("コンテンツ:\n%s\n\n", result.Content)
	fmt.Printf("見つかったリンク: %d\n", len(result.Links))
	for i, link := range result.Links {
		if i >= 5 {
			fmt.Printf("  ... および %d 個以上のリンク\n", len(result.Links)-5)
			break
		}
		fmt.Printf("  - %s\n", link)
	}
}

再利用可能なクライアントパッケージ

コードをよりきれいで保つために、再利用可能なOllamaクライアントパッケージを作成してください:

// ollama/client.go
package ollama

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

type Client struct {
	apiKey     string
	httpClient *http.Client
	baseURL    string
}

func NewClient() (*Client, error) {
	apiKey := os.Getenv("OLLAMA_API_KEY")
	if apiKey == "" {
		return nil, fmt.Errorf("OLLAMA_API_KEY環境変数が設定されていません")
	}

	return &Client{
		apiKey: apiKey,
		httpClient: &http.Client{
			Timeout: 30 * time.Second,
		},
		baseURL: "https://ollama.com/api",
	}, nil
}

type SearchResult struct {
	Title   string `json:"title"`
	URL     string `json:"url"`
	Content string `json:"content"`
}

type SearchResponse struct {
	Results []SearchResult `json:"results"`
}

type FetchResponse struct {
	Title   string   `json:"title"`
	Content string   `json:"content"`
	Links   []string `json:"links"`
}

func (c *Client) WebSearch(query string, maxResults int) (*SearchResponse, error) {
	payload := map[string]interface{}{
		"query":       query,
		"max_results": maxResults,
	}
	return doRequest[SearchResponse](c, "/web_search", payload)
}

func (c *Client) WebFetch(url string) (*FetchResponse, error) {
	payload := map[string]string{"url": url}
	return doRequest[Fetch旗下](c, "/web_fetch", payload)
}

func doRequest[T any](c *Client, endpoint string, payload interface{}) (*T, error) {
	jsonData, err := json.Marshal(payload)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", c.baseURL+endpoint, bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", "Bearer "+c.apiKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("APIエラー (ステータス %d): %s", resp.StatusCode, string(body))
	}

	var result T
	if err := json.Unmarshal(body, &result); err != nil {
		return nil, err
	}

	return &result, nil
}

使用方法:

package main

import (
	"fmt"
	"log"
	"ollama-search/ollama"
)

func main() {
	client, err := ollama.NewClient()
	if err != nil {
		log.Fatal(err)
	}

	// 検索
	results, err := client.WebSearch("Ollamaの新機能", 5)
	if err != nil {
		log.Fatal(err)
	}

	for _, r := range results.Results {
		fmt.Printf("- %s\n  %s\n\n", r.Title, r.URL)
	}

	// 取得
	page, err := client.WebFetch("https://ollama.com")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("ページ: %s\n", page.Title)
}

検索エージェントの構築

GoベースのOllama検索エージェントに最適なモデルは? qwen3gpt-oss、およびクラウドモデルqwen3:480b-clouddeepseek-v3.1-cloudなどのツール使用能力の強いモデルが最適です。Qwen3モデルを使用して検索結果を処理または再評価する必要がある場合は、OllamaとQwen3 Embeddingモデルを使用してテキストドキュメントを再評価する方法に関するガイドをご覧ください。

以下は検索エージェントの完全な実装です:

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
)

// チャット用の型
type Message struct {
	Role      string     `json:"role"`
	Content   string     `json:"content,omitempty"`
	ToolCalls []ToolCall `json:"tool_calls,omitempty"`
	ToolName  string     `json:"tool_name,omitempty"`
}

type ToolCall struct {
	Function FunctionCall `json:"function"`
}

type FunctionCall struct {
	Name      string                 `json:"name"`
	Arguments map[string]interface{} `json:"arguments"`
}

type Tool struct {
	Type     string       `json:"type"`
	Function ToolFunction `json:"function"`
}

type ToolFunction struct {
	Name        string                 `json:"name"`
	Description string                 `json:"description"`
	Parameters  map[string]interface{} `json:"parameters"`
}

type ChatRequest struct {
	Model    string    `json:"model"`
	Messages []Message `json:"messages"`
	Tools    []Tool    `json:"tools,omitempty"`
	Stream   bool      `json:"stream"`
}

type ChatResponse struct {
	Message Message `json:"message"`
}

// 検索エージェントはLLMとWeb検索を統合します
type SearchAgent struct {
	model      string
	ollamaURL  string
	apiKey     string
	maxIter    int
	httpClient *http.Client
}

func NewSearchAgent(model string) *SearchAgent {
	return &SearchAgent{
		model:      model,
		ollamaURL:  "http://localhost:11434/api/chat",
		apiKey:     os.Getenv("OLLAMA_API_KEY"),
		maxIter:    10,
		httpClient: &http.Client{},
	}
}

func (a *SearchAgent) Query(question string) (string, error) {
	tools := []Tool{
		{
			Type: "function",
			Function: ToolFunction{
				Name:        "web_search",
				Description: "インターネットで最新情報を検索",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"query": map[string]string{
							"type":        "string",
							"description": "検索クエリ",
						},
					},
					"required": []string{"query"},
				},
			},
		},
		{
			Type: "function",
			Function: ToolFunction{
				Name:        "web_fetch",
				Description: "ウェブページのフルコンテンツを取得",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"url": map[string]string{
							"type":        "string",
							"description": "取得するURL",
						},
					},
					"required": []string{"url"},
				},
			},
		},
	}

	messages := []Message{
		{Role: "user", Content: question},
	}

	for i := 0; i < a.maxIter; i++ {
		response, err := a.chat(messages, tools)
		if err != nil {
			return "", fmt.Errorf("チャットエラー: %w", err)
		}

		messages = append(messages, response.Message)

		// ツール呼び出しがない場合は最終的な答え
		if len(response.Message.ToolCalls) == 0 {
			return response.Message.Content, nil
		}

		// ツール呼び出しを実行
		for _, toolCall := range response.Message.ToolCalls {
			fmt.Printf("🔧 呼び出しています: %s\n", toolCall.Function.Name)

			result, err := a.executeTool(toolCall)
			if err != nil {
				result = fmt.Sprintf("エラー: %v", err)
			}

			// コンテキスト制限に合わせて切り詰める
			if len(result) > 8000 {
				result = result[:8000] + "... [切り詰められました]"
			}

			messages = append(messages, Message{
				Role:     "tool",
				Content:  result,
				ToolName: toolCall.Function.Name,
			})
		}
	}

	return "", fmt.Errorf("最大反復回数に達しました")
}

func (a *SearchAgent) chat(messages []Message, tools []Tool) (*ChatResponse, error) {
	reqBody := ChatRequest{
		Model:    a.model,
		Messages: messages,
		Tools:    tools,
		Stream:   false,
	}

	jsonData, _ := json.Marshal(reqBody)

	resp, err := a.httpClient.Post(a.ollamaURL, "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)

	var chatResp ChatResponse
	if err := json.Unmarshal(body, &chatResp); err != nil {
		return nil, err
	}

	return &chatResp, nil
}

func (a *SearchAgent) executeTool(toolCall ToolCall) (string, error) {
	switch toolCall.Function.Name {
	case "web_search":
		query, ok := toolCall.Function.Arguments["query"].(string)
		if !ok {
			return "", fmt.Errorf("無効なクエリ引数")
		}
		return a.webSearch(query)

	case "web_fetch":
		url, ok := toolCall.Function.Arguments["url"].(string)
		if !ok {
			return "", fmt.Errorf("無効なURL引数")
		}
		return a.webFetch(url)

	default:
		return "", fmt.Errorf("不明なツール: %s", toolCall.Function.Name)
	}
}

func (a *SearchAgent) webSearch(query string) (string, error) {
	payload := map[string]interface{}{"query": query, "max_results": 5}
	jsonData, _ := json.Marshal(payload)

	req, _ := http.NewRequest("POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
	req.Header.Set("Authorization", "Bearer "+a.apiKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := a.httpClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	return string(body), nil
}

func (a *SearchAgent) webFetch(url string) (string, error) {
	payload := map[string]string{"url": url}
	jsonData, _ := json.Marshal(payload)

	req, _ := http.NewRequest("POST", "https://ollama.com/api/web_fetch", bytes.NewBuffer(jsonData))
	req.Header.Set("Authorization", "Bearer "+a.apiKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := a.httpClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	return string(body), nil
}

func main() {
	agent := NewSearchAgent("qwen3:4b")

	answer, err := agent.Query("Ollamaの最新機能は?")
	if err != nil {
		fmt.Printf("エラー: %v\n", err)
		return
	}

	fmt.Println("\n📝 答え:")
	fmt.Println(answer)
}

Goで大規模なWeb検索応答を処理するにはどうすればよいですか? モデルのコンテキストに応答内容を切り詰める前に、8000文字程度に制限してください。

並列検索

Goは並列処理に優れています。以下は複数の検索を並列で実行する方法です。Ollamaが並列リクエストをどのように処理するかを理解することで、並列検索の実装を最適化できます。

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"sync"
)

type SearchResult struct {
	Query   string
	Results []struct {
		Title   string `json:"title"`
		URL     string `json:"url"`
		Content string `json:"content"`
	} `json:"results"`
	Error error
}

func concurrentSearch(queries []string) []SearchResult {
	results := make([]SearchResult, len(queries))
	var wg sync.WaitGroup

	for i, query := range queries {
		wg.Add(1)
		go func(idx int, q string) {
			defer wg.Done()

			result := SearchResult{Query: q}

			payload := map[string]string{"query": q}
			jsonData, _ := json.Marshal(payload)

			req, _ := http.NewRequest("POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
			req.Header.Set("Authorization", "Bearer "+os.Getenv("OLLAMA_API_KEY"))
			req.Header.Set("Content-Type", "application/json")

			resp, err := http.DefaultClient.Do(req)
			if err != nil {
				result.Error = err
				results[idx] = result
				return
			}
			defer resp.Body.Close()

			body, _ := io.ReadAll(resp.Body)
			json.Unmarshal(body, &result)
			results[idx] = result
		}(i, query)
	}

	wg.Wait()
	return results
}

func main() {
	queries := []string{
		"Ollamaの最新機能",
		"ローカルLLMのデプロイ",
		"AI検索エージェントGo",
	}

	results := concurrentSearch(queries)

	for _, r := range results {
		fmt.Printf("\n🔍 クエリ: %s\n", r.Query)
		if r.Error != nil {
			fmt.Printf("   エラー: %v\n", r.Error)
			continue
		}
		for _, item := range r.Results[:min(3, len(r.Results))] {
			fmt.Printf("   • %s\n", item.Title)
		}
	}
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

コンテキストとキャンセル

タイムアウトやキャンセルをサポートするためのコンテキストを追加してください:

package main

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

func webSearchWithContext(ctx context.Context, query string) (string, error) {
	payload := map[string]string{"query": query}
	jsonData, _ := json.Marshal(payload)

	req, err := http.NewRequestWithContext(ctx, "POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
	if err != nil {
		return "", err
	}

	req.Header.Set("Authorization", "Bearer "+os.Getenv("OLLAMA_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	return string(body), nil
}

func main() {
	// 10秒のタイムアウト付きコンテキストを作成
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	result, err := webSearchWithContext(ctx, "Ollama web search API")
	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			fmt.Println("リクエストがタイムアウトしました")
		} else {
			fmt.Printf("エラー: %v\n", err)
		}
		return
	}

	fmt.Println(result)
}

推奨モデル

Go検索エージェントに使用するコンテキスト長さは? 約32000トークンのコンテキスト長さを使用してください。検索エージェントは、ウェブ検索結果が広範囲であるため、フルコンテキスト長さを使用するのが最適です。Ollamaモデルを管理したり、別の場所に移動したりする必要がある場合は、Ollamaモデルを別のドライブまたはフォルダに移動する方法に関するガイドをご覧ください。

モデル パラメータ 最適な用途
qwen3:4b 4B すばやいローカル検索
qwen3 8B 一般的な目的のエージェント
gpt-oss 各種 研究タスク
qwen3:480b-cloud 480B 複雑な推論(クラウド)
gpt-oss:120b-cloud 120B 長文研究(クラウド)
deepseek-v3.1-cloud - 高度な分析(クラウド)

テキストとビジュアルコンテンツを組み合わせた高度なAIアプリケーションを構築したい場合は、クロスモーダル埋め込みを活用して、テキストのみのクエリにとどまらない検索機能を拡張してください。

最佳実践

  1. エラーハンドリング: いつもエラーをチェックし、APIの失敗をGracefully処理してください
  2. タイムアウト: ネットワークリクエストにタイムアウトを使用してください
  3. レート制限: OllamaのAPIレート制限を尊重してください。OllamaのAPIの変更の可能性については、Ollama Enshittificationの最初の兆候をご覧ください
  4. 結果の切り詰め: コンテキスト制限に合わせて結果を~8000文字に切り詰めます
  5. 並列リクエスト: ゴルーチンを使用して検索を並列に実行してください
  6. 接続の再利用: 性能向上のためHTTPクライアントを再利用してください
  7. テスト: 検索実装のための包括的なユニットテストを書くことをお勧めします。Goのユニットテストのベストプラクティスに従って、コードが堅牢で保守可能であることを保証してください

有用なリンク