用 Elasticsearch 构建一个 ChatGPT connector 来查询 GitHub issues

作者:来自 Elastic Tomás Murúa

学习如何构建自定义 ChatGPT connector,并部署一个使用混合搜索来查询内部 GitHub issues 的 Elasticsearch MCP server。

Agent Builder 现在作为技术预览提供。你可以通过 Elastic Cloud Trial 开始使用,并在这里查看 Agent Builder 的文档


最近,OpenAI 在 Pro/Business/Enterprise 和 Edu 方案中发布了 ChatGPT 的 custom connectors 功能。除了提供开箱即用的 Gmail、GitHub、Dropbox 等 connectors 外,你也可以通过 MCP servers 来创建自定义 connectors。

自定义 connectors 让你能够把现有的 ChatGPT connectors 与 Elasticsearch 等额外数据源结合,从而获得更完整的答案。

本文将构建一个 MCP server,把 ChatGPT 连接到一个包含内部 GitHub issues 和 pull requests 信息的 Elasticsearch index。这样就能用自然语言查询,并由你的 Elasticsearch 数据来回答。

我们会使用 FastMCP 在 Google Colab 上部署 MCP server,并通过 ngrok 获取一个 ChatGPT 可连接的公共 URL,而不需要复杂的基础设施。

如果你想了解 MCP 及其生态系统的完整介绍,请参考 The Current State of MCP

前提条件

开始之前,你需要:

  • Elasticsearch cluster(8.X 或更高版本)

  • 对你的 index 有读取权限的 Elasticsearch API key

  • Google 账号(用于 Google Colab)

  • Ngrok 账号(免费方案即可)- 类似国内的花生壳

  • 具有 Pro/Enterprise/Business 或 Edu 方案的 ChatGPT 账号

理解 ChatGPT MCP connector 的要求

ChatGPT MCP connectors 需要实现两个工具:searchfetch 。更多细节可参考 OpenAI Docs

根据用户查询,从你的 Elasticsearch index 返回相关结果列表。

它接收:

  • 一段用户的自然语言查询字符串。
  • 示例: "Find issues related to Elasticsearch migration."

它返回:

一个包含 result 键的对象,result 是一个结果对象数组。每个结果包含:

  • id ------ 文档的唯一标识符
  • title ------ issue 或 PR 的标题
  • url ------ issue/PR 的链接

在我们的实现中:

bash 复制代码
`

1.  return {
2.      "results": [
3.          {
4.              "id": "PR-612",
5.              "title": "Fix memory leak in WebSocket notification service",
6.              "url": "https://internal-git.techcorp.com/pulls/612"
7.          },
8.          # ... more results
9.      ]
10.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

Fetch tool

获取指定文档的完整内容。

它接收:

一段来自搜索结果的 Elasticsearch 文档 ID 字符串

示例: "Get me the details of PR-578."

它返回:

一个完整的文档对象,包含:

  • id ------ 文档的唯一标识符
  • title ------ issue 或 PR 的标题
  • text ------ 完整的 issue/PR 描述和细节
  • url ------ issue/PR 的链接
  • type ------ 文档类型( issue 、 pull_request )
  • status ------ 当前状态( open 、 in_progress 、 resolved )
  • priority ------ 优先级( low 、 medium 、 high 、 critical )
  • assignee ------ 负责此 issue/PR 的人
  • created_date ------ 创建时间
  • resolved_date ------ 解决时间(如果适用)
  • labels ------ 文档的标签
  • related_pr ------ 关联的 pull request ID
bash 复制代码
`

1.  return {
2.      "id": "PR-578",
3.      "title": "Security hotfix: Patch SQL injection vulnerabilities",
4.      "text": "Description: CRITICAL SECURITY FIX for ISSUE-1889. Patches SQL...",
5.      "url": "https://internal-git.techcorp.com/pulls/578",
6.      "type": "pull_request",
7.      "status": "closed",
8.      "priority": "critical",
9.      "assignee": "sarah_dev",
10.      "created_date": "2025-09-19",
11.      "resolved_date": "2025-09-19",
12.      "labels": "security, hotfix, sql",
13.      "related_pr": null
14.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

注意:这个示例使用的是扁平结构,所有字段都在根级。OpenAI 的要求是灵活的,也支持嵌套的 metadata 对象。

GitHub issues 和 PRs 数据集

在这个教程中,我们会使用一个内部 GitHub 数据集,包含 issues 和 pull requests。它代表了一个场景:你想通过 ChatGPT 查询私有的、内部的数据。

数据集在这里可以找到。我们会使用 bulk API 来更新这些数据的 index。

这个数据集包含:

  • 带有描述、状态、优先级和负责人信息的 issues

  • 带有代码变更、评审和部署信息的 pull requests

  • issues 和 PRs 之间的关联关系(例如:PR-578 修复 ISSUE-1889)

  • 标签、日期以及其他 metadata

Index mappings

这个 index 使用下面的 mappings 来支持带 ELSER混合搜索。字段 text_semantic 用于语义搜索,而其他字段用于 keyword 搜索。

bash 复制代码
`

1.  {
2.    "mappings": {
3.      "properties": {
4.        "id": {
5.          "type": "keyword"
6.        },
7.        "title": {
8.          "type": "text"
9.        },
10.        "text": {
11.          "type": "text"
12.        },
13.        "text_semantic": {
14.          "type": "semantic_text",
15.          "inference_id": ".elser-2-elasticsearch"
16.        },
17.        "url": {
18.          "type": "keyword"
19.        },
20.        "type": {
21.          "type": "keyword"
22.        },
23.        "status": {
24.          "type": "keyword"
25.        },
26.        "priority": {
27.          "type": "keyword"
28.        },
29.        "assignee": {
30.          "type": "keyword"
31.        },
32.        "created_date": {
33.          "type": "date",
34.          "format": "iso8601"
35.        },
36.        "resolved_date": {
37.          "type": "date",
38.          "format": "iso8601"
39.        },
40.        "labels": {
41.          "type": "keyword"
42.        },
43.        "related_pr": {
44.          "type": "keyword"
45.        }
46.      }
47.    }
48.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

构建 MCP server

我们的 MCP server 实现了两个遵循 OpenAI 规范的工具,使用混合搜索将 semantic search 与 text matching 结合,以获得更好的结果。

使用带有 RRF ( Reciprocal Rank Fusion ) 的混合搜索,结合 semantic search 和 text matching:

python 复制代码
`

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

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

12.          try:
13.              # Hybrid search with RRF (Reciprocal Rank Fusion)
14.              response = es_client.search(
15.                  index=ELASTICSEARCH_INDEX,
16.                  size=10,
17.                  source=["id", "title", "url", "type", "priority"],
18.                  retriever={
19.                      "rrf": {
20.                          "retrievers": [
21.                              {
22.                                  # Semantic search with ELSER
23.                                  "standard": {
24.                                      "query": {
25.                                          "semantic": {
26.                                              "field": "text_semantic",
27.                                              "query": query
28.                                          }
29.                                      }
30.                                  }
31.                              },
32.                              {
33.                                  # Text search (BM25) for keyword matching
34.                                  "standard": {
35.                                      "query": {
36.                                          "multi_match": {
37.                                              "query": query,
38.                                              "fields": [
39.                                                  "title^3",
40.                                                  "text^2",
41.                                                  "assignee^2",
42.                                                  "type",
43.                                                  "labels",
44.                                                  "priority"
45.                                              ],
46.                                              "type": "best_fields",
47.                                              "fuzziness": "AUTO"
48.                                          }
49.                                      }
50.                                  }
51.                              }
52.                          ],
53.                          "rank_window_size": 50,
54.                          "rank_constant": 60
55.                      }
56.                  }
57.              )

59.              results = []
60.              if response and 'hits' in response:
61.                  for hit in response['hits']['hits']:
62.                      source = hit['_source']
63.                      results.append({
64.                          "id": source.get('id', hit['_id']),
65.                          "title": source.get('title', 'Unknown'),
66.                          "url": source.get('url', '')
67.                      })

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

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

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)收起代码块![](https://csdnimg.cn/release/blogv2/dist/pc/img/arrowup-line-top-White.png)

要点:

  • 使用混合搜索RRF :将语义搜索( [ELSER](https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-elser "ELSER"))与文本搜索( BM25)结合以获得更好的结果。

  • 多字段匹配 ( multi-match ):在多个字段中搜索并加权( title^3text^2assignee^2)。符号 ^ 会乘以相关性得分,使标题匹配优先于内容匹配。

  • 模糊匹配[fuzziness: AUTO](https://www.elastic.co/docs/reference/elasticsearch/rest-apis/common-options#fuzziness "fuzziness: AUTO") 通过允许近似匹配来处理拼写错误和笔误。

  • RRF 参数调整

    • rank_window_size: 50 ------ 指定在合并前,每个检索器(语义和文本)考虑的前 N 个结果。

    • rank_constant: 60 ------ 决定各个结果集中某文档对最终排名的影响量。

  • 只返回必需字段 :按照 OpenAI 规范只返回 idtitleurl,避免不必要地暴露额外字段。

Fetch tool

通过文档 ID 获取文档详情(如果存在):

python 复制代码
`

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

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

12.          try:
13.              # Search by the 'id' field (not _id) since IDs are stored as a field
14.              response = es_client.search(
15.                  index=ELASTICSEARCH_INDEX,
16.                  body={
17.                      "query": {
18.                          "term": {
19.                              "id": id  # Search by your custom 'id' field
20.                          }
21.                      },
22.                      "size": 1
23.                  }
24.              )

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

29.              hit = response['hits']['hits'][0]
30.              source = hit['_source']

32.              result = {
33.                  "id": source.get('id', id),
34.                  "title": source.get('title', 'Unknown'),
35.                  "text": source.get('text', ''),
36.                  "url": source.get('url', ''),
37.                  "type": source.get('type', ''),
38.                  "status": source.get('status', ''),
39.                  "priority": source.get('priority', ''),
40.                  "assignee": source.get('assignee', ''),
41.                  "created_date": source.get('created_date', ''),
42.                  "resolved_date": source.get('resolved_date', ''),
43.                  "labels": source.get('labels', ''),
44.                  "related_pr": source.get('related_pr', '')
45.              }

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

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

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

要点:

  • 按文档 ID 字段搜索:在自定义 id 字段上使用 term query
  • 返回完整文档:包含带有所有内容的完整 text 字段
  • 扁平结构:所有字段都在根级,符合 Elasticsearch 的文档结构

在 Google Colab 上部署

我们将使用 Google Colab 运行 MCP server,并用 ngrok 将其公开,使 ChatGPT 可以连接。

步骤 1:打开 Google Colab notebook

访问我们的预配置 notebook Elasticsearch MCP for ChatGPT

步骤 2:配置你的凭证

你需要三条信息:

  • Elasticsearch URL :你的 Elasticsearch cluster URL
  • Elasticsearch API Key :对你的 index 有读取权限的 API key
  • Ngrok Auth Token :来自 ngrok 的免费 token。我们将使用 ngrok 将 MCP URL 暴露到互联网,以便 ChatGPT 可以连接

获取你的 ngrok token

  • 在 ngrok 注册一个免费账号
  • 打开 ngrok dashboard
  • 复制你的 auth token

在 Google Colab 中添加 secrets

在 Google Colab notebook 中:

  1. 点击左侧栏的钥匙图 标以打开 Secrets

  2. 添加以下三个 secrets:

    ini 复制代码
    `
    
    1.  ELASTICSEARCH_URL=https://your-cluster.elastic.com:443
    2.  ELASTICSEARCH_API_KEY=your-api-key
    3.  NGROK_TOKEN=your-ngrok-token
    
    `AI写代码
  3. 为每个 secret 启用 notebook 访问权限

步骤 3:运行 notebook

  1. 点击 Runtime ,然后选择 Run all 来执行所有单元格

  2. 等待服务器启动(大约 30 秒)

  3. 查看输出,找到你的公共 ngrok URL

  4. 输出将显示类似如下内容:

连接到 ChatGPT

现在我们将把 MCP server 连接到你的 ChatGPT 账号。

1)打开 ChatGPT 并进入 Settings

2)导航到 Connectors 。如果你使用的是 Pro 账号,需要在 connectors 中开启开发者模式(developer mode

如果你使用的是 ChatGPT Enterprise 或 Business,需要将 connector 发布到你的工作区。

3)点击 Create

注意:在 Business、Enterprise 和 Edu 工作区中,只有工作区所有者、管理员以及具有相应权限的用户(Enterprise/Edu)可以添加自定义 connectors。普通成员角色的用户无法自行添加自定义 connectors。

一旦 connector 由所有者或管理员添加并启用,它就可以被工作区的所有成员使用。

4)输入所需信息以及以 /sse/ 结尾的 ngrok URL。注意 "sse" 后的斜杠 /,缺少它将无法工作:

  • Name:Elasticsearch MCP
  • Description:用于搜索和获取 GitHub 内部信息的自定义 MCP

5)点击 Create 保存自定义 MCP。

如果你的服务器正在运行,连接会立即生效。无需额外身份验证,因为 Elasticsearch API key 已在服务器中配置。

测试 MCP server

在提问之前,你需要选择 ChatGPT 应该使用哪个 connector。

Prompt 1:搜索 issues

提问: "Find issues related to Elasticsearch migration" 并确认调用了 actions tool。

ChatGPT 会使用你的查询调用 search tool。你可以看到它正在查找可用工具,并准备调用 Elasticsearch tool,同时在对工具执行任何操作之前,会先向用户确认。

Tool 调用请求:

markdown 复制代码
`

1.  {
2.    "query": "Elasticsearch migration issues"
3.  }

`AI写代码

Tool response:

bash 复制代码
`

1.  {
2.    "results": [
3.      {
4.        "id": "PR-598",
5.        "title": "Elasticsearch 8.x migration - Application code changes",
6.        "url": "https://internal-git.techcorp.com/pulls/598"
7.      },
8.      {
9.        "id": "ISSUE-1712",
10.        "title": "Migrate from Elasticsearch 7.x to 8.x",
11.        "url": "https://internal-git.techcorp.com/issues/1712"
12.      },
13.      {
14.        "id": "RFC-045",
15.        "title": "Design Proposal: Microservices Migration Architecture",
16.        "url": "https://internal-git.techcorp.com/rfcs/045"
17.      }
18.      // ... 7 more results
19.    ]
20.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

ChatGPT 会处理结果,并以自然、对话式的格式展示给用户。

幕后流程

  1. ChatGPT 调用 search("Elasticsearch migration")

  2. Elasticsearch 执行混合搜索

    1. Semantic search 理解 "upgrade" 和 "version compatibility" 等概念
    2. Text search 查找 "Elasticsearch" 和 "migration" 的精确匹配
    3. RRF 将两种方法的结果合并并排序
  3. 返回前 10 个匹配结果,包括 idtitleurl

  4. ChatGPT 识别 "ISSUE-1712: migrate from Elasticsearch 7.x to 8.x" 为最相关结果

Prompt 2:获取完整详情

提问: "Get me details of ISSUE-1889"

ChatGPT 识别到你想获取特定 issue 的详细信息,并调用 fetch tool,同时在对工具执行任何操作之前会先向用户确认。

Tool 调用请求:

markdown 复制代码
`

1.  {
2.    "id": "ISSUE-1889"
3.  }

`AI写代码

Tool 响应:

bash 复制代码
`

1.  {
2.    "id": "ISSUE-1889",
3.    "title": "SQL injection vulnerability in search endpoint",
4.    "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.",
5.    "url": "https://internal-git.techcorp.com/issues/1889",
6.    "type": "issue",
7.    "status": "closed",
8.    "priority": "critical",
9.    "assignee": "sarah_dev",
10.    "created_date": "2025-09-18",
11.    "resolved_date": "2025-09-19",
12.    "labels": "security, vulnerability, bug, sql",
13.    "related_pr": "PR-578"
14.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

ChatGPT 会整合信息,并以清晰的方式呈现。

幕后流程

Prompt: "Get me the details of ISSUE-1889"

  • ChatGPT 调用 fetch("ISSUE-1889")
  • Elasticsearch 检索完整文档
  • 返回一个所有字段都在根级的完整文档
  • ChatGPT 整合信息,并带上适当引用进行回答

结论

本文中,我们构建了一个自定义 MCP server,将 ChatGPT 连接到 Elasticsearch,使用专门的 search 和 fetch MCP 工具,使得可以通过自然语言查询私有数据。

这种 MCP 模式适用于任何 Elasticsearch index、文档、产品、日志或其他希望通过自然语言查询的数据。

原文:www.elastic.co/search-labs...

相关推荐
Dxy12393102161 小时前
ES查询语句介绍
大数据·elasticsearch·搜索引擎
武子康2 小时前
大数据-171 Elasticsearch-Head 与 Kibana 7.3.0 实战:安装要点、连通性与常见坑
大数据·后端·elasticsearch
Violet_YSWY2 小时前
git清理缓存
git·elasticsearch·缓存
safestar20122 小时前
Elasticsearch性能优化实战:从GB到PB级数据的性能演进之路
大数据·elasticsearch·性能优化
Huathy-雨落江南,浮生若梦2 小时前
ElasticSearch9入门(四)聚合
elasticsearch
豆奶特浓63 小时前
谢飞机迎战金融风控面试:从Spring WebFlux、Elasticsearch到AI模型,他能扛住吗?
java·elasticsearch·微服务·ai·面试·spring webflux·金融风控
Dxy12393102163 小时前
Elasticsearch数据更新简介
大数据·elasticsearch·搜索引擎
G皮T19 小时前
【Elasticsearch】索引别名 aliases
大数据·elasticsearch·搜索引擎·es·索引·索引别名·aliases
爱跑步的程序员~20 小时前
Elasticsearch倒排索引
java·大数据·elasticsearch·搜索引擎·全文检索