使用 FastAPI 的 WebSockets 和 Elasticsearch 来构建实时应用

作者:来自 Elastic Jeffrey Rengifo

学习如何使用 FastAPI WebSockets 和 Elasticsearch 构建实时应用程序。

更多阅读:使用 FastAPI 构建 Elasticsearch API

想要获得 Elastic 认证吗?看看下一次 Elasticsearch Engineer 培训什么时候开始!

Elasticsearch 拥有许多新功能,可以帮助你为你的使用场景构建最佳搜索解决方案。深入学习我们的示例笔记本,了解更多内容,开始免费的云试用,或者立即在本地机器上尝试 Elastic。


WebSockets一种同时双向通信协议。它的理念是客户端和服务器可以保持一个打开的连接,同时互相发送消息,从而尽可能降低延迟。这种方式常见于实时应用,比如聊天、活动通知或交易平台,在这些场景中延迟是关键,并且存在持续的信息交换。

想象一下你创建了一个消息应用,想在用户收到新消息时通知他们。你可以每隔 5 或 10 秒通过发送 HTTP 请求轮询服务器,直到有新消息,或者你可以保持一个 WebSockets 连接,让服务器推送一个事件,客户端监听后在消息到达时立即显示通知标记。

在这种情况下,Elasticsearch 能够在数据集上实现快速而灵活的搜索,使其非常适合需要即时结果的实时应用。

在这篇文章中,我们将使用 FastAPI 的 WebSockets 功能和 Elasticsearch 创建一个实时应用程序。

先决条件

  • Python 版本 3.x
  • 一个 Elasticsearch 实例(自托管或 Elastic Cloud 上)
  • 一个具有写权限的 Elasticsearch API key

本文使用的所有代码可以在这里找到。

使用场景

为了向你展示如何将 WebSockets 与 FastAPI 和 Elasticsearch 一起使用,我们将采用一个使用场景:作为店主的你,想在某个查询被执行时通知所有用户,以吸引他们的注意力。这模拟了搜索驱动应用中的实时互动,比如促销活动或产品兴趣提醒。

在这个使用场景中,我们将构建一个应用,客户可以搜索产品,并在其他用户执行了在监控列表中的搜索时收到通知。

用户 A 搜索 "Kindle",用户 B 会实时收到通知。

数据摄取

在这一部分,我们将创建索引映射,并使用一个 Python 脚本摄取所需的数据。你可以在博客仓库中找到以下脚本

摄取脚本

创建一个名为 ingest_data.py 的新文件,其中包含用于处理数据摄取的 Python 逻辑。

安装 Elasticsearch 库以处理对 Elasticsearch 的请求:

css 复制代码
`pip install elasticsearch -q`AI写代码

现在导入依赖,并使用 API key 和 Elasticsearch 端点 URL 初始化 Elasticsearch 客户端。

ini 复制代码
`

1.  import json
2.  import os

4.  from elasticsearch import Elasticsearch

6.  es_client = Elasticsearch(
7.      hosts=[os.environ["ELASTICSEARCH_ENDPOINT"]],
8.      api_key=os.environ["ELASTICSEARCH_API_KEY"],
9.  )

`AI写代码

创建一个方法,在名为 "products" 的索引下设置索引映射。

python 复制代码
`

1.  PRODUCTS_INDEX = "products"

3.  def create_products_index():
4.      try:
5.          mapping = {
6.              "mappings": {
7.                  "properties": {
8.                      "product_name": {"type": "text"},
9.                      "price": {"type": "float"},
10.                      "description": {"type": "text"},
11.                  }
12.              }
13.          }

15.          es_client.indices.create(index=PRODUCTS_INDEX, body=mapping)
16.          print(f"Index {PRODUCTS_INDEX} created successfully")
17.      except Exception as e:
18.          print(f"Error creating index: {e}")

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

现在使用 bulk API 加载产品文档,将它们推送到 Elasticsearch。数据将位于项目仓库中的 NDJSON 文件中。

python 复制代码
`

1.  def load_products_from_ndjson():
2.      try:
3.          if not os.path.exists("products.ndjson"):
4.              print("Error: products.ndjson file not found!")
5.              return

7.          products_loaded = 0
8.          with open("products.ndjson", "r") as f:
9.              for line in f:
10.                  if line.strip():
11.                      product_data = json.loads(line.strip())
12.                      es_client.index(index=PRODUCTS_INDEX, body=product_data)
13.                      products_loaded += 1

15.          print(f"Successfully loaded {products_loaded} products into Elasticsearch")

17.      except Exception as e:
18.          print(f"Error loading products: {e}")

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

最后,调用已创建的方法。

markdown 复制代码
`

1.  if __name__ == "__main__":
2.      create_products_index()
3.      load_products_from_ndjson()

`AI写代码

在终端中使用以下命令运行脚本。

go 复制代码
`python ingest_data.py`AI写代码

完成后,让我们继续构建应用。

markdown 复制代码
`

1.  Index products created successfully
2.  Successfully loaded 25 products into Elasticsearch

`AI写代码

WebSockets 应用

为了提高可读性,应用的界面将简化。完整的应用仓库可以在这里找到。

该图展示了 WebSocket 应用如何与 Elasticsearch 和多个用户交互的高级概览。

应用结构

lua 复制代码
`

1.  |-- websockets_elasticsearch_app
2.  |-- ingest_data.py
3.  |-- index.html
4.  |-- main.py

`AI写代码

安装并导入依赖

安装 FastAPI 和 WebSocket 支持。Uvicorn 将作为本地服务器,Pydantic 用于定义数据模型,Elasticsearch 客户端允许脚本连接到集群并发送数据。

css 复制代码
`pip install websockets fastapi pydantic uvicorn -q`AI写代码

FastAPI 提供了易用、轻量且高性能的工具来构建 web 应用,而 Uvicorn 作为 ASGI 服务器来运行它。Pydantic 在 FastAPI 内部用于数据验证和解析,使定义结构化数据更容易。WebSockets 提供了低级协议支持,使服务器和客户端之间能够实现实时双向通信。之前安装的 Elasticsearch Python 库将在此应用中用于处理数据检索。

现在,导入构建后端所需的库。

python 复制代码
`

1.  import json
2.  import os
3.  import uvicorn
4.  from datetime import datetime
5.  from typing import Dict, List

7.  from elasticsearch import Elasticsearch
8.  from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
9.  from fastapi.responses import FileResponse
10.  from pydantic import BaseModel, Field

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

Elasticsearch 客户端

定义 Elasticsearch 端点和 API key 的环境变量,并实例化一个 Elasticsearch 客户端来处理与 Elasticsearch 集群的连接。

ini 复制代码
`

1.  os.environ["ELASTICSEARCH_ENDPOINT"] = getpass(
2.      "Insert the Elasticsearch endpoint here: "
3.  )
4.  os.environ["ELASTICSEARCH_API_KEY"] = getpass("Insert the Elasticsearch API key here: ")

7.  es_client = Elasticsearch(
8.      hosts=[os.environ["ELASTICSEARCH_ENDPOINT"]],
9.      api_key=os.environ["ELASTICSEARCH_API_KEY"],
10.  )

12.  PRODUCTS_INDEX = "products"

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

数据模型和应用设置

现在是创建 FastAPI 实例的时候了,它将处理 REST API 和 WebSocket 路由。然后,我们将使用 Pydantic 定义几个数据模型。

  • Product 模型描述每个产品的结构。
  • SearchNotification 模型定义我们将发送给其他用户的消息。
  • SearchResponse 模型定义 Elasticsearch 结果的返回方式。

这些模型有助于在整个应用中保持一致性和可读性,并在代码 IDE 中提供数据验证、默认值和自动补全。

python 复制代码
`

1.  app = FastAPI(title="Elasticsearch - FastAPI with websockets")

3.  class Product(BaseModel):
4.      product_name: str
5.      price: float
6.      description: str

9.  class SearchNotification(BaseModel):
10.      session_id: str
11.      query: str
12.      timestamp: datetime = Field(default_factory=datetime.now)

15.  class SearchResponse(BaseModel):
16.      query: str
17.      results: List[Dict]
18.      total: int

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

WebSockets 端点设置

当用户连接到 /ws 端点时,WebSocket 连接会保持打开状态并添加到全局列表中。这允许服务器即时向所有连接的客户端广播消息。如果用户断开连接,他们的连接将被移除。

python 复制代码
`

1.  # Store active WebSocket connections
2.  connections: List[WebSocket] = []

5.  @app.websocket("/ws")
6.  async def websocket_endpoint(websocket: WebSocket):
7.      await websocket.accept()
8.      connections.append(websocket)
9.      print(f"Client connected. Total connections: {len(connections)}")

11.      try:
12.          while True:
13.              await websocket.receive_text()

15.      except WebSocketDisconnect:
16.          connections.remove(websocket)
17.          print(f"Client disconnected. Total connections: {len(connections)}")

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

搜索端点

现在让我们查看发生实时交互的代码。

当用户执行搜索时,会查询 Elasticsearch 并返回结果。同时,如果查询在全局监控列表中,所有其他已连接用户会收到通知,提示有人找到了其中的某个产品。通知中包含查询内容。

session_id 参数用于避免将通知发送回发起搜索的用户。

css 复制代码
`

1.  @app.get("/search")
2.  async def search_products(q: str, session_id: str = "unknown"):
3.      # List of search terms that should trigger a notification
4.      WATCH_LIST = ["iphone", "kindle"]

6.      try:
7.          query_body = {
8.              "query": {
9.                  "bool": {
10.                      "should": [
11.                          {"match": {"product_name": q}},
12.                          {"match_phrase": {"description": q}},
13.                      ],
14.                      "minimum_should_match": 1,
15.                  }
16.              },
17.              "size": 20,
18.          }

20.          response = es_client.search(index=PRODUCTS_INDEX, body=query_body)

22.          results = []
23.          for hit in response["hits"]["hits"]:
24.              product = hit["_source"]
25.              product["score"] = hit["_score"]
26.              results.append(product)

28.          results_count = response["hits"]["total"]["value"]

30.          # Only send notification if the search term matches
31.          if q.lower() in WATCH_LIST:
32.              notification = SearchNotification(
33.                  session_id=session_id, query=q, results_count=results_count
34.              )

36.              for connection in connections.copy():
37.                  try:
38.                      await connection.send_text(
39.                          json.dumps(
40.                              {
41.                                  "type": "search",
42.                                  "session_id": session_id,
43.                                  "query": q,
44.                                  "timestamp": notification.timestamp.isoformat(),
45.                              }
46.                          )
47.                      )
48.                  except:
49.                      connections.remove(connection)

51.          return SearchResponse(query=q, results=results, total=results_count)

53.      except Exception as e:
54.          status_code = getattr(e, "status_code", 500)
55.          return HTTPException(status_code=status_code, detail=str(e))

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

注意:session_id 仅基于当前时间戳以简化处理 ------ 在生产环境中,你需要使用更可靠的方法。

客户端

为了展示应用流程,创建一个前端页面,使用简单的 HTML,包括搜索输入框、结果区域和用于通知的对话框。

xml 复制代码
`

1.  <!DOCTYPE html>
2.  <html lang="en">
3.    <body>
4.      <h1>🛍️ TechStore - Find Your Perfect Product</h1>

6.      <form onsubmit="event.preventDefault(); searchProducts();">
7.        <p>
8.          <label for="searchQuery">Search Products:</label><br />
9.          <input
10.            type="text"
11.            id="searchQuery"
12.            placeholder="Search for phones, laptops, headphones..."
13.            size="50"
14.            required />
15.          <button type="submit">🔍 Search</button>
16.        </p>
17.      </form>

19.      <!-- HTML Dialog for notifications -->
20.      <dialog id="notificationDialog">
21.        <div>
22.          <h2>🔔 Live Search Activity</h2>
23.          <p id="notificationMessage"></p>
24.          <p>
25.            <button onclick="closeNotification()" autofocus>OK</button>
26.          </p>
27.        </div>
28.      </dialog>

30.      <div id="searchResults">
31.        <h2>Search Results</h2>
32.      </div>

34.      <script>
35.       ...
36.      </script>
37.    </body>
38.  </html>

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

通知使用了 元素用于演示,但在真实应用中,你可能会使用 toast 或小徽章来显示。在实际场景中,这类通知可用于显示有多少用户正在搜索某些产品、提供库存实时更新,或突出显示返回成功结果的热门搜索查询。

Script 标签

在 标签内,包含将前端连接到后端 WebSocket 端点的逻辑。让我们看看下面的代码片段。

ini 复制代码
`

1.  let ws = null;
2.  let sessionId = null;

5.  window.onload = function () {
6.    sessionId = "session_" + Date.now();
7.    connectWebSocket();
8.  };

`AI写代码

页面加载时,会生成一个唯一的 session ID 并连接到 WebSocket。

ini 复制代码
`

1.  function connectWebSocket() {
2.    ws = new WebSocket("ws://localhost:8000/ws");

4.    ws.onopen = function () {
5.      console.log("Connected to WebSocket");
6.    };

8.    ws.onmessage = function (event) {
9.      try {
10.        const notification = JSON.parse(event.data);
11.        if (notification.type === "search") {
12.          showSearchNotification(notification);
13.        }
14.      } catch (error) {
15.        console.error("Error parsing notification:", error);
16.      }
17.    };

19.    ws.onclose = function () {
20.      console.log("Disconnected from WebSocket");
21.    };

23.    ws.onerror = function (error) {
24.      console.error("WebSocket error:", error);
25.    };
26.  }

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

函数 connectWebSocket 使用 ws = new WebSocket("ws://localhost:8000/ws") 建立 WebSocket 连接。语句 ws.onopen 通知后端已创建新连接。然后,ws.onmessage 监听其他用户在商店中搜索时发送的通知。

javascript 复制代码
``

1.  function showSearchNotification(notification) {
2.     // Skip notifications from the same session (same browser window)
3.     if (notification.session_id === sessionId) {
4.        return;
5.     }

7.    const dialog = document.getElementById("notificationDialog");
8.    const messageElement = document.getElementById("notificationMessage");

10.    messageElement.innerHTML = `<p><strong>Hot search alert!</strong> Other users are looking for <em>"${notification.query}"</em> right now.</p>`;

12.    // Show the notification dialog
13.    dialog.showModal();
14.  }

16.  function closeNotification() {
17.    const dialog = document.getElementById("notificationDialog");
18.    dialog.close();
19.  }

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

函数 showSearchNotification 在屏幕上显示通过 WebSockets 接收到的通知,而 closeNotification 函数用于关闭 showSearchNotification 显示的消息。

javascript 复制代码
``

1.  async function searchProducts() {
2.      const query = document.getElementById("searchQuery").value.trim();

4.      const response = await fetch(
5.              `/search?q=${encodeURIComponent(
6.                query
7.              )}&session_id=${encodeURIComponent(sessionId)}`
8.            );
9.      const data = await response.json();

11.      if (response.ok) {
12.        displaySearchResults(data);
13.      } else {
14.        throw new Error(data.error || "Search failed");
15.      }
16.  }

18.  function displaySearchResults(data) {
19.    const resultsDiv = document.getElementById("searchResults");

21.    let html = `<h2>Found ${data.total} products for "${data.query}"</h2>`;

23.    data.results.forEach((product) => {
24.      html += `
25.                      <ul>
26.    <li><strong>${product.product_name}</strong></li>
27.    <li>💰 $${product.price.toFixed(2)}</li>
28.    <li>${product.description}</li>
29.  </ul>
30.                  `;
31.    });

33.    resultsDiv.innerHTML = html;
34.  }

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

searchProducts() 函数将用户的查询发送到后端,并通过调用 displaySearchResults 函数更新结果区域中匹配的产品。

渲染视图和主方法

最后,在浏览器访问应用时渲染 HTML 页面并启动服务器。

csharp 复制代码
`

1.  @app.get("/")
2.  async def get_main_page():
3.      return FileResponse("index.html")

5.  if __name__ == "__main__":
6.      uvicorn.run(app, host="0.0.0.0", port=8000)

`AI写代码

运行应用

使用 uvicorn 运行 FastAPI 应用。

css 复制代码
`uvicorn main:app --host 0.0.0.0 --port 8000`AI写代码

现在应用已上线!

markdown 复制代码
`

1.  INFO:     Started server process [61820]
2.  INFO:     Waiting for application startup.
3.  INFO:     Application startup complete.

`AI写代码

测试应用

访问 localhost:8000/ 渲染应用视图,并观察控制台的情况:

arduino 复制代码
`

1.  INFO:     127.0.0.1:53422 - "GET / HTTP/1.1" 200 OK
2.  INFO:     ('127.0.0.1', 53425) - "WebSocket /ws" [accepted]
3.  Client connected. Total connections: 1
4.  INFO:     connection open

`AI写代码

当视图被打开时,服务器会收到一个 WebSocket 连接。每打开一个新页面,都会增加一个连接。例如,如果你在三个不同的浏览器标签中打开页面,你将在控制台看到三个连接:

arduino 复制代码
`

1.  INFO:     ('127.0.0.1', 53503) - "WebSocket /ws" [accepted]
2.  Client connected. Total connections: 2
3.  INFO:     connection open
4.  INFO:     ('127.0.0.1', 53511) - "WebSocket /ws" [accepted]
5.  Client connected. Total connections: 3
6.  INFO:     connection open

`AI写代码

如果关闭一个标签,对应的连接也会关闭:

markdown 复制代码
`

1.  Client disconnected. Total connections: 2
2.  INFO:     connection closed

`AI写代码

当有多个活跃客户端连接时,如果一个用户搜索了某个产品,并且该搜索词在监控列表中,其他已连接的客户端将实时收到通知。

可选步骤是使用 Tailwind 应用一些样式。这可以改善 UI,使其看起来现代且视觉上更吸引人。完整的带有更新 UI 的代码可以在这里找到。

结论

在本文中,我们学习了如何使用 Elasticsearch 和 FastAPI 基于搜索创建实时通知。我们选择了一个固定的产品列表来发送通知,但你可以探索更多自定义流程,让用户选择自己想要接收通知的产品或查询,甚至使用 Elasticsearch 的 percolate 查询根据产品规格配置通知。

我们还尝试了一个接收通知的单用户池。使用 WebSockets,你可以选择向所有用户广播,或者选择特定用户。一个常见的模式是定义用户可以订阅的 "消息组",就像群聊一样。

原文:Using FastAPI's WebSockets and Elasticsearch to build a real-time app - Elasticsearch Labs

相关推荐
百思可瑞教育20 小时前
Git 对象存储:理解底层原理,实现高效排错与存储优化
大数据·git·elasticsearch·搜索引擎
陆小叁1 天前
基于Flink CDC实现联系人与标签数据实时同步至ES的实践
java·elasticsearch·flink
2501_930104042 天前
GitCode 疑难问题诊疗:全方位指南
大数据·elasticsearch·gitcode
健康平安的活着2 天前
es7.17.x es服务yellow状态的排查&查看节点,分片状态数量
大数据·elasticsearch·搜索引擎
Elasticsearch2 天前
Elastic 的托管 OTLP 端点:为 SRE 提供更简单、可扩展的 OpenTelemetry
elasticsearch
Yusei_05232 天前
迅速掌握Git通用指令
大数据·git·elasticsearch
水无痕simon3 天前
5 索引的操作
数据库·elasticsearch
Qlittleboy5 天前
tp5集成elasticsearch笔记
大数据·笔记·elasticsearch
Elasticsearch5 天前
Elasticsearch:使用 Gradio 来创建一个简单的 RAG 应用界面
elasticsearch