Elasticsearch 是一个强大的 RESTful 搜索和分析引擎,能够处理越来越多的用例。 它将集中存储你的数据,以实现闪电般的快速搜索、微调相关性以及可轻松扩展的强大分析。 关于如何使用 Elastic Stack(又名 ELK 堆栈)将数据摄取到 Elasticsearch 的资源有很多。在今天的文章中,我将详细介绍如何使用 Node.js 从零开始来把地震的实时数据采集到 Elasticsearch 中。
如果你选择的编程语言是 JavaScript,并且你需要使用 RESTful API 方法从第三方应用程序获取数据,那么使用 Node.js 获取数据是一个不错的选择。 你还可以托管服务器,让它持续实时摄取数据。 该演示将向您展示如何设置一个 Node.js + Express.js 服务器,该服务器实时将数据提取到 Elasticsearch 中,然后可以对这些数据进行分析并以有意义的方式采取行动。
对于此演示,我们将使用 USGS 实时发布的公开可用的全球地震数据。
准备工作
Elasticsearch 及 Kibana
如果你还没有安装好自己的 Elasticsearch 及 Kibana 的话,那么请参考我之前的文章:
- 如何在 Linux,MacOS 及 Windows 上进行安装 Elasticsearch
- Kibana:如何在 Linux,MacOS 及 Windows上安装 Elastic 栈中的 Kibana
在今天的展示中,我将使用 Elastic Stack 8.x 来进行展示。在安装的时候,请参考相应的 Elastic Stack 8.x 的文章来进行安装。
Node.js
你需要安装好自己的 Node.js 来完成下面的练习。你可以参考 Node.js 链接来进行相应的安装。
实时数据
根据 USGS 网上所提供的信息,我们可以在地址 earthquake.usgs.gov/earthquakes… 找到相应的地震信息数据。我们可以通过如下的命令来进行查看:
curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson | jq .
复制代码
如上所示,它是一个以 JSON 格式给出来的数据信息。这个数据会实时发生变化,我们可以通过反复访问这个接口来得到所需要的地震信息。在这里,我们需要注意的是:
-
"time"****: 1672471359610,这是一个时间信息,可以作为我们的 timestamp 来对它进行分析。我们将最终把它存入到 @timestamp 里。
-
"id"****: "nc73827101",这是一个地震特有的 id,我们将以这个 id 成为数据的 id。
-
"geometry",这个是地震发生的地理位置。我们可以需要在 Elasticsearch 中为它定一下为 geo_point 数据类型。我们将把它变为:
虽然数据有很多,但是我们最终需要的数据格式是这样的:
1. {
2. "mag": 1.13,
3. "place": "11km ENE of Coachella, CA",
4. "@timestamp": 2022-05-02T20:07:53.266Z,
5. "url": "https://earthquake.usgs.gov/earthquakes/eventpage/ci40240408",
6. "sig": 20,
7. "type": "earthquake",
8. "depth": 2.09,
9. "coordinates": {
10. "lat": 33.7276667,
11. "lon": -116.0736667
12. }
13. }
复制代码
在接下来的步骤里,我来详细介绍如何达到我们最终的目的。
创建 Node.js 应用
创建最基本的 express 应用
我们将从 0 开始一步一步地创建 Node.js 应用。首先我们在自己的电脑中创建一个目录:
mkdir earthquake_app
复制代码
1. $ pwd
2. /Users/liuxg/demos
3. $ mkdir earthquake_app
4. $ cd earthquake_app/
复制代码
我们进入到该目录中,并打入如下的命令:
npm init -y
复制代码
`
1. $ npm init -y
2. Wrote to /Users/liuxg/demos/earthquake_app/package.json:
4. {
5. "name": "earthquake_app",
6. "version": "1.0.0",
7. "description": "",
8. "main": "index.js",
9. "scripts": {
10. "test": "echo \"Error: no test specified\" && exit 1"
11. },
12. "keywords": [],
13. "author": "",
14. "license": "ISC"
15. }
18. $ ls
19. package.json
`
复制代码
上述命令生成一个叫做 package.json 的文件。在以后安装的 packages,它也会自动添加到这个文件中。默认的设置显然不是我们想要的。我们需要对它做一些修改。
在接下来的代码中,我们将会使用如下的一些 packages:
- @elastic/elasticsearch
- axios
- config
- cors
- express
- log-timestamp
- nodemon
我们可以通过如下的命令来进行安装:
npm i @elastic/elasticsearch axios config cors express log-timestamp nodemon
复制代码
1. $ npm i @elastic/elasticsearch axios config cors express log-timestamp nodemon
2. npm notice Beginning October 4, 2021, all connections to the npm registry - including for package installation - must use TLS 1.2 or higher. You are currently using plaintext http to connect. Please visit the GitHub blog for more information: https://github.blog/2021-08-23-npm-registry-deprecating-tls-1-0-tls-1-1/
3. npm notice Beginning October 4, 2021, all connections to the npm registry - including for package installation - must use TLS 1.2 or higher. You are currently using plaintext http to connect. Please visit the GitHub blog for more information: https://github.blog/2021-08-23-npm-registry-deprecating-tls-1-0-tls-1-1/
5. added 118 packages in 17s
7. 11 packages are looking for funding
8. run `npm fund` for details
复制代码
由于我之前已经安装过,所以我上面显示的信息和你的可能会有所不同。我们再次来查看 package.json 文件:
`
1. $ pwd
2. /Users/liuxg/demos/earthquake_app
3. $ ls
4. node_modules package-lock.json package.json
5. $ cat package.json
6. {
7. "name": "earthquake_app",
8. "version": "1.0.0",
9. "description": "",
10. "main": "index.js",
11. "scripts": {
12. "test": "echo \"Error: no test specified\" && exit 1"
13. },
14. "keywords": [],
15. "author": "",
16. "license": "ISC",
17. "dependencies": {
18. "@elastic/elasticsearch": "^8.5.0",
19. "axios": "^1.2.2",
20. "config": "^3.3.8",
21. "cors": "^2.8.5",
22. "express": "^4.18.2",
23. "log-timestamp": "^0.3.0",
24. "nodemon": "^2.0.20"
25. }
26. }
`
复制代码
很显然,我们最新安装的 packages 已经自动添加到 package.json 文件中了。
我们接下来创建一个叫做 server 的子目录,并在它里面创建一个叫做 server.js 的文件:
1. $ pwd
2. /Users/liuxg/demos/earthquake_app
3. $ mkdir server
4. $ touch server/server.js
复制代码
在上面,我们创建了一个叫做 server.js 的文件。这个将来就是我们需要运行的 server 脚本。为了能够让我们的 package.json 文件的配置能让 npm 进行运行,我们需要对它进行修改。
`
1. $ pwd
2. /Users/liuxg/demos/earthquake_app
3. $ cat package.json
4. {
5. "name": "earthquake_app",
6. "version": "1.0.0",
7. "description": "",
8. "main": "sever.js",
9. "scripts": {
10. "start": "nodemon server/server.js",
11. "test": "echo \"Error: no test specified\" && exit 1"
12. },
13. "keywords": [],
14. "author": "",
15. "license": "ISC",
16. "dependencies": {
17. "@elastic/elasticsearch": "^8.5.0",
18. "axios": "^1.2.2",
19. "config": "^3.3.8",
20. "cors": "^2.8.5",
21. "express": "^4.18.2",
22. "log-timestamp": "^0.3.0",
23. "nodemon": "^2.0.20"
24. }
25. }
`
复制代码
很多人可能会奇怪,为啥使用 nodemon 来启动脚本。它的好处是当我们修改好 server.js 里的脚本,那么它会自动重新启动服务器的运行,而不需要我们每次都需要打入如下的命令:
npm start
复制代码
接下为了验证我们的 express 应用是否能成功地运行,我们修改 server.js 为如下的代码:
server/server.js
1. onst express = require('express');
3. const app = express();
5. const port = 5001;
7. app.get('/', (req, res) => {
8. res.send('Hello World!')
9. })
11. app.listen(port, () => console.log(`Server listening at http://localhost:${port}`));
复制代码
我们接下来使用如下的命令来进行启动:
npm start
复制代码
1. $ pwd
2. /Users/liuxg/demos/earthquake_app
3. $ npm start
5. > earthquake_app@1.0.0 start
6. > nodemon server/server.js
8. [nodemon] 2.0.20
9. [nodemon] to restart at any time, enter `rs`
10. [nodemon] watching path(s): *.*
11. [nodemon] watching extensions: js,mjs,json
12. [nodemon] starting `node server/server.js`
13. Server listening at http://localhost:5001
复制代码
我们可以看到服务器已经成功地运行起来了,并且它运行于 5001 端口上。我们可以通过浏览器来进行访问它的网址:
上面显示我们的服务器运行正常。
安全地连接 Node.js 服务器到 Elasticsearch
接下来,我们需要创建代码来安全地连接 Node.js 服务到我们本地部署的 Elasticsearch 中。我们可以参考之前的文章 “Elasticsearch:使用最新的 Nodejs client 8.x 来创建索引并搜索”。我们可以在项目的更目录下创建如下的两个子目录:
1. mkdir config
2. mkdir -p server/elasticsearch
复制代码
1. $ pwd
2. /Users/liuxg/demos/earthquake_app
3. $ mkdir config
4. $ mkdir -p server/elasticsearch
5. $ ls -d */
6. config/ node_modules/ server/
复制代码
在 config 子目录下,我们创建如下的一个叫做 default.json 的文件。这个是用来配置如何连接到 Elasticsearch 的:
config/default.json
1. {
2. "elastic": {
3. "elasticsearch_endpoint": "https://localhost:9200",
4. "username": "elastic",
5. "password": "-pK6Yth+mU8O-f+Q*F3i",
6. "apiKey": "eVBKOFhJVUJUN1gwSDQyLU5halY6R1BVRjNOUmpRYUtkTmpXTUZHdWZVUQ==",
7. "certificate": "/Users/liuxg/elastic/elasticsearch-8.5.3/config/certs/http_ca.crt",
8. "caFingerprint": "E3D36275D9FA80CF96F74E6537FC74E7952511A75E01605EBCFB8FC9F08F598C"
9. }
10. }
复制代码
我们先不要着急来了解这些配置参数。有些我们可能并不一定要用到。这些设置针对我们每个人的 Elasticsearch 的安装的不同而不同。在上面的参数解释如下:
- elasticsearch_endpoint:这个是 Elasticsearch 的访问地址
- username:这个是访问 Elasticsearch 的用户名,你可以不选用超级用户 elastic,而且在生产环境中,也不是推荐的方法
- password:这个是上面 username 账号的密码
- apiKey:这个是访问 Elasticsearch 所需要的 apiKey。你可以参考 “Elasticsearch:使用最新的 Nodejs client 8.x 来创建索引并搜索” 来了解如何进行生产。在下面的代码中,我们也可以使用 code 来进行生成
- certificate:这个是证书的位置。每个 Elasticsearch 集群都会有一个生成的证书位置。我们需要填入这个位置信息
- caFingerprint:这个是证书的 fingerprint 信息。我们可以采用 fingerprint 来进行连接。在本演示中,我将不使用这种方式。更多信息,请参考 Connecting | Elasticsearch JavaScript Client [master] | Elastic
我们在 elasticsearch 目录下创建一个叫做 client.js 的文件:
server/elasticsearch/client.js
`1. const { Client } = require('@elastic/elasticsearch');
2. const config = require('config');
3. const fs = require('fs')
5. const elasticConfig = config.get('elastic');
7. // const client = new Client ( {
8. // node: elasticConfig.elasticsearch_endpoint,
9. // auth: {
10. // apiKey: elasticConfig.apiKey
11. // },
12. // tls: {
13. // ca: fs.readFileSync(elasticConfig.certificate),
14. // rejectUnauthorized: true
15. // }
16. // });
18. const client = new Client ( {
19. node: elasticConfig.elasticsearch_endpoint,
20. auth: {
21. username: elasticConfig.username,
22. password: elasticConfig.password
23. },
24. tls: {
25. ca: fs.readFileSync(elasticConfig.certificate),
26. rejectUnauthorized: true
27. }
28. });
31. client.ping()
32. .then(response => console.log("You are connected to Elasticsearch!"))
33. .catch(error => console.error("Elasticsearch is not connected."))
35. module.exports = client;` 
复制代码
在上面,我使用了两种方法来连接到 Elasticsearch。一种是通过 username/password 的方式来进行连接:
1. const client = new Client ( {
2. node: elasticConfig.elasticsearch_endpoint,
3. auth: {
4. username: elasticConfig.username,
5. password: elasticConfig.password
6. },
7. tls: {
8. ca: fs.readFileSync(elasticConfig.certificate),
9. rejectUnauthorized: true
10. }
11. });
复制代码
而另外一种就是被注释掉的那个方法:
1. const client = new Client ( {
2. node: elasticConfig.elasticsearch_endpoint,
3. auth: {
4. apiKey: elasticConfig.apiKey
5. },
6. tls: {
7. ca: fs.readFileSync(elasticConfig.certificate),
8. rejectUnauthorized: true
9. }
10. });
复制代码
这个也是被推荐的方法。在实际的使用中,我们更推荐使用 API key 来进行连接。
我们首先来使用 username/password 的方式来进行连接。我们需要修改我们的 server.js 来进行验证:
server/server.js
1. const express = require('express');
2. const client = require('./elasticsearch/client');
4. const app = express();
6. const port = 5001;
8. app.get('/', (req, res) => {
9. res.send('Hello World!')
10. })
12. app.listen(port, () => console.log(`Server listening at http://localhost:${port}`));
复制代码
我们重新运行服务器。我们可以看到如下的输出:
1. $ pwd
2. /Users/liuxg/demos/earthquake_app
3. $ npm start
5. > earthquake_app@1.0.0 start
6. > nodemon server/server.js
8. [nodemon] 2.0.20
9. [nodemon] to restart at any time, enter `rs`
10. [nodemon] watching path(s): *.*
11. [nodemon] watching extensions: js,mjs,json
12. [nodemon] starting `node server/server.js`
13. Server listening at http://localhost:5001
14. You are connected to Elasticsearch!
复制代码
上面的输出表明我们已经能够成功地连接到 Elasticsearch 了。
使用代码获取 API key
我们接下来可以通过代码来获得 API key,尽管我们可以通过其它的方法来获得。请详细阅读 “Elasticsearch:创建 API key 接口访问 Elasticsearch”。在这里,我们可以使用 Node.js 代码来动态地生成一个 API key。我们在 server 目录下创建如下的一个文件:
sever/create-api-key.js
``
1. const client = require('./elasticsearch/client');
3. async function generateApiKeys(opts) {
4. const body = await client.security.createApiKey({
5. body: {
6. name: 'earthquake_app',
7. role_descriptors: {
8. earthquakes_example_writer: {
9. cluster: ['monitor'],
10. index: [
11. {
12. names: ['earthquakes'],
13. privileges: ['create_index', 'write', 'read', 'manage'],
14. },
15. ],
16. },
17. },
18. },
19. });
20. return Buffer.from(`${body.id}:${body.api_key}`).toString('base64');
21. }
23. generateApiKeys()
24. .then(console.log)
25. .catch((err) => {
26. console.error(err);
27. process.exit(1);
28. });
``
复制代码
我们使用如下的命令来运行这个 Node.js 的代码:
1. $ pwd
2. /Users/liuxg/demos/earthquake_app
3. $ ls server/create-api-key.js
4. server/create-api-key.js
5. $ node server/create-api-key.js
6. You are connected to Elasticsearch!
7. emZJSGFZVUJUN1gwSDQyLWRLaS06LVpHaXR1bm5RQnEybE4zOWoyd0g5Zw==
复制代码
我们可以把上面命令生成的 API key 写入到之前的 default.json 文件中。这样我们也可以通过 API key 的方式来访问 Elasticsearch 了,如果我们需要的话。这样 client.js 实际上可以写成:
server/elasticsearch/client.js
`1. const { Client } = require('@elastic/elasticsearch');
2. const config = require('config');
3. const fs = require('fs')
5. const elasticConfig = config.get('elastic');
7. const client = new Client ( {
8. node: elasticConfig.elasticsearch_endpoint,
9. auth: {
10. apiKey: elasticConfig.apiKey
11. },
12. tls: {
13. ca: fs.readFileSync(elasticConfig.certificate),
14. rejectUnauthorized: true
15. }
16. });
18. // const client = new Client ( {
19. // node: elasticConfig.elasticsearch_endpoint,
20. // auth: {
21. // username: elasticConfig.username,
22. // password: elasticConfig.password
23. // },
24. // tls: {
25. // ca: fs.readFileSync(elasticConfig.certificate),
26. // rejectUnauthorized: true
27. // }
28. // });
30. client.ping()
31. .then(response => console.log("You are connected to Elasticsearch!"))
32. .catch(error => console.error("Elasticsearch is not connected."))
34. module.exports = client;` 
复制代码
我们重新运行 server.js,我们可以看到如下的输出:
1. $ pwd
2. /Users/liuxg/demos/earthquake_app
3. $ npm start
5. > earthquake_app@1.0.0 start
6. > nodemon server/server.js
8. [nodemon] 2.0.20
9. [nodemon] to restart at any time, enter `rs`
10. [nodemon] watching path(s): *.*
11. [nodemon] watching extensions: js,mjs,json
12. [nodemon] starting `node server/server.js`
13. Server listening at http://localhost:5001
14. You are connected to Elasticsearch!
复制代码
很显然,我们的 API key 方式是成功的。使用 API key 的好处是我们不必要暴露用户的密码在代码中,而且,我们甚至可以为这个 API key 来设置有效时间及权限。可以授予最小所需要的权限,以确保安全。
设置 RESTful API 调用以从源检索数据
现在我们的服务器正在运行并且 Elasticsearch 已连接,我们需要测试对 USGS 的 API 调用以接收初始数据。 在项目的根目录下,创建一个名为 routes 的文件夹和一个名为 api 的子文件夹。 在 api 文件夹中,创建一个名为 data.js 的文件并添加以下代码:
1. $ pwd
2. /Users/liuxg/demos/earthquake_app
3. $ mkdir -p server/routes/api
复制代码
我在 routes/api 目录下创建一个如下的 data.js 文件:
server/routes/api/data.js
``
1. require('log-timestamp');
2. const express = require('express');
3. const router = express.Router();
4. const axios = require('axios')
5. const client = require('../../elasticsearch/client');
6. const URL = `https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson`;
8. router.get('/earthquakes', async function (req, res) {
9. console.log('Loading Application...');
11. //======= Check that Elasticsearch is up and running =======\\
12. pingElasticsearch = async () => {
13. await client.ping(
14. function(error,res) {
15. if (error) {
16. console.error('elasticsearch cluster is down!');
17. } else {
18. console.log('Elasticsearch Ready');
19. }
20. }
21. );
22. }
24. // ====== Get Data From USGS and then index into Elasticsearch
25. indexAllDocs = async () => {
26. try {
27. console.log('Getting Data From Host')
29. const EARTHQUAKES = await axios.get(`${URL}`,{
30. headers: {
31. 'Content-Type': [
32. 'application/json',
33. 'charset=utf-8'
34. ]
35. }
36. });
38. console.log('Data Received!')
40. results = EARTHQUAKES.data.features
42. console.log('Indexing Data...')
44. console.log(results)
45. res.json(results)
47. if (EARTHQUAKES.data.length) {
48. indexAllDocs();
49. } else {
50. console.log('All Data Has Been Indexed!');
51. };
52. } catch (err) {
53. console.log(err)
54. };
56. console.log('Preparing For The Next Data Check...');
57. }
59. console.log("Ping the Elasticsearch server");
60. pingElasticsearch()
62. console.log("Get data from USGS");
63. indexAllDocs()
64. });
66. module.exports = router;
``
复制代码
上面的代码使用 npm 包 Axios 对 USGS 地震 API 进行异步 API 调用。 收到数据后,它将显示为 JSON。 你还可以看到我们在页面顶部导入了一个名为 log-timestamp 的依赖项。 这将允许我们将时间戳添加到每个 console.log。
我们接下来修改 server.js 如下:
server/server.js
``
1. const express = require('express');
2. const client = require('./elasticsearch/client');
4. const app = express();
6. const port = 5001;
8. //Define Routes
9. const data = require('./routes/api/data')
10. app.use('/api/data', data);
12. app.get('/', (req, res) => {
13. res.send('Hello World!')
14. })
16. app.listen(port, () => console.log(`Server listening at http://localhost:${port}`));
``
复制代码
重新运行我们的 server.js。我们通过 Postman 或者其它的工具来对我们的 REST 接口进行访问:
localhost:5000/api/data/earthquakes
复制代码
从上面的输出中,我们可以看出来设计的 REST 接口工作是正常的。它含有一些收集来的数据。在所收集来的数据中,有一些数据是我们并不需要的。我们最终需要的数据是这样的:
1. {
2. "mag": 1.13,
3. "place": "11km ENE of Coachella, CA",
4. "time": 2022-05-02T20:07:53.266Z,
5. "url": "https://earthquake.usgs.gov/earthquakes/eventpage/ci40240408",
6. "sig": 20,
7. "type": "earthquake",
8. "depth": 2.09,
9. "coordinates": {
10. "lat": 33.7276667,
11. "lon": -116.0736667
12. }
13. }
复制代码
也就是说我们可以删除一下不需要的字段,并且我们需要转换一些字段,比如把 time 字段转换为我们想要的 @timestamp 字段。另外在写入 Elasticsearch 时,我们需要预先针对 coodinates 字段进行定义。它是一个 geo_point 类型的字段。
定义 mapping 及 pipeline
如上所示,我们需要的字段如上。我们可以如下的一个 earthquakes 索引。我们在 Kibana 的 console 中打入如下的命令:
`
1. PUT earthquakes
2. {
3. "mappings": {
4. "properties": {
5. "@timestamp": {
6. "type": "date"
7. },
8. "coordinates": {
9. "type": "geo_point"
10. },
11. "depth": {
12. "type": "float"
13. },
14. "mag": {
15. "type": "float"
16. },
17. "place": {
18. "type": "text",
19. "fields": {
20. "keyword": {
21. "type": "keyword"
22. }
23. }
24. },
25. "sig": {
26. "type": "short"
27. },
28. "type": {
29. "type": "keyword"
30. },
31. "url": {
32. "enabled": false
33. }
34. }
35. }
36. }
`
复制代码
在上面,我们针对索引的字段类型做如下的说明:
- @timestamp:这是一个 date 字段类型的字段。我们希望的格式是 2022-05-02T20:07:53.266Z 而不是以 EPOC 形式显示的值,比如 1672471359610。这个字段有 time 转换而来
- coordinates:这个是一个 geo_point 的字段。是地震发生的地理位置
- place:这是一个 multi-field 字段。我们希望对这个字段进行统计,也可以针对它进行搜索
- sig:这字段我们使用 short 类型,而不是 long。这样可以省去存储空间
- type:这是一个 keyword 类型的字段。它只可以做数据分析统计之用
- url:这个字段,我们既不想对它进行搜索,也不想对它进行统计,所有设置 enabled 为 false。这样可以省去分词的时间,从而提高摄入数据的速度
为此,我们可以针对上面的 data.js 做更进一步的修改:
server/routes/api/data.js
``
1. const express = require('express');
2. const router = express.Router();
3. const axios = require('axios')
4. const client = require('../../elasticsearch/client');
5. const URL = `https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson`;
7. //======= Check that Elasticsearch is up and running =======\\
8. function pingElasticsearch() {
9. console.log("ping .....")
10. client.ping({
11. requestTimeout: 30000,
12. }, function(error,res) {
13. if (error) {
14. console.error('elasticsearch cluster is down!');
15. } else {
16. console.log('Elasticsearch Ready');
17. }
18. });
19. };
21. // ====== Get Data From USGS and then index into Elasticsearch
22. indexAllDocs = async () => {
23. try {
24. const EARTHQUAKES = await axios.get(`${URL}`,{
25. headers: {
26. 'Content-Type': [
27. 'application/json',
28. 'charset=utf-8'
29. ]
30. }
31. });
33. console.log('Getting Data From Host')
35. results = EARTHQUAKES.data.features
37. results.map(
38. async (results) => (
39. (earthquakeObject = {
40. place: results.properties.place, //
41. time: results.properties.time, //
42. url: results.properties.url, //
43. sig: results.properties.sig, //
44. mag: results.properties.mag, //
45. type: results.properties.type, //
46. longitude: results.geometry.coordinates[0], //
47. latitude: results.geometry.coordinates[1], //
48. depth: results.geometry.coordinates[2], //
49. }),
50. await client.index({
51. index: 'earthquakes',
52. id: results.id,
53. body: earthquakeObject
54. })
55. )
56. );
58. if (EARTHQUAKES.data.length) {
59. indexAllDocs();
60. } else {
61. console.log('All Data Has Been Indexed!');
62. };
63. } catch (err) {
64. console.log(err)
65. };
67. console.log('Preparing For The Next Data Check...');
68. }
71. //================== Official API Call ==================\\
72. router.get('/earthquakes', function (req, res) {
73. res.send('Running Application...');
74. console.log('Loading Application...')
76. indexAllDocs(res);
78. });
80. module.exports = router;
``
复制代码
在上面,我们添加了把文档写入 Elasticsearch 的代码部分。我们使用地震数据的 id 作为 Elasticsearch 文档的 id。等服务器运行起来后,我们需要在 terminal 中打入如下的命令:
curl -XGET http://localhost:5001/api/data/earthquakes
复制代码
我们可以在 Kibana 中通过如下的命令来查看文档:
GET earthquakes/_search?filter_path=**.hits
复制代码
我们可以看到如下的结果:
`
1. {
2. "hits": {
3. "hits": [
4. {
5. "_index": "earthquakes",
6. "_id": "nc73827281",
7. "_score": 1,
8. "_source": {
9. "place": "10km S of Laytonville, CA",
10. "time": 1672505649740,
11. "url": "https://earthquake.usgs.gov/earthquakes/eventpage/nc73827281",
12. "sig": 63,
13. "mag": 2.02,
14. "type": "earthquake",
15. "longitude": -123.4981689,
16. "latitude": 39.5991669,
17. "depth": 4.59
18. }
19. },
20. ...
`
复制代码
很显然,这个文档的 source 和我们之前的想要的格式还是不太一样。为了能够使的 time 转换为 @timestamp,我们可以在 Node.js 的代码中进行相应的转换。我们也可以采用 ingest pipeline 来实现相应的操作。我们定义如下的 ingest pipeine。
`
1. POST _ingest/pipeline/_simulate
2. {
3. "pipeline": {
4. "description": "This is for data transform for earthquake data",
5. "processors": [
6. {
7. "date": {
8. "field": "time",
9. "formats": [
10. "UNIX_MS"
11. ]
12. }
13. }
14. ]
15. },
16. "docs": [
17. {
18. "_source": {
19. "place": "16km N of Borrego Springs, CA",
20. "time": 1672507053210,
21. "url": "https://earthquake.usgs.gov/earthquakes/eventpage/ci40152271",
22. "sig": 10,
23. "mag": 0.81,
24. "type": "earthquake",
25. "longitude": -116.368,
26. "latitude": 33.4013333,
27. "depth": 2.91
28. }
29. }
30. ]
31. }
`
复制代码
在上面的命令中,我们使用 date processor 来把 time 转换为所需要的格式,并在默认的情况下把 target 设置为 @timestamp。上面命令运行的结果为:
`
1. {
2. "docs": [3. {4. "doc": {5. "_index": "_index",6. "_id": "_id",7. "_version": "-3",8. "_source": {9. "sig": 10,10. "mag": 0.81,11. "depth": 2.91,12. "@timestamp": "2022-12-31T17:17:33.210Z",13. "latitude": 33.4013333,14. "place": "16km N of Borrego Springs, CA",15. "time": 1672507053210,16. "type": "earthquake",17. "url": "https://earthquake.usgs.gov/earthquakes/eventpage/ci40152271",18. "longitude": -116.36819. },20. "_ingest": {21. "timestamp": "2023-01-01T00:31:03.544821Z"22. }23. }24. }25. ]
26. }
`
复制代码
从上面的输出中,我们可以看出来 @timestamp 字段已经生成。它的值由 time 字段转换而来。我们还发现 latitude 及 longitude 并不是按照我们需要的格式来显示的。我们需要把它转化为另外一个像如下的对象:
我们可以通过 rename processor 来操作:
`
1. POST _ingest/pipeline/_simulate
2. {
3. "pipeline": {
4. "description": "This is for data transform for earthquake data",
5. "processors": [
6. {
7. "date": {
8. "field": "time",
9. "formats": [
10. "UNIX_MS"
11. ]
12. }
13. },
14. {
15. "rename": {
16. "field": "latitude",
17. "target_field": "coordinates.lat"
18. }
19. },
20. {
21. "rename": {
22. "field": "longitude",
23. "target_field": "coordinates.lon"
24. }
25. }
26. ]
27. },
28. "docs": [
29. {
30. "_source": {
31. "place": "16km N of Borrego Springs, CA",
32. "time": 1672507053210,
33. "url": "https://earthquake.usgs.gov/earthquakes/eventpage/ci40152271",
34. "sig": 10,
35. "mag": 0.81,
36. "type": "earthquake",
37. "longitude": -116.368,
38. "latitude": 33.4013333,
39. "depth": 2.91
40. }
41. }
42. ]
43. }
`
复制代码
在上面的命令中,我们通过 rename processor 来重新命名 longitude 及 latitude 两个字段。运行上面的代码,我们可以看到如下的结果:
`
1. {
2. "docs": [3. {4. "doc": {5. "_index": "_index",6. "_id": "_id",7. "_version": "-3",8. "_source": {9. "sig": 10,10. "mag": 0.81,11. "depth": 2.91,12. "@timestamp": "2022-12-31T17:17:33.210Z",13. "coordinates": {14. "lon": -116.368,15. "lat": 33.401333316. },17. "place": "16km N of Borrego Springs, CA",18. "time": 1672507053210,19. "type": "earthquake",20. "url": "https://earthquake.usgs.gov/earthquakes/eventpage/ci40152271"21. },22. "_ingest": {23. "timestamp": "2023-01-01T00:38:42.729604Z"24. }25. }26. }27. ]
28. }
`
复制代码
很显然,我们看到了一个新的 coordinates 的字段。它是一个 object。我们发现有一个多余的字段叫做 time。这个并不是我们所需要的。我们可以通过 remove processor 来删除这个字段。
`
1. POST _ingest/pipeline/_simulate
2. {
3. "pipeline": {
4. "description": "This is for data transform for earthquake data",
5. "processors": [
6. {
7. "date": {
8. "field": "time",
9. "formats": [
10. "UNIX_MS"
11. ]
12. }
13. },
14. {
15. "rename": {
16. "field": "latitude",
17. "target_field": "coordinates.lat"
18. }
19. },
20. {
21. "rename": {
22. "field": "longitude",
23. "target_field": "coordinates.lon"
24. }
25. },
26. {
27. "remove": {
28. "field": "time"
29. }
30. }
31. ]
32. },
33. "docs": [
34. {
35. "_source": {
36. "place": "16km N of Borrego Springs, CA",
37. "time": 1672507053210,
38. "url": "https://earthquake.usgs.gov/earthquakes/eventpage/ci40152271",
39. "sig": 10,
40. "mag": 0.81,
41. "type": "earthquake",
42. "longitude": -116.368,
43. "latitude": 33.4013333,
44. "depth": 2.91
45. }
46. }
47. ]
48. }
`
复制代码
我们运行上面的命令。我们再次查看输出的结果:
`
1. {
2. "docs": [3. {4. "doc": {5. "_index": "_index",6. "_id": "_id",7. "_version": "-3",8. "_source": {9. "sig": 10,10. "mag": 0.81,11. "depth": 2.91,12. "@timestamp": "2022-12-31T17:17:33.210Z",13. "coordinates": {14. "lon": -116.368,15. "lat": 33.401333316. },17. "place": "16km N of Borrego Springs, CA",18. "type": "earthquake",19. "url": "https://earthquake.usgs.gov/earthquakes/eventpage/ci40152271"20. },21. "_ingest": {22. "timestamp": "2023-01-01T00:44:46.919265Z"23. }24. }25. }26. ]
27. }
`
复制代码
很显然这个时候,我们的 time 字段不见了。
在上面,我们通过 _simulate 的端点测试好了我们的 ingest pipeline。接下来,是我们使用命令来创建这个 pipeline 的时候了。我们使用如下的命令来创建这个 pipeline:
`
1. PUT _ingest/pipeline/earthquake_data_pipeline
2. {
3. "description": "This is for data transform for earthquake data",
4. "processors": [
5. {
6. "date": {
7. "field": "time",
8. "formats": [
9. "UNIX_MS"
10. ]
11. }
12. },
13. {
14. "rename": {
15. "field": "latitude",
16. "target_field": "coordinates.lat"
17. }
18. },
19. {
20. "rename": {
21. "field": "longitude",
22. "target_field": "coordinates.lon"
23. }
24. },
25. {
26. "remove": {
27. "field": "time"
28. }
29. }
30. ]
31. }
`
复制代码
运行上面的命令。这样我们就创建了一个叫做 earthquake_data_pipeline 的 ingest pipeline。
接下来,我们需要删除之前所创建的索引,因为它包含我们不需要的一些字段:
DELETE earthquakes
复制代码
我们再次运行之前创建索引 earthquakes 的命令:
`
1. PUT earthquakes
2. {
3. "mappings": {
4. "properties": {
5. "@timestamp": {
6. "type": "date"
7. },
8. "coordinates": {
9. "type": "geo_point"
10. },
11. "depth": {
12. "type": "float"
13. },
14. "mag": {
15. "type": "float"
16. },
17. "place": {
18. "type": "text",
19. "fields": {
20. "keyword": {
21. "type": "keyword"
22. }
23. }
24. },
25. "sig": {
26. "type": "short"
27. },
28. "type": {
29. "type": "keyword"
30. },
31. "url": {
32. "enabled": false
33. }
34. }
35. }
36. }
`
复制代码
我们接下来需要修改 data.js 文件来使用这个 ingest pipeline:
server/routes/api/data.js
``
1. const express = require('express');
2. const router = express.Router();
3. const axios = require('axios')
4. const client = require('../../elasticsearch/client');
5. const URL = `https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson`;
7. //======= Check that Elasticsearch is up and running =======\\
8. function pingElasticsearch() {
9. console.log("ping .....")
10. client.ping({
11. requestTimeout: 30000,
12. }, function(error,res) {
13. if (error) {
14. console.error('elasticsearch cluster is down!');
15. } else {
16. console.log('Elasticsearch Ready');
17. }
18. });
19. };
21. // ====== Get Data From USGS and then index into Elasticsearch
22. indexAllDocs = async () => {
23. try {
24. const EARTHQUAKES = await axios.get(`${URL}`,{
25. headers: {
26. 'Content-Type': [
27. 'application/json',
28. 'charset=utf-8'
29. ]
30. }
31. });
33. console.log('Getting Data From Host')
35. results = EARTHQUAKES.data.features
37. results.map(
38. async (results) => (
39. (earthquakeObject = {
40. place: results.properties.place,
41. time: results.properties.time,
42. url: results.properties.url,
43. sig: results.properties.sig,
44. mag: results.properties.mag,
45. type: results.properties.type,
46. longitude: results.geometry.coordinates[0],
47. latitude: results.geometry.coordinates[1],
48. depth: results.geometry.coordinates[2],
49. }),
50. await client.index({
51. index: 'earthquakes',
52. id: results.id,
53. body: earthquakeObject,
54. pipeline: 'earthquake_data_pipeline'
55. })
56. )
57. );
59. if (EARTHQUAKES.data.length) {
60. indexAllDocs();
61. } else {
62. console.log('All Data Has Been Indexed!');
63. };
64. } catch (err) {
65. console.log(err)
66. };
68. console.log('Preparing For The Next Data Check...');
69. }
72. //================== Official API Call ==================\\
73. router.get('/earthquakes', function (req, res) {
74. res.send('Running Application...');
75. console.log('Loading Application...')
77. setInterval(() => {
78. pingElasticsearch()
79. indexAllDocs(res);
80. }, 120000);
82. });
84. module.exports = router;
``
复制代码
在上面的代码中,我对一下的两处做了修改:
我们再次使用如下的命令来启动对数据的采集:
curl -XGET http://localhost:5001/api/data/earthquakes
复制代码
稍等一点时间(超过2分钟),我们到 Kibana 中来查看数据:
GET earthquakes/_search
复制代码
我们可以看到如下的数据:
`
1. {
2. "took": 0,
3. "timed_out": false,
4. "_shards": {
5. "total": 1,
6. "successful": 1,
7. "skipped": 0,
8. "failed": 0
9. },
10. "hits": {
11. "total": {
12. "value": 9,
13. "relation": "eq"
14. },
15. "max_score": 1,
16. "hits": [
17. {
18. "_index": "earthquakes",
19. "_id": "us7000j1cr",
20. "_score": 1,
21. "_source": {
22. "sig": 340,
23. "mag": 4.7,
24. "depth": 181.449,
25. "@timestamp": "2023-01-01T06:39:45.239Z",
26. "coordinates": {
27. "lon": 70.8869,
28. "lat": 36.5351
29. },
30. "place": "36 km S of Jurm, Afghanistan",
31. "type": "earthquake",
32. "url": "https://earthquake.usgs.gov/earthquakes/eventpage/us7000j1cr"
33. }
34. },
35. ...
`
复制代码
从上面,我们可以看出来有9个地震数据已经被写入。我们可以让应用运行一段时间,它可能会有更多的数据进来。比如:
`
1. {
2. "took": 0,
3. "timed_out": false,
4. "_shards": {
5. "total": 1,
6. "successful": 1,
7. "skipped": 0,
8. "failed": 0
9. },
10. "hits": {
11. "total": {
12. "value": 10,
13. "relation": "eq"
14. },
15. "max_score": 1,
16. "hits": [
17. {
18. "_index": "earthquakes",
19. "_id": "nc73827436",
20. "_score": 1,
21. "_source": {
22. "sig": 252,
23. "mag": 4.04,
24. "depth": 4.51,
25. "@timestamp": "2023-01-01T06:49:08.930Z",
26. "coordinates": {
27. "lon": -121.220665,
28. "lat": 36.5789986
29. },
30. "place": "9km NW of Pinnacles, CA",
31. "type": "earthquake",
32. "url": "https://earthquake.usgs.gov/earthquakes/eventpage/nc73827436"
33. }
34. },
`
复制代码
我们可以看到10个数据。
从上面的数据中,我们可以看到最终的数据结构就是我们想要的数据结构。
在接下来的文章中,我将详细描述如何对这个数据进行可视化。我将使用 Kibana 来进行展示,也会使用 Web 来进行搜索。请参阅文章 “Elasticsearch:使用 Node.js 将实时数据提取到 Elasticsearch 中(二)”
为了方便大家的学习,我把源代码放在这里:github.com/liu-xiao-gu…
参考: