无论才能、知识多么卓著,如果缺乏热情,则无异纸上画饼充饥,无补于事。

从白日梦到现实:推出 Elastic 的管道查询语言 ES|QL

原文:https://elasticstack.blog.csdn ... 86439


!(https://img-blog.csdnimg.cn/a5 ... 63.png)

今天,我们很高兴地宣布 Elastic® 的新管道查询语言 [ES|QL](https://www.elastic.co/guide/e ... .html "ES|QL")(Elasticsearch 查询语言)的技术预览版,它可以转换、丰富和简化数据调查。 ES|QL 由新的查询引擎提供支持,通过并发处理提供高级搜索功能,无论数据源和结构如何,都可以提高速度和效率。 通过在单个屏幕上创建聚合和可视化来快速解决问题,以实现迭代和流畅的工作流程。

ES|QL 介绍

Elasticsearch 的演变
-----------------

在过去 13 年中,[Elasticsearch®](https://www.elastic.co/elasticsearch/ "Elasticsearch®") 取得了显着发展,适应了用户需求和不断变化的数字环境。 Elasticsearch 最初用于全文搜索,后来根据用户反馈扩展到支持更广泛的用例。 在整个过程中,Elasticsearch [Query DSL](https://www.elastic.co/guide/e ... .html "Query DSL")(我们第一个采用的搜索语言)为过滤器、聚合和其他操作提供了丰富的查询集。 这种基于 JSON 的 DSL 最终成为我们 [_search](https://www.elastic.co/guide/e ... .html "_search") API 端点的基础。

随着时间的推移和需求的多样化,逐渐显现出用户需要的不仅仅是查询 DSL 所提供的功能。我们开始在查询 DSL 下引入和融合了其他 DSL,用于[脚本编写](https://www.elastic.co/guide/e ... .html "脚本编写")、安全调查中的[事件](https://www.elastic.co/guide/e ... .html "事件")等等。然而,尽管这些扩展功能非常多才多艺,它们并没有完全满足用户的一些需求。

用户想要一种能够:

*   简化威胁和安全调查,同时通过提供全面和迭代方法的单个查询观察和解决生产问题
*   通过单一界面搜索、丰富、聚合和可视化更多内容来简化数据调查
*   使用高级搜索功能(例如带有并发处理的查找)提高查询大量数据的速度和效率,无论来源和结构如何

更多阅读,请点击 https://elasticstack.blog.csdn ... 86439
继续阅读 »
原文:https://elasticstack.blog.csdn ... 86439


!(https://img-blog.csdnimg.cn/a5 ... 63.png)

今天,我们很高兴地宣布 Elastic® 的新管道查询语言 [ES|QL](https://www.elastic.co/guide/e ... .html "ES|QL")(Elasticsearch 查询语言)的技术预览版,它可以转换、丰富和简化数据调查。 ES|QL 由新的查询引擎提供支持,通过并发处理提供高级搜索功能,无论数据源和结构如何,都可以提高速度和效率。 通过在单个屏幕上创建聚合和可视化来快速解决问题,以实现迭代和流畅的工作流程。

ES|QL 介绍

Elasticsearch 的演变
-----------------

在过去 13 年中,[Elasticsearch®](https://www.elastic.co/elasticsearch/ "Elasticsearch®") 取得了显着发展,适应了用户需求和不断变化的数字环境。 Elasticsearch 最初用于全文搜索,后来根据用户反馈扩展到支持更广泛的用例。 在整个过程中,Elasticsearch [Query DSL](https://www.elastic.co/guide/e ... .html "Query DSL")(我们第一个采用的搜索语言)为过滤器、聚合和其他操作提供了丰富的查询集。 这种基于 JSON 的 DSL 最终成为我们 [_search](https://www.elastic.co/guide/e ... .html "_search") API 端点的基础。

随着时间的推移和需求的多样化,逐渐显现出用户需要的不仅仅是查询 DSL 所提供的功能。我们开始在查询 DSL 下引入和融合了其他 DSL,用于[脚本编写](https://www.elastic.co/guide/e ... .html "脚本编写")、安全调查中的[事件](https://www.elastic.co/guide/e ... .html "事件")等等。然而,尽管这些扩展功能非常多才多艺,它们并没有完全满足用户的一些需求。

用户想要一种能够:

*   简化威胁和安全调查,同时通过提供全面和迭代方法的单个查询观察和解决生产问题
*   通过单一界面搜索、丰富、聚合和可视化更多内容来简化数据调查
*   使用高级搜索功能(例如带有并发处理的查找)提高查询大量数据的速度和效率,无论来源和结构如何

更多阅读,请点击 https://elasticstack.blog.csdn ... 86439 收起阅读 »

ES 关于 remote_cluster 的一记小坑

最近有小伙伴找到我们说 Kibana 上添加不了 Remote Cluster,填完信息点 Save 直接跳回原界面了。具体页面,就和没添加前一样。

我们和小伙伴虽然隔着网线但还是进行了深入、详细的交流,梳理出来了如下信息:

  1. 两个集群:集群 A 和集群 B ,版本都是 7.10.0 ;
    1. 集群 A 没区分节点角色;
    2. 集群 B 设置了 独立的 master 节点、coordinator 节点和 data 节点,其中 data 节点还带 remote_cluster_client 角色;
  2. 在集群 A 的 Kibana 可以添加 集群 B 为远程集群;
  3. 在集群 B 添加 集群 A 就不行,Kibana 跳回之前的页面;
  4. 网络组确认已经放开策略,网络测试也正常;

翻看了 ES 和 Kibana 的日志, ES 日志中有连接失败的错误信息, Kibana 日志中无对应输出。

[2023-09-13T11:38:41,055][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.mode] from [SNIFF] to [sniff]
[2023-09-13T11:38:41,055][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.seeds] from [[]] to [["127.0.0.1:7102"]]
[2023-09-13T11:38:41,056][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.mode] from [SNIFF] to [sniff]
[2023-09-13T11:38:41,056][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.seeds] from [[]] to [["127.0.0.1:7102"]]
[2023-09-13T11:38:41,057][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.mode] from [SNIFF] to [sniff]
[2023-09-13T11:38:41,057][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.seeds] from [[]] to [["127.0.0.1:7102"]]
[2023-09-13T11:38:41,093][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.mode] from [SNIFF] to [sniff]
[2023-09-13T11:38:41,094][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.seeds] from [[]] to [["127.0.0.1:7102"]]
[2023-09-13T11:38:41,094][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.mode] from [SNIFF] to [sniff]
[2023-09-13T11:38:41,094][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.seeds] from [[]] to [["127.0.0.1:7102"]]
[2023-09-13T11:38:41,095][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.mode] from [SNIFF] to [sniff]
[2023-09-13T11:38:41,096][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.seeds] from [[]] to [["127.0.0.1:7102"]]
[2023-09-13T11:38:51,099][WARN ][o.e.t.RemoteClusterService] [710-1] failed to connect to new remote cluster test within 10s
[2023-09-13T11:39:11,101][WARN ][o.e.t.SniffConnectionStrategy] [710-1] fetching nodes from external cluster [test] failed
org.elasticsearch.transport.ConnectTransportException: [][127.0.0.1:7102] handshake_timeout[30s]
    at org.elasticsearch.transport.TransportHandshaker.lambda$sendHandshake$1(TransportHandshaker.java:73) ~[elasticsearch-7.10.2.jar:7.10.2]
    at org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingRunnable.run(ThreadContext.java:684) ~[elasticsearch-7.10.2.jar:7.10.2]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130) [?:?]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630) [?:?]
    at java.lang.Thread.run(Thread.java:832) [?:?]

已经测试过网络是通的,开始尝试把集群 B 的角色配置统统去掉再尝试。
嘿,添加成功了。
接下来为了找出问题,开始一步一步给每个节点再加回原来的角色。而且如果节点原来没有 remote_cluster_client 角色,这次也一并加上。
操作过程

  • 先从 data 节点开始,恢复角色,尝试添加远程集群,失败。
  • 接着 coordinate 节点 , 恢复角色且新增 remote_cluster_client 角色,尝试添加远程集群,失败。
  • 最后 master 节点,恢复角色且新增 remote_cluster_client 角色,尝试添加远程集群,成功。

最终直到所有节点都有 remote_cluster_client 角色后,才成功添加远程集群。
结论: ES 集群添加远程集群所有节点都必须拥有 remote_cluster_client 角色。
事情到这里,似乎没什么问题。但当我们对这个结论进行检验时,又有了新发现。
上面的场景忽视了一个因素 -- Kibana 。于是我们搭建了一个两个节点的集群,其中节点 A 是全角色节点,节点 B 只是 data 节点。让 Kibana 分别连接两个节点进行测试。
验证结果

  • 当 Kibana 连接节点 A 时,可以正常添加。
  • Kibana 连接节点 B 时,添加失败,跳回界面。

结论:ES 集群添加远程集群时,Kibana 连接的 ES 节点必须拥有 remote_cluster_client 角色。
很显然,这个结论更合理。

原因分析
我们是通过 Kibana 界面操作去添加远程集群的, Kibana 连接的节点就被当作 remote_client 。该节点要向远程集群发起连接并执行相关调用。但这一切有个前提,该节点必须有 remote_cluster_client 角色才能向远程集群发起连接。 引申
我们的 CCS 操作也必须发送到一个具有 remote_cluster_client 角色的节点,才能成功执行。

参考连接 https://www.elastic.co/guide/en/elasticsearch/reference/7.10/modules-node.html

继续阅读 »

最近有小伙伴找到我们说 Kibana 上添加不了 Remote Cluster,填完信息点 Save 直接跳回原界面了。具体页面,就和没添加前一样。

我们和小伙伴虽然隔着网线但还是进行了深入、详细的交流,梳理出来了如下信息:

  1. 两个集群:集群 A 和集群 B ,版本都是 7.10.0 ;
    1. 集群 A 没区分节点角色;
    2. 集群 B 设置了 独立的 master 节点、coordinator 节点和 data 节点,其中 data 节点还带 remote_cluster_client 角色;
  2. 在集群 A 的 Kibana 可以添加 集群 B 为远程集群;
  3. 在集群 B 添加 集群 A 就不行,Kibana 跳回之前的页面;
  4. 网络组确认已经放开策略,网络测试也正常;

翻看了 ES 和 Kibana 的日志, ES 日志中有连接失败的错误信息, Kibana 日志中无对应输出。

[2023-09-13T11:38:41,055][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.mode] from [SNIFF] to [sniff]
[2023-09-13T11:38:41,055][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.seeds] from [[]] to [["127.0.0.1:7102"]]
[2023-09-13T11:38:41,056][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.mode] from [SNIFF] to [sniff]
[2023-09-13T11:38:41,056][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.seeds] from [[]] to [["127.0.0.1:7102"]]
[2023-09-13T11:38:41,057][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.mode] from [SNIFF] to [sniff]
[2023-09-13T11:38:41,057][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.seeds] from [[]] to [["127.0.0.1:7102"]]
[2023-09-13T11:38:41,093][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.mode] from [SNIFF] to [sniff]
[2023-09-13T11:38:41,094][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.seeds] from [[]] to [["127.0.0.1:7102"]]
[2023-09-13T11:38:41,094][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.mode] from [SNIFF] to [sniff]
[2023-09-13T11:38:41,094][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.seeds] from [[]] to [["127.0.0.1:7102"]]
[2023-09-13T11:38:41,095][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.mode] from [SNIFF] to [sniff]
[2023-09-13T11:38:41,096][INFO ][o.e.c.s.ClusterSettings  ] [710-1] updating [cluster.remote.test.seeds] from [[]] to [["127.0.0.1:7102"]]
[2023-09-13T11:38:51,099][WARN ][o.e.t.RemoteClusterService] [710-1] failed to connect to new remote cluster test within 10s
[2023-09-13T11:39:11,101][WARN ][o.e.t.SniffConnectionStrategy] [710-1] fetching nodes from external cluster [test] failed
org.elasticsearch.transport.ConnectTransportException: [][127.0.0.1:7102] handshake_timeout[30s]
    at org.elasticsearch.transport.TransportHandshaker.lambda$sendHandshake$1(TransportHandshaker.java:73) ~[elasticsearch-7.10.2.jar:7.10.2]
    at org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingRunnable.run(ThreadContext.java:684) ~[elasticsearch-7.10.2.jar:7.10.2]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130) [?:?]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630) [?:?]
    at java.lang.Thread.run(Thread.java:832) [?:?]

已经测试过网络是通的,开始尝试把集群 B 的角色配置统统去掉再尝试。
嘿,添加成功了。
接下来为了找出问题,开始一步一步给每个节点再加回原来的角色。而且如果节点原来没有 remote_cluster_client 角色,这次也一并加上。
操作过程

  • 先从 data 节点开始,恢复角色,尝试添加远程集群,失败。
  • 接着 coordinate 节点 , 恢复角色且新增 remote_cluster_client 角色,尝试添加远程集群,失败。
  • 最后 master 节点,恢复角色且新增 remote_cluster_client 角色,尝试添加远程集群,成功。

最终直到所有节点都有 remote_cluster_client 角色后,才成功添加远程集群。
结论: ES 集群添加远程集群所有节点都必须拥有 remote_cluster_client 角色。
事情到这里,似乎没什么问题。但当我们对这个结论进行检验时,又有了新发现。
上面的场景忽视了一个因素 -- Kibana 。于是我们搭建了一个两个节点的集群,其中节点 A 是全角色节点,节点 B 只是 data 节点。让 Kibana 分别连接两个节点进行测试。
验证结果

  • 当 Kibana 连接节点 A 时,可以正常添加。
  • Kibana 连接节点 B 时,添加失败,跳回界面。

结论:ES 集群添加远程集群时,Kibana 连接的 ES 节点必须拥有 remote_cluster_client 角色。
很显然,这个结论更合理。

原因分析
我们是通过 Kibana 界面操作去添加远程集群的, Kibana 连接的节点就被当作 remote_client 。该节点要向远程集群发起连接并执行相关调用。但这一切有个前提,该节点必须有 remote_cluster_client 角色才能向远程集群发起连接。 引申
我们的 CCS 操作也必须发送到一个具有 remote_cluster_client 角色的节点,才能成功执行。

参考连接 https://www.elastic.co/guide/en/elasticsearch/reference/7.10/modules-node.html

收起阅读 »

如何在ES中搜索值为空的键值对

问题背景

今天早上,接到开发那边一个特殊的查询需求,在 Kibana 中搜索一个 json 类型日志中值为一个空大括号的键值对, 具体的日志示例如下:

{
    "clientIp": "10.111.121.51",
    "query": "{}",
    "serviceUrl": "/aaa/bbb/cc",
}

也就是说针对这个类型的日志过滤出 query 值为空的请求 "query": "{}", 开发同学测试了直接在 kibana 中查询这个字符串 "query": "{}" 根本查不到我们想要的结果。 我们使用的是 ELK 8.3 的全家桶, 这个日志数据使用的默认 standard analyzer 的分词器。

初步分析

我们先对这个要查询的字符串进行下分词测试:

GET /_analyze
{
"analyzer" : "standard",
"text": "\"query\":\"{}\""
}

结果不出所料,我们想要空大括号在分词的时候直接就被干掉了,仅保留了 query 这一个 token:

{
  "tokens": [{
    "token": "query",
    "start_offset": 1,
    "end_offset": 6,
    "type": "<ALPHANUM>",
    "position": 0
  }]
}

我们使用的 standard analyzer 在数据写入分词时直接抛弃掉{}等特殊字符,看来直接搜索 "query": "{}" 关键词这条路肯定是走不通。

换个思路

在网上搜索了一下解决的办法,有些搜索特殊字符的办法,但需要修改分词器,我们已经写入的日志数据量比较大,不太愿意因为这个搜索请求来修改分词器再 reindex。 但是我们的日志格式是固定的,serviceUrl 这个键值对总是在 query 后面的,那么我们可以结合前后文实现相同的 搜索效果:

GET /_analyze
{
"analyzer" : "standard",
"text": "\"query\":\"{}\",\"serviceUrl\""
}

可以看到这段被分为 2 个相邻的单词

{
"tokens": [{
"token": "query",
"start_offset": 1,
"end_offset": 6,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "serviceurl",
"start_offset": 14,
"end_offset": 24,
"type": "<ALPHANUM>",
"position": 1
}
]
}

那么通过搜索 query 和 serviceUrl 为相邻的 2 个字是完全可以实现 query 的值为空的同样的查询效果。 为了确认在我们已经写入的数据中 query 和 serviceurl 也是相邻的,我们通过 ES termvectors API 确认了已经在 es 中的数据和我们这里测试的情况相同:

GET /<index>/_termvectors/<_id>?fields=message

"query" : {
  "term_freq" : 1,
  "tokens" : [
    {
      "position" : 198,
      "start_offset" : 2138,
      "end_offset" : 2143
    }
  ]
},
"serviceurl" : {
  "term_freq" : 1,
  "tokens" : [
    {
      "position" : 199,
      "start_offset" : 2151,
      "end_offset" : 2161
    }
  ]
},

这里我们可以看到 query 在 message 字段里面出现一次,其 end_offset 和 serviceurl 的 start_offset 之前也是相差 8, 和我们测试的结果相同。 这个时候我们就将原来的查询需求,转化为了对 "query serviceurl" 进行按顺序的精准查询就行了, 使用 match_phrase 可以达到我们的目的。

GET /_search
"query": {
    "match_phrase": {
        "message": {
            "query": "query serviceurl",
            "slop" : 0

        }
    }
}

这里顺便说一下,slop 这个参数,slop=n 表示,表示可以隔 n 个字(英文词)进行匹配, 这里设置为 0 就强制要求 query 和 serviceurl 这 2 个单词必须相邻,0 也是 slop 的默认值,在这个请求中是可以省略的,这是为什么 match_phrase 是会获得精准查询的原因之一。 好了,我们通过 console 确定了有效的 query 之后,对于开发同学查看日志只需要在 Kibana 的搜索栏中直接使用双引号引起来的精确搜索 "query serviceurl" 就可以了。

继续深挖一下,ngram 分词器

虽然开发同学搜索的问题解决了,但我仍然不太满意,毕竟这次的问题我们的日志格式是固定的,如果我们一定要搜索到 "query": "{}" 这个应该怎么办呢? 首先很明确,使用我们默认的 standard analyzer 不修改任何参数肯定是不行的,"{}" 这些特殊字符都直接被干掉了, 参考了网上找到的这篇文章,https://blog.csdn.net/fox_233/article/details/127388058 按照这个 ngram 分词器的思路,我动手对我们的需求进行了下测试

首先先看看我们使用 ngram 分词器的分词效果, 我们这里简化了一下,去掉了原来的双引号,以避免过多 \:

GET _analyze
{
  "tokenizer": "ngram",
  "text": "query:{}"
}

{
  "tokens" : [
    {
      "token" : "q",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "word",
      "position" : 0
    },
    ...
    {
      "token" : "{",
      "start_offset" : 6,
      "end_offset" : 7,
      "type" : "word",
      "position" : 12
    },
    {
      "token" : "{}",
      "start_offset" : 6,
      "end_offset" : 8,
      "type" : "word",
      "position" : 13
    },
    {
      "token" : "}",
      "start_offset" : 7,
      "end_offset" : 8,
      "type" : "word",
      "position" : 14
    }
  ]
}

可以很明显的看到大括号被成功的分词了,果然是有戏。 直接定义一个 index 实战一下搜索效果

PUT specialchar_debug
{
  "settings": {
    "analysis": {
      "analyzer": {
        "specialchar_analyzer": {
          "tokenizer": "specialchar_tokenizer"
        }
      },
      "tokenizer": {
        "specialchar_tokenizer": {
          "type": "ngram",
          "min_gram": 1,
          "max_gram": 2
        }
      }
    }
  },
  "mappings": {
      "properties": {
        "text": {
          "analyzer": "specialchar_analyzer",
          "type": "text"
        }
      }
    }
}

插入几条测试数据:

PUT specialchar_debug/_doc/1
{  "text": "query:{},serviceUrl"
}

PUT specialchar_debug/_doc/2
{  "text": "query:{aaa},serviceUrl"
}

PUT specialchar_debug/_doc/3
{  "text": "query:{bbb}, ccc, serviceUrl"
}

我们再测试一下搜索效果,

GET specialchar_debug/_search
{
  "query": {
    "match_phrase": {
      "text": "query:{}"
    }
  }
}

结果完全是我们想要的,看来这个方案可行

"hits" : [
  {
    "_index" : "specialchar_debug",
    "_id" : "1",
    "_score" : 2.402917,
    "_source" : {
      "text" : "query:{},serviceUrl"
    }
  }
]

小结

对于日志系统,我们一直在使用 ES 默认的 standard analyzer 的分词器, 基本上满足我们生产遇到的 99% 的需求,但面对特殊字符的这种搜索请求,确实比较无奈。这次遇到的空键值对的需求,我们通过搜索 2 个相邻的键绕过了问题。 如果一定要搜索这个字符串的话,我们也可以使用 ngram 分词器重新进行分词再进行处理, 条条大路通罗马。

作者介绍

卞弘智,研发工程师,10 多年的 SRE 经验,工作经历涵盖 DevOps,日志处理系统,监控和告警系统研发,WAF 和网关等系统基础架构领域,致力于通过优秀的开源软件推动自动化和智能化基础架构平台的演进。

继续阅读 »

问题背景

今天早上,接到开发那边一个特殊的查询需求,在 Kibana 中搜索一个 json 类型日志中值为一个空大括号的键值对, 具体的日志示例如下:

{
    "clientIp": "10.111.121.51",
    "query": "{}",
    "serviceUrl": "/aaa/bbb/cc",
}

也就是说针对这个类型的日志过滤出 query 值为空的请求 "query": "{}", 开发同学测试了直接在 kibana 中查询这个字符串 "query": "{}" 根本查不到我们想要的结果。 我们使用的是 ELK 8.3 的全家桶, 这个日志数据使用的默认 standard analyzer 的分词器。

初步分析

我们先对这个要查询的字符串进行下分词测试:

GET /_analyze
{
"analyzer" : "standard",
"text": "\"query\":\"{}\""
}

结果不出所料,我们想要空大括号在分词的时候直接就被干掉了,仅保留了 query 这一个 token:

{
  "tokens": [{
    "token": "query",
    "start_offset": 1,
    "end_offset": 6,
    "type": "<ALPHANUM>",
    "position": 0
  }]
}

我们使用的 standard analyzer 在数据写入分词时直接抛弃掉{}等特殊字符,看来直接搜索 "query": "{}" 关键词这条路肯定是走不通。

换个思路

在网上搜索了一下解决的办法,有些搜索特殊字符的办法,但需要修改分词器,我们已经写入的日志数据量比较大,不太愿意因为这个搜索请求来修改分词器再 reindex。 但是我们的日志格式是固定的,serviceUrl 这个键值对总是在 query 后面的,那么我们可以结合前后文实现相同的 搜索效果:

GET /_analyze
{
"analyzer" : "standard",
"text": "\"query\":\"{}\",\"serviceUrl\""
}

可以看到这段被分为 2 个相邻的单词

{
"tokens": [{
"token": "query",
"start_offset": 1,
"end_offset": 6,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "serviceurl",
"start_offset": 14,
"end_offset": 24,
"type": "<ALPHANUM>",
"position": 1
}
]
}

那么通过搜索 query 和 serviceUrl 为相邻的 2 个字是完全可以实现 query 的值为空的同样的查询效果。 为了确认在我们已经写入的数据中 query 和 serviceurl 也是相邻的,我们通过 ES termvectors API 确认了已经在 es 中的数据和我们这里测试的情况相同:

GET /<index>/_termvectors/<_id>?fields=message

"query" : {
  "term_freq" : 1,
  "tokens" : [
    {
      "position" : 198,
      "start_offset" : 2138,
      "end_offset" : 2143
    }
  ]
},
"serviceurl" : {
  "term_freq" : 1,
  "tokens" : [
    {
      "position" : 199,
      "start_offset" : 2151,
      "end_offset" : 2161
    }
  ]
},

这里我们可以看到 query 在 message 字段里面出现一次,其 end_offset 和 serviceurl 的 start_offset 之前也是相差 8, 和我们测试的结果相同。 这个时候我们就将原来的查询需求,转化为了对 "query serviceurl" 进行按顺序的精准查询就行了, 使用 match_phrase 可以达到我们的目的。

GET /_search
"query": {
    "match_phrase": {
        "message": {
            "query": "query serviceurl",
            "slop" : 0

        }
    }
}

这里顺便说一下,slop 这个参数,slop=n 表示,表示可以隔 n 个字(英文词)进行匹配, 这里设置为 0 就强制要求 query 和 serviceurl 这 2 个单词必须相邻,0 也是 slop 的默认值,在这个请求中是可以省略的,这是为什么 match_phrase 是会获得精准查询的原因之一。 好了,我们通过 console 确定了有效的 query 之后,对于开发同学查看日志只需要在 Kibana 的搜索栏中直接使用双引号引起来的精确搜索 "query serviceurl" 就可以了。

继续深挖一下,ngram 分词器

虽然开发同学搜索的问题解决了,但我仍然不太满意,毕竟这次的问题我们的日志格式是固定的,如果我们一定要搜索到 "query": "{}" 这个应该怎么办呢? 首先很明确,使用我们默认的 standard analyzer 不修改任何参数肯定是不行的,"{}" 这些特殊字符都直接被干掉了, 参考了网上找到的这篇文章,https://blog.csdn.net/fox_233/article/details/127388058 按照这个 ngram 分词器的思路,我动手对我们的需求进行了下测试

首先先看看我们使用 ngram 分词器的分词效果, 我们这里简化了一下,去掉了原来的双引号,以避免过多 \:

GET _analyze
{
  "tokenizer": "ngram",
  "text": "query:{}"
}

{
  "tokens" : [
    {
      "token" : "q",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "word",
      "position" : 0
    },
    ...
    {
      "token" : "{",
      "start_offset" : 6,
      "end_offset" : 7,
      "type" : "word",
      "position" : 12
    },
    {
      "token" : "{}",
      "start_offset" : 6,
      "end_offset" : 8,
      "type" : "word",
      "position" : 13
    },
    {
      "token" : "}",
      "start_offset" : 7,
      "end_offset" : 8,
      "type" : "word",
      "position" : 14
    }
  ]
}

可以很明显的看到大括号被成功的分词了,果然是有戏。 直接定义一个 index 实战一下搜索效果

PUT specialchar_debug
{
  "settings": {
    "analysis": {
      "analyzer": {
        "specialchar_analyzer": {
          "tokenizer": "specialchar_tokenizer"
        }
      },
      "tokenizer": {
        "specialchar_tokenizer": {
          "type": "ngram",
          "min_gram": 1,
          "max_gram": 2
        }
      }
    }
  },
  "mappings": {
      "properties": {
        "text": {
          "analyzer": "specialchar_analyzer",
          "type": "text"
        }
      }
    }
}

插入几条测试数据:

PUT specialchar_debug/_doc/1
{  "text": "query:{},serviceUrl"
}

PUT specialchar_debug/_doc/2
{  "text": "query:{aaa},serviceUrl"
}

PUT specialchar_debug/_doc/3
{  "text": "query:{bbb}, ccc, serviceUrl"
}

我们再测试一下搜索效果,

GET specialchar_debug/_search
{
  "query": {
    "match_phrase": {
      "text": "query:{}"
    }
  }
}

结果完全是我们想要的,看来这个方案可行

"hits" : [
  {
    "_index" : "specialchar_debug",
    "_id" : "1",
    "_score" : 2.402917,
    "_source" : {
      "text" : "query:{},serviceUrl"
    }
  }
]

小结

对于日志系统,我们一直在使用 ES 默认的 standard analyzer 的分词器, 基本上满足我们生产遇到的 99% 的需求,但面对特殊字符的这种搜索请求,确实比较无奈。这次遇到的空键值对的需求,我们通过搜索 2 个相邻的键绕过了问题。 如果一定要搜索这个字符串的话,我们也可以使用 ngram 分词器重新进行分词再进行处理, 条条大路通罗马。

作者介绍

卞弘智,研发工程师,10 多年的 SRE 经验,工作经历涵盖 DevOps,日志处理系统,监控和告警系统研发,WAF 和网关等系统基础架构领域,致力于通过优秀的开源软件推动自动化和智能化基础架构平台的演进。

收起阅读 »

用极限网关实现ES容灾,简单!

身为 IT 人士,大伙身边的各种系统肯定不少吧。系统虽多,但最最最重要的那套、那几套,大伙肯定是捧在手心,关怀备至。如此重要的系统,万一发生故障了且短期无法恢复,该如何保障业务持续运行? 有过这方面思考或经验的同学,肯定脱口而出--切灾备啊。 是的,接下来我来介绍下我们的 ES 灾备方案。当然如果你有更好的,请使用各种可用的渠道联系我们。

总体设计

通过极限网关将应用对主集群的写操作,复制到灾备集群。应用发送的读请求则直接转发到主集群,并将响应结果转发给应用。应用对网关无感知,访问方式与访问 ES 集群一样。

方案优势

  • 轻量级

极限网关使用 Golang 编写,安装包很小,只有 10MB 左右,没有任何外部环境依赖,部署安装都非常简单,只需要下载对应平台的二进制可执行文件,启动网关程序的二进制程序文件执行即可。

  • 跨版本支持

极限网关针对不同的 Elasticsearch 版本做了兼容和针对性处理,能够让业务代码无缝的进行适配,后端 Elasticsearch 集群版本升级能够做到无缝过渡,降低版本升级和数据迁移的复杂度。

  • 高可用

极限网关内置多种高可用解决方案,前端请求入口支持基于虚拟 IP 的双机热备,后端集群支持集群拓扑的自动感知,节点上下线能自动发现,自动处理后端故障,自动进行请求的重试和迁移。

  • 灵活性

主备集群都是可读可写,切换迅速,只需切换网关到另一套配置即可。回切灵活,恢复使用原配置即可。

架构图

网关程序部署

下载

根据操作系统和平台选择下面相应的安装包: 解压到指定目录:

mkdir gateway
tar -zxf xxx.gz -C gateway

修改网关配置

在此 下载 网关配置,默认网关会加载配置文件 gateway.yml ,如果要指定其他配置文件使用 -config 选项指定。 网关配置文件内容较多,下面展示必要部分。

  #primary
  PRIMARY_ENDPOINT: http://192.168.56.3:7171
  PRIMARY_USERNAME: elastic
  PRIMARY_PASSWORD: password
  PRIMARY_MAX_QPS_PER_NODE: 10000
  PRIMARY_MAX_BYTES_PER_NODE: 104857600 #100MB/s
  PRIMARY_MAX_CONNECTION_PER_NODE: 200
  PRIMARY_DISCOVERY_ENABLED: false
  PRIMARY_DISCOVERY_REFRESH_ENABLED: false
  #backup
  BACKUP_ENDPOINT: http://192.168.56.3:9200
  BACKUP_USERNAME: admin
  BACKUP_PASSWORD: admin
  BACKUP_MAX_QPS_PER_NODE: 10000
  BACKUP_MAX_BYTES_PER_NODE: 104857600 #100MB/s
  BACKUP_MAX_CONNECTION_PER_NODE: 200
  BACKUP_DISCOVERY_ENABLED: false
  BACKUP_DISCOVERY_REFRESH_ENABLED: false

PRIMARY_ENDPOINT:配置主集群地址和端口
PRIMARY_USERNAME、PRIMARY_PASSWORD: 访问主集群的用户信息
BACKUP_ENDPOINT:配置备集群地址和端口
BACKUP_USERNAME、BACKUP_PASSWORD: 访问备集群的用户信息

运行网关

前台运行 直接运行网关程序即可启动极限网关了,如下:

./gateway-linux-amd64

后台运行

./gateway-linux-amd64 -service install
Success
./gateway-linux-amd64 -service start
Success

卸载服务

./gateway-linux-amd64 -service stop
Success
./gateway-linux-amd64 -service uninstall
Success

灾备功能测试

在灾备场景下,为保证数据一致性,对集群的访问操作都通过网关进行。注意只有 bulk API 的操作才会被复制到备集群。 在此次测试中,网关灾备配置功能为:

  • 主备集群正常时

读写请求正常执行; 写请求被记录到队列,备集群实时消费队列数据。

  • 当主集群故障时

写入请求报错,主备集群都不写入数据; 查询请求转到备集群执行,并返回结果给客户端。

  • 当备集群故障时

读写请求都正常执行; 写操作记录到磁盘队列,待备集群恢复后,自动消费队列数据直到两个集群一致。

主备集群正常时写入、查询测试

写入数据

# 通过网关写入数据
curl -X POST "localhost:18000/_bulk?pretty" -H 'Content-Type: application/json' -uelastic:password -d'
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "create" : { "_index" : "test", "_id" : "2" } }
{ "field2" : "value2" }
'

查询数据

# 查询主集群
curl 192.168.56.3:7171/test/_search?pretty -uelastic:password

# 查询备集群
curl 192.168.56.3:9200/test/_search?pretty -uadmin:admin

# 查询网关,网关转发给主集群执行
curl 192.168.56.3:18000/test/_search?pretty -uelastic:password

主备集群都已写入数据,且数据一致。通过网关查询,也正常返回。

删除和更新文档

# 通过网关删除和更新文档
curl -X POST "192.168.56.3:18000/_bulk?pretty" -H 'Content-Type: application/json' -uelastic:password -d'
{ "delete" : { "_index" : "test", "_id" : "1" } }
{ "update" : {"_id" : "2", "_index" : "test"} }
{ "doc" : {"field2" : "value2-updated"} }
'

查询数据
# 查询主集群
curl 192.168.56.3:7171/test/_search?pretty -uelastic:password

# 查询备集群
curl 192.168.56.3:9200/test/_search?pretty -uadmin:admin

两个集群都已执行删除和更新操作,数据一致。

主集群故障时写入、查询测试

为模拟主集群故障,直接关闭主集群。

写入数据

# 通过网关写入数据
curl -X POST "192.168.56.3:18000/_bulk?pretty" -H 'Content-Type: application/json' -uelastic:password -d'
{ "index" : { "_index" : "test", "_id" : "3" } }
{ "field3" : "value3" }
{ "create" : { "_index" : "test", "_id" : "4" } }
{ "field4" : "value4" }
'

写入数据报错

查询数据

# 通过网关查询,因为主集群不可用,网关将查询转发到备集群执行
curl 192.168.56.3:18000/test/_search?pretty -uelastic:password

正常查询到数据,说明请求被转发到了备集群执行。

备集群故障时写入、查询测试

为模拟备集群故障,直接关闭备集群。

写入数据

# 通过网关写入数据
curl -X POST "192.168.56.3:18000/_bulk?pretty" -H 'Content-Type: application/json' -uelastic:password -d'
{ "index" : { "_index" : "test", "_id" : "5" } }
{ "field5" : "value5" }
{ "create" : { "_index" : "test", "_id" : "6" } }
{ "field6" : "value6" }
'

数据正常写入。

查询数据

# 通过网关查询
curl 192.168.56.3:18000/test/_search?pretty -uelastic:password

查询成功返回。主集群成功写入了两条新数据。同时此数据会被记录到备集群的队列中,待备集群恢复后,会消费此队列追数据。

恢复备集群

启动备集群。

查询数据

等待片刻或通过 INFINI Console 确定网关队列消费完毕后,查询备集群的数据。 (生产和消费 offset 相同,说明消费完毕。)

# 查询备集群的数据
curl 192.168.56.3:9200/test/_search?pretty -uadmin:admin

备集群启动后自动消费队列数据,消费完后备集群数据达到与主集群数据一致。

灾备切换

测试了这么多,终于到切换的时刻了。切换前我们判断下主系统是否短期无法修复。

如果我们判断主用系统无法短时间恢复,要执行切换。非常简单,我们直接将配置文件中定义的主备集群互换,然后重启网关程序就行了。但我们推荐在相同主机上另部署一套网关程序--网关B,先前那套用网关A指代。网关B中的配置文件把原备集群定义为主集群,原主集群定义为备集群。若要执行切换,我们先停止网关A,然后启动网关B,此时应用连接到网关(端口不变),就把原备系统当作主系统使用,把原主系统当作备系统,也就完成了主备系统的切换。

灾备回切

当原主集群修复后,正常启动,就会从消费队列追写修复期间产生数据直到主备数据一致,同样我们可通过 INFINI Console 查看消费的进度。如果大家还是担心数据的一致性,INFINI Console 还能帮大家做[校验数据]()任务,做到数据完全一致后(文档数量及文档内容一致),才进行回切。

回切也非常简单,停止网关B,启动网关A即可。

网关高可用

网关自带浮动 IP 模块,可进行双机热备。客户端通过 VIP 连接网关,网关出现故障时,VIP 漂移到备网关。 视频教程戳这里

这样的优点是简单,不足是只有一个网关在线提供服务。如果想多个网关在线提供服务,则需搭配分布式消息系统一起工作,架构如下。

前端通过负载均衡将流量分散到多个在线网关,网关将消息存入分布式消息系统。此时,网关可看作无状态应用,可根据需要扩缩规模。

以上就是我介绍的ES灾备方案,是不是相当灵活了。有问题还是那句话 Call me 。

关于极限网关

INFINI Gateway 是一个面向搜索场景的高性能数据网关,所有请求都经过网关处理后再转发到后端的搜索业务集群。基于 INFINI Gateway,可以实现索引级别的限速限流、常见查询的缓存加速、查询请求的审计、查询结果的动态修改等等。

官网文档:https://www.infinilabs.com/docs/latest/gateway

继续阅读 »

身为 IT 人士,大伙身边的各种系统肯定不少吧。系统虽多,但最最最重要的那套、那几套,大伙肯定是捧在手心,关怀备至。如此重要的系统,万一发生故障了且短期无法恢复,该如何保障业务持续运行? 有过这方面思考或经验的同学,肯定脱口而出--切灾备啊。 是的,接下来我来介绍下我们的 ES 灾备方案。当然如果你有更好的,请使用各种可用的渠道联系我们。

总体设计

通过极限网关将应用对主集群的写操作,复制到灾备集群。应用发送的读请求则直接转发到主集群,并将响应结果转发给应用。应用对网关无感知,访问方式与访问 ES 集群一样。

方案优势

  • 轻量级

极限网关使用 Golang 编写,安装包很小,只有 10MB 左右,没有任何外部环境依赖,部署安装都非常简单,只需要下载对应平台的二进制可执行文件,启动网关程序的二进制程序文件执行即可。

  • 跨版本支持

极限网关针对不同的 Elasticsearch 版本做了兼容和针对性处理,能够让业务代码无缝的进行适配,后端 Elasticsearch 集群版本升级能够做到无缝过渡,降低版本升级和数据迁移的复杂度。

  • 高可用

极限网关内置多种高可用解决方案,前端请求入口支持基于虚拟 IP 的双机热备,后端集群支持集群拓扑的自动感知,节点上下线能自动发现,自动处理后端故障,自动进行请求的重试和迁移。

  • 灵活性

主备集群都是可读可写,切换迅速,只需切换网关到另一套配置即可。回切灵活,恢复使用原配置即可。

架构图

网关程序部署

下载

根据操作系统和平台选择下面相应的安装包: 解压到指定目录:

mkdir gateway
tar -zxf xxx.gz -C gateway

修改网关配置

在此 下载 网关配置,默认网关会加载配置文件 gateway.yml ,如果要指定其他配置文件使用 -config 选项指定。 网关配置文件内容较多,下面展示必要部分。

  #primary
  PRIMARY_ENDPOINT: http://192.168.56.3:7171
  PRIMARY_USERNAME: elastic
  PRIMARY_PASSWORD: password
  PRIMARY_MAX_QPS_PER_NODE: 10000
  PRIMARY_MAX_BYTES_PER_NODE: 104857600 #100MB/s
  PRIMARY_MAX_CONNECTION_PER_NODE: 200
  PRIMARY_DISCOVERY_ENABLED: false
  PRIMARY_DISCOVERY_REFRESH_ENABLED: false
  #backup
  BACKUP_ENDPOINT: http://192.168.56.3:9200
  BACKUP_USERNAME: admin
  BACKUP_PASSWORD: admin
  BACKUP_MAX_QPS_PER_NODE: 10000
  BACKUP_MAX_BYTES_PER_NODE: 104857600 #100MB/s
  BACKUP_MAX_CONNECTION_PER_NODE: 200
  BACKUP_DISCOVERY_ENABLED: false
  BACKUP_DISCOVERY_REFRESH_ENABLED: false

PRIMARY_ENDPOINT:配置主集群地址和端口
PRIMARY_USERNAME、PRIMARY_PASSWORD: 访问主集群的用户信息
BACKUP_ENDPOINT:配置备集群地址和端口
BACKUP_USERNAME、BACKUP_PASSWORD: 访问备集群的用户信息

运行网关

前台运行 直接运行网关程序即可启动极限网关了,如下:

./gateway-linux-amd64

后台运行

./gateway-linux-amd64 -service install
Success
./gateway-linux-amd64 -service start
Success

卸载服务

./gateway-linux-amd64 -service stop
Success
./gateway-linux-amd64 -service uninstall
Success

灾备功能测试

在灾备场景下,为保证数据一致性,对集群的访问操作都通过网关进行。注意只有 bulk API 的操作才会被复制到备集群。 在此次测试中,网关灾备配置功能为:

  • 主备集群正常时

读写请求正常执行; 写请求被记录到队列,备集群实时消费队列数据。

  • 当主集群故障时

写入请求报错,主备集群都不写入数据; 查询请求转到备集群执行,并返回结果给客户端。

  • 当备集群故障时

读写请求都正常执行; 写操作记录到磁盘队列,待备集群恢复后,自动消费队列数据直到两个集群一致。

主备集群正常时写入、查询测试

写入数据

# 通过网关写入数据
curl -X POST "localhost:18000/_bulk?pretty" -H 'Content-Type: application/json' -uelastic:password -d'
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "create" : { "_index" : "test", "_id" : "2" } }
{ "field2" : "value2" }
'

查询数据

# 查询主集群
curl 192.168.56.3:7171/test/_search?pretty -uelastic:password

# 查询备集群
curl 192.168.56.3:9200/test/_search?pretty -uadmin:admin

# 查询网关,网关转发给主集群执行
curl 192.168.56.3:18000/test/_search?pretty -uelastic:password

主备集群都已写入数据,且数据一致。通过网关查询,也正常返回。

删除和更新文档

# 通过网关删除和更新文档
curl -X POST "192.168.56.3:18000/_bulk?pretty" -H 'Content-Type: application/json' -uelastic:password -d'
{ "delete" : { "_index" : "test", "_id" : "1" } }
{ "update" : {"_id" : "2", "_index" : "test"} }
{ "doc" : {"field2" : "value2-updated"} }
'

查询数据
# 查询主集群
curl 192.168.56.3:7171/test/_search?pretty -uelastic:password

# 查询备集群
curl 192.168.56.3:9200/test/_search?pretty -uadmin:admin

两个集群都已执行删除和更新操作,数据一致。

主集群故障时写入、查询测试

为模拟主集群故障,直接关闭主集群。

写入数据

# 通过网关写入数据
curl -X POST "192.168.56.3:18000/_bulk?pretty" -H 'Content-Type: application/json' -uelastic:password -d'
{ "index" : { "_index" : "test", "_id" : "3" } }
{ "field3" : "value3" }
{ "create" : { "_index" : "test", "_id" : "4" } }
{ "field4" : "value4" }
'

写入数据报错

查询数据

# 通过网关查询,因为主集群不可用,网关将查询转发到备集群执行
curl 192.168.56.3:18000/test/_search?pretty -uelastic:password

正常查询到数据,说明请求被转发到了备集群执行。

备集群故障时写入、查询测试

为模拟备集群故障,直接关闭备集群。

写入数据

# 通过网关写入数据
curl -X POST "192.168.56.3:18000/_bulk?pretty" -H 'Content-Type: application/json' -uelastic:password -d'
{ "index" : { "_index" : "test", "_id" : "5" } }
{ "field5" : "value5" }
{ "create" : { "_index" : "test", "_id" : "6" } }
{ "field6" : "value6" }
'

数据正常写入。

查询数据

# 通过网关查询
curl 192.168.56.3:18000/test/_search?pretty -uelastic:password

查询成功返回。主集群成功写入了两条新数据。同时此数据会被记录到备集群的队列中,待备集群恢复后,会消费此队列追数据。

恢复备集群

启动备集群。

查询数据

等待片刻或通过 INFINI Console 确定网关队列消费完毕后,查询备集群的数据。 (生产和消费 offset 相同,说明消费完毕。)

# 查询备集群的数据
curl 192.168.56.3:9200/test/_search?pretty -uadmin:admin

备集群启动后自动消费队列数据,消费完后备集群数据达到与主集群数据一致。

灾备切换

测试了这么多,终于到切换的时刻了。切换前我们判断下主系统是否短期无法修复。

如果我们判断主用系统无法短时间恢复,要执行切换。非常简单,我们直接将配置文件中定义的主备集群互换,然后重启网关程序就行了。但我们推荐在相同主机上另部署一套网关程序--网关B,先前那套用网关A指代。网关B中的配置文件把原备集群定义为主集群,原主集群定义为备集群。若要执行切换,我们先停止网关A,然后启动网关B,此时应用连接到网关(端口不变),就把原备系统当作主系统使用,把原主系统当作备系统,也就完成了主备系统的切换。

灾备回切

当原主集群修复后,正常启动,就会从消费队列追写修复期间产生数据直到主备数据一致,同样我们可通过 INFINI Console 查看消费的进度。如果大家还是担心数据的一致性,INFINI Console 还能帮大家做[校验数据]()任务,做到数据完全一致后(文档数量及文档内容一致),才进行回切。

回切也非常简单,停止网关B,启动网关A即可。

网关高可用

网关自带浮动 IP 模块,可进行双机热备。客户端通过 VIP 连接网关,网关出现故障时,VIP 漂移到备网关。 视频教程戳这里

这样的优点是简单,不足是只有一个网关在线提供服务。如果想多个网关在线提供服务,则需搭配分布式消息系统一起工作,架构如下。

前端通过负载均衡将流量分散到多个在线网关,网关将消息存入分布式消息系统。此时,网关可看作无状态应用,可根据需要扩缩规模。

以上就是我介绍的ES灾备方案,是不是相当灵活了。有问题还是那句话 Call me 。

关于极限网关

INFINI Gateway 是一个面向搜索场景的高性能数据网关,所有请求都经过网关处理后再转发到后端的搜索业务集群。基于 INFINI Gateway,可以实现索引级别的限速限流、常见查询的缓存加速、查询请求的审计、查询结果的动态修改等等。

官网文档:https://www.infinilabs.com/docs/latest/gateway

收起阅读 »

给 ES 插上向量检索的翅膀 | DataFunSummit 2023 峰会演讲内容速达

近日,由 DataFun 主办的 DataFunSummit 2023 数据基础架构峰会 圆满落下帷幕,本次峰会邀请了腾讯、百度、字节、极限科技、Zilliz 等众多企业技术专家为大家带来分布式存储以及向量数据库的架构原理、性能优化与实践解析分享。

向量数据库架构与实践论坛 中,极限科技搜索引擎研发工程师张磊受邀出席做了《给 ES 插上向量检索的翅膀》的主题演讲。据介绍,本次演讲主要介绍了 Elasticsearch(ES)与向量技术的融合,展示其在不同行业中的应用场景和优势,同时也对 ES 与向量的技术细节进行详细讨论,并通过具体案例演示如何利用向量提升搜索能力。

讲师介绍

张磊,极限科技 Easysearch 引擎研发工程师,2013 年开始接触 Elasticsearch,10 余年搜索相关经验,之前主要做一些围绕 Elasticsearch 在日志检索和公安大数据相关业务的开发,对 Elasticsearch 和 Lucene 源码比较熟悉,目前专注于公司内部搜索产品的开发。

《给 ES 插上向量检索的翅膀》PPT 内容

更多 PPT 内容参见 https://elasticsearch.cn/slides/322

关于 Easysearch

INFINI Easysearch 是一个分布式的近实时搜索与分析引擎,核心引擎基于开源的 Apache Lucene。Easysearch 衍生自基于开源协议 Apache 2.0 的 Elasticsearch 7.10 版本。Easysearch 的目标是提供一个轻量级的 Elasticsearch 可替代版本,并继续完善和支持更多的企业级功能。与 Elasticsearch 相比,Easysearch 更关注在搜索业务场景的优化和继续保持其产品的简洁与易用性。

官网文档:https://www.infinilabs.com/docs/latest/easysearch

下载地址:https://www.infinilabs.com/download

继续阅读 »

近日,由 DataFun 主办的 DataFunSummit 2023 数据基础架构峰会 圆满落下帷幕,本次峰会邀请了腾讯、百度、字节、极限科技、Zilliz 等众多企业技术专家为大家带来分布式存储以及向量数据库的架构原理、性能优化与实践解析分享。

向量数据库架构与实践论坛 中,极限科技搜索引擎研发工程师张磊受邀出席做了《给 ES 插上向量检索的翅膀》的主题演讲。据介绍,本次演讲主要介绍了 Elasticsearch(ES)与向量技术的融合,展示其在不同行业中的应用场景和优势,同时也对 ES 与向量的技术细节进行详细讨论,并通过具体案例演示如何利用向量提升搜索能力。

讲师介绍

张磊,极限科技 Easysearch 引擎研发工程师,2013 年开始接触 Elasticsearch,10 余年搜索相关经验,之前主要做一些围绕 Elasticsearch 在日志检索和公安大数据相关业务的开发,对 Elasticsearch 和 Lucene 源码比较熟悉,目前专注于公司内部搜索产品的开发。

《给 ES 插上向量检索的翅膀》PPT 内容

更多 PPT 内容参见 https://elasticsearch.cn/slides/322

关于 Easysearch

INFINI Easysearch 是一个分布式的近实时搜索与分析引擎,核心引擎基于开源的 Apache Lucene。Easysearch 衍生自基于开源协议 Apache 2.0 的 Elasticsearch 7.10 版本。Easysearch 的目标是提供一个轻量级的 Elasticsearch 可替代版本,并继续完善和支持更多的企业级功能。与 Elasticsearch 相比,Easysearch 更关注在搜索业务场景的优化和继续保持其产品的简洁与易用性。

官网文档:https://www.infinilabs.com/docs/latest/easysearch

下载地址:https://www.infinilabs.com/download

收起阅读 »

ES数据没了?谁动了我的数据?

背景

我们在使用 Elasticsearch 的时候,可能会遇到数据“丢”了的情况。有可能是数据没成功写入 ES 集群,也可能是数据被误删了。

针对数据被误删,有没有好的解决办法呢?

其实我们可以把“删除数据”这个操作管理起来。当 ES 集群接收到删除数据命令的时候,先不执行该命令,而是生成一条删除数据的记录,经过管理人员批准后,该命令才会执行。这样不仅可以管理数据的删除,还可以进行删除操作的追踪:什么人,什么时间,发送了什么样的删除指令,从哪个 IP 发送的,以什么身份登录的等等。

要实现这个解决办法,我们可借助 INFINI Gateway 和 Console 的帮助。

方案架构

方案效果

  • INFINI Gateway 作为 ES 集群的代理,接收所有请求
  • INFINI Gateway 对删除数据操作进行拦截,在 Console UI 界面生成记录
  • 管理人员 在 Console UI 界面审批操作记录,审批通过操作被执行

方案演示

测试数据准备

测试索引 test1,一共有 3 条数据。message 内容分别是"line 1","line 2"和"line 3"。

启动 INFINI Gateway 及 Console

网关配置新增内容

增加对 DELETE 操作的捕获,不直接执行,写入队列中。后续由队列生成特定的记录。

router:
  - name: my_router
    default_flow: default_flow
    tracing_flow: logging_flow
    rules:
      - method:
          - "DELETE"
        pattern:
          - "/{any_index}"
          - "/{any_index}/{any_type}"
          - "/{any_index}/{any_type}/{any_docid}"
        flow:
          - audit_flow
      - method:
          - "*"
        pattern:
          - "/{any_index}/_delete_by_query"
          - "/_delete_by_query"
        flow:
          - audit_flow
flow:
  - name: audit_flow
    filter:
      - logging:
          queue_name: del_queue
pipeline:
  - name: del_queue_ingest
    auto_start: true
    keep_running: true
    processor:
      - json_indexing:
          input_queue: "del_queue"
          idle_timeout_in_seconds: 1
          elasticsearch: "logging-server"
          index_name: "del_requests"
          worker_size: 1
          bulk_size_in_kb: 1

执行删除操作

ES 支持多种删除操作,简单总结归纳如下:

  1. 删除指定文档 id
  2. 删除索引
  3. 根据查询删除指定数据(_delete_by_query)

执行删除操作之前,先通过 INFINI Gateway 访问 ES 集群,证明可正常访问数据。 执行上述的几种删除命令,注意要发给 INFINI Gateway 的 8000 端口。

数据查询验证数据还在

Console 界面查看未批准的删除记录

所有删除操作,都被记录,待审批

Console 界面进行审批通过

选择一条记录,批准执行。Operation-approve

数据查询验证数据

"message": "line 2"的文档已被删除。

Console 界面查看历史记录

继续批准测试

批准删除一条文档

"message": "line 1" 的文档不在了。

批准删除索引

索引不在了。

至此我们演示了如何利用 INFINI Gateway 和 Console 对 ES 集群删除操作进行管控,本文只是抛砖引玉,相信还有更多有意思的场景等待大家发掘。

继续阅读 »

背景

我们在使用 Elasticsearch 的时候,可能会遇到数据“丢”了的情况。有可能是数据没成功写入 ES 集群,也可能是数据被误删了。

针对数据被误删,有没有好的解决办法呢?

其实我们可以把“删除数据”这个操作管理起来。当 ES 集群接收到删除数据命令的时候,先不执行该命令,而是生成一条删除数据的记录,经过管理人员批准后,该命令才会执行。这样不仅可以管理数据的删除,还可以进行删除操作的追踪:什么人,什么时间,发送了什么样的删除指令,从哪个 IP 发送的,以什么身份登录的等等。

要实现这个解决办法,我们可借助 INFINI Gateway 和 Console 的帮助。

方案架构

方案效果

  • INFINI Gateway 作为 ES 集群的代理,接收所有请求
  • INFINI Gateway 对删除数据操作进行拦截,在 Console UI 界面生成记录
  • 管理人员 在 Console UI 界面审批操作记录,审批通过操作被执行

方案演示

测试数据准备

测试索引 test1,一共有 3 条数据。message 内容分别是"line 1","line 2"和"line 3"。

启动 INFINI Gateway 及 Console

网关配置新增内容

增加对 DELETE 操作的捕获,不直接执行,写入队列中。后续由队列生成特定的记录。

router:
  - name: my_router
    default_flow: default_flow
    tracing_flow: logging_flow
    rules:
      - method:
          - "DELETE"
        pattern:
          - "/{any_index}"
          - "/{any_index}/{any_type}"
          - "/{any_index}/{any_type}/{any_docid}"
        flow:
          - audit_flow
      - method:
          - "*"
        pattern:
          - "/{any_index}/_delete_by_query"
          - "/_delete_by_query"
        flow:
          - audit_flow
flow:
  - name: audit_flow
    filter:
      - logging:
          queue_name: del_queue
pipeline:
  - name: del_queue_ingest
    auto_start: true
    keep_running: true
    processor:
      - json_indexing:
          input_queue: "del_queue"
          idle_timeout_in_seconds: 1
          elasticsearch: "logging-server"
          index_name: "del_requests"
          worker_size: 1
          bulk_size_in_kb: 1

执行删除操作

ES 支持多种删除操作,简单总结归纳如下:

  1. 删除指定文档 id
  2. 删除索引
  3. 根据查询删除指定数据(_delete_by_query)

执行删除操作之前,先通过 INFINI Gateway 访问 ES 集群,证明可正常访问数据。 执行上述的几种删除命令,注意要发给 INFINI Gateway 的 8000 端口。

数据查询验证数据还在

Console 界面查看未批准的删除记录

所有删除操作,都被记录,待审批

Console 界面进行审批通过

选择一条记录,批准执行。Operation-approve

数据查询验证数据

"message": "line 2"的文档已被删除。

Console 界面查看历史记录

继续批准测试

批准删除一条文档

"message": "line 1" 的文档不在了。

批准删除索引

索引不在了。

至此我们演示了如何利用 INFINI Gateway 和 Console 对 ES 集群删除操作进行管控,本文只是抛砖引玉,相信还有更多有意思的场景等待大家发掘。

收起阅读 »

Elasticsearch:如何在 Elastic 中实现图片相似度搜索

原文: Elasticsearch:如何在 Elastic 中实现图片相似度搜索

作者:Radovan Ondas

在本文章,我们将了解如何通过几个步骤在 Elastic 中实施相似图像搜索。 开始设置应用程序环境,然后导入 NLP 模型,最后完成为你的图像集生成嵌入。

Elastic 图像相似性搜索概览 >>

Elasticsearch:如何在 Elastic 中实现图片相似度搜索

如何设置环境

第一步是为你的应用程序设置环境。 一般要求包括:

  • Git
  • Python 3.9
  • Docker
  • 数百张图片

使用数百张图像以确保获得最佳效果非常重要。

转到工作文件夹并检查创建的存储库代码。 然后导航到存储库文件夹。


1.  git clone https://github.com/radoondas/flask-elastic-image-search.git
2.  cd flask-elastic-image-search

1.  $ git clone https://github.com/radoondas/flask-elastic-image-search.git
2.  Cloning into 'flask-elastic-image-search'...
3.  remote: Enumerating objects: 105, done.
4.  remote: Counting objects: 100% (105/105), done.
5.  remote: Compressing objects: 100% (72/72), done.
6.  remote: Total 105 (delta 37), reused 94 (delta 27), pack-reused 0
7.  Receiving objects: 100% (105/105), 20.72 MiB | 9.75 MiB/s, done.
8.  Resolving deltas: 100% (37/37), done.
9.  $ cd flask-elastic-image-search/
10.  $ pwd
11.  /Users/liuxg/python/flask-elastic-image-search

因为你将使用 Python 来运行代码,所以你需要确保满足所有要求并且环境已准备就绪。 现在创建虚拟环境并安装所有依赖项。


1.  python3 -m venv .venv
2.  source .venv/bin/activate
3.  pip install -r requirements.txt

安装

如果你还没有安装好自己的 Elasticsearch 及 Kibana,请参考如下的文章来进行安装:

特别注意的是:我们将以最新的 Elastic Stack 8.6.1 来进行展示。请参考 Elastic Stack 8.x 的文章进行安装。

启动白金版试用功能

由于上传模型是一个白金版的功能,我们需要启动试用功能。更多关于订阅的信息,请参考网址:订阅 | Elastic Stack 产品和支持 | Elastic

这样我们就成功地启动了白金版试用功能。 

Elasticsearch 集群和嵌入模型

登录到你的帐户以启动 Elasticsearch 集群。 设置一个小型集群:

  • 一个具有 2GB 内存的 HOT 节点
  • 一个具有 4GB 内存的 ML(机器学习)节点(此节点的大小很重要,因为你将导入 Elasticsearch 的 NLP 模型会消耗约 1.5GB 的内存。)

部署准备就绪后,转到 Kibana 并检查机器学习节点的容量。 你将在视图中看到一个机器学习节点。 目前没有加载模型。

使用 Eland 库从 OpenAI 上传 CLIP 嵌入模型。 Eland 是一个 Python Elasticsearch 客户端,用于在 Elasticsearch 中探索和分析数据,能够处理文本和图像。 您将使用此模型从文本输入生成嵌入并查询匹配图像。 在 Eland 库的文档中找到更多详细信息。

对于下一步,你将需要 Elasticsearch 端点。 你可以从部署详细信息部分的 Elasticsearch 云控制台获取它。

 

在本示例中,我们将使用本地部署来进行展示,所以,我们并不必要完成上面的步骤。

Eland

Eland 可以通过 pip 从 PyPI 安装。在安装之前,我们需要安装好自己的 Python。


1.  $ python --version
2.  Python 3.10.2

可以使用 Pip 从 PyPI 安装 Eland:

python -m pip install eland

也可以使用 Conda 从 Conda Forge 安装 Eland:

conda install -c conda-forge eland

希望在不安装 Eland 的情况下使用它的用户,为了只运行可用的脚本,可以构建 Docker 容器:


1.  git clone https://github.com/elastic/eland
2.  cd eland
3.  docker build -t elastic/eland .

Eland 将 Hugging Face 转换器模型到其 TorchScript 表示的转换和分块过程封装在一个 Python 方法中; 因此,这是推荐的导入方法。

  1. 安装 Eland Python 客户端
  2. 运行 eland_import_hub_model 脚本。 例如:
1.  eland_import_hub_model --url <clusterUrl> \ 
2.  --hub-model-id elastic/distilbert-base-cased-finetuned-conll03-english \ 
3.  --task-type ner 

上传模型

我们使用如下的命令来进行上传模型:


1.  eland_import_hub_model --url https://<user>:<password>@<hostname>:<port> \
2.    --hub-model-id sentence-transformers/clip-ViT-B-32-multilingual-v1 \
3.    --task-type text_embedding \
4.    --ca-certs <your certificate> \
5.    --start

针对我的情况:


1.  eland_import_hub_model --url https://elastic:ZgzSt2vHNwA6yPn-fllr@localhost:9200 \
2.    --hub-model-id sentence-transformers/clip-ViT-B-32-multilingual-v1 \
3.    --task-type text_embedding \
4.    --ca-certs /Users/liuxg/elastic/elasticsearch-8.6.1/config/certs/http_ca.crt \
5.    --start

请注意: 你需要根据自己的 Elasticsearch 访问端点,用户名及密码来修改上面的设置,同时你需要根据自己的配置修改上面的证书路径。

运行上面的命令:

上面显示,我们已经成功地上传了模型。我们可以到 Kibana 中进行查看:

上面显示我们已经上传了所需要的 CLIP 模型,并且它的状态是 started。

如何创建图像嵌入

在设置 Elasticsearch 集群并导入嵌入模型后,你需要矢量化图像数据并为数据集中的每个图像创建图像嵌入。 

 要创建图像嵌入,请使用简单的 Python 脚本。 你可以在此处找到该脚本:create-image-embeddings.py。 该脚本将遍历你的图像目录并生成单独的图像嵌入。 它将使用名称和相对路径创建文档,并使用提供的映射将其保存到 Elasticsearch 索引 my-image-embeddings 中。

将所有图像(照片)放入文件夹 app/static/images。 使用带有子文件夹的目录结构来组织图像。 所有图像准备就绪后,使用几个参数执行脚本。

至少要有几百张图像才能获得合理的结果,这一点至关重要。 图像太少不会产生预期的结果,因为你要搜索的空间非常小,而且到搜索向量的距离也非常相似。我尝试在网上下载很多的照片,但是感觉一张一张地下载非常麻烦。你可以在谷歌浏览器中添加插件 Image downloader - Imageye。它可以方便地把很多照片一次下载下来。

在 image_embeddings 文件夹中,运行脚本并为变量使用你的值。


1.  cd image_embeddings
2.  python3 create-image-embeddings.py \
3.    --es_host='https://localhost:9200' \
4.    --es_user='elastic' --es_password='ZgzSt2vHNwA6yPn-fllr' \
5.    --ca_certs='/Users/liuxg/elastic/elasticsearch-8.6.1/config/certs/http_ca.crt'

根据图像的数量、它们的大小、你的 CPU 和你的网络连接,此任务将需要一些时间。 在尝试处理完整数据集之前,先试验少量图像。脚本完成后,你可以使用 Kibana 开发工具验证索引 my-image-embeddings 是否存在并具有相应的文档。

我们在Kibana 中进行查看:

GET _cat/indices/my-image-embeddings?v

 上面命令的响应为:


1.  health status index               uuid                   pri rep docs.count docs.deleted store.size pri.store.size
2.  yellow open   my-image-embeddings h6oUBdHCScWmXOZaf57oWg   1   1        145            0      1.4mb          1.4mb

查看文档,你会看到非常相似的 JSON 对象(如示例)。 你将在图像文件夹中看到图像名称、图像 ID 和相对路径。 此路径用于前端应用程序以在搜索时正确显示图像。JSON 文档中最重要的部分是包含 CLIP 模型生成的密集矢量的 image_embedding。 当应用程序正在搜索图像或类似图像时使用此矢量。

GET my-image-embeddings/_search

1.  {
2.     "_index": "my-image-embeddings",
3.     "_id": "_g9ACIUBMEjlQge4tztV",
4.     "_score": 6.703597,
5.     "_source": {
6.       "image_id": "IMG_4032",
7.       "image_name": "IMG_4032.jpeg",
8.       "image_embedding": [
9.         -0.3415695130825043,
10.         0.1906963288784027,
11.         .....
12.         -0.10289803147315979,
13.         -0.15871885418891907
14.         ],
15.       "relative_path": "phone/IMG_4032.jpeg"
16.     }
17.  }

使用 Flask 应用程序搜索图像

现在你的环境已全部设置完毕,你可以进行下一步,使用我们作为概念证明提供的 Flask 应用程序,使用自然语言实际搜索图像并查找相似图像。 该 Web 应用程序具有简单的 UI,使图像搜索变得简单。 你可以在此 GitHub 存储库中访问原型 Flask 应用程序。

后台应用程序执行两个任务。 在搜索框中输入搜索字符串后,文本将使用机器学习 _infer 端点进行矢量化。 然后,针对带有向量的索引 my-image-embeddings 执行带有密集向量的查询。

你可以在示例中看到这两个查询。 第一个 API 调用使用 _infer 端点,结果是一个密集矢量。


1.  POST _ml/trained_models/sentence-transformers__clip-vit-b-32-multilingual-v1/_infer
2.  {
3.    "docs" : [
4.      {"text_field": "Yellow mountain is the most beautiful mountain in China"}
5.      ]
6.  }

上面的响应如下:

在第二个任务中,搜索查询,我们将使用密集矢量并获得按分数排序的图像。 

`

1.  GET my-image-embeddings/_search
2.  {
3.    "fields": [
4.      "image_id",
5.      "image_name",
6.      "relative_path"
7.    ],
8.    "_source": false,
9.    "knn": {
10.      "field": "image_embedding",
11.      "k": 5,
12.      "num_candidates": 10,
13.      "query_vector": [
14.          0.03395160660147667,
15.          0.007704082876443863,
16.          0.14996188879013062,
17.          -0.10693030804395676,
18.          ...
19.          0.05140634626150131,
20.          0.07114913314580917
21.     ]
22.    }
23.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

要启动并运行 Flask 应用程序,请导航到存储库的根文件夹并配置 .env 文件。 配置文件中的值用于连接到 Elasticsearch 集群。 你需要为以下变量插入值。 这些与图像嵌入生成中使用的值相同。 

.env


1.  ES_HOST='URL:PORT'
2.  ES_USER='elastic'
3.  ES_PWD='password'

为了能够使得我们自构建的 Elasticsearch 集群能够被正确地访问,我们必须把 Elasticsearch 的根证书拷贝到 Flask 应用的相应目录中:

flask-elastic-image-search/app/conf/ca.crt


1.  (.venv) $ pwd
2.  /Users/liuxg/python/flask-elastic-image-search/app/conf
3.  (.venv) $ cp ~/elastic/elasticsearch-8.6.1/config/certs/http_ca.crt ca.crt
4.  overwrite ca.crt? (y/n [n]) y

在上面,我们替换了仓库中原有的证书文件 ca.crt。

准备就绪后,运行主文件夹中的 flask 应用程序并等待它启动。


1.  # In the main directory 
2.  $ flask run --port=5001

如果应用程序启动,你将看到类似于下面的输出,它在末尾指示你需要访问哪个 URL 才能访问该应用程序。

恭喜! 你的应用程序现在应该已启动并正在运行,并且可以通过互联网浏览器在 http://127.0.0.1:5001 上访问。

导航到图像搜索选项卡并输入描述你最佳图像的文本。 尝试使用非关键字或描述性文字。

在下面的示例中,输入的文本是 “Yellow mountain is the most beautiful mountain in China”。 结果显示在我们的数据集中。 如果用户喜欢结果集中的一张特定图像,只需单击它旁边的按钮,就会显示类似的图像。 用户可以无限次地这样做,并通过图像数据集构建自己的路径。

我们尝试另外的一个例子。这次我们输入:I love beautiful girls。

搜索也可以通过简单地上传图像来进行。 该应用程序会将图像转换为矢量并在数据集中搜索相似的图像。 为此,导航到第三个选项卡 “Similar Image”,从磁盘上传图像,然后点击 “Search”。

我们可以看到相似的图片。我们尝试使用一个女孩的照片再试试:

因为我们在 Elasticsearch 中使用的 NLP(sentence-transformers/clip-ViT-B-32-multilingual-v1)模型是多语言的,支持多语言推理,所以尽量搜索自己语言的图片。 然后也使用英文文本验证结果。我们尝试使用 “黄山是中国最漂亮的山”:

请务必注意,使用的模型是通用模型,这些模型非常准确,但你获得的结果会因用例或其他因素而异。 如果你需要更高的精度,则必须采用通用模型或开发自己的模型 —— CLIP 模型只是一个起点。

代码摘要

你可以在 GitHub 存储库中找到完整的代码。 你可能正在检查 routes.py 中的代码,它实现了应用程序的主要逻辑。 除了明显的路线定义之外,你还应该关注定义 _infer 和 _search 端点(infer_trained_modelknn_search_images)的方法。 生成图像嵌入的代码位于 create-image-embeddings.py文件中。

总结

现在你已经设置了 Flask 应用程序,你可以轻松地搜索你自己的图像集! Elastic 在平台内提供了矢量搜索的原生集成,避免了与外部进程的通信。 你可以灵活地开发和使用你可能使用 PyTorch 开发的自定义嵌入模型。

语义图像搜索具有其他传统图像搜索方法的以下优点:

  • 更高的准确度:向量相似性捕获上下文和关联,而不依赖于图像的文本元描述。
  • 增强的用户体验:与猜测哪些关键字可能相关相比,描述你正在寻找的内容或提供示例图像。
  • 图像数据库的分类:不用担心对图像进行分类——相似性搜索可以在一堆图像中找到相关图像,而无需对它们进行组织。

如果你的用例更多地依赖于文本数据,你可以在以前的博客中了解更多关于实现语义搜索和将自然语言处理应用于文本的信息。 对于文本数据,向量相似度与传统关键词评分的结合呈现了两全其美的效果。

准备好开始了吗? 在我们的虚拟活动中心报名参加矢量搜索实践研讨会,并在我们的在线论坛中与社区互动。

继续阅读 »

原文: Elasticsearch:如何在 Elastic 中实现图片相似度搜索

作者:Radovan Ondas

在本文章,我们将了解如何通过几个步骤在 Elastic 中实施相似图像搜索。 开始设置应用程序环境,然后导入 NLP 模型,最后完成为你的图像集生成嵌入。

Elastic 图像相似性搜索概览 >>

Elasticsearch:如何在 Elastic 中实现图片相似度搜索

如何设置环境

第一步是为你的应用程序设置环境。 一般要求包括:

  • Git
  • Python 3.9
  • Docker
  • 数百张图片

使用数百张图像以确保获得最佳效果非常重要。

转到工作文件夹并检查创建的存储库代码。 然后导航到存储库文件夹。


1.  git clone https://github.com/radoondas/flask-elastic-image-search.git
2.  cd flask-elastic-image-search

1.  $ git clone https://github.com/radoondas/flask-elastic-image-search.git
2.  Cloning into 'flask-elastic-image-search'...
3.  remote: Enumerating objects: 105, done.
4.  remote: Counting objects: 100% (105/105), done.
5.  remote: Compressing objects: 100% (72/72), done.
6.  remote: Total 105 (delta 37), reused 94 (delta 27), pack-reused 0
7.  Receiving objects: 100% (105/105), 20.72 MiB | 9.75 MiB/s, done.
8.  Resolving deltas: 100% (37/37), done.
9.  $ cd flask-elastic-image-search/
10.  $ pwd
11.  /Users/liuxg/python/flask-elastic-image-search

因为你将使用 Python 来运行代码,所以你需要确保满足所有要求并且环境已准备就绪。 现在创建虚拟环境并安装所有依赖项。


1.  python3 -m venv .venv
2.  source .venv/bin/activate
3.  pip install -r requirements.txt

安装

如果你还没有安装好自己的 Elasticsearch 及 Kibana,请参考如下的文章来进行安装:

特别注意的是:我们将以最新的 Elastic Stack 8.6.1 来进行展示。请参考 Elastic Stack 8.x 的文章进行安装。

启动白金版试用功能

由于上传模型是一个白金版的功能,我们需要启动试用功能。更多关于订阅的信息,请参考网址:订阅 | Elastic Stack 产品和支持 | Elastic

这样我们就成功地启动了白金版试用功能。 

Elasticsearch 集群和嵌入模型

登录到你的帐户以启动 Elasticsearch 集群。 设置一个小型集群:

  • 一个具有 2GB 内存的 HOT 节点
  • 一个具有 4GB 内存的 ML(机器学习)节点(此节点的大小很重要,因为你将导入 Elasticsearch 的 NLP 模型会消耗约 1.5GB 的内存。)

部署准备就绪后,转到 Kibana 并检查机器学习节点的容量。 你将在视图中看到一个机器学习节点。 目前没有加载模型。

使用 Eland 库从 OpenAI 上传 CLIP 嵌入模型。 Eland 是一个 Python Elasticsearch 客户端,用于在 Elasticsearch 中探索和分析数据,能够处理文本和图像。 您将使用此模型从文本输入生成嵌入并查询匹配图像。 在 Eland 库的文档中找到更多详细信息。

对于下一步,你将需要 Elasticsearch 端点。 你可以从部署详细信息部分的 Elasticsearch 云控制台获取它。

 

在本示例中,我们将使用本地部署来进行展示,所以,我们并不必要完成上面的步骤。

Eland

Eland 可以通过 pip 从 PyPI 安装。在安装之前,我们需要安装好自己的 Python。


1.  $ python --version
2.  Python 3.10.2

可以使用 Pip 从 PyPI 安装 Eland:

python -m pip install eland

也可以使用 Conda 从 Conda Forge 安装 Eland:

conda install -c conda-forge eland

希望在不安装 Eland 的情况下使用它的用户,为了只运行可用的脚本,可以构建 Docker 容器:


1.  git clone https://github.com/elastic/eland
2.  cd eland
3.  docker build -t elastic/eland .

Eland 将 Hugging Face 转换器模型到其 TorchScript 表示的转换和分块过程封装在一个 Python 方法中; 因此,这是推荐的导入方法。

  1. 安装 Eland Python 客户端
  2. 运行 eland_import_hub_model 脚本。 例如:
1.  eland_import_hub_model --url <clusterUrl> \ 
2.  --hub-model-id elastic/distilbert-base-cased-finetuned-conll03-english \ 
3.  --task-type ner 

上传模型

我们使用如下的命令来进行上传模型:


1.  eland_import_hub_model --url https://<user>:<password>@<hostname>:<port> \
2.    --hub-model-id sentence-transformers/clip-ViT-B-32-multilingual-v1 \
3.    --task-type text_embedding \
4.    --ca-certs <your certificate> \
5.    --start

针对我的情况:


1.  eland_import_hub_model --url https://elastic:ZgzSt2vHNwA6yPn-fllr@localhost:9200 \
2.    --hub-model-id sentence-transformers/clip-ViT-B-32-multilingual-v1 \
3.    --task-type text_embedding \
4.    --ca-certs /Users/liuxg/elastic/elasticsearch-8.6.1/config/certs/http_ca.crt \
5.    --start

请注意: 你需要根据自己的 Elasticsearch 访问端点,用户名及密码来修改上面的设置,同时你需要根据自己的配置修改上面的证书路径。

运行上面的命令:

上面显示,我们已经成功地上传了模型。我们可以到 Kibana 中进行查看:

上面显示我们已经上传了所需要的 CLIP 模型,并且它的状态是 started。

如何创建图像嵌入

在设置 Elasticsearch 集群并导入嵌入模型后,你需要矢量化图像数据并为数据集中的每个图像创建图像嵌入。 

 要创建图像嵌入,请使用简单的 Python 脚本。 你可以在此处找到该脚本:create-image-embeddings.py。 该脚本将遍历你的图像目录并生成单独的图像嵌入。 它将使用名称和相对路径创建文档,并使用提供的映射将其保存到 Elasticsearch 索引 my-image-embeddings 中。

将所有图像(照片)放入文件夹 app/static/images。 使用带有子文件夹的目录结构来组织图像。 所有图像准备就绪后,使用几个参数执行脚本。

至少要有几百张图像才能获得合理的结果,这一点至关重要。 图像太少不会产生预期的结果,因为你要搜索的空间非常小,而且到搜索向量的距离也非常相似。我尝试在网上下载很多的照片,但是感觉一张一张地下载非常麻烦。你可以在谷歌浏览器中添加插件 Image downloader - Imageye。它可以方便地把很多照片一次下载下来。

在 image_embeddings 文件夹中,运行脚本并为变量使用你的值。


1.  cd image_embeddings
2.  python3 create-image-embeddings.py \
3.    --es_host='https://localhost:9200' \
4.    --es_user='elastic' --es_password='ZgzSt2vHNwA6yPn-fllr' \
5.    --ca_certs='/Users/liuxg/elastic/elasticsearch-8.6.1/config/certs/http_ca.crt'

根据图像的数量、它们的大小、你的 CPU 和你的网络连接,此任务将需要一些时间。 在尝试处理完整数据集之前,先试验少量图像。脚本完成后,你可以使用 Kibana 开发工具验证索引 my-image-embeddings 是否存在并具有相应的文档。

我们在Kibana 中进行查看:

GET _cat/indices/my-image-embeddings?v

 上面命令的响应为:


1.  health status index               uuid                   pri rep docs.count docs.deleted store.size pri.store.size
2.  yellow open   my-image-embeddings h6oUBdHCScWmXOZaf57oWg   1   1        145            0      1.4mb          1.4mb

查看文档,你会看到非常相似的 JSON 对象(如示例)。 你将在图像文件夹中看到图像名称、图像 ID 和相对路径。 此路径用于前端应用程序以在搜索时正确显示图像。JSON 文档中最重要的部分是包含 CLIP 模型生成的密集矢量的 image_embedding。 当应用程序正在搜索图像或类似图像时使用此矢量。

GET my-image-embeddings/_search

1.  {
2.     "_index": "my-image-embeddings",
3.     "_id": "_g9ACIUBMEjlQge4tztV",
4.     "_score": 6.703597,
5.     "_source": {
6.       "image_id": "IMG_4032",
7.       "image_name": "IMG_4032.jpeg",
8.       "image_embedding": [
9.         -0.3415695130825043,
10.         0.1906963288784027,
11.         .....
12.         -0.10289803147315979,
13.         -0.15871885418891907
14.         ],
15.       "relative_path": "phone/IMG_4032.jpeg"
16.     }
17.  }

使用 Flask 应用程序搜索图像

现在你的环境已全部设置完毕,你可以进行下一步,使用我们作为概念证明提供的 Flask 应用程序,使用自然语言实际搜索图像并查找相似图像。 该 Web 应用程序具有简单的 UI,使图像搜索变得简单。 你可以在此 GitHub 存储库中访问原型 Flask 应用程序。

后台应用程序执行两个任务。 在搜索框中输入搜索字符串后,文本将使用机器学习 _infer 端点进行矢量化。 然后,针对带有向量的索引 my-image-embeddings 执行带有密集向量的查询。

你可以在示例中看到这两个查询。 第一个 API 调用使用 _infer 端点,结果是一个密集矢量。


1.  POST _ml/trained_models/sentence-transformers__clip-vit-b-32-multilingual-v1/_infer
2.  {
3.    "docs" : [
4.      {"text_field": "Yellow mountain is the most beautiful mountain in China"}
5.      ]
6.  }

上面的响应如下:

在第二个任务中,搜索查询,我们将使用密集矢量并获得按分数排序的图像。 

`

1.  GET my-image-embeddings/_search
2.  {
3.    "fields": [
4.      "image_id",
5.      "image_name",
6.      "relative_path"
7.    ],
8.    "_source": false,
9.    "knn": {
10.      "field": "image_embedding",
11.      "k": 5,
12.      "num_candidates": 10,
13.      "query_vector": [
14.          0.03395160660147667,
15.          0.007704082876443863,
16.          0.14996188879013062,
17.          -0.10693030804395676,
18.          ...
19.          0.05140634626150131,
20.          0.07114913314580917
21.     ]
22.    }
23.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

要启动并运行 Flask 应用程序,请导航到存储库的根文件夹并配置 .env 文件。 配置文件中的值用于连接到 Elasticsearch 集群。 你需要为以下变量插入值。 这些与图像嵌入生成中使用的值相同。 

.env


1.  ES_HOST='URL:PORT'
2.  ES_USER='elastic'
3.  ES_PWD='password'

为了能够使得我们自构建的 Elasticsearch 集群能够被正确地访问,我们必须把 Elasticsearch 的根证书拷贝到 Flask 应用的相应目录中:

flask-elastic-image-search/app/conf/ca.crt


1.  (.venv) $ pwd
2.  /Users/liuxg/python/flask-elastic-image-search/app/conf
3.  (.venv) $ cp ~/elastic/elasticsearch-8.6.1/config/certs/http_ca.crt ca.crt
4.  overwrite ca.crt? (y/n [n]) y

在上面,我们替换了仓库中原有的证书文件 ca.crt。

准备就绪后,运行主文件夹中的 flask 应用程序并等待它启动。


1.  # In the main directory 
2.  $ flask run --port=5001

如果应用程序启动,你将看到类似于下面的输出,它在末尾指示你需要访问哪个 URL 才能访问该应用程序。

恭喜! 你的应用程序现在应该已启动并正在运行,并且可以通过互联网浏览器在 http://127.0.0.1:5001 上访问。

导航到图像搜索选项卡并输入描述你最佳图像的文本。 尝试使用非关键字或描述性文字。

在下面的示例中,输入的文本是 “Yellow mountain is the most beautiful mountain in China”。 结果显示在我们的数据集中。 如果用户喜欢结果集中的一张特定图像,只需单击它旁边的按钮,就会显示类似的图像。 用户可以无限次地这样做,并通过图像数据集构建自己的路径。

我们尝试另外的一个例子。这次我们输入:I love beautiful girls。

搜索也可以通过简单地上传图像来进行。 该应用程序会将图像转换为矢量并在数据集中搜索相似的图像。 为此,导航到第三个选项卡 “Similar Image”,从磁盘上传图像,然后点击 “Search”。

我们可以看到相似的图片。我们尝试使用一个女孩的照片再试试:

因为我们在 Elasticsearch 中使用的 NLP(sentence-transformers/clip-ViT-B-32-multilingual-v1)模型是多语言的,支持多语言推理,所以尽量搜索自己语言的图片。 然后也使用英文文本验证结果。我们尝试使用 “黄山是中国最漂亮的山”:

请务必注意,使用的模型是通用模型,这些模型非常准确,但你获得的结果会因用例或其他因素而异。 如果你需要更高的精度,则必须采用通用模型或开发自己的模型 —— CLIP 模型只是一个起点。

代码摘要

你可以在 GitHub 存储库中找到完整的代码。 你可能正在检查 routes.py 中的代码,它实现了应用程序的主要逻辑。 除了明显的路线定义之外,你还应该关注定义 _infer 和 _search 端点(infer_trained_modelknn_search_images)的方法。 生成图像嵌入的代码位于 create-image-embeddings.py文件中。

总结

现在你已经设置了 Flask 应用程序,你可以轻松地搜索你自己的图像集! Elastic 在平台内提供了矢量搜索的原生集成,避免了与外部进程的通信。 你可以灵活地开发和使用你可能使用 PyTorch 开发的自定义嵌入模型。

语义图像搜索具有其他传统图像搜索方法的以下优点:

  • 更高的准确度:向量相似性捕获上下文和关联,而不依赖于图像的文本元描述。
  • 增强的用户体验:与猜测哪些关键字可能相关相比,描述你正在寻找的内容或提供示例图像。
  • 图像数据库的分类:不用担心对图像进行分类——相似性搜索可以在一堆图像中找到相关图像,而无需对它们进行组织。

如果你的用例更多地依赖于文本数据,你可以在以前的博客中了解更多关于实现语义搜索和将自然语言处理应用于文本的信息。 对于文本数据,向量相似度与传统关键词评分的结合呈现了两全其美的效果。

准备好开始了吗? 在我们的虚拟活动中心报名参加矢量搜索实践研讨会,并在我们的在线论坛中与社区互动。

收起阅读 »

Web Scraper + Elasticsearch + Kibana + SearchKit 打造的豆瓣电影top250 搜索演示系统

Web Scraper + Elasticsearch + Kibana + SearchKit 打造的豆瓣电影top250 搜索演示系统

作者:小森同学

声明:电影数据来源于“豆瓣电影”,如有侵权,请联系删除

1.png

2.png

Web Scraper

{
    "_id": "top250",
    "startUrl": ["https://movie.douban.com/top250?start=[0-225:25]&filter="],
    "selectors": [{
        "id": "container",
        "multiple": true,
        "parentSelectors": ["_root"],
        "selector": ".grid_view li",
        "type": "SelectorElement"
    }, {
        "id": "name",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "",
        "selector": "span.title:nth-of-type(1)",
        "type": "SelectorText"
    }, {
        "id": "number",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "",
        "selector": "em",
        "type": "SelectorText"
    }, {
        "id": "score",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "",
        "selector": "span.rating_num",
        "type": "SelectorText"
    }, {
        "id": "review",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "",
        "selector": "span.inq",
        "type": "SelectorText"
    }, {
        "id": "year",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "\\d{4}",
        "selector": "p:nth-of-type(1)",
        "type": "SelectorText"
    }, {
        "id": "tour_guide",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "^导演: \\S*",
        "selector": "p:nth-of-type(1)",
        "type": "SelectorText"
    }, {
        "id": "type",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "[^/]+$",
        "selector": "p:nth-of-type(1)",
        "type": "SelectorText"
    }, {
        "id": "area",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "[^\\/]+(?=\\/[^\\/]*$)",
        "selector": "p:nth-of-type(1)",
        "type": "SelectorText"
    }, {
        "id": "detail_link",
        "multiple": false,
        "parentSelectors": ["container"],
        "selector": ".hd a",
        "type": "SelectorLink"
    }, {
        "id": "director",
        "multiple": false,
        "parentSelectors": ["detail_link"],
        "regex": "",
        "selector": "span:nth-of-type(1) .attrs a",
        "type": "SelectorText"
    }, {
        "id": "screenwriter",
        "multiple": false,
        "parentSelectors": ["detail_link"],
        "regex": "(?<=编剧: )[\\u4e00-\\u9fa5A-Za-z0-9/()\\·\\s]+(?=主演)",
        "selector": "div#info",
        "type": "SelectorText"
    }, {
        "id": "film_length",
        "multiple": false,
        "parentSelectors": ["detail_link"],
        "regex": "\\d+",
        "selector": "span[property='v:runtime']",
        "type": "SelectorText"
    }, {
        "id": "IMDb",
        "multiple": false,
        "parentSelectors": ["detail_link"],
        "regex": "(?<=[IMDb:\\s+])\\S*(?=\\d*$)",
        "selector": "div#info",
        "type": "SelectorText"
    }, {
        "id": "language",
        "multiple": false,
        "parentSelectors": ["detail_link"],
        "regex": "(?<=语言: )\\S+",
        "selector": "div#info",
        "type": "SelectorText"
    }, {
        "id": "alias",
        "multiple": false,
        "parentSelectors": ["detail_link"],
        "regex": "(?<=又名: )[\\u4e00-\\u9fa5A-Za-z0-9/()\\s]+(?=IMDb)",
        "selector": "div#info",
        "type": "SelectorText"
    }, {
        "id": "pic",
        "multiple": false,
        "parentSelectors": ["container"],
        "selector": "img",
        "type": "SelectorImage"
    }]
}

elasticsearch

{
    "mappings": {
      "properties": {
        "IMDb": {
          "type": "keyword",
          "copy_to": [
            "all"
          ]
        },
        "alias": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "all": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "area": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "director": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "film_length": {
          "type": "long"
        },
        "id": {
          "type": "keyword"
        },
        "language": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "link": {
          "type": "keyword"
        },
        "name": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "number": {
          "type": "long"
        },
        "photo": {
          "type": "keyword"
        },
        "review": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "score": {
          "type": "double"
        },
        "screenwriter": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "type": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "year": {
          "type": "long"
        }
      }
    }
  }

kibana

需要使用pipeline对索引字段进行处理,如对type 通过空格进行分割为数组等,可以参照官方文档或其他博客。

制作仪表板省略, 请自行搜索

SearchKit

参考 https://github.com/searchkit/searchkit-starter-app

继续阅读 »

Web Scraper + Elasticsearch + Kibana + SearchKit 打造的豆瓣电影top250 搜索演示系统

作者:小森同学

声明:电影数据来源于“豆瓣电影”,如有侵权,请联系删除

1.png

2.png

Web Scraper

{
    "_id": "top250",
    "startUrl": ["https://movie.douban.com/top250?start=[0-225:25]&filter="],
    "selectors": [{
        "id": "container",
        "multiple": true,
        "parentSelectors": ["_root"],
        "selector": ".grid_view li",
        "type": "SelectorElement"
    }, {
        "id": "name",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "",
        "selector": "span.title:nth-of-type(1)",
        "type": "SelectorText"
    }, {
        "id": "number",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "",
        "selector": "em",
        "type": "SelectorText"
    }, {
        "id": "score",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "",
        "selector": "span.rating_num",
        "type": "SelectorText"
    }, {
        "id": "review",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "",
        "selector": "span.inq",
        "type": "SelectorText"
    }, {
        "id": "year",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "\\d{4}",
        "selector": "p:nth-of-type(1)",
        "type": "SelectorText"
    }, {
        "id": "tour_guide",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "^导演: \\S*",
        "selector": "p:nth-of-type(1)",
        "type": "SelectorText"
    }, {
        "id": "type",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "[^/]+$",
        "selector": "p:nth-of-type(1)",
        "type": "SelectorText"
    }, {
        "id": "area",
        "multiple": false,
        "parentSelectors": ["container"],
        "regex": "[^\\/]+(?=\\/[^\\/]*$)",
        "selector": "p:nth-of-type(1)",
        "type": "SelectorText"
    }, {
        "id": "detail_link",
        "multiple": false,
        "parentSelectors": ["container"],
        "selector": ".hd a",
        "type": "SelectorLink"
    }, {
        "id": "director",
        "multiple": false,
        "parentSelectors": ["detail_link"],
        "regex": "",
        "selector": "span:nth-of-type(1) .attrs a",
        "type": "SelectorText"
    }, {
        "id": "screenwriter",
        "multiple": false,
        "parentSelectors": ["detail_link"],
        "regex": "(?<=编剧: )[\\u4e00-\\u9fa5A-Za-z0-9/()\\·\\s]+(?=主演)",
        "selector": "div#info",
        "type": "SelectorText"
    }, {
        "id": "film_length",
        "multiple": false,
        "parentSelectors": ["detail_link"],
        "regex": "\\d+",
        "selector": "span[property='v:runtime']",
        "type": "SelectorText"
    }, {
        "id": "IMDb",
        "multiple": false,
        "parentSelectors": ["detail_link"],
        "regex": "(?<=[IMDb:\\s+])\\S*(?=\\d*$)",
        "selector": "div#info",
        "type": "SelectorText"
    }, {
        "id": "language",
        "multiple": false,
        "parentSelectors": ["detail_link"],
        "regex": "(?<=语言: )\\S+",
        "selector": "div#info",
        "type": "SelectorText"
    }, {
        "id": "alias",
        "multiple": false,
        "parentSelectors": ["detail_link"],
        "regex": "(?<=又名: )[\\u4e00-\\u9fa5A-Za-z0-9/()\\s]+(?=IMDb)",
        "selector": "div#info",
        "type": "SelectorText"
    }, {
        "id": "pic",
        "multiple": false,
        "parentSelectors": ["container"],
        "selector": "img",
        "type": "SelectorImage"
    }]
}

elasticsearch

{
    "mappings": {
      "properties": {
        "IMDb": {
          "type": "keyword",
          "copy_to": [
            "all"
          ]
        },
        "alias": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "all": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "area": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "director": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "film_length": {
          "type": "long"
        },
        "id": {
          "type": "keyword"
        },
        "language": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "link": {
          "type": "keyword"
        },
        "name": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "number": {
          "type": "long"
        },
        "photo": {
          "type": "keyword"
        },
        "review": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "score": {
          "type": "double"
        },
        "screenwriter": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "type": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          },
          "copy_to": [
            "all"
          ],
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "year": {
          "type": "long"
        }
      }
    }
  }

kibana

需要使用pipeline对索引字段进行处理,如对type 通过空格进行分割为数组等,可以参照官方文档或其他博客。

制作仪表板省略, 请自行搜索

SearchKit

参考 https://github.com/searchkit/searchkit-starter-app

收起阅读 »

如何正确处理 Elasticsearch 摄取管道故障

Elasticsearch 中的 ingest node 给开发者们带来了极大的方便。Ingest node 是 Elasticsearch 集群中的一部分。相比较 Logstash 而言,它易于扩展。同时它有大量的处理器供开发者使用。我们可以充分利用它多数据进行清洗,转换,丰富等等操作。在我们摄入数据的时候,由于数据格式的问题,往往会发生错误,那么我们该如何处理这些错误呢?在 Logstash 的实现中,我们可以利用 Dead Letter Queue 来记录失败的文档。那么我们可以在 ingest pipeline 里,实现同样的机制吗?请详细阅读文章 “如何正确处理 Elasticsearch 摄取管道故障” https://elasticstack.blog.csdn ... 73575 在文章中,我们实例了一种叫做 Dead Letter Index,它可以记录失败的文档,并可以回放让我们修正摄入文档中的错误。
继续阅读 »
Elasticsearch 中的 ingest node 给开发者们带来了极大的方便。Ingest node 是 Elasticsearch 集群中的一部分。相比较 Logstash 而言,它易于扩展。同时它有大量的处理器供开发者使用。我们可以充分利用它多数据进行清洗,转换,丰富等等操作。在我们摄入数据的时候,由于数据格式的问题,往往会发生错误,那么我们该如何处理这些错误呢?在 Logstash 的实现中,我们可以利用 Dead Letter Queue 来记录失败的文档。那么我们可以在 ingest pipeline 里,实现同样的机制吗?请详细阅读文章 “如何正确处理 Elasticsearch 摄取管道故障” https://elasticstack.blog.csdn ... 73575 在文章中,我们实例了一种叫做 Dead Letter Index,它可以记录失败的文档,并可以回放让我们修正摄入文档中的错误。 收起阅读 »

Elasticsearch:使用 pipelines 路由文档到想要的 Elasticsearch 索引中去

原文地址 elasticstack.blog.csdn.net

路由文件

当应用程序需要向 Elasticsearch 添加文档时,它们首先要知道目标索引是什么。在很多的应用案例中,特别是针对时序数据,我们想把每个月的数据写入到一个特定的索引中。一方面便于管理索引,另外一方面在将来搜索的时候可以按照每个月的索引来进行搜索,这样速度更快,更便捷。

当你处于某种类型的文档总是转到特定索引的琐碎情况时,这似乎很明显,但当你的索引名称可能根据杂项参数(无论它们是否在你的系统外部 - 当前例如日期 - 或者你尝试存储的文档的固有属性 - 大多数时候是文档字段之一的值)。

当发生最后一种情况时(我们指的是索引名称可以变化的情况),在向 Elasticsearch 发出索引命令之前,你的应用程序需要计算目标索引的名称。

此外 —— 即使一开始这看起来像是一种反模式 —— 你可以有多个应用程序需要在索引中索引相同类型的文档,这些文档的名称可能会发生变化。 现在你必须维护跨多个组件重复的索引名称计算逻辑:就可维护性和敏捷性而言,这不是好消息。

Logstash —— Elastic Stack 的一个知名成员 —— 可以帮助集中这样的逻辑,但代价是维护另一个正在运行的软件,这需要配置、知识等。

我们想要在本文中展示的是通过将索引名称计算委托给 Elasticsearch 而不是我们的应用程序来解决此问题的解决方案。

Date index name processor 介绍

处理器的目的是通过使用日期数学索引名称支持,根据文档中的日期或时间戳字段将文档指向基于正确时间的索引。

处理器根据提供的索引名称前缀、正在处理的文档中的日期或时间戳字段以及提供的日期舍入,使用日期数学索引名称表达式设置 _index 元数据字段。

首先,此处理器从正在处理的文档中的字段中获取日期或时间戳。 或者,可以根据字段值应如何解析为日期来配置日期格式。 然后这个日期、提供的索引名称前缀和提供的日期舍入被格式化为日期数学索引名称表达式。 此处还可以选择日期格式指定日期应如何格式化为日期数学索引名称表达式。

将文档指向每月索引的示例管道,该索引以基于 date1 字段中的日期的 my-index-prefix 开头:


1.  PUT _ingest/pipeline/monthlyindex
2.  {
3.    "description": "monthly date-time index naming",
4.    "processors" : [
5.      {
6.        "date_index_name" : {
7.          "field" : "date1",
8.          "index_name_prefix" : "my-",
9.          "date_rounding" : "M"
10.        }
11.      }
12.    ]
13.  }

使用该管道进行索引请求:


1.  PUT /my-index/_doc/1?pipeline=monthlyindex
2.  {
3.    "date1" : "2016-04-25T12:02:01.789Z"
4.  }

上面命令运行的结果是:


1.  {
2.    "_index": "my-index-2016-04-01",
3.    "_id": "1",
4.    "_version": 1,
5.    "result": "created",
6.    "_shards": {
7.      "total": 2,
8.      "successful": 1,
9.      "failed": 0
10.    },
11.    "_seq_no": 0,
12.    "_primary_term": 1
13.  }

上面的请求不会将这个文档索引到 my-index 索引中,而是索引到 my-index-2016-04-01 索引中,因为它是按月取整的。 这是因为 date-index-name-processor 覆盖了文档的 _index 属性。

要查看导致上述文档被索引到 my-index-2016-04-01 中的实际索引请求中提供的索引的日期数学(date-math)值,我们可以使用模拟请求检查处理器的效果。


1.  POST _ingest/pipeline/_simulate
2.  {
3.    "pipeline" :
4.    {
5.      "description": "monthly date-time index naming",
6.      "processors" : [
7.        {
8.          "date_index_name" : {
9.            "field" : "date1",
10.            "index_name_prefix" : "my-",
11.            "date_rounding" : "M"
12.          }
13.        }
14.      ]
15.    },
16.    "docs": [
17.      {
18.        "_source": {
19.          "date1": "2016-04-25T12:02:01.789Z"
20.        }
21.      }
22.    ]
23.  }

上面命令返回结果:

`

1.  {
2.    "docs": [
3.      {
4.        "doc": {
5.          "_index": "<my-index-{2016-04-25||/M{yyyy-MM-dd|UTC}}>",
6.          "_id": "_id",
7.          "_version": "-3",
8.          "_source": {
9.            "date1": "2016-04-25T12:02:01.789Z"
10.          },
11.          "_ingest": {
12.            "timestamp": "2023-02-23T01:15:52.214364Z"
13.          }
14.        }
15.      }
16.    ]
17.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

以上示例显示 _index 设置为 <my-index-{2016-04-25||/M{yyyy-MM-dd|UTC}}>。 Elasticsearch 将其理解为 2016-04-01,如日期数学索引名称文档中所述。

日期索引名称选项

以下是使用 date index name 的一些选项

no-处理器的描述。 用于描述处理器的用途或其配置。
ifno-有条件地执行处理器。 请参阅有条件地运行处理器
ignore_failurenofalse忽略处理器的故障。 请参阅处理管道故障
on_failureno-处理处理器的故障。 请参阅处理管道故障
tagno-处理器的标识符。 用于调试和指标

使用案例 - 基于时间的时序索引

这是一个众所周知的用例,通常在您要处理日志时发现。这个想法是索引文档,索引的名称由根名称和根据日志事件的日期计算的值组成。 该日期实际上是你要索引的文档的一个字段。

这不是本文的重点,但以这种方式索引文档有几个优点,包括更容易的数据管理、启用冷/暖架构等。让我们举个例子。

假设我们必须处理来自多个来源的数据 —— 例如物联网。 我们的每个对象每分钟都会向不同的后端发送一些数据(是的,这真的很可悲,但我们的对象不通过相同的网络进行通信,因此选择通过多个系统来处理这个问题)。

对象发送的数据被转换成如下所示的 JSON 文档:


1.  POST data/_doc?pipeline=compute-index-name
2.  {
3.    "objectId": 1234,
4.    "manufacturer": "SHIELD",
5.    "payload": "some_data",
6.    "date": "2019-04-01T12:10:30.000Z"
7.  }

我们有一个用于传输数据的对象的 UID、一个制造商 ID、一个有效负载部分和一个日期字段。

索引名称计算

假设我们要将对象的数据存储在名为 data-{YYYYMMDD} 的索引中,其中根名称是数据后跟日期模式。基于上面的例子,后端收到这个文档应该怎么办呢?

首先它必须解析它以提取日期字段的值,然后它必须根据它在文档中找到的日期计算目标索引名称。 最后,它向 Elasticsearch 向刚刚计算出名称的索引发出索引请求。


1.  document.date = "2019-04-01T12:10:30Z"
2.  index.name = "" + "20190401"

在我们的例子中,我们有几个后端必须知道如何计算索引名称,因此必须知道索引的命名逻辑。

如果索引名的计算直接由 Elasticsearch 进行,岂不是更聪明?

Ingest pipeline 的力量

从 Elasticsearch 的第 5 版开始,我们现在有了一种称为摄取的节点。默认情况下,集群的所有节点都具有 ingest 类型。这些节点有权在索引文档之前执行所谓的管道。 管道是一组处理器,每个处理器都可以以某种特定方式转换输入文档。当一个文档被摄入到 Elasticsearch 集群时,它的工作流是这样的。

从上面,我们可以看出来,在文档被写入之前,它必须经过 ingest node 进行处理。我们可以通过 date index name processor 来获得我们想要的 index 名称,进而写入到我们想要的索引中去。 这里有用的是,管道不仅可以转换文档的固有数据,还可以修改文档元数据,特别是它的 _index 属性。

现在让我们回到我们的例子。我们建议定义一个管道来完成这项工作,而不是将索引名称计算委托给应用程序。根据文档,此处理器允许你定义包含日期的字段名称、索引的根名称(前缀)以及计算附加到此前缀的日期的舍入方法。

如果我们想将 IoT 数据添加到模式为 data-{YYYYMMDD} 的索引中,我们只需创建如下所示的管道:


1.  PUT _ingest/pipeline/compute-index-name
2.  {
3.    "description": "Set the document destination index by appending a prefix and its 'date' field",
4.    "processors": [
5.      {
6.        "date_index_name": {
7.          "field": "date",
8.          "index_name_prefix": "",
9.          "date_rounding": "d",
10.          "index_name_format": "yyyyMMdd"
11.        }
12.      }
13.    ]
14.  }

一个索引 = 一个管道?

好的,现在我们知道如何定义一个管道来为特定的目标索引建立一个名称。 但是我们可以通过操纵文档元数据来做更多的事情!

假设我们有不同类型的文档,每个文档都有一个日期字段,但需要在不同的索引中进行索引。计算目标索引名称的逻辑对于每种文档类型都是相同的,但使用上述策略将导致创建多个管道。

让我们试着做一些更简单和可重用的东西。

回到我们的示例,我们现在有两种文档类型:一种需要在 adata-{YYYYMMDD} 索引(和以前一样)中建立索引,另一种其目的地是名为 new_data-{YYYYMMDD} 的索引。

目标为 new_data 的文档具有以下结构:


1.  {
2.    "newObjectId": 1234,
3.    "source": "HYDRA",
4.    "payload": "some_data",
5.    "date": "2019-04-02T13:10:30.000Z"
6.  }

该结构与标准 IoT 文档略有不同,但重要的是日期字段存在于两个映射中。

现在我们要定义一个管道来计算我们两种文档类型的目标索引。 我们所要做的就是通过分析通过索引 API 发出的请求目的地来构建目的地索引名称。


1.  PUT _ingest/pipeline/compute-index-name
2.  {
3.    "description": "Set the document destination index by appending the requested index and its 'date' field",
4.    "processors": [
5.      {
6.        "date_index_name": {
7.          "field": "date",
8.          "index_name_prefix": "{{ _index }}-",
9.          "date_rounding": "d",
10.          "index_name_format": "yyyyMMdd"
11.        }
12.      }
13.    ]
14.  }

请注意,索引名称前缀现在位于名为_index 的索引元数据字段中。 通过使用这个字段,我们的管道现在是通用的并且可以与任何索引一起使用 —— 假设目标索引是根据相同的规则计算的。

使用我们的 “路由” 管道

现在我们有了一个能够根据文档的日期字段计算目标索引名称的通用管道,让我们看看如何让 Elasticsearch 使用它。

我们可以通过两种方式告诉 Elasticsearch 使用管道,让我们评估一下。

Index API 调用

第一个 —— 也是直接的解决方案——是使用 Index API 的管道参数。

换句话说:每次你想索引一个文档,你必须告诉 Elasticsearch 要使用的管道。


1.  POST data/_doc?pipeline=compute-index-name
2.  {
3.    "objectId": 1234,
4.    "manufacturer": "SHIELD",
5.    "payload": "some_data",
6.    "date": "2019-04-01T12:10:30.000Z"
7.  }

现在,每次我们通过指示 compute-index-name 管道将文档添加到索引中时,该文档将被添加到正确的基于时间的索引中。 在此示例中,目标索引将为 data-20190401 。

我们提供给 Index API 的 data 索引名称呢? 它可以被看作是一个索引:它只是用来执行 API 调用并且是真正目标索引的根,它不一定存在!

默认管道:引入 “虚拟索引”

索引默认管道(default pipeline)是使用管道的另一种有用方式:当你创建索引时,有一个名为 index.default_pipeline 的设置可以设置为管道的名称,只要你将文档添加到相应的索引就会执行该管道并且没有管道被添加到 API 调用中。 你还可以在索引文档时使用特殊管道名称 _none 来绕过此默认索引。 通过使用此功能,你可以定义我称之为 “虚拟索引” 的内容,并将其与默认管道相关联,该默认管道将充当我们上面看到的路由管道。

让我们将其应用到我们的示例中。

我们假设我们的通用路由管道 compute-index-name 已经创建。 我们现在可以创建一个名为 data 的索引,它将使用此管道作为其默认管道。


1.  PUT data
2.  {
3.    "settings" : {
4.      "index" : {
5.        "number_of_shards" : 3, 
6.        "number_of_replicas" : 1,
7.        "default_pipeline" : "compute-index-name"
8.      }
9.    }
10.  }

现在,每次我们要求 Elasticsearch 为数据索引中的文档编制索引时,计算索引名称管道将负责该文档的实际路由。 因此,数据索引中永远不会有单个文档被索引,但我们将调用管道的责任完全委托给 Elasticsearch。

运行完上面的命令后,我们来尝试写入一个文档:


1.  POST data/_doc
2.  {
3.    "objectId": 1234,
4.    "manufacturer": "SHIELD",
5.    "payload": "some_data",
6.    "date": "2019-04-01T12:10:30.000Z"
7.  }

上面的命令返回的结果是:


1.  {
2.    "_index": "data-20190401",
3.    "_id": "2DMGfIYBaog4blQ55Qr7",
4.    "_version": 1,
5.    "result": "created",
6.    "_shards": {
7.      "total": 2,
8.      "successful": 1,
9.      "failed": 0
10.    },
11.    "_seq_no": 1,
12.    "_primary_term": 1
13.  }

结论

我们刚刚在这里看到了如何利用 Elasticsearch 中的管道功能根据文档的固有属性来路由文档。Ingest pipeline 不仅仅可以替代 Logstash 过滤器:你可以定义复杂的管道,使用多个处理器(一个特定的处理器甚至允许你调用另一个管道)、条件等。有关 ingest pipeline 的更多文章,请参阅 “Elastic:开发者上手指南” 文章中的 “Ingest pipeline” 章节。

在我看来,本文末尾看到的 “虚拟索引” 非常有趣。 包含创建这样一个并非真正的索引的索引只是为了创建路由管道的入口点的功能甚至可以成为 Elasticsearch 的一个新的和有用的功能,为什么不呢?

继续阅读 »

原文地址 elasticstack.blog.csdn.net

路由文件

当应用程序需要向 Elasticsearch 添加文档时,它们首先要知道目标索引是什么。在很多的应用案例中,特别是针对时序数据,我们想把每个月的数据写入到一个特定的索引中。一方面便于管理索引,另外一方面在将来搜索的时候可以按照每个月的索引来进行搜索,这样速度更快,更便捷。

当你处于某种类型的文档总是转到特定索引的琐碎情况时,这似乎很明显,但当你的索引名称可能根据杂项参数(无论它们是否在你的系统外部 - 当前例如日期 - 或者你尝试存储的文档的固有属性 - 大多数时候是文档字段之一的值)。

当发生最后一种情况时(我们指的是索引名称可以变化的情况),在向 Elasticsearch 发出索引命令之前,你的应用程序需要计算目标索引的名称。

此外 —— 即使一开始这看起来像是一种反模式 —— 你可以有多个应用程序需要在索引中索引相同类型的文档,这些文档的名称可能会发生变化。 现在你必须维护跨多个组件重复的索引名称计算逻辑:就可维护性和敏捷性而言,这不是好消息。

Logstash —— Elastic Stack 的一个知名成员 —— 可以帮助集中这样的逻辑,但代价是维护另一个正在运行的软件,这需要配置、知识等。

我们想要在本文中展示的是通过将索引名称计算委托给 Elasticsearch 而不是我们的应用程序来解决此问题的解决方案。

Date index name processor 介绍

处理器的目的是通过使用日期数学索引名称支持,根据文档中的日期或时间戳字段将文档指向基于正确时间的索引。

处理器根据提供的索引名称前缀、正在处理的文档中的日期或时间戳字段以及提供的日期舍入,使用日期数学索引名称表达式设置 _index 元数据字段。

首先,此处理器从正在处理的文档中的字段中获取日期或时间戳。 或者,可以根据字段值应如何解析为日期来配置日期格式。 然后这个日期、提供的索引名称前缀和提供的日期舍入被格式化为日期数学索引名称表达式。 此处还可以选择日期格式指定日期应如何格式化为日期数学索引名称表达式。

将文档指向每月索引的示例管道,该索引以基于 date1 字段中的日期的 my-index-prefix 开头:


1.  PUT _ingest/pipeline/monthlyindex
2.  {
3.    "description": "monthly date-time index naming",
4.    "processors" : [
5.      {
6.        "date_index_name" : {
7.          "field" : "date1",
8.          "index_name_prefix" : "my-",
9.          "date_rounding" : "M"
10.        }
11.      }
12.    ]
13.  }

使用该管道进行索引请求:


1.  PUT /my-index/_doc/1?pipeline=monthlyindex
2.  {
3.    "date1" : "2016-04-25T12:02:01.789Z"
4.  }

上面命令运行的结果是:


1.  {
2.    "_index": "my-index-2016-04-01",
3.    "_id": "1",
4.    "_version": 1,
5.    "result": "created",
6.    "_shards": {
7.      "total": 2,
8.      "successful": 1,
9.      "failed": 0
10.    },
11.    "_seq_no": 0,
12.    "_primary_term": 1
13.  }

上面的请求不会将这个文档索引到 my-index 索引中,而是索引到 my-index-2016-04-01 索引中,因为它是按月取整的。 这是因为 date-index-name-processor 覆盖了文档的 _index 属性。

要查看导致上述文档被索引到 my-index-2016-04-01 中的实际索引请求中提供的索引的日期数学(date-math)值,我们可以使用模拟请求检查处理器的效果。


1.  POST _ingest/pipeline/_simulate
2.  {
3.    "pipeline" :
4.    {
5.      "description": "monthly date-time index naming",
6.      "processors" : [
7.        {
8.          "date_index_name" : {
9.            "field" : "date1",
10.            "index_name_prefix" : "my-",
11.            "date_rounding" : "M"
12.          }
13.        }
14.      ]
15.    },
16.    "docs": [
17.      {
18.        "_source": {
19.          "date1": "2016-04-25T12:02:01.789Z"
20.        }
21.      }
22.    ]
23.  }

上面命令返回结果:

`

1.  {
2.    "docs": [
3.      {
4.        "doc": {
5.          "_index": "<my-index-{2016-04-25||/M{yyyy-MM-dd|UTC}}>",
6.          "_id": "_id",
7.          "_version": "-3",
8.          "_source": {
9.            "date1": "2016-04-25T12:02:01.789Z"
10.          },
11.          "_ingest": {
12.            "timestamp": "2023-02-23T01:15:52.214364Z"
13.          }
14.        }
15.      }
16.    ]
17.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

以上示例显示 _index 设置为 <my-index-{2016-04-25||/M{yyyy-MM-dd|UTC}}>。 Elasticsearch 将其理解为 2016-04-01,如日期数学索引名称文档中所述。

日期索引名称选项

以下是使用 date index name 的一些选项

no-处理器的描述。 用于描述处理器的用途或其配置。
ifno-有条件地执行处理器。 请参阅有条件地运行处理器
ignore_failurenofalse忽略处理器的故障。 请参阅处理管道故障
on_failureno-处理处理器的故障。 请参阅处理管道故障
tagno-处理器的标识符。 用于调试和指标

使用案例 - 基于时间的时序索引

这是一个众所周知的用例,通常在您要处理日志时发现。这个想法是索引文档,索引的名称由根名称和根据日志事件的日期计算的值组成。 该日期实际上是你要索引的文档的一个字段。

这不是本文的重点,但以这种方式索引文档有几个优点,包括更容易的数据管理、启用冷/暖架构等。让我们举个例子。

假设我们必须处理来自多个来源的数据 —— 例如物联网。 我们的每个对象每分钟都会向不同的后端发送一些数据(是的,这真的很可悲,但我们的对象不通过相同的网络进行通信,因此选择通过多个系统来处理这个问题)。

对象发送的数据被转换成如下所示的 JSON 文档:


1.  POST data/_doc?pipeline=compute-index-name
2.  {
3.    "objectId": 1234,
4.    "manufacturer": "SHIELD",
5.    "payload": "some_data",
6.    "date": "2019-04-01T12:10:30.000Z"
7.  }

我们有一个用于传输数据的对象的 UID、一个制造商 ID、一个有效负载部分和一个日期字段。

索引名称计算

假设我们要将对象的数据存储在名为 data-{YYYYMMDD} 的索引中,其中根名称是数据后跟日期模式。基于上面的例子,后端收到这个文档应该怎么办呢?

首先它必须解析它以提取日期字段的值,然后它必须根据它在文档中找到的日期计算目标索引名称。 最后,它向 Elasticsearch 向刚刚计算出名称的索引发出索引请求。


1.  document.date = "2019-04-01T12:10:30Z"
2.  index.name = "" + "20190401"

在我们的例子中,我们有几个后端必须知道如何计算索引名称,因此必须知道索引的命名逻辑。

如果索引名的计算直接由 Elasticsearch 进行,岂不是更聪明?

Ingest pipeline 的力量

从 Elasticsearch 的第 5 版开始,我们现在有了一种称为摄取的节点。默认情况下,集群的所有节点都具有 ingest 类型。这些节点有权在索引文档之前执行所谓的管道。 管道是一组处理器,每个处理器都可以以某种特定方式转换输入文档。当一个文档被摄入到 Elasticsearch 集群时,它的工作流是这样的。

从上面,我们可以看出来,在文档被写入之前,它必须经过 ingest node 进行处理。我们可以通过 date index name processor 来获得我们想要的 index 名称,进而写入到我们想要的索引中去。 这里有用的是,管道不仅可以转换文档的固有数据,还可以修改文档元数据,特别是它的 _index 属性。

现在让我们回到我们的例子。我们建议定义一个管道来完成这项工作,而不是将索引名称计算委托给应用程序。根据文档,此处理器允许你定义包含日期的字段名称、索引的根名称(前缀)以及计算附加到此前缀的日期的舍入方法。

如果我们想将 IoT 数据添加到模式为 data-{YYYYMMDD} 的索引中,我们只需创建如下所示的管道:


1.  PUT _ingest/pipeline/compute-index-name
2.  {
3.    "description": "Set the document destination index by appending a prefix and its 'date' field",
4.    "processors": [
5.      {
6.        "date_index_name": {
7.          "field": "date",
8.          "index_name_prefix": "",
9.          "date_rounding": "d",
10.          "index_name_format": "yyyyMMdd"
11.        }
12.      }
13.    ]
14.  }

一个索引 = 一个管道?

好的,现在我们知道如何定义一个管道来为特定的目标索引建立一个名称。 但是我们可以通过操纵文档元数据来做更多的事情!

假设我们有不同类型的文档,每个文档都有一个日期字段,但需要在不同的索引中进行索引。计算目标索引名称的逻辑对于每种文档类型都是相同的,但使用上述策略将导致创建多个管道。

让我们试着做一些更简单和可重用的东西。

回到我们的示例,我们现在有两种文档类型:一种需要在 adata-{YYYYMMDD} 索引(和以前一样)中建立索引,另一种其目的地是名为 new_data-{YYYYMMDD} 的索引。

目标为 new_data 的文档具有以下结构:


1.  {
2.    "newObjectId": 1234,
3.    "source": "HYDRA",
4.    "payload": "some_data",
5.    "date": "2019-04-02T13:10:30.000Z"
6.  }

该结构与标准 IoT 文档略有不同,但重要的是日期字段存在于两个映射中。

现在我们要定义一个管道来计算我们两种文档类型的目标索引。 我们所要做的就是通过分析通过索引 API 发出的请求目的地来构建目的地索引名称。


1.  PUT _ingest/pipeline/compute-index-name
2.  {
3.    "description": "Set the document destination index by appending the requested index and its 'date' field",
4.    "processors": [
5.      {
6.        "date_index_name": {
7.          "field": "date",
8.          "index_name_prefix": "{{ _index }}-",
9.          "date_rounding": "d",
10.          "index_name_format": "yyyyMMdd"
11.        }
12.      }
13.    ]
14.  }

请注意,索引名称前缀现在位于名为_index 的索引元数据字段中。 通过使用这个字段,我们的管道现在是通用的并且可以与任何索引一起使用 —— 假设目标索引是根据相同的规则计算的。

使用我们的 “路由” 管道

现在我们有了一个能够根据文档的日期字段计算目标索引名称的通用管道,让我们看看如何让 Elasticsearch 使用它。

我们可以通过两种方式告诉 Elasticsearch 使用管道,让我们评估一下。

Index API 调用

第一个 —— 也是直接的解决方案——是使用 Index API 的管道参数。

换句话说:每次你想索引一个文档,你必须告诉 Elasticsearch 要使用的管道。


1.  POST data/_doc?pipeline=compute-index-name
2.  {
3.    "objectId": 1234,
4.    "manufacturer": "SHIELD",
5.    "payload": "some_data",
6.    "date": "2019-04-01T12:10:30.000Z"
7.  }

现在,每次我们通过指示 compute-index-name 管道将文档添加到索引中时,该文档将被添加到正确的基于时间的索引中。 在此示例中,目标索引将为 data-20190401 。

我们提供给 Index API 的 data 索引名称呢? 它可以被看作是一个索引:它只是用来执行 API 调用并且是真正目标索引的根,它不一定存在!

默认管道:引入 “虚拟索引”

索引默认管道(default pipeline)是使用管道的另一种有用方式:当你创建索引时,有一个名为 index.default_pipeline 的设置可以设置为管道的名称,只要你将文档添加到相应的索引就会执行该管道并且没有管道被添加到 API 调用中。 你还可以在索引文档时使用特殊管道名称 _none 来绕过此默认索引。 通过使用此功能,你可以定义我称之为 “虚拟索引” 的内容,并将其与默认管道相关联,该默认管道将充当我们上面看到的路由管道。

让我们将其应用到我们的示例中。

我们假设我们的通用路由管道 compute-index-name 已经创建。 我们现在可以创建一个名为 data 的索引,它将使用此管道作为其默认管道。


1.  PUT data
2.  {
3.    "settings" : {
4.      "index" : {
5.        "number_of_shards" : 3, 
6.        "number_of_replicas" : 1,
7.        "default_pipeline" : "compute-index-name"
8.      }
9.    }
10.  }

现在,每次我们要求 Elasticsearch 为数据索引中的文档编制索引时,计算索引名称管道将负责该文档的实际路由。 因此,数据索引中永远不会有单个文档被索引,但我们将调用管道的责任完全委托给 Elasticsearch。

运行完上面的命令后,我们来尝试写入一个文档:


1.  POST data/_doc
2.  {
3.    "objectId": 1234,
4.    "manufacturer": "SHIELD",
5.    "payload": "some_data",
6.    "date": "2019-04-01T12:10:30.000Z"
7.  }

上面的命令返回的结果是:


1.  {
2.    "_index": "data-20190401",
3.    "_id": "2DMGfIYBaog4blQ55Qr7",
4.    "_version": 1,
5.    "result": "created",
6.    "_shards": {
7.      "total": 2,
8.      "successful": 1,
9.      "failed": 0
10.    },
11.    "_seq_no": 1,
12.    "_primary_term": 1
13.  }

结论

我们刚刚在这里看到了如何利用 Elasticsearch 中的管道功能根据文档的固有属性来路由文档。Ingest pipeline 不仅仅可以替代 Logstash 过滤器:你可以定义复杂的管道,使用多个处理器(一个特定的处理器甚至允许你调用另一个管道)、条件等。有关 ingest pipeline 的更多文章,请参阅 “Elastic:开发者上手指南” 文章中的 “Ingest pipeline” 章节。

在我看来,本文末尾看到的 “虚拟索引” 非常有趣。 包含创建这样一个并非真正的索引的索引只是为了创建路由管道的入口点的功能甚至可以成为 Elasticsearch 的一个新的和有用的功能,为什么不呢?

收起阅读 »

Elasticsearch:如何在 Elasticsearch 中正确使用同义词功能

原文地址 elasticstack.blog.csdn.net

同义词用于提高搜索质量并扩大匹配范围。 例如,搜索 England 的用户可能希望找到包含 British 或 UK 的文档,尽管这三个词完全不同。

Elasticsearch 中的同义词功能非常强大,如果实施得当,可以使你的搜索引擎更加健壮和强大。 在本文中,我们将通过简单的代码片段介绍在实践中实现同义词功能的要点。 特别是,我们将介绍如何更新现有索引的同义词,这是一个相对高级的话题。

在今天的展示中,我将使用最新的 Elastic Stack 8.6.0,尽管版本不对我们的展示有任何的影响。

准备

我们将使用 Docker 在本地启动一个 Elasticsearch 服务器,并使用 Kibana 来管理索引和运行命令。 如果你以前从未使用过 Elasticsearch 或想快速复习一下,这篇文章可能会对你有所帮助。 如果你在 Docker 中运行 Elasticsearch 时遇到问题,这篇文章很可能会帮助你解决问题。在今天的文章中,我们尝试使用 docker 来部署一个没有安全功能的 Elasticsearch 集群。

准备就绪后,让我们开始探索 Elasticsearch 中的同义词功能的旅程。

我们将在本文中使用的 docker-compose.yaml 文件包含以下内容,稍后我们将向其添加更多功能:

docker-compose.yml

`

1.  version: "3.9"
2.  services:
3.    elasticsearch:
4.      image: elasticsearch:8.6.0
5.      environment:
6.        - discovery.type=single-node
7.        - ES_JAVA_OPTS=-Xms1g -Xmx1g
8.        - xpack.security.enabled=false
9.      volumes:
10.        - type: volume
11.          source: es_data
12.          target: /usr/share/elasticsearch/data
13.      ports:
14.        - target: 9200
15.          published: 9200
16.      networks:
17.        - elastic

19.    kibana:
20.      image: kibana:8.6.0
21.      ports:
22.        - target: 5601
23.          published: 5601
24.      depends_on:
25.        - elasticsearch
26.      networks:
27.        - elastic      

29.  volumes:
30.    es_data:
31.      driver: local

33.  networks:
34.    elastic:
35.      name: elastic
36.      driver: bridge

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

你可以使用以下命令之一启动 Elasticsearch 和 Kibana:

docker-compose up

或者

docker-compose up -d

如果加上 -d 选项的话,Elasticsearch 会以 daemon 的形式来运行。上面是一种最为简单的方式来启动 Elasticsearch 集群及 Kibana。由于它没有设置安全,我们无需输入任何凭证就可以直接进入到 Kibana 了。

使用带有同义词列表的标准同义词 token 过滤器

让我们首先使用带有同义词列表的标准同义词标记过滤器创建一个索引。 在 Kibana 中运行以下命令,我们将在稍后解释详细信息: 

`

1.  PUT synonyms
2.  {
3.    "settings": {
4.      "index": {
5.        "analysis": {
6.          "analyzer": {
7.            "index_analyzer": {
8.              "tokenizer": "standard",
9.              "filter": [
10.                "lowercase",
11.                "synonym_filter"
12.              ]
13.            }
14.          },
15.          "filter": {
16.            "synonym_filter": {
17.              "type": "synonym",
18.              "synonyms": [
19.                "elk => Elastic Stack",
20.                "elkb => Elastic Stack"
21.              ]
22.            }
23.          }
24.        }
25.      }
26.    },
27.    "mappings": {
28.      "properties": {
29.        "name": {
30.          "type": "text",
31.          "analyzer": "index_analyzer"
32.        }
33.      }
34.    }
35.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

这里的要点

  1. 请注意设置键的嵌套级别。 settings => index => analysis => analyzer/filter 都是内置关键字。 但是,index_analyzer 和 synonym_filter 分别是自定义分析器和过滤器的自定义名称。
  2. 我们需要创建一个 type 为 synonym 的自定义过滤器。 synonym 选项明确提供了同义词列表。 这通常应该只用于测试,因为更新同义词列表不方便,我们稍后会看到。
  3. 本文中使用了 Solr 同义词。 对于此示例,使用了显式映射,这意味着 => 左侧的标记将替换为右侧的标记。 稍后我们将使用等同的同义词,这意味着提供的 token 被等同对待。
  4. synonym_filter 添加到名为 index_analyzer 的新自定义分析器的过滤器列表中。 通常过滤器的顺序很重要。 然而,对于同义词过滤器来说,它有点特殊,可能会让我们中的许多人感到惊讶。 在此示例中,即使 synonym_filter 过滤器放在小写过滤器之后,此过滤器返回的标记也会传递给小写过滤器,因此也会变成小写。 因此,你不需要在同义词列表或同义词文件中提供小写 token。
  5. 最后,在文档的映射中,为名称字段指定了自定义分析器。

我们知道在早期的 Elastic 产品中 elk 就是 Elastic Stack 的代名词。之后随着 Beats 的加入,很多开发者也把 elkb 当做 Elastic Stack 的代名词。要测试在索引中创建的分析器,我们可以调用 _analyze 端点:


1.  GET /synonyms/_analyze
2.  {
3.    "analyzer": "index_analyzer",
4.    "text": "elk is powerful"
5.  }

上面命令的输出为:

`

1.  {
2.    "tokens": [
3.      {
4.        "token": "elastic",
5.        "start_offset": 0,
6.        "end_offset": 3,
7.        "type": "SYNONYM",
8.        "position": 0
9.      },
10.      {
11.        "token": "is",
12.        "start_offset": 4,
13.        "end_offset": 6,
14.        "type": "<ALPHANUM>",
15.        "position": 1
16.      },
17.      {
18.        "token": "stack",
19.        "start_offset": 4,
20.        "end_offset": 6,
21.        "type": "SYNONYM",
22.        "position": 1
23.      },
24.      {
25.        "token": "powerful",
26.        "start_offset": 7,
27.        "end_offset": 15,
28.        "type": "<ALPHANUM>",
29.        "position": 2
30.      }
31.    ]
32.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

从上面的输出中,我们可以看到 type 为 SNONYM 的 token 为 elastic 及 stack。让我们向索引中添加一些文档并测试它在搜索中是否正常工作:


1.  PUT /synonyms/_doc/1
2.  {
3.    "name": "elk is very powerful"
4.  }

6.  PUT /synonyms/_doc/2
7.  {
8.    "name": "elkb is useful"
9.  }

11.  PUT /synonyms/_doc/3
12.  {
13.    "name": "Elastic Stack is so widely used"
14.  }

我们可以使用 match 关键字进行简单的搜索:


1.  GET /synonyms/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "match": {
5.        "name": "elk"
6.      }
7.    }
8.  }

如果没有问题,所有三个文件都应该被搜索到:

`

1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "synonyms",
6.          "_id": "2",
7.          "_score": 0.31931418,
8.          "_source": {
9.            "name": "elkb is useful"
10.          }
11.        },
12.        {
13.          "_index": "synonyms",
14.          "_id": "1",
15.          "_score": 0.29086044,
16.          "_source": {
17.            "name": "elk is very powerful"
18.          }
19.        },
20.        {
21.          "_index": "synonyms",
22.          "_id": "3",
23.          "_score": 0.24686477,
24.          "_source": {
25.            "name": "Elastic Stack is so widely used"
26.          }
27.        }
28.      ]
29.    }
30.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

索引时间 vs 搜索时间进行同义词操作

如你所见,在上面的示例中,只创建了一个分析器,它用于索引和搜索。

不鼓励在索引(indexing)步骤中对所有文档应用同义词,因为它有一些主要缺点:

  • 如果不重新索引所有内容,就无法更新同义词列表,这在实践中是非常低效的。
  • 搜索分数会受到影响,因为同义词 token 也会被计算在内。
  • 索引过程变得更加耗时并且索引将变得更大。 对于小数据集来说可以忽略不计,但对于大数据集来说非常重要。

因此,最好在搜索步骤中只应用同义词,这样可以克服所有三个缺点。 为此,我们需要创建一个用于搜索的新分析器。

使用 search_analyzer 并应用搜索时间同义词

在 Kibana 中运行以下命令以创建具有搜索时同义词的新索引:

`

1.  PUT synonym_graph
2.  {
3.    "settings": {
4.      "index": {
5.        "analysis": {
6.          "analyzer": {
7.            "index_analyzer": {
8.              "tokenizer": "standard",
9.              "filter": [
10.                "lowercase"
11.              ]
12.            },
13.            "search_analyzer": {
14.              "tokenizer": "standard",
15.              "filter": [
16.                "lowercase",
17.                "synonym_filter"
18.              ]
19.            }
20.          },
21.          "filter": {
22.            "synonym_filter": {
23.              "type": "synonym_graph",
24.              "synonyms": [
25.                "elk => Elastic Stack",
26.                "elkb => Elastic Stack"
27.              ]
28.            }
29.          }
30.        }
31.      }
32.    },
33.    "mappings": {
34.      "properties": {
35.        "name": {
36.          "type": "text",
37.          "analyzer": "index_analyzer",
38.          "search_analyzer": "search_analyzer"
39.        }
40.      }
41.    }
42.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

关键点

  • 该类型现在更改为 synonym_graph,这是一个更复杂的同义词过滤器,旨在仅用作搜索分析器的一部分。 它可以更恰当地处理多词同义词,推荐用于搜索时分析。 但是,你可以继续使用原来的 synonym 类型,它在这篇文章中的表现是一样的。
  • 同义词过滤器从索引时间分析器中删除并添加到搜索时间分析器中。
  • search_analyzer 是为 name 字段明确指定的。 如果未指定,则相同的分析器 (index_analyzer) 将用于索引和搜索。

分析器应该返回与以前相同的 token。 然而,当你用这些命令为三个文档建立索引并再次执行相同的搜索后,结果会有所不同:


1.  PUT /synonym_graph/_doc/1
2.  {
3.    "name": "elk is very powerful"
4.  }

6.  PUT /synonym_graph/_doc/2
7.  {
8.    "name": "elkb is useful"
9.  }

11.  PUT /synonym_graph/_doc/3
12.  {
13.    "name": "Elastic Stack is so widely used"
14.  }

我们使用如下的命令来进行搜索:


1.  GET /synonym_graph/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "match": {
5.        "name": "elk"
6.      }
7.    }
8.  }

这一次,只有如下的结果返回。甚至 “elk is very powerful” 这个文档也没有被返回:


1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "synonym_graph",
6.          "_id": "3",
7.          "_score": 2.3589978,
8.          "_source": {
9.            "name": "Elastic Stack is so widely used"
10.          }
11.        }
12.      ]
13.    }
14.  }

原因是同义词过滤器仅在搜索时应用。 搜索查询 elk 被替换为同义词标记 “Elastic Stack”。 然而,索引中的文档没有被同义词过滤器(synonym_filter)过滤,因此 “elk” 只是被标记为 elk 而没有被 Elastic Stack 替换。 类似于 elkb。 结果,只能匹配 “Elastic Stack is so widely used”。

为了使其像前面的示例一样正常工作,我们需要将同义词规则从显式映射更改为等效同义词。 让我们按如下方式更新同义词过滤器:


1.  ......
2.    "filter": {
3.      "synonym_filter": {
4.        "type": "synonym_graph",
5.          "synonyms": [
6.             "elk, elkb, Elastic Stack"
7.          ]
8.      }
9.    }
10.  ......

要更改现有索引的同义词,我们可以重新创建索引并重新索引所有文档,这是愚蠢且低效的。

更好的方法是更新索引的设置。 但是,我们需要在更新设置之前关闭索引,然后重新打开它才能访问它:


1.  POST /synonym_graph/_close

4.  PUT /synonym_graph/_settings
5.  {
6.    "settings": {
7.      "index.analysis.filter.synonym_filter.synonyms": [
8.        "elk, elkb, Elastic Stack"
9.      ]
10.    }
11.  }

13.  POST /synonym_graph/_open

请注意更新索引设置的特殊语法。

运行上述命令后,我们可以通过如下命令的返回值来进行验证:

GET synonym_graph

上面的命令返回:

`

1.  {
2.    "synonym_graph": {
3.      "aliases": {},
4.      "mappings": {
5.        "properties": {
6.          "name": {
7.            "type": "text",
8.            "analyzer": "index_analyzer",
9.            "search_analyzer": "search_analyzer"
10.          }
11.        }
12.      },
13.      "settings": {
14.        "index": {
15.          "routing": {
16.            "allocation": {
17.              "include": {
18.                "_tier_preference": "data_content"
19.              }
20.            }
21.          },
22.          "number_of_shards": "1",
23.          "provided_name": "synonym_graph",
24.          "creation_date": "1673501061514",
25.          "analysis": {
26.            "filter": {
27.              "synonym_filter": {
28.                "type": "synonym_graph",
29.                "synonyms": [
30.                  "elk, elkb, Elastic Stack"
31.                ]
32.              }
33.            },
34.            "analyzer": {
35.              "index_analyzer": {
36.                "filter": [
37.                  "lowercase"
38.                ],
39.                "tokenizer": "standard"
40.              },
41.              "search_analyzer": {
42.                "filter": [
43.                  "lowercase",
44.                  "synonym_filter"
45.                ],
46.                "tokenizer": "standard"
47.              }
48.            }
49.          },
50.          "number_of_replicas": "1",
51.          "uuid": "UCIWtpQMTsCc1TwnvsywHA",
52.          "version": {
53.            "created": "8060099"
54.          }
55.        }
56.      }
57.    }
58.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

让我们使用 _analyzer 端点测试 search_analyzer 并查看生成的 token:


1.  GET /synonym_graph/_analyze
2.  {
3.    "analyzer": "search_analyzer",
4.    "text": "elk"
5.  }

上述命令返回:

它表明 elk 搜索查询被三个同义词的 token 替换和扩展(由 expand 选项控制)。 它还证明,如果在索引时应用等效同义词,则结果索引的大小可以显着增加。

然后当我们再次执行相同的搜索时:


1.  GET /synonym_graph/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "match": {
5.        "name": "elk"
6.      }
7.    }
8.  }

这次搜索的结果是:

`

1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "synonym_graph",
6.          "_id": "3",
7.          "_score": 1.6949677,
8.          "_source": {
9.            "name": "Elastic Stack is so widely used"
10.          }
11.        },
12.        {
13.          "_index": "synonym_graph",
14.          "_id": "2",
15.          "_score": 1.1220688,
16.          "_source": {
17.            "name": "elkb is useful"
18.          }
19.        },
20.        {
21.          "_index": "synonym_graph",
22.          "_id": "1",
23.          "_score": 1.0126972,
24.          "_source": {
25.            "name": "elk is very powerful"
26.          }
27.        }
28.      ]
29.    }
30.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

我们可以看到三个文档都被搜索出来了。

使用同义词文件

上面我们一直在创建索引时直接指定同义词列表。 但是,当你有大量同义词时,将它们全部添加到索引中会很麻烦。 更好的方法是将它们存储在一个文件中,然后动态地将它们加载到索引中。 使用同义词文件有很多好处,其中包括:

  • 方便维护大量的同义词。
  • 可以被不同的索引使用。
  • 可以在不关闭索引的情况下动态重新加载。

首先,我们需要先将同义词放入一个文件中。 每行都是一个同义词规则,与上面演示的相同。 更多细节可以在官方文档中找到。

我们将创建的同义词文件称为 synonyms.txt,但可以任意命名。 它具有以下内容:


1.  $ pwd
2.  /Users/liuxg/data/docker8
3.  $ ls
4.  docker-compose.yml synonyms.txt
5.  $ cat synonyms.txt 
6.  # This is a comment! The file is named synonyms.txt.
7.  elk,elkb,Elastic Stack

然后我们需要将同义词文件绑定到 Docker 容器中。 更新 docker-compose.yaml 如下:

docker-compose.yml

`

1.  version: "3.9"
2.  services:
3.    elasticsearch:
4.      image: elasticsearch:8.6.0
5.      environment:
6.        - discovery.type=single-node
7.        - ES_JAVA_OPTS=-Xms1g -Xmx1g
8.        - xpack.security.enabled=false
9.      volumes:
10.        - type: volume
11.          source: es_data
12.          target: /usr/share/elasticsearch/data
13.        - type: bind
14.          source: ./synonyms.txt
15.          target: /usr/share/elasticsearch/config/synonyms.txt      
16.      ports:
17.        - target: 9200
18.          published: 9200
19.      networks:
20.        - elastic

22.    kibana:
23.      image: kibana:8.6.0
24.      ports:
25.        - target: 5601
26.          published: 5601
27.      depends_on:
28.        - elasticsearch
29.      networks:
30.        - elastic      

32.  volumes:
33.    es_data:
34.      driver: local

36.  networks:
37.    elastic:
38.      name: elastic
39.      driver: bridge

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

我们可以使用 CTRL+C 来终止之前运行的 docker,然后再次使用如下命令来启动:

docker-compose up

请注意,同义词文件已加载到容器中的 config 文件夹中。你可以进入容器并使用以下两个命令之一检查它:


1.  # User docker
2.  docker exec -it elasticsearch-1  bash

4.  # User docker-compose
5.  docker-compose exec elasticsearch bash

现在我们需要停止并重新启动服务以使更改生效。 请注意,仅重新启动服务将不起作用。


1.  docker-compose stop elasticsearch
2.  docker-compose up -d elasticsearch

1.  $ docker ps
2.  CONTAINER ID   IMAGE                 COMMAND                  CREATED          STATUS          PORTS                              NAMES
3.  3ae4b728dd44   kibana:8.6.0          "/bin/tini -- /usr/l…"   23 seconds ago   Up 21 seconds   0.0.0.0:5601->5601/tcp             docker8-kibana-1
4.  878c82384761   elasticsearch:8.6.0   "/bin/tini -- /usr/l…"   23 seconds ago   Up 22 seconds   0.0.0.0:9200->9200/tcp, 9300/tcp   docker8-elasticsearch-1
5.  $ docker exec -it docker8-elasticsearch-1 bash
6.  elasticsearch@878c82384761:~$ pwd
7.  /usr/share/elasticsearch
8.  elasticsearch@878c82384761:~$ ls
9.  LICENSE.txt  NOTICE.txt  README.asciidoc  bin  config  data  jdk  lib  logs  modules  plugins
10.  elasticsearch@878c82384761:~$ cd config/
11.  elasticsearch@878c82384761:~/config$ ls
12.  elasticsearch-plugins.example.yml  jvm.options             log4j2.properties  synonyms.txt
13.  elasticsearch.keystore             jvm.options.d           role_mapping.yml   users
14.  elasticsearch.yml                  log4j2.file.properties  roles.yml          users_roles

从上面的输出中,我们可以看到 synonyms.txt 已经被成功地加载到容器里了。

然后我们可以使用同义词文件创建一个新索引:

`

1.  PUT /synonym_graph_file
2.  {
3.    "settings": {
4.      "index": {
5.        "analysis": {
6.          "analyzer": {
7.            "index_analyzer": {
8.              "tokenizer": "standard",
9.              "filter": [
10.                "lowercase"
11.              ]
12.            },
13.            "search_analyzer": {
14.              "tokenizer": "standard",
15.              "filter": [
16.                "lowercase",
17.                "synonym_filter"
18.              ]
19.            }
20.          },
21.          "filter": {
22.            "synonym_filter": {
23.              "type": "synonym_graph",
24.              "synonyms_path": "synonyms.txt",
25.              "updateable": true
26.            }
27.          }
28.        }
29.      }
30.    },
31.    "mappings": {
32.      "properties": {
33.        "name": {
34.          "type": "text",
35.          "analyzer": "index_analyzer",
36.          "search_analyzer": "search_analyzer"
37.        }
38.      }
39.    }
40.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

关键点

  • 对于 synonyms_path,它是同义词文件相对于 Elasticsearch 服务器中 config 文件夹的路径。
  • 添加了一个新的 updateable 字段,它指定相应的过滤器是否可更新。 我们很快就会看到如何在不关闭和打开索引的情况下重新加载搜索分析器。

这个新索引 synonym_graph_file 的行为应该与前一个 synonym_graph 的行为相同。

现在让我们在同义词文件中添加更多的同义词,其内容如下:


1.  $ pwd
2.  /Users/liuxg/data/docker8
3.  $ ls
4.  docker-compose.yml synonyms.txt
5.  $ cat synonyms.txt 
6.  # This is a comment! The file is named synonyms.txt.
7.  elk,elkb,Elastic Stack
8.  JS => JavaScript
9.  TS => TypeScript
10.  Py => Python

添加同义词后,我们可以关闭并打开索引使其生效。 然而,由于我们将同义词过滤器标记为可更新,我们可以重新加载搜索分析器以使更改立即生效,而无需关闭索引,因此无需停机。

要重新加载索引的搜索分析器,我们需要调用 _reload_search_analyzers 端点:

POST /synonym_graph_file/_reload_search_analyzers

上面的命令输出为:

`

1.  {
2.    "_shards": {
3.      "total": 2,
4.      "successful": 1,
5.      "failed": 0
6.    },
7.    "reload_details": [
8.      {
9.        "index": "synonym_graph_file",
10.        "reloaded_analyzers": [
11.          "search_analyzer"
12.        ],
13.        "reloaded_node_ids": [
14.          "tZLy82KRTaiCdpsbkEYnuA"
15.        ]
16.      }
17.    ]
18.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

现在,当我们分析 JS 字符串时,我们将看到返回的 javascript token。


1.  GET /synonym_graph_file/_analyze
2.  {
3.    "analyzer": "search_analyzer",
4.    "text": "JS"
5.  }

上面的命令返回:


1.  {
2.    "tokens": [
3.      {
4.        "token": "javascript",
5.        "start_offset": 0,
6.        "end_offset": 2,
7.        "type": "SYNONYM",
8.        "position": 0
9.      }
10.    ]
11.  }

这里应该注意两件重要的事情:

  • 如果同义词过滤器的 updateable 设置为true,那么对应的分析器只能作为 search_analyzer 使用,不能用于索引,即使类型是同义词。
  • updateable 选项只能在同义词文件与 synonyms_path 选项一起使用时使用,而不是在同义词直接通过 synonyms 选项提供时使用。

恭喜你到达这里! 我们已经涵盖了在 Elasticsearch 中使用同义词功能的所有要点。

我们已经分别介绍了如何在索引时间和搜索时间分析步骤中使用同义词。 此外,还介绍了如何直接提供同义词列表,以及如何通过文件提供。 最后但同样重要的是,介绍了关于如何更新现有索引的同义词列表的不同方法。 建议重新加载索引的搜索分析器,因为它不会给服务带来停机时间。

继续阅读 »

原文地址 elasticstack.blog.csdn.net

同义词用于提高搜索质量并扩大匹配范围。 例如,搜索 England 的用户可能希望找到包含 British 或 UK 的文档,尽管这三个词完全不同。

Elasticsearch 中的同义词功能非常强大,如果实施得当,可以使你的搜索引擎更加健壮和强大。 在本文中,我们将通过简单的代码片段介绍在实践中实现同义词功能的要点。 特别是,我们将介绍如何更新现有索引的同义词,这是一个相对高级的话题。

在今天的展示中,我将使用最新的 Elastic Stack 8.6.0,尽管版本不对我们的展示有任何的影响。

准备

我们将使用 Docker 在本地启动一个 Elasticsearch 服务器,并使用 Kibana 来管理索引和运行命令。 如果你以前从未使用过 Elasticsearch 或想快速复习一下,这篇文章可能会对你有所帮助。 如果你在 Docker 中运行 Elasticsearch 时遇到问题,这篇文章很可能会帮助你解决问题。在今天的文章中,我们尝试使用 docker 来部署一个没有安全功能的 Elasticsearch 集群。

准备就绪后,让我们开始探索 Elasticsearch 中的同义词功能的旅程。

我们将在本文中使用的 docker-compose.yaml 文件包含以下内容,稍后我们将向其添加更多功能:

docker-compose.yml

`

1.  version: "3.9"
2.  services:
3.    elasticsearch:
4.      image: elasticsearch:8.6.0
5.      environment:
6.        - discovery.type=single-node
7.        - ES_JAVA_OPTS=-Xms1g -Xmx1g
8.        - xpack.security.enabled=false
9.      volumes:
10.        - type: volume
11.          source: es_data
12.          target: /usr/share/elasticsearch/data
13.      ports:
14.        - target: 9200
15.          published: 9200
16.      networks:
17.        - elastic

19.    kibana:
20.      image: kibana:8.6.0
21.      ports:
22.        - target: 5601
23.          published: 5601
24.      depends_on:
25.        - elasticsearch
26.      networks:
27.        - elastic      

29.  volumes:
30.    es_data:
31.      driver: local

33.  networks:
34.    elastic:
35.      name: elastic
36.      driver: bridge

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

你可以使用以下命令之一启动 Elasticsearch 和 Kibana:

docker-compose up

或者

docker-compose up -d

如果加上 -d 选项的话,Elasticsearch 会以 daemon 的形式来运行。上面是一种最为简单的方式来启动 Elasticsearch 集群及 Kibana。由于它没有设置安全,我们无需输入任何凭证就可以直接进入到 Kibana 了。

使用带有同义词列表的标准同义词 token 过滤器

让我们首先使用带有同义词列表的标准同义词标记过滤器创建一个索引。 在 Kibana 中运行以下命令,我们将在稍后解释详细信息: 

`

1.  PUT synonyms
2.  {
3.    "settings": {
4.      "index": {
5.        "analysis": {
6.          "analyzer": {
7.            "index_analyzer": {
8.              "tokenizer": "standard",
9.              "filter": [
10.                "lowercase",
11.                "synonym_filter"
12.              ]
13.            }
14.          },
15.          "filter": {
16.            "synonym_filter": {
17.              "type": "synonym",
18.              "synonyms": [
19.                "elk => Elastic Stack",
20.                "elkb => Elastic Stack"
21.              ]
22.            }
23.          }
24.        }
25.      }
26.    },
27.    "mappings": {
28.      "properties": {
29.        "name": {
30.          "type": "text",
31.          "analyzer": "index_analyzer"
32.        }
33.      }
34.    }
35.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

这里的要点

  1. 请注意设置键的嵌套级别。 settings => index => analysis => analyzer/filter 都是内置关键字。 但是,index_analyzer 和 synonym_filter 分别是自定义分析器和过滤器的自定义名称。
  2. 我们需要创建一个 type 为 synonym 的自定义过滤器。 synonym 选项明确提供了同义词列表。 这通常应该只用于测试,因为更新同义词列表不方便,我们稍后会看到。
  3. 本文中使用了 Solr 同义词。 对于此示例,使用了显式映射,这意味着 => 左侧的标记将替换为右侧的标记。 稍后我们将使用等同的同义词,这意味着提供的 token 被等同对待。
  4. synonym_filter 添加到名为 index_analyzer 的新自定义分析器的过滤器列表中。 通常过滤器的顺序很重要。 然而,对于同义词过滤器来说,它有点特殊,可能会让我们中的许多人感到惊讶。 在此示例中,即使 synonym_filter 过滤器放在小写过滤器之后,此过滤器返回的标记也会传递给小写过滤器,因此也会变成小写。 因此,你不需要在同义词列表或同义词文件中提供小写 token。
  5. 最后,在文档的映射中,为名称字段指定了自定义分析器。

我们知道在早期的 Elastic 产品中 elk 就是 Elastic Stack 的代名词。之后随着 Beats 的加入,很多开发者也把 elkb 当做 Elastic Stack 的代名词。要测试在索引中创建的分析器,我们可以调用 _analyze 端点:


1.  GET /synonyms/_analyze
2.  {
3.    "analyzer": "index_analyzer",
4.    "text": "elk is powerful"
5.  }

上面命令的输出为:

`

1.  {
2.    "tokens": [
3.      {
4.        "token": "elastic",
5.        "start_offset": 0,
6.        "end_offset": 3,
7.        "type": "SYNONYM",
8.        "position": 0
9.      },
10.      {
11.        "token": "is",
12.        "start_offset": 4,
13.        "end_offset": 6,
14.        "type": "<ALPHANUM>",
15.        "position": 1
16.      },
17.      {
18.        "token": "stack",
19.        "start_offset": 4,
20.        "end_offset": 6,
21.        "type": "SYNONYM",
22.        "position": 1
23.      },
24.      {
25.        "token": "powerful",
26.        "start_offset": 7,
27.        "end_offset": 15,
28.        "type": "<ALPHANUM>",
29.        "position": 2
30.      }
31.    ]
32.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

从上面的输出中,我们可以看到 type 为 SNONYM 的 token 为 elastic 及 stack。让我们向索引中添加一些文档并测试它在搜索中是否正常工作:


1.  PUT /synonyms/_doc/1
2.  {
3.    "name": "elk is very powerful"
4.  }

6.  PUT /synonyms/_doc/2
7.  {
8.    "name": "elkb is useful"
9.  }

11.  PUT /synonyms/_doc/3
12.  {
13.    "name": "Elastic Stack is so widely used"
14.  }

我们可以使用 match 关键字进行简单的搜索:


1.  GET /synonyms/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "match": {
5.        "name": "elk"
6.      }
7.    }
8.  }

如果没有问题,所有三个文件都应该被搜索到:

`

1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "synonyms",
6.          "_id": "2",
7.          "_score": 0.31931418,
8.          "_source": {
9.            "name": "elkb is useful"
10.          }
11.        },
12.        {
13.          "_index": "synonyms",
14.          "_id": "1",
15.          "_score": 0.29086044,
16.          "_source": {
17.            "name": "elk is very powerful"
18.          }
19.        },
20.        {
21.          "_index": "synonyms",
22.          "_id": "3",
23.          "_score": 0.24686477,
24.          "_source": {
25.            "name": "Elastic Stack is so widely used"
26.          }
27.        }
28.      ]
29.    }
30.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

索引时间 vs 搜索时间进行同义词操作

如你所见,在上面的示例中,只创建了一个分析器,它用于索引和搜索。

不鼓励在索引(indexing)步骤中对所有文档应用同义词,因为它有一些主要缺点:

  • 如果不重新索引所有内容,就无法更新同义词列表,这在实践中是非常低效的。
  • 搜索分数会受到影响,因为同义词 token 也会被计算在内。
  • 索引过程变得更加耗时并且索引将变得更大。 对于小数据集来说可以忽略不计,但对于大数据集来说非常重要。

因此,最好在搜索步骤中只应用同义词,这样可以克服所有三个缺点。 为此,我们需要创建一个用于搜索的新分析器。

使用 search_analyzer 并应用搜索时间同义词

在 Kibana 中运行以下命令以创建具有搜索时同义词的新索引:

`

1.  PUT synonym_graph
2.  {
3.    "settings": {
4.      "index": {
5.        "analysis": {
6.          "analyzer": {
7.            "index_analyzer": {
8.              "tokenizer": "standard",
9.              "filter": [
10.                "lowercase"
11.              ]
12.            },
13.            "search_analyzer": {
14.              "tokenizer": "standard",
15.              "filter": [
16.                "lowercase",
17.                "synonym_filter"
18.              ]
19.            }
20.          },
21.          "filter": {
22.            "synonym_filter": {
23.              "type": "synonym_graph",
24.              "synonyms": [
25.                "elk => Elastic Stack",
26.                "elkb => Elastic Stack"
27.              ]
28.            }
29.          }
30.        }
31.      }
32.    },
33.    "mappings": {
34.      "properties": {
35.        "name": {
36.          "type": "text",
37.          "analyzer": "index_analyzer",
38.          "search_analyzer": "search_analyzer"
39.        }
40.      }
41.    }
42.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

关键点

  • 该类型现在更改为 synonym_graph,这是一个更复杂的同义词过滤器,旨在仅用作搜索分析器的一部分。 它可以更恰当地处理多词同义词,推荐用于搜索时分析。 但是,你可以继续使用原来的 synonym 类型,它在这篇文章中的表现是一样的。
  • 同义词过滤器从索引时间分析器中删除并添加到搜索时间分析器中。
  • search_analyzer 是为 name 字段明确指定的。 如果未指定,则相同的分析器 (index_analyzer) 将用于索引和搜索。

分析器应该返回与以前相同的 token。 然而,当你用这些命令为三个文档建立索引并再次执行相同的搜索后,结果会有所不同:


1.  PUT /synonym_graph/_doc/1
2.  {
3.    "name": "elk is very powerful"
4.  }

6.  PUT /synonym_graph/_doc/2
7.  {
8.    "name": "elkb is useful"
9.  }

11.  PUT /synonym_graph/_doc/3
12.  {
13.    "name": "Elastic Stack is so widely used"
14.  }

我们使用如下的命令来进行搜索:


1.  GET /synonym_graph/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "match": {
5.        "name": "elk"
6.      }
7.    }
8.  }

这一次,只有如下的结果返回。甚至 “elk is very powerful” 这个文档也没有被返回:


1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "synonym_graph",
6.          "_id": "3",
7.          "_score": 2.3589978,
8.          "_source": {
9.            "name": "Elastic Stack is so widely used"
10.          }
11.        }
12.      ]
13.    }
14.  }

原因是同义词过滤器仅在搜索时应用。 搜索查询 elk 被替换为同义词标记 “Elastic Stack”。 然而,索引中的文档没有被同义词过滤器(synonym_filter)过滤,因此 “elk” 只是被标记为 elk 而没有被 Elastic Stack 替换。 类似于 elkb。 结果,只能匹配 “Elastic Stack is so widely used”。

为了使其像前面的示例一样正常工作,我们需要将同义词规则从显式映射更改为等效同义词。 让我们按如下方式更新同义词过滤器:


1.  ......
2.    "filter": {
3.      "synonym_filter": {
4.        "type": "synonym_graph",
5.          "synonyms": [
6.             "elk, elkb, Elastic Stack"
7.          ]
8.      }
9.    }
10.  ......

要更改现有索引的同义词,我们可以重新创建索引并重新索引所有文档,这是愚蠢且低效的。

更好的方法是更新索引的设置。 但是,我们需要在更新设置之前关闭索引,然后重新打开它才能访问它:


1.  POST /synonym_graph/_close

4.  PUT /synonym_graph/_settings
5.  {
6.    "settings": {
7.      "index.analysis.filter.synonym_filter.synonyms": [
8.        "elk, elkb, Elastic Stack"
9.      ]
10.    }
11.  }

13.  POST /synonym_graph/_open

请注意更新索引设置的特殊语法。

运行上述命令后,我们可以通过如下命令的返回值来进行验证:

GET synonym_graph

上面的命令返回:

`

1.  {
2.    "synonym_graph": {
3.      "aliases": {},
4.      "mappings": {
5.        "properties": {
6.          "name": {
7.            "type": "text",
8.            "analyzer": "index_analyzer",
9.            "search_analyzer": "search_analyzer"
10.          }
11.        }
12.      },
13.      "settings": {
14.        "index": {
15.          "routing": {
16.            "allocation": {
17.              "include": {
18.                "_tier_preference": "data_content"
19.              }
20.            }
21.          },
22.          "number_of_shards": "1",
23.          "provided_name": "synonym_graph",
24.          "creation_date": "1673501061514",
25.          "analysis": {
26.            "filter": {
27.              "synonym_filter": {
28.                "type": "synonym_graph",
29.                "synonyms": [
30.                  "elk, elkb, Elastic Stack"
31.                ]
32.              }
33.            },
34.            "analyzer": {
35.              "index_analyzer": {
36.                "filter": [
37.                  "lowercase"
38.                ],
39.                "tokenizer": "standard"
40.              },
41.              "search_analyzer": {
42.                "filter": [
43.                  "lowercase",
44.                  "synonym_filter"
45.                ],
46.                "tokenizer": "standard"
47.              }
48.            }
49.          },
50.          "number_of_replicas": "1",
51.          "uuid": "UCIWtpQMTsCc1TwnvsywHA",
52.          "version": {
53.            "created": "8060099"
54.          }
55.        }
56.      }
57.    }
58.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

让我们使用 _analyzer 端点测试 search_analyzer 并查看生成的 token:


1.  GET /synonym_graph/_analyze
2.  {
3.    "analyzer": "search_analyzer",
4.    "text": "elk"
5.  }

上述命令返回:

它表明 elk 搜索查询被三个同义词的 token 替换和扩展(由 expand 选项控制)。 它还证明,如果在索引时应用等效同义词,则结果索引的大小可以显着增加。

然后当我们再次执行相同的搜索时:


1.  GET /synonym_graph/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "match": {
5.        "name": "elk"
6.      }
7.    }
8.  }

这次搜索的结果是:

`

1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "synonym_graph",
6.          "_id": "3",
7.          "_score": 1.6949677,
8.          "_source": {
9.            "name": "Elastic Stack is so widely used"
10.          }
11.        },
12.        {
13.          "_index": "synonym_graph",
14.          "_id": "2",
15.          "_score": 1.1220688,
16.          "_source": {
17.            "name": "elkb is useful"
18.          }
19.        },
20.        {
21.          "_index": "synonym_graph",
22.          "_id": "1",
23.          "_score": 1.0126972,
24.          "_source": {
25.            "name": "elk is very powerful"
26.          }
27.        }
28.      ]
29.    }
30.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

我们可以看到三个文档都被搜索出来了。

使用同义词文件

上面我们一直在创建索引时直接指定同义词列表。 但是,当你有大量同义词时,将它们全部添加到索引中会很麻烦。 更好的方法是将它们存储在一个文件中,然后动态地将它们加载到索引中。 使用同义词文件有很多好处,其中包括:

  • 方便维护大量的同义词。
  • 可以被不同的索引使用。
  • 可以在不关闭索引的情况下动态重新加载。

首先,我们需要先将同义词放入一个文件中。 每行都是一个同义词规则,与上面演示的相同。 更多细节可以在官方文档中找到。

我们将创建的同义词文件称为 synonyms.txt,但可以任意命名。 它具有以下内容:


1.  $ pwd
2.  /Users/liuxg/data/docker8
3.  $ ls
4.  docker-compose.yml synonyms.txt
5.  $ cat synonyms.txt 
6.  # This is a comment! The file is named synonyms.txt.
7.  elk,elkb,Elastic Stack

然后我们需要将同义词文件绑定到 Docker 容器中。 更新 docker-compose.yaml 如下:

docker-compose.yml

`

1.  version: "3.9"
2.  services:
3.    elasticsearch:
4.      image: elasticsearch:8.6.0
5.      environment:
6.        - discovery.type=single-node
7.        - ES_JAVA_OPTS=-Xms1g -Xmx1g
8.        - xpack.security.enabled=false
9.      volumes:
10.        - type: volume
11.          source: es_data
12.          target: /usr/share/elasticsearch/data
13.        - type: bind
14.          source: ./synonyms.txt
15.          target: /usr/share/elasticsearch/config/synonyms.txt      
16.      ports:
17.        - target: 9200
18.          published: 9200
19.      networks:
20.        - elastic

22.    kibana:
23.      image: kibana:8.6.0
24.      ports:
25.        - target: 5601
26.          published: 5601
27.      depends_on:
28.        - elasticsearch
29.      networks:
30.        - elastic      

32.  volumes:
33.    es_data:
34.      driver: local

36.  networks:
37.    elastic:
38.      name: elastic
39.      driver: bridge

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

我们可以使用 CTRL+C 来终止之前运行的 docker,然后再次使用如下命令来启动:

docker-compose up

请注意,同义词文件已加载到容器中的 config 文件夹中。你可以进入容器并使用以下两个命令之一检查它:


1.  # User docker
2.  docker exec -it elasticsearch-1  bash

4.  # User docker-compose
5.  docker-compose exec elasticsearch bash

现在我们需要停止并重新启动服务以使更改生效。 请注意,仅重新启动服务将不起作用。


1.  docker-compose stop elasticsearch
2.  docker-compose up -d elasticsearch

1.  $ docker ps
2.  CONTAINER ID   IMAGE                 COMMAND                  CREATED          STATUS          PORTS                              NAMES
3.  3ae4b728dd44   kibana:8.6.0          "/bin/tini -- /usr/l…"   23 seconds ago   Up 21 seconds   0.0.0.0:5601->5601/tcp             docker8-kibana-1
4.  878c82384761   elasticsearch:8.6.0   "/bin/tini -- /usr/l…"   23 seconds ago   Up 22 seconds   0.0.0.0:9200->9200/tcp, 9300/tcp   docker8-elasticsearch-1
5.  $ docker exec -it docker8-elasticsearch-1 bash
6.  elasticsearch@878c82384761:~$ pwd
7.  /usr/share/elasticsearch
8.  elasticsearch@878c82384761:~$ ls
9.  LICENSE.txt  NOTICE.txt  README.asciidoc  bin  config  data  jdk  lib  logs  modules  plugins
10.  elasticsearch@878c82384761:~$ cd config/
11.  elasticsearch@878c82384761:~/config$ ls
12.  elasticsearch-plugins.example.yml  jvm.options             log4j2.properties  synonyms.txt
13.  elasticsearch.keystore             jvm.options.d           role_mapping.yml   users
14.  elasticsearch.yml                  log4j2.file.properties  roles.yml          users_roles

从上面的输出中,我们可以看到 synonyms.txt 已经被成功地加载到容器里了。

然后我们可以使用同义词文件创建一个新索引:

`

1.  PUT /synonym_graph_file
2.  {
3.    "settings": {
4.      "index": {
5.        "analysis": {
6.          "analyzer": {
7.            "index_analyzer": {
8.              "tokenizer": "standard",
9.              "filter": [
10.                "lowercase"
11.              ]
12.            },
13.            "search_analyzer": {
14.              "tokenizer": "standard",
15.              "filter": [
16.                "lowercase",
17.                "synonym_filter"
18.              ]
19.            }
20.          },
21.          "filter": {
22.            "synonym_filter": {
23.              "type": "synonym_graph",
24.              "synonyms_path": "synonyms.txt",
25.              "updateable": true
26.            }
27.          }
28.        }
29.      }
30.    },
31.    "mappings": {
32.      "properties": {
33.        "name": {
34.          "type": "text",
35.          "analyzer": "index_analyzer",
36.          "search_analyzer": "search_analyzer"
37.        }
38.      }
39.    }
40.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

关键点

  • 对于 synonyms_path,它是同义词文件相对于 Elasticsearch 服务器中 config 文件夹的路径。
  • 添加了一个新的 updateable 字段,它指定相应的过滤器是否可更新。 我们很快就会看到如何在不关闭和打开索引的情况下重新加载搜索分析器。

这个新索引 synonym_graph_file 的行为应该与前一个 synonym_graph 的行为相同。

现在让我们在同义词文件中添加更多的同义词,其内容如下:


1.  $ pwd
2.  /Users/liuxg/data/docker8
3.  $ ls
4.  docker-compose.yml synonyms.txt
5.  $ cat synonyms.txt 
6.  # This is a comment! The file is named synonyms.txt.
7.  elk,elkb,Elastic Stack
8.  JS => JavaScript
9.  TS => TypeScript
10.  Py => Python

添加同义词后,我们可以关闭并打开索引使其生效。 然而,由于我们将同义词过滤器标记为可更新,我们可以重新加载搜索分析器以使更改立即生效,而无需关闭索引,因此无需停机。

要重新加载索引的搜索分析器,我们需要调用 _reload_search_analyzers 端点:

POST /synonym_graph_file/_reload_search_analyzers

上面的命令输出为:

`

1.  {
2.    "_shards": {
3.      "total": 2,
4.      "successful": 1,
5.      "failed": 0
6.    },
7.    "reload_details": [
8.      {
9.        "index": "synonym_graph_file",
10.        "reloaded_analyzers": [
11.          "search_analyzer"
12.        ],
13.        "reloaded_node_ids": [
14.          "tZLy82KRTaiCdpsbkEYnuA"
15.        ]
16.      }
17.    ]
18.  }

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

现在,当我们分析 JS 字符串时,我们将看到返回的 javascript token。


1.  GET /synonym_graph_file/_analyze
2.  {
3.    "analyzer": "search_analyzer",
4.    "text": "JS"
5.  }

上面的命令返回:


1.  {
2.    "tokens": [
3.      {
4.        "token": "javascript",
5.        "start_offset": 0,
6.        "end_offset": 2,
7.        "type": "SYNONYM",
8.        "position": 0
9.      }
10.    ]
11.  }

这里应该注意两件重要的事情:

  • 如果同义词过滤器的 updateable 设置为true,那么对应的分析器只能作为 search_analyzer 使用,不能用于索引,即使类型是同义词。
  • updateable 选项只能在同义词文件与 synonyms_path 选项一起使用时使用,而不是在同义词直接通过 synonyms 选项提供时使用。

恭喜你到达这里! 我们已经涵盖了在 Elasticsearch 中使用同义词功能的所有要点。

我们已经分别介绍了如何在索引时间和搜索时间分析步骤中使用同义词。 此外,还介绍了如何直接提供同义词列表,以及如何通过文件提供。 最后但同样重要的是,介绍了关于如何更新现有索引的同义词列表的不同方法。 建议重新加载索引的搜索分析器,因为它不会给服务带来停机时间。

收起阅读 »

回顾 2022 年 — 回顾 Elastic 这一年

作者:Elastic Marketing

2022 年对 Elastic 来说是非凡的一年,我们在可观察性、安全性和企业搜索解决方案、新客户和深化合作伙伴关系方面引入了数十项创新。

在我们花点时间回顾过去的一年时,我们汇总了 2022 年的热门博文。希望你喜欢!

Elastic 8.0

在年初之际,我们还推出了 Elastic 8.0 的新篇章:速度、规模、相关性和简单性的新时代。 Elastic 8.0 是 7.x 系列多年投资的结晶,旨在减少内存使用和查询开销,并引入新功能以增强相关性。

例如,我们提高了日期直方图和搜索聚合的速度,增强了页面缓存的性能,并创建了一个新的 “预过滤” 搜索阶段。 此外,我们通过减少内存堆减少了资源需求以降低客户的总拥有成本,引入了使用更少存储的新方法,并使我们的客户能够通过新的冻结层和可搜索快照轻松地将计算与存储分离。

在 8.0 中,我们还为 Elasticsearch 带来了一整套原生矢量搜索功能,使客户和员工能够使用他们自己的词汇和语言搜索和接收高度相关的结果。

Machine Learning Relevance Tuning

作为 7.x 发布版中两年多工作的结晶,使向量搜索的实现更加实用,我们还引入了对近似最近邻搜索的原生支持 — 使得可以将基于向量的查询作用于基于向量词库进行比较快速、大规模地比较。 查看我们的自然语言处理 (NLP) 博客系列,获取部署矢量搜索的快速入门指南。

Elastic Security

随着 Elastic Security for Cloud 的推出,这也是 Elastic Security 具有里程碑意义的一年。 Elastic Security for Cloud 扩展了我们的 SIEM、安全分析和端点安全功能,在单个统一平台中提供了风险和状态管理、威胁监控和工作负载保护的新功能。

我们通过再次加倍承诺公开和透明的安全来结束这一年。 我们对开放式安全的承诺在 Forrester Wave™:安全分析平台,2022 年第 4 季度中得到进一步认可,其中 Elastic 被评为领导者,并指出“Elastic 在开放式产品中提供了令人难以置信的灵活性和可视化。”
 
更多阅读:https://elasticstack.blog.csdn ... 94458
继续阅读 »
作者:Elastic Marketing

2022 年对 Elastic 来说是非凡的一年,我们在可观察性、安全性和企业搜索解决方案、新客户和深化合作伙伴关系方面引入了数十项创新。

在我们花点时间回顾过去的一年时,我们汇总了 2022 年的热门博文。希望你喜欢!

Elastic 8.0

在年初之际,我们还推出了 Elastic 8.0 的新篇章:速度、规模、相关性和简单性的新时代。 Elastic 8.0 是 7.x 系列多年投资的结晶,旨在减少内存使用和查询开销,并引入新功能以增强相关性。

例如,我们提高了日期直方图和搜索聚合的速度,增强了页面缓存的性能,并创建了一个新的 “预过滤” 搜索阶段。 此外,我们通过减少内存堆减少了资源需求以降低客户的总拥有成本,引入了使用更少存储的新方法,并使我们的客户能够通过新的冻结层和可搜索快照轻松地将计算与存储分离。

在 8.0 中,我们还为 Elasticsearch 带来了一整套原生矢量搜索功能,使客户和员工能够使用他们自己的词汇和语言搜索和接收高度相关的结果。

Machine Learning Relevance Tuning

作为 7.x 发布版中两年多工作的结晶,使向量搜索的实现更加实用,我们还引入了对近似最近邻搜索的原生支持 — 使得可以将基于向量的查询作用于基于向量词库进行比较快速、大规模地比较。 查看我们的自然语言处理 (NLP) 博客系列,获取部署矢量搜索的快速入门指南。

Elastic Security

随着 Elastic Security for Cloud 的推出,这也是 Elastic Security 具有里程碑意义的一年。 Elastic Security for Cloud 扩展了我们的 SIEM、安全分析和端点安全功能,在单个统一平台中提供了风险和状态管理、威胁监控和工作负载保护的新功能。

我们通过再次加倍承诺公开和透明的安全来结束这一年。 我们对开放式安全的承诺在 Forrester Wave™:安全分析平台,2022 年第 4 季度中得到进一步认可,其中 Elastic 被评为领导者,并指出“Elastic 在开放式产品中提供了令人难以置信的灵活性和可视化。”
 
更多阅读:https://elasticstack.blog.csdn ... 94458 收起阅读 »

Elasticsearch:使用 Node.js 将实时数据提取到 Elasticsearch 中(一)

Elasticsearch 是一个强大的 RESTful 搜索和分析引擎,能够处理越来越多的用例。 它将集中存储你的数据,以实现闪电般的快速搜索、微调相关性以及可轻松扩展的强大分析。 关于如何使用 Elastic Stack(又名 ELK 堆栈)将数据摄取到 Elasticsearch 的资源有很多。在今天的文章中,我将详细介绍如何使用 Node.js 从零开始来把地震的实时数据采集到 Elasticsearch 中。

如果你选择的编程语言是 JavaScript,并且你需要使用 RESTful API 方法从第三方应用程序获取数据,那么使用 Node.js 获取数据是一个不错的选择。 你还可以托管服务器,让它持续实时摄取数据。 该演示将向您展示如何设置一个 Node.js + Express.js 服务器,该服务器实时将数据提取到 Elasticsearch 中,然后可以对这些数据进行分析并以有意义的方式采取行动。

对于此演示,我们将使用 USGS 实时发布的公开可用的全球地震数据。
更多阅读 https://elasticstack.blog.csdn ... 05743
继续阅读 »
Elasticsearch 是一个强大的 RESTful 搜索和分析引擎,能够处理越来越多的用例。 它将集中存储你的数据,以实现闪电般的快速搜索、微调相关性以及可轻松扩展的强大分析。 关于如何使用 Elastic Stack(又名 ELK 堆栈)将数据摄取到 Elasticsearch 的资源有很多。在今天的文章中,我将详细介绍如何使用 Node.js 从零开始来把地震的实时数据采集到 Elasticsearch 中。

如果你选择的编程语言是 JavaScript,并且你需要使用 RESTful API 方法从第三方应用程序获取数据,那么使用 Node.js 获取数据是一个不错的选择。 你还可以托管服务器,让它持续实时摄取数据。 该演示将向您展示如何设置一个 Node.js + Express.js 服务器,该服务器实时将数据提取到 Elasticsearch 中,然后可以对这些数据进行分析并以有意义的方式采取行动。

对于此演示,我们将使用 USGS 实时发布的公开可用的全球地震数据。
更多阅读 https://elasticstack.blog.csdn ... 05743 收起阅读 »

Observability:从零开始创建 Java 微服务并监控它 (一)

在本教程中,你将学习如何使用 Elastic 可观察性监控 Java 应用程序:日志、基础设施指标、APM 和正常运行时间。通过本教程,你将学到:
 
  • 创建示例 Java 应用程序。
  • 使用 Filebeat 提取日志并在 Kibana 中查看你的日志。
  • 使用 Metricbeat Prometheus 模块获取指标并在 Kibana 中查看你的指标。
  • 使用 Elastic APM Java 代理检测你的应用程序。
  • 使用 Heartbeat 监控您的服务并在 Kibana 中查看您的正常运行时间数据。

 在下面的展示中,我将使用最新的 Elastic Stack 8.5.2 来进行展示。为了方便大家的学习,源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。
更多阅读:https://elasticstack.blog.csdn ... 13010
继续阅读 »
在本教程中,你将学习如何使用 Elastic 可观察性监控 Java 应用程序:日志、基础设施指标、APM 和正常运行时间。通过本教程,你将学到:
 
  • 创建示例 Java 应用程序。
  • 使用 Filebeat 提取日志并在 Kibana 中查看你的日志。
  • 使用 Metricbeat Prometheus 模块获取指标并在 Kibana 中查看你的指标。
  • 使用 Elastic APM Java 代理检测你的应用程序。
  • 使用 Heartbeat 监控您的服务并在 Kibana 中查看您的正常运行时间数据。

 在下面的展示中,我将使用最新的 Elastic Stack 8.5.2 来进行展示。为了方便大家的学习,源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。
更多阅读:https://elasticstack.blog.csdn ... 13010 收起阅读 »

Elasticsearch:将关系数据库中的数据提取到 Elasticsearch 集群中

本指南介绍了如何使用 Logstash JDBC 输入插件通过 Logstash 将关系数据库中的数据提取到 Elasticsearch 集群中。 它演示了如何使用 Logstash 高效地复制记录并从关系数据库接收更新,然后将它们发送到 Elasticsearch 中。

此处提供的代码和方法已经过 MySQL 测试。 他们应该也适用于其他关系数据库。

Logstash Java 数据库连接 (JDBC) 输入插件使你能够从许多流行的关系数据库(包括 MySQL 和 Postgres)中提取数据。 从概念上讲,JDBC 输入插件运行一个循环,该循环定期轮询关系数据库以查找自该循环的最后一次迭代以来插入或修改的记录。

原文链接:https://elasticstack.blog.csdn ... 63743
继续阅读 »
本指南介绍了如何使用 Logstash JDBC 输入插件通过 Logstash 将关系数据库中的数据提取到 Elasticsearch 集群中。 它演示了如何使用 Logstash 高效地复制记录并从关系数据库接收更新,然后将它们发送到 Elasticsearch 中。

此处提供的代码和方法已经过 MySQL 测试。 他们应该也适用于其他关系数据库。

Logstash Java 数据库连接 (JDBC) 输入插件使你能够从许多流行的关系数据库(包括 MySQL 和 Postgres)中提取数据。 从概念上讲,JDBC 输入插件运行一个循环,该循环定期轮询关系数据库以查找自该循环的最后一次迭代以来插入或修改的记录。

原文链接:https://elasticstack.blog.csdn ... 63743 收起阅读 »