ブログ

GitHubのイシューをElasticsearchでクエリするChatGPTコネクターの構築

カスタムChatGPTコネクターの構築方法と、ハイブリッド検索して内部のGitHubイシューをクエリするElasticsearch MCPサーバーをデプロイする方法を学びましょう。

最近、OpenAIはPro/Business/EnterpriseおよびEduプラン向けにChatGPT向けのカスタムコネクター機能を発表しました。これは、Gmail、GitHub、Dropboxなどのデータを活用するためのすぐに使えるコネクターへの追加となります。MCPサーバーを使用してカスタムコネクターを作成できます。

カスタムコネクターを使用すると、既存のChatGPTコネクターをElasticsearchなどの追加のデータソースと組み合わせて、包括的な回答を得ることができます。

この記事では、内部のGitHubの課題とプルリクエストに関する情報を含むElasticsearchインデックスにChatGPTを接続するMCPサーバーを構築します。これにより、Elasticsearchデータを使用して自然言語クエリに回答できるようになります。

Google ColabのFastMCPとngrokを使ってMCPサーバーをデプロイし、ChatGPTが接続できる公開URLを取得し、複雑なインフラ構築の必要性を排除します。

MCPとそのエコシステムの包括的な概要については、MCPの現在の状態をご参照ください。

要件

始める前に必要なものは次のとおりです。

  • Elasticsearchクラスター(8.X以降)

  • インデックスへの読み取りアクセス権を持つ Elasticsearch APIキー

  • Googleアカウント(Google Colab用)

  • Ngrokアカウント(無料プランでも可)

  • Pro/Enterprise/BusinessまたはEduプランのChatGPTアカウント

ChatGPT MCPコネクターの要件を理解する

ChatGPT MCPコネクターには、searchfetchの2つのツールを実装する必要があります。詳細については、OpenAIドキュメントをご覧ください。

検索ツール

ユーザークエリに基づいて、Elasticsearchインデックスから関連する結果のリストを返します。

受け取るもの:

  • ユーザーの自然言語クエリを含む単一の文字列。

  • 例:「Elasticsearch移行に関連するイシューを見つけて」

返されるもの:

  • 結果オブジェクトの配列を含むresultキーを持つオブジェクト。各結果には以下が含まれます。

    • id - 一意の文書識別子

    • title - イシューまたはPRタイトル

    • url - イシュー/PRへのリンク

実装内容:

return {
    "results": [
        {
            "id": "PR-612",
            "title": "Fix memory leak in WebSocket notification service",
            "url": "https://internal-git.techcorp.com/pulls/612"
        },
        # ... more results
    ]
}

フェッチ・ツール

特定の文書の完全な内容を取得します。

受け取るもの:

  • 検索結果からElasticsearch文書IDを入力する単一の文字列

  • 例:「PR-578の詳細を教えてください。」

返されるもの:

  • 以下を含む完全な文書オブジェクト:

    • id - 一意の文書識別子

    • title - イシューまたはPRタイトル

    • text - 完全なイシュー・PRの説明と詳細

    • url - イシュー/PRへのリンク

    • type - 文書の種類(issue, pull_request)

    • status - 現在のステータス(open, in_progress, resolved)

    • priority - 優先度レベル(low, medium, high, critical)

    • assignee - イシュー/PRの担当者

    • created_date - 作成された時期

    • resolved_date - 解決された時期(該当する場合)

    • labels 文書に関連するタグ

    • related_pr - 関連するプルリクエストID

return {
    "id": "PR-578",
    "title": "Security hotfix: Patch SQL injection vulnerabilities",
    "text": "Description: CRITICAL SECURITY FIX for ISSUE-1889. Patches SQL...",
    "url": "https://internal-git.techcorp.com/pulls/578",
    "type": "pull_request",
    "status": "closed",
    "priority": "critical",
    "assignee": "sarah_dev",
    "created_date": "2025-09-19",
    "resolved_date": "2025-09-19",
    "labels": "security, hotfix, sql",
    "related_pr": null
}

:この例では、すべてのフィールドがルートレベルにあるフラット構造を使用しています。OpenAIの要件は柔軟で、ネストされたメタデータオブジェクトもサポートしています。

GitHubのデータセットとプルリクエストデータセット

このチュートリアルでは、イシューとプルリクエストを含む内部GitHubデータセットを使用します。これは、ChatGPTを通じてプライベートな内部データをクエリするシナリオを表しています。

データセットはこちらからご覧いただけます。そして、Bulk APIを使ってデータのインデックスを更新します。

このデータセットには以下が含まれます。

  • 説明、ステータス、優先順位、担当者に関する問題

  • コード変更、レビュー、導入情報を含むプルリクエスト

  • イシューとPRの関係(例:PR-578がISSUE-1889を修正)

  • ラベル、日付、その他のメタデータ

インデックスマッピング

インデックスは、ELSERとのハイブリッド検索をサポートするために以下のマッピングを使用します。text_semanticはセマンティック検索に使用され、他のフィールドはキーワード検索を可能にします。

{
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "title": {
        "type": "text"
      },
      "text": {
        "type": "text"
      },
      "text_semantic": {
        "type": "semantic_text",
        "inference_id": ".elser-2-elasticsearch"
      },
      "url": {
        "type": "keyword"
      },
      "type": {
        "type": "keyword"
      },
      "status": {
        "type": "keyword"
      },
      "priority": {
        "type": "keyword"
      },
      "assignee": {
        "type": "keyword"
      },
      "created_date": {
        "type": "date",
        "format": "iso8601"
      },
      "resolved_date": {
        "type": "date",
        "format": "iso8601"
      },
      "labels": {
        "type": "keyword"
      },
      "related_pr": {
        "type": "keyword"
      }
    }
  }
}

MCPサーバーを構築する

当社のMCPサーバーは、OpenAI仕様に従って2つのツールを実装しています。ハイブリッド検索を使用してセマンティックマッチングとテキストマッチングを組み合わせることで、より良い結果が得られます。

検索ツール

RRF(相互ランク融合)を用いたハイブリッド検索を使用し、セマンティック検索とテキストマッチングを組み合わせています。

@mcp.tool()
    async def search(query: str) -> Dict[str, List[Dict[str, Any]]]:
        """
        Search for internal issues and PRs using hybrid search (semantic + text with RRF).
        Returns list with id, title, and url per OpenAI spec.
        """
        if not query or not query.strip():
            return {"results": []}

        logger.info(f"Searching for: '{query}'")

        try:
            # Hybrid search with RRF (Reciprocal Rank Fusion)
            response = es_client.search(
                index=ELASTICSEARCH_INDEX,
                size=10,
                source=["id", "title", "url", "type", "priority"],
                retriever={
                    "rrf": {
                        "retrievers": [
                            {
                                # Semantic search with ELSER
                                "standard": {
                                    "query": {
                                        "semantic": {
                                            "field": "text_semantic",
                                            "query": query
                                        }
                                    }
                                }
                            },
                            {
                                # Text search (BM25) for keyword matching
                                "standard": {
                                    "query": {
                                        "multi_match": {
                                            "query": query,
                                            "fields": [
                                                "title^3",
                                                "text^2",
                                                "assignee^2",
                                                "type",
                                                "labels",
                                                "priority"
                                            ],
                                            "type": "best_fields",
                                            "fuzziness": "AUTO"
                                        }
                                    }
                                }
                            }
                        ],
                        "rank_window_size": 50,
                        "rank_constant": 60
                    }
                }
            )

            results = []
            if response and 'hits' in response:
                for hit in response['hits']['hits']:
                    source = hit['_source']
                    results.append({
                        "id": source.get('id', hit['_id']),
                        "title": source.get('title', 'Unknown'),
                        "url": source.get('url', '')
                    })

            logger.info(f"Found {len(results)} results")
            return {"results": results}

        except Exception as e:
            logger.error(f"Search error: {e}")
            raise ValueError(f"Search failed: {str(e)}")

主なポイント:

  • RRFを用いたハイブリッド検索: より良い結果を得るために、セマンティック検索(ELSER)とテキスト検索(BM25)を組み合わせます。

  • 複数一致クエリ:ブースティングを使用して複数のフィールドを検索します(title^3, text^2, assignee^2)。キャレット記号(^)は関連性スコアを乗算し、コンテンツよりもタイトルの一致を優先します。

  • あいまい一致: fuzziness: AUTO は近似一致を許可することでタイプミスやスペルミスを処理します。

  • RRFのパラメーター調整:

    • rank_window_size: 50 - マージする前に、各リトリーバー(セマンティックとテキスト)からの上位結果をいくつ考慮するかを指定します。

    • rank_constant: 60 - この値は、個々の結果セット内の文書が最終的なランク付け結果にどの程度影響を与えるかを決定します。

  • 必須フィールドのみを返す: idtitleurlはOpenAIの仕様に従い、追加のフィールドを不必要に公開しないようにします。

フェッチ・ツール

文書IDが存在する場合は、そのIDで文書の詳細を取得します。

@mcp.tool()
    async def fetch(id: str) -> Dict[str, Any]:
        """
        Retrieve complete issue/PR details by ID.
        Returns id, title, text, url.
        """
        if not id:
            raise ValueError("ID is required")

        logger.info(f"Fetching: {id}")

        try:
            # Search by the 'id' field (not _id) since IDs are stored as a field
            response = es_client.search(
                index=ELASTICSEARCH_INDEX,
                body={
                    "query": {
                        "term": {
                            "id": id  # Search by your custom 'id' field
                        }
                    },
                    "size": 1
                }
            )

            if not response or not response['hits']['hits']:
                raise ValueError(f"Document with id '{id}' not found")

            hit = response['hits']['hits'][0]
            source = hit['_source']

            result = {
                "id": source.get('id', id),
                "title": source.get('title', 'Unknown'),
                "text": source.get('text', ''),
                "url": source.get('url', ''),
                "type": source.get('type', ''),
                "status": source.get('status', ''),
                "priority": source.get('priority', ''),
                "assignee": source.get('assignee', ''),
                "created_date": source.get('created_date', ''),
                "resolved_date": source.get('resolved_date', ''),
                "labels": source.get('labels', ''),
                "related_pr": source.get('related_pr', '')
            }

            logger.info(f"Fetched: {result['title']}")
            return result

        except Exception as e:
            logger.error(f"Fetch error: {e}")
            raise ValueError(f"Failed to fetch '{id}': {str(e)}")

主なポイント:

  • 文書IDのフィールドで検索:カスタム idフィールドに用語クエリを使用します

  • 完全な文書を返す: すべてのコンテンツを含む完全なtextフィールドが含まれます

  • フラットな構造:すべてのフィールドがルートレベルにあり、Elasticsearchのドキュメント構造に一致します。

Google Colabにデプロイする

Google Colabを使用してMCPサーバーを実行し、ngrokで公開することで、ChatGPTが接続できるようにします。

ステップ1:Google Colabノートブックを開く

事前設定されたノートブックElasticsearch MCP for ChatGPTにアクセスします。

ステップ2:認証情報を設定する

次の3つの情報が必要になります。

  • Elasticsearch URL:お客様のElasticsearchクラスタリングURL

  • Elasticsearch API キー:インデックスへの読み取りアクセス権を持つAPIキー

  • Ngrok認証トークン:ngrokからの無料トークン。ngrokを使ってMCPのURLをインターネットに公開し、ChatGPTが接続できるようにします。

ngrokトークンの取得

  1. ngrokで無料アカウントに登録します。

  2. ngrokダッシュボードにアクセスします。

  3. 認証トークンをコピーします。

Google Colabにシークレットを追加する

Google Colabノートブック内で:

  1. 左側のサイドバーにあるキーアイコンをクリックして、シークレットを開きます。

  2. 次の3つのシークレットを追加します。

ELASTICSEARCH_URL=https://your-cluster.elastic.com:443
ELASTICSEARCH_API_KEY=your-api-key
NGROK_TOKEN=your-ngrok-token

3. 各シークレットのノートブックアクセスを有効にします。

Google Colabへのシークレットの追加

ステップ3:ノートブックを実行する

  1. ランタイムをクリックし、次にすべて実行をクリックして、すべてのセルを実行します。

  2. サーバーの起動を待ちます(約30秒)。

  3. 公開ngrok URLを示す出力を探します。

4. 出力は次のようになります。

Google Collabでノートブックを実行した結果

ChatGPTに接続する

次に、MCPサーバーをあなたのChatGPTアカウントに接続します。

  1. ChatGPTを開き、設定に移動します。

  2. コネクターに移動します。Proアカウントを使用している場合は、コネクタで開発者モードをオンにする必要があります。

ChatGPTアカウントへのMPCサーバーの接続

ChatGPT EnterpriseまたはBusinessを使用している場合は、コネクターを職場に公開する必要があります。

3. 作成をクリックします。

ChatGPTへのコネクターの追加

:Business、Enterprise、Eduワークスペースでは、ワークスペースの所有者、管理者、およびそれぞれの設定が有効になっているユーザー(Enterprise/Eduの場合)のみがカスタムコネクターを追加できます。通常のメンバーロールのユーザーには、自分でカスタムコネクターを追加する権限がありません。

コネクターが所有者または管理者ユーザーによって追加され有効化されると、ワークスペースのすべてのメンバーが使用できるようになります。

4. 必要な情報と、/sse/で終わるngrokのURLを入力します。「sse」の後の「/」に注意してください。これがない場合、動作しません。

  • Name: Elasticsearch MCP

  • Description: GitHubの内部情報を検索および取得するためのカスタムMCP。

Elastic MCPコネクターの作成

5. 作成を押してカスタムMCPを保存します。

作成をクリックしてカスタムMCPコネクタを保存する

サーバーが稼働していれば、接続は瞬時に完了します。追加の認証は不要で、Elasticsearch APIキーはサーバー上で設定されています。

MCPサーバーをテストする

質問する前に、ChatGPTが使用するコネクターを選択する必要があります。

ChatGPTが使用するコネクターを選択する

プロンプト1:イシューを検索する

「Elasticsearchの移行に関連するイシューを見つけて」と質問し、アクションツールの呼び出しを確認します。

ChatGPTに「Elasticsearchの移行に関連する問題を見つける」ように依頼し、アクションツールの呼び出しを確認します。

ChatGPT はクエリを使用してsearchツールを呼び出します。利用可能なツールを検索し、Elasticsearchツールを呼び出す準備をし、ツールに対して何らかのアクションを実行する前にユーザーに確認していることがわかります。

ツール呼び出しリクエスト:

{
  "query": "Elasticsearch migration issues"
}

ツールの応答:

{
  "results": [
    {
      "id": "PR-598",
      "title": "Elasticsearch 8.x migration - Application code changes",
      "url": "https://internal-git.techcorp.com/pulls/598"
    },
    {
      "id": "ISSUE-1712",
      "title": "Migrate from Elasticsearch 7.x to 8.x",
      "url": "https://internal-git.techcorp.com/issues/1712"
    },
    {
      "id": "RFC-045",
      "title": "Design Proposal: Microservices Migration Architecture",
      "url": "https://internal-git.techcorp.com/rfcs/045"
    }
    // ... 7 more results
  ]
}

ChatGPTは結果を処理し、自然で会話的な形式で提示します。

ChatGPTがツール呼び出しリクエストとツール呼び出しレスポンスの結果を処理する方法

仕組み

プロンプト:「Elasticsearch移行に関連するイシューを見つけて」

1. ChatGPTの呼び出し search(“Elasticsearch migration”)

2. Elasticsearchがハイブリッド検索を実行する

  • セマンティック検索は「アップグレード」や「バージョン互換性」などの概念を理解します。

  • テキスト検索で「Elasticsearch」と「migration」の完全一致を見つけます。

  • RRFは両方のアプローチの結果を組み合わせてランク付けします。

3. idtitleを含むトップ10のマッチングイベントを返します。 url

4. ChatGPTは「ISSUE-1712: migrate from Elasticsearch 7.x to 8.x」を最も関連性の高い結果として特定します。

プロンプト2:完全な詳細を取得する

質問:「ISSUE-1889の詳細を教えて」

ChatGPTは、ユーザーが特定のイシューに関する詳細な情報を求めていることを認識し、フェッチツールを呼び出し、ツールに対して何らかのアクションを実行する前にユーザーに確認します。

ChatGPTは、あなたが特定のイシューに関する詳細な情報を求めていることを認識し、fetchツールを呼び出し、ツールに対して何らかのアクションを起こす前にユーザーに確認します。

ツール呼び出しリクエスト:

{
  "id": "ISSUE-1889"
}

ツールの応答:

{
  "id": "ISSUE-1889",
  "title": "SQL injection vulnerability in search endpoint",
  "text": "Description: Security audit identified SQL injection vulnerability in /api/v1/search endpoint. User input from query parameter is not properly sanitized before being used in raw SQL query. Severity: HIGH - Immediate action required Affected Code: - File: services/search/query_builder.py - Line: 145-152 - Issue: String concatenation used instead of parameterized queries Investigation: - @security_team_alice: Confirmed exploitable with UNION-based injection - @sarah_dev: Checking all other endpoints for similar patterns - @john_backend: Found 3 more instances in legacy codebase Remediation: - Rewrite using SQLAlchemy ORM or parameterized queries - Add input validation and sanitization - Implement WAF rules as additional layer - Security regression tests Comments: - @tech_lead_mike: Stop all other work, this is P0 - @sarah_dev: PR-578 ready with fixes for all 4 vulnerable endpoints - @alex_devops: Deployed hotfix to production 2025-09-19 at 14:30 UTC - @security_team_alice: Verified fix, conducting full pentest next week Resolution: All vulnerable endpoints patched. Added pre-commit hooks to catch raw SQL queries. Security training scheduled for team.",
  "url": "https://internal-git.techcorp.com/issues/1889",
  "type": "issue",
  "status": "closed",
  "priority": "critical",
  "assignee": "sarah_dev",
  "created_date": "2025-09-18",
  "resolved_date": "2025-09-19",
  "labels": "security, vulnerability, bug, sql",
  "related_pr": "PR-578"
}

ChatGPTは情報を統合し、明確に提示します。

ChatGPTがどのように情報を統合して提示するか
ChatGPTがどのように情報を提示するか

仕組み

プロンプト:「ISSUE-1889の詳細を教えて」

  1. ChatGPT呼び出し fetch(“ISSUE-1889”)

  2. Elasticsearchが完全な文書を取得する

  3. すべてのフィールドがルートレベルにある完全な文書を返す

  4. ChatGPTは情報を統合し、適切な引用で回答する

まとめ

この記事では、専用の検索およびフェッチMCPツールを使用してChatGPTをElasticsearchに接続するカスタムMCPサーバーを構築し、プライベートデータに対する自然言語クエリを可能にしました。

このMCPパターンは、自然言語を使用してクエリしたい任意のElasticsearchインデックス、ドキュメント、製品、ログ、またはその他のデータで機能します。

関連記事

チャットボックスを超えたエージェントビルダー:Augmented Infrastructureの導入

Alexander Wert

Elasticsearchによるエージェントメモリの管理

Someshwaran Mohankumar

Elasticsearch Inference APIとHugging Faceモデルを組み合わせて使用

Jeffrey Rengifo

Elastic Agent Builder と GPT-OSS を使用した HR 向け AI エージェントの構築

Tomás Murúa

TypeScriptを使用したElasticsearch MCPサーバーの作成

Jeffrey Rengifo

最先端の検索体験を構築する準備はできましたか?

十分に高度な検索は 1 人の努力だけでは実現できません。Elasticsearch は、データ サイエンティスト、ML オペレーター、エンジニアなど、あなたと同じように検索に情熱を傾ける多くの人々によって支えられています。ぜひつながり、協力して、希望する結果が得られる魔法の検索エクスペリエンスを構築しましょう。

はじめましょう