不为失败找理由,要为成功找方法。

logstash 写ES 报错【400 Encountered a retryable error. Will Retry with exponential backoff】

回复

juin 回复了问题 • 1 人关注 • 1 个回复 • 6298 次浏览 • 2020-03-08 20:21 • 来自相关话题

搜索字母数据查询不到结果

sxwinter 回复了问题 • 4 人关注 • 3 个回复 • 6372 次浏览 • 2020-03-07 14:48 • 来自相关话题

关于数字.字母的匹配问题

sxwinter 回复了问题 • 5 人关注 • 6 个回复 • 4466 次浏览 • 2020-03-07 14:31 • 来自相关话题

安装ES的服务器,对于磁盘的块大小即block size有什么要求?

medcl 回复了问题 • 2 人关注 • 1 个回复 • 1717 次浏览 • 2020-03-07 13:49 • 来自相关话题

Elasticsearch 7.6 利用机器学习来检测文本语言

medcl 发表了文章 • 0 个评论 • 3838 次浏览 • 2020-03-07 13:22 • 来自相关话题

Elasticsearch 新近发布的 7.6 版本里面包含了很多激动人心的功能,而最让我感兴趣的是利用机器学习来自动检测语言的功能。

功能初探


检测文本语言本身不是什么稀奇事,之前做爬虫的时候,就做过对网页正文进行语言的检测,有很多成熟的方案,而最好的就属 Google Chrome 团队开源的 CLD 系列(全名:Compact Language Detector)了,能够检测多达 80 种各种语言,我用过CLD2,是基于 C++ 贝叶斯分类器实现的,而 CLD3 则是基于神经网络实现的,无疑更加准确,这次 Elasticsearch 将这个非常小的功能也直接集成到了默认的发行包里面,对于使用者来说可以说是带来很大的方便。

多语言的痛点


相信很多朋友,在实际的业务场景中,对碰到过一个字段同时存在多个语种的文本内容的情况,尤其是出海的产品,比如类似大众点评的 APP 吧,一个餐馆下面,来自七洲五湖四海的朋友都来品尝过了,自然要留下点评语是不,德国的朋友使用的是德语,法国的朋友使用的是法语,广州的朋友用的是粤语,那对于开发这个 APP 的后台工程师可就犯难了,如果这些评论都存在一个字段里面,就不好设置一个统一的分词器,因为不同的语言他们的切分规则肯定是不一样的,最简单的例子,比如中文和英文,设置的分词不对,查询结果就会不精准。

相信也有很多人用过这样的解决方案,既然一个字段搞不定,那就把这个字段复制几份,英文字段存一份,中文字段存一份,用 MultiField 来做,这样虽然可以解决一部分问题,但是同样会带来容量和资源的浪费,和查询时候具体该选择哪个字段来参与查询的挑战。

而利用 7.6 的这个新功能,可以在创建索引的时候,可以自动的根据内容进行推理,从而影响索引文档的构成,进而做到特定的文本进特定的字段,从而提升查询体验和性能,关于这个功能,Elastic 官网这里也有一篇博客2,提供了详细的例子。

实战上手


看上去不错,但是鲁迅说过,网上得来终觉浅,觉知此事要躬行,来, 今天一起跑一遍看看具体怎么个用法。

功能剖析


首先,这个功能叫 Language identification,是机器学习的一个 Feature,但是不能单独使用,要结合 Ingest Node 的一个 inference ingest processor 来使用,Ingest processor 是在 Elasticsearch 里面运行的数据预处理器,部分功能类似于 Logstash 的数据解析,对于简单数据操作场景,完全可以替代掉 Logstash,简化部署架构。

Elasticsearch 在 7.6 的包里面,默认打包了提前训练好的机器学习模型,就是 Language identification 需要调用的语言检测模型,名称是固定的 lang_ident_model_1,这也是 Elasticsearch 自带的第一个模型包,大家了解一下就好。

那这个模型包在什么位置呢,我们来解刨一下:

<br /> $unzip /usr/share/elasticsearch/modules/x-pack-ml/x-pack-ml-7.6.0.jar <br /> $/org/elasticsearch/xpack/ml/inference$ tree<br /> .<br /> |-- ingest<br /> | |-- InferenceProcessor$Factory.class<br /> | `-- InferenceProcessor.class<br /> |-- loadingservice<br /> | |-- LocalModel$1.class<br /> | |-- LocalModel.class<br /> | |-- Model.class<br /> | `-- ModelLoadingService.class<br /> `-- persistence<br /> |-- InferenceInternalIndex.class<br /> |-- TrainedModelDefinitionDoc$1.class<br /> |-- TrainedModelDefinitionDoc$Builder.class<br /> |-- TrainedModelDefinitionDoc.class<br /> |-- TrainedModelProvider.class<br /> `-- lang_ident_model_1.json<br /> <br /> 3 directories, 12 files<br />

可以看到,在 persistence 目录就有这个模型包,是 json 格式的,里面有个压缩的二进制编码后的字段。

Jietu20200306-234807.png



查看模型信息


我们还可以通过新的 API 来获取这个模型信息,以后模型多了之后会比较有用:

<br /> GET _ml/inference/lang_ident_model_1<br /> {<br /> "count" : 1,<br /> "trained_model_configs" : [<br /> {<br /> "model_id" : "lang_ident_model_1",<br /> "created_by" : "_xpack",<br /> "version" : "7.6.0",<br /> "description" : "Model used for identifying language from arbitrary input text.",<br /> "create_time" : 1575548914594,<br /> "tags" : [<br /> "lang_ident",<br /> "prepackaged"<br /> ],<br /> "input" : {<br /> "field_names" : [<br /> "text"<br /> ]<br /> },<br /> "estimated_heap_memory_usage_bytes" : 1053992,<br /> "estimated_operations" : 39629,<br /> "license_level" : "basic"<br /> }<br /> ]<br /> }<br />

Ingest Pipeline 模拟测试


好了,基本的了解就到这里了,我们开始动手吧,既然要和 Ingest 结合使用,自然免不了要定义 Ingest Pipeline,也就是说定一个解析规则,索引的时候会调用这个规则来处理输入的索引文档。Ingest Pipeline 的调试是个问题,好在Ingest 提供了模拟调用的方法,我们测试一下:

<br /> <br /> POST _ingest/pipeline/_simulate<br /> {<br /> "pipeline":{<br /> "processors":[<br /> {<br /> "inference":{<br /> "model_id":"lang_ident_model_1", <br /> "inference_config":{<br /> "classification":{<br /> "num_top_classes":5 <br /> }<br /> },<br /> "field_mappings":{<br /> <br /> }<br /> }<br /> }<br /> ]<br /> },<br /> "docs":[<br /> {<br /> "_source":{ <br /> "text":"新冠病毒让你在家好好带着,你服不服"<br /> }<br /> }<br /> ]<br /> }<br />

上面是借助 Ingest 的推理 Process 来模拟调用这个机器学习模型进行文本判断的方法,第一部分是设置 processor 的定义,设置了一个 inference processor,也就是要进行语言模型的检测,第二部分 docs 则是输入了一个 json 文档,作为测试的输入源,运行结果如下:

<br /> {<br /> "docs" : [<br /> {<br /> "doc" : {<br /> "_index" : "_index",<br /> "_type" : "_doc",<br /> "_id" : "_id",<br /> "_source" : {<br /> "text" : "新冠病毒让你在家好好带着,你服不服",<br /> "ml" : {<br /> "inference" : {<br /> "top_classes" : [<br /> {<br /> "class_name" : "zh",<br /> "class_probability" : 0.9999872511022145,<br /> "class_score" : 0.9999872511022145<br /> },<br /> {<br /> "class_name" : "ja",<br /> "class_probability" : 1.061491174235718E-5,<br /> "class_score" : 1.061491174235718E-5<br /> },<br /> {<br /> "class_name" : "hy",<br /> "class_probability" : 6.304673023324264E-7,<br /> "class_score" : 6.304673023324264E-7<br /> },<br /> {<br /> "class_name" : "ta",<br /> "class_probability" : 4.1374037676410867E-7,<br /> "class_score" : 4.1374037676410867E-7<br /> },<br /> {<br /> "class_name" : "te",<br /> "class_probability" : 2.0709260170937159E-7,<br /> "class_score" : 2.0709260170937159E-7<br /> }<br /> ],<br /> "predicted_value" : "zh",<br /> "model_id" : "lang_ident_model_1"<br /> }<br /> }<br /> },<br /> "_ingest" : {<br /> "timestamp" : "2020-03-06T15:58:44.783736Z"<br /> }<br /> }<br /> }<br /> ]<br /> }<br />

可以看到,第一条返回结果,zh 表示中文语言类型,可能性为 0.9999872511022145,基本上无限接近肯定了,这个是中文文本,而第二位和剩下的就明显得分比较低了,如果你看到是他们的得分开头是 1.x 和 6.x 等,是不是觉得,不对啊,后面的得分怎么反而大一些,哈哈,你仔细看会发现它后面其实还有 -E 啥的尾巴呢,这个是科学计数法,其实数值远远小于 0。

创建一个 Pipeline


简单模拟倒是证明这个功能 work 了,那具体怎么使用,一起看看吧。

首先创建一个 Pipeline:

<br /> <br /> PUT _ingest/pipeline/lang_detect_add_tag<br /> {<br /> "description": "检测文本,添加语种标签",<br /> "processors": [<br /> {<br /> "inference": {<br /> "model_id": "lang_ident_model_1",<br /> "inference_config": {<br /> "classification": {<br /> "num_top_classes": 2<br /> }<br /> },<br /> "field_mappings": {<br /> "contents": "text"<br /> }<br /> }<br /> },<br /> {<br /> "set": {<br /> "field": "tag",<br /> "value": "{{ml.inference.predicted_value}}"<br /> }<br /> }<br /> ]<br /> }<br />

可以看到,我们定义了一个 ID 为 lang_detect_add_tag 的 Ingest Pipeline,并且我们设置了这个推理模型的参数,只返回 2 个分类结果,和设置了 content 字段作为检测对象。同时,我们还定义了一个新的 set processor,这个的意思是设置一个名为 tag 的字段,它的值是来自于一个其它的字段的变量引用,也就是把检测到的文本对应的语种存成一个标签字段。

测试这个 Pipeline


这个 Pipeline 创建完之后,我们同样可以对这个 Pipeline 进行模拟测试,模拟的好处是不会实际创建索引,方便调试。

<br /> POST /_ingest/pipeline/lang_detect_add_tag/_simulate<br /> {<br /> "docs": [<br /> {<br /> "_index": "index",<br /> "_id": "id",<br /> "_source": {<br /> "contents": "巴林境内新型冠状病毒肺炎确诊病例累计达56例"<br /> }<br /> },<br /> {<br /> "_index": "index",<br /> "_id": "id",<br /> "_source": {<br /> "contents": "Watch live: WHO gives a coronavirus update as global cases top 100,000"<br /> }<br /> }<br /> ]<br /> }<br />
返回结果:

<br /> {<br /> "docs" : [<br /> {<br /> "doc" : {<br /> "_index" : "index",<br /> "_type" : "_doc",<br /> "_id" : "id",<br /> "_source" : {<br /> "tag" : "zh",<br /> "contents" : "巴林境内新型冠状病毒肺炎确诊病例累计达56例",<br /> "ml" : {<br /> "inference" : {<br /> "top_classes" : [<br /> {<br /> "class_name" : "zh",<br /> "class_probability" : 0.999812378112116,<br /> "class_score" : 0.999812378112116<br /> },<br /> {<br /> "class_name" : "ja",<br /> "class_probability" : 1.8175264877915687E-4,<br /> "class_score" : 1.8175264877915687E-4<br /> }<br /> ],<br /> "predicted_value" : "zh",<br /> "model_id" : "lang_ident_model_1"<br /> }<br /> }<br /> },<br /> "_ingest" : {<br /> "timestamp" : "2020-03-06T16:21:26.981249Z"<br /> }<br /> }<br /> },<br /> {<br /> "doc" : {<br /> "_index" : "index",<br /> "_type" : "_doc",<br /> "_id" : "id",<br /> "_source" : {<br /> "tag" : "en",<br /> "contents" : "Watch live: WHO gives a coronavirus update as global cases top 100,000",<br /> "ml" : {<br /> "inference" : {<br /> "top_classes" : [<br /> {<br /> "class_name" : "en",<br /> "class_probability" : 0.9896669173070857,<br /> "class_score" : 0.9896669173070857<br /> },<br /> {<br /> "class_name" : "tg",<br /> "class_probability" : 0.0033122788575614993,<br /> "class_score" : 0.0033122788575614993<br /> }<br /> ],<br /> "predicted_value" : "en",<br /> "model_id" : "lang_ident_model_1"<br /> }<br /> }<br /> },<br /> "_ingest" : {<br /> "timestamp" : "2020-03-06T16:21:26.981261Z"<br /> }<br /> }<br /> }<br /> ]<br /> }<br />

继续完善 Pipeline


可以看到,两个文档分别都正确识别了语种,并且创建了对应的 tag 字段,不过这个时候,文档里面的 ml 对象字段,就显得有点多余了,可以使用 remove processor 来删除这个字段。

<br /> PUT _ingest/pipeline/lang_detect_add_tag<br /> {<br /> "description": "检测文本,添加语种标签",<br /> "processors": [<br /> {<br /> "inference": {<br /> "model_id": "lang_ident_model_1",<br /> "inference_config": {<br /> "classification": {<br /> "num_top_classes": 2<br /> }<br /> },<br /> "field_mappings": {<br /> "contents": "text"<br /> }<br /> }<br /> },<br /> {<br /> "set": {<br /> "field": "tag",<br /> "value": "{{ml.inference.predicted_value}}"<br /> }<br /> },<br /> {<br /> "remove": {<br /> "field": "ml"<br /> }<br /> }<br /> ]<br /> }<br />

索引文档并调用 Pipeline


那索引的时候,怎么使用这个 Pipeline 呢,看下面的例子:

<br /> <br /> POST news/_doc/1?pipeline=lang_detect_add_tag<br /> {<br /> "contents":"""<br /> On Friday, he added: "In a globalised world, the only option is to stand together. All countries should really make sure that we stand together." Meanwhile, Italy—the country worst affected in Europe—reported 41 new COVID-19 deaths in just 24 hours. The country's civil protection agency said on Thursday evening that 3,858 people had been infected and 148 had died.<br /> """<br /> }<br /> <br /> GET news/_doc/1<br />

上面的这个例子就不贴返回值了,大家自己试试。

另外一个例子


那回到最开始的场景,如果要根据检测结果来分别存储文本到不同的字段,怎么做呢,这里贴一下官网博客的例子:

<br /> POST _ingest/pipeline/_simulate<br /> {<br /> "pipeline": {<br /> "processors": [<br /> {<br /> "inference": {<br /> "model_id": "lang_ident_model_1",<br /> "inference_config": {<br /> "classification": {<br /> "num_top_classes": 1<br /> }<br /> },<br /> "field_mappings": {<br /> "contents": "text"<br /> },<br /> "target_field": "_ml.lang_ident"<br /> }<br /> },<br /> {<br /> "rename": {<br /> "field": "contents",<br /> "target_field": "contents.default"<br /> }<br /> },<br /> {<br /> "rename": {<br /> "field": "_ml.lang_ident.predicted_value",<br /> "target_field": "contents.language"<br /> }<br /> },<br /> {<br /> "script": {<br /> "lang": "painless",<br /> "source": "ctx.contents.supported = (['de', 'en', 'ja', 'ko', 'zh'].contains(ctx.contents.language))"<br /> }<br /> },<br /> {<br /> "set": {<br /> "if": "ctx.contents.supported",<br /> "field": "contents.{{contents.language}}",<br /> "value": "{{contents.default}}",<br /> "override": false<br /> }<br /> }<br /> ]<br /> },<br /> "docs": [<br /> {<br /> "_source": {<br /> "contents": "Das leben ist kein Ponyhof"<br /> }<br /> },<br /> {<br /> "_source": {<br /> "contents": "The rain in Spain stays mainly in the plains"<br /> }<br /> },<br /> {<br /> "_source": {<br /> "contents": "オリンピック大会"<br /> }<br /> },<br /> {<br /> "_source": {<br /> "contents": "로마는 하루아침에 이루어진 것이 아니다"<br /> }<br /> },<br /> {<br /> "_source": {<br /> "contents": "授人以鱼不如授人以渔"<br /> }<br /> },<br /> {<br /> "_source": {<br /> "contents": "Qui court deux lievres a la fois, n’en prend aucun"<br /> }<br /> },<br /> {<br /> "_source": {<br /> "contents": "Lupus non timet canem latrantem"<br /> }<br /> },<br /> {<br /> "_source": {<br /> "contents": "This is mostly English but has a touch of Latin since we often just say, Carpe diem"<br /> }<br /> }<br /> ]<br /> }<br />

返回结果:

<br /> {<br /> "docs" : [<br /> {<br /> "doc" : {<br /> "_index" : "_index",<br /> "_type" : "_doc",<br /> "_id" : "_id",<br /> "_source" : {<br /> "contents" : {<br /> "de" : "Das leben ist kein Ponyhof",<br /> "default" : "Das leben ist kein Ponyhof",<br /> "language" : "de",<br /> "supported" : true<br /> },<br /> "_ml" : {<br /> "lang_ident" : {<br /> "top_classes" : [<br /> {<br /> "class_name" : "de",<br /> "class_probability" : 0.9996006023972855,<br /> "class_score" : 0.9996006023972855<br /> }<br /> ],<br /> "model_id" : "lang_ident_model_1"<br /> }<br /> }<br /> },<br /> "_ingest" : {<br /> "timestamp" : "2020-03-06T16:31:36.211596Z"<br /> }<br /> }<br /> },<br /> {<br /> "doc" : {<br /> "_index" : "_index",<br /> "_type" : "_doc",<br /> "_id" : "_id",<br /> "_source" : {<br /> "contents" : {<br /> "en" : "The rain in Spain stays mainly in the plains",<br /> "default" : "The rain in Spain stays mainly in the plains",<br /> "language" : "en",<br /> "supported" : true<br /> },<br /> "_ml" : {<br /> "lang_ident" : {<br /> "top_classes" : [<br /> {<br /> "class_name" : "en",<br /> "class_probability" : 0.9988809847231199,<br /> "class_score" : 0.9988809847231199<br /> }<br /> ],<br /> "model_id" : "lang_ident_model_1"<br /> }<br /> }<br /> },<br /> "_ingest" : {<br /> "timestamp" : "2020-03-06T16:31:36.211611Z"<br /> }<br /> }<br /> },<br /> {<br /> "doc" : {<br /> "_index" : "_index",<br /> "_type" : "_doc",<br /> "_id" : "_id",<br /> "_source" : {<br /> "contents" : {<br /> "default" : "オリンピック大会",<br /> "language" : "ja",<br /> "ja" : "オリンピック大会",<br /> "supported" : true<br /> },<br /> "_ml" : {<br /> "lang_ident" : {<br /> "top_classes" : [<br /> {<br /> "class_name" : "ja",<br /> "class_probability" : 0.9993823252841599,<br /> "class_score" : 0.9993823252841599<br /> }<br /> ],<br /> "model_id" : "lang_ident_model_1"<br /> }<br /> }<br /> },<br /> "_ingest" : {<br /> "timestamp" : "2020-03-06T16:31:36.211618Z"<br /> }<br /> }<br /> },<br /> {<br /> "doc" : {<br /> "_index" : "_index",<br /> "_type" : "_doc",<br /> "_id" : "_id",<br /> "_source" : {<br /> "contents" : {<br /> "default" : "로마는 하루아침에 이루어진 것이 아니다",<br /> "language" : "ko",<br /> "ko" : "로마는 하루아침에 이루어진 것이 아니다",<br /> "supported" : true<br /> },<br /> "_ml" : {<br /> "lang_ident" : {<br /> "top_classes" : [<br /> {<br /> "class_name" : "ko",<br /> "class_probability" : 0.9999939196272863,<br /> "class_score" : 0.9999939196272863<br /> }<br /> ],<br /> "model_id" : "lang_ident_model_1"<br /> }<br /> }<br /> },<br /> "_ingest" : {<br /> "timestamp" : "2020-03-06T16:31:36.211624Z"<br /> }<br /> }<br /> },<br /> {<br /> "doc" : {<br /> "_index" : "_index",<br /> "_type" : "_doc",<br /> "_id" : "_id",<br /> "_source" : {<br /> "contents" : {<br /> "default" : "授人以鱼不如授人以渔",<br /> "language" : "zh",<br /> "zh" : "授人以鱼不如授人以渔",<br /> "supported" : true<br /> },<br /> "_ml" : {<br /> "lang_ident" : {<br /> "top_classes" : [<br /> {<br /> "class_name" : "zh",<br /> "class_probability" : 0.9999810103320087,<br /> "class_score" : 0.9999810103320087<br /> }<br /> ],<br /> "model_id" : "lang_ident_model_1"<br /> }<br /> }<br /> },<br /> "_ingest" : {<br /> "timestamp" : "2020-03-06T16:31:36.211629Z"<br /> }<br /> }<br /> },<br /> {<br /> "doc" : {<br /> "_index" : "_index",<br /> "_type" : "_doc",<br /> "_id" : "_id",<br /> "_source" : {<br /> "contents" : {<br /> "default" : "Qui court deux lievres a la fois, n’en prend aucun",<br /> "language" : "fr",<br /> "supported" : false<br /> },<br /> "_ml" : {<br /> "lang_ident" : {<br /> "top_classes" : [<br /> {<br /> "class_name" : "fr",<br /> "class_probability" : 0.9999669852240882,<br /> "class_score" : 0.9999669852240882<br /> }<br /> ],<br /> "model_id" : "lang_ident_model_1"<br /> }<br /> }<br /> },<br /> "_ingest" : {<br /> "timestamp" : "2020-03-06T16:31:36.211635Z"<br /> }<br /> }<br /> },<br /> {<br /> "doc" : {<br /> "_index" : "_index",<br /> "_type" : "_doc",<br /> "_id" : "_id",<br /> "_source" : {<br /> "contents" : {<br /> "default" : "Lupus non timet canem latrantem",<br /> "language" : "la",<br /> "supported" : false<br /> },<br /> "_ml" : {<br /> "lang_ident" : {<br /> "top_classes" : [<br /> {<br /> "class_name" : "la",<br /> "class_probability" : 0.614050940088811,<br /> "class_score" : 0.614050940088811<br /> }<br /> ],<br /> "model_id" : "lang_ident_model_1"<br /> }<br /> }<br /> },<br /> "_ingest" : {<br /> "timestamp" : "2020-03-06T16:31:36.21164Z"<br /> }<br /> }<br /> },<br /> {<br /> "doc" : {<br /> "_index" : "_index",<br /> "_type" : "_doc",<br /> "_id" : "_id",<br /> "_source" : {<br /> "contents" : {<br /> "en" : "This is mostly English but has a touch of Latin since we often just say, Carpe diem",<br /> "default" : "This is mostly English but has a touch of Latin since we often just say, Carpe diem",<br /> "language" : "en",<br /> "supported" : true<br /> },<br /> "_ml" : {<br /> "lang_ident" : {<br /> "top_classes" : [<br /> {<br /> "class_name" : "en",<br /> "class_probability" : 0.9997901768317939,<br /> "class_score" : 0.9997901768317939<br /> }<br /> ],<br /> "model_id" : "lang_ident_model_1"<br /> }<br /> }<br /> },<br /> "_ingest" : {<br /> "timestamp" : "2020-03-06T16:31:36.211646Z"<br /> }<br /> }<br /> }<br /> ]<br /> }<br />

可以看到 Ingest Processor 非常灵活,且功能强大,所有的相关操作都可以在 Ingest processor 里面进行处理,再结合脚本做一下规则判断,对原始的字段重命名即可满足我们的文档处理需求。

小结


今天我们聊了聊 Language Identity 这个功能,也聊了聊 Ingest Pipeline 的使用,怎么样,这个功能是不是很赞呀,如果有类似使用场景的朋友,可以自己试试看。另外值得注意的是,如果文本长度太小可能会识别不准,CLD3 设计的文本长度要超过 200 个字符。

相关链接


使用Elasticsearch实现同段和同句搜索

trycatchfinal 发表了文章 • 4 个评论 • 5029 次浏览 • 2020-03-07 10:38 • 来自相关话题

同句搜索要求搜索多个关键词时,返回的文章不只要包含关键词,而且这些关键词必须在同一句中。
同段搜素类似,只是范围为同一段落。

SpanQuery

同段、同句搜索,使用常用的term、match查询,没有找到办法可以实现。
Elasticsearch提供了SpanQuery,官方文档中如下的介绍:

Span queries are low-level positional queries which provide expert control over the order and proximity of the specified terms. These are typically used to implement very specific queries on legal documents or patents.

上面提到,SpanQuery常常应用在法律或专利的特定搜索。这些领域,常常提供同段 /同句搜索 。
下面我们看一下三种类型的SpanQuery,能否实现我们的需求:

准备数据

<br /> PUT article<br /> <br /> <br /> POST article/_mapping<br /> {<br /> "properties": {<br /> "maincontent": {<br /> "type": "text"<br /> }<br /> }<br /> }<br /> <br /> <br /> POST article/_doc/1<br /> {<br /> "maincontent":"the quick red fox jumps over the sleepy cat"<br /> }<br /> <br /> <br /> POST article/_doc/2<br /> {<br /> "maincontent":"the quick brown fox jumps over the lazy dog"<br /> }<br />

SpanTermQuery

SpanTermQuery 和 Term Query类似, 下面的查询会返回_id为1的doc。
the quick red fox jumps over the sleepy cat
<br /> POST article/_search<br /> {<br /> "profile": "true",<br /> "query": {<br /> "span_term": {<br /> "maincontent": {<br /> "value": "red"<br /> }<br /> }<br /> }<br /> }<br />

SpanNearQuery

SpanNearQuery 表示邻近搜索,查找多个term是否邻近,slop可以设置邻近距离,如果设置为0,那么代表两个term是挨着的,相当于matchphase
in_order参数,代表文档中的term和查询设置的term保持相同的顺序。
<br /> POST article/_search<br /> {<br /> "query": {<br /> "span_near": {<br /> "clauses": [<br /> {<br /> "span_term": {<br /> "maincontent": {<br /> "value": "quick"<br /> }<br /> }<br /> },<br /> {<br /> "span_term": {<br /> "maincontent": {<br /> "value": "brown"<br /> }<br /> }<br /> }<br /> ],<br /> "slop": 0,<br /> "in_order": true<br /> }<br /> }<br /> }<br />
上面的查询会返回_id为2的doc。

the quick brown fox jumps over the lazy dog

SpanNotQuery

SpanNotQuery非常重要,它要求两个SpanQuery的跨度,不能够重合。
看下面的例子:

  • include: 匹配的SpanQuery,例子为需要一个包含quick和fox两个词的邻近搜索。
  • exclude:设置一个SpanQuery,要求include中的SpanQuery不能包含这个SpanQuery
    <br /> POST article/_search<br /> {<br /> "query": {<br /> "span_not": {<br /> "include": {<br /> "span_near": {<br /> "clauses": [<br /> {<br /> "span_term": {<br /> "maincontent": {<br /> "value": "quick"<br /> }<br /> }<br /> },<br /> {<br /> "span_term": {<br /> "maincontent": {<br /> "value": "fox"<br /> }<br /> }<br /> }<br /> ],<br /> "slop": 1,<br /> "in_order": true<br /> }<br /> },<br /> "exclude": {<br /> "span_term": {<br /> "maincontent": {<br /> "value": "red"<br /> }<br /> }<br /> }<br /> }<br /> }<br /> }<br />
    上面的查询会返回_id为2的doc。
    因为_id为1的文档,虽然quick red fox符合include中的SpanQuery,但是red也符合exclude中的SpanQuery。因此,这篇文章需要排除掉。
    the quick red fox jumps over the sleepy cat

    同句/同段搜索原理

    同句搜索,反向来说,就是搜索词不能够跨句。再进一步,就是搜索词之间不能够有等其他标点符号。
    其对应的查询类似如下:
    <br /> POST article/_search<br /> {<br /> "query": {<br /> "span_not": {<br /> "include": {<br /> "span_near": {<br /> "clauses": [<br /> {<br /> "span_term": {<br /> "maincontent": {<br /> "value": "word1"<br /> }<br /> }<br /> },<br /> {<br /> "span_term": {<br /> "maincontent": {<br /> "value": "word2"<br /> }<br /> }<br /> }<br /> ],<br /> "slop": 1,<br /> "in_order": true<br /> }<br /> },<br /> "exclude": {<br /> "span_term": {<br /> "maincontent": {<br /> "value": "。/?/!"<br /> }<br /> }<br /> }<br /> }<br /> }<br /> }<br />
    同段搜素类似,对应分隔符变为\n,或者<p>,</p>

    同段/同句搜索实现

    文本为HTML格式

    创建索引

    <br /> PUT sample1<br /> {<br /> "settings": {<br /> "number_of_replicas": 0,<br /> "number_of_shards": 1,<br /> "analysis": {<br /> "analyzer": {<br /> "maincontent_analyzer": {<br /> "type": "custom",<br /> "char_filter": [<br /> "sentence_paragrah_mapping",<br /> "html_strip"<br /> ],<br /> "tokenizer": "ik_max_word"<br /> }<br /> },<br /> "char_filter": {<br /> "sentence_paragrah_mapping": {<br /> "type": "mapping",<br /> "mappings": [<br /> """<h1> => \u0020paragraph\u0020""",<br /> """</h1> => \u0020sentence\u0020paragraph\u0020 """,<br /> """<h2> => \u0020paragraph\u0020""",<br /> """</h2> => \u0020sentence\u0020paragraph\u0020 """,<br /> """<p> => \u0020paragraph\u0020""",<br /> """</p> => \u0020sentence\u0020paragraph\u0020 """,<br /> """! => \u0020sentence\u0020 """,<br /> """? => \u0020sentence\u0020 """,<br /> """。 => \u0020sentence\u0020 """,<br /> """? => \u0020sentence\u0020 """,<br /> """! => \u0020sentence\u0020"""<br /> ]<br /> }<br /> }<br /> }<br /> },<br /> "mappings": {<br /> "properties": {<br /> "mainContent": {<br /> "type": "text",<br /> "analyzer": "maincontent_analyzer",<br /> "search_analyzer": "ik_smart"<br /> }<br /> }<br /> }<br /> }<br />
    我们创建了一个名称为sentence_paragrah_mapping的char filter,它的目的有两个:

  • 替换p,h1,h2标签为统一的分段符:paragraph
  • 替换中英文 ,, 标点符号为统一的分页符:sentence

    有几个细节,需要说明:

  • paragraphsentence前后都需要添加空格,并且需要使用Unicode \u0020表示空格。
    ```

    期望

    hello world! => hello world sentence

    不合理的配置,可能会出现下面的情况

    hello world! => hello worldsentence
    ```

  • </p>,</h1>,</h2>的结尾标签需要添加paragraphsentence两个分隔符,避免结尾没有标点符号的情况

    ```

    期望

    hello world

    hello china

    => paragraph hello world sentence paragraph hello china sentence

    #

    ,,只使用paragraph替换的结果
    # 此时 hello world hello china 为同句

    hello world

    hello china

    => paragraph hello world paragraph hello china sentence

    # 上面配置结果有些冗余:有两个连续的paragraph
    # 如果能保证HTML文本都符合标准,可以只替换

    ,,,不替换

    ,

    ,


    hello world

    hello china

    => paragraph hello world sentence paragraph paragraph hello china sentence
    ```

  • 注意sentence_paragrah_mapping和html_strip的配置顺序

    插入测试数据

    ```
    POST sample1/_doc/1
    {
    "mainContent":"

    java python javascript

    oracle mysql sqlserver

    "
    }

    测试分词

    POST sample1/_analyze
    {
    "text": ["

    java python javascript

    oracle mysql sqlserver

    "],
    "analyzer": "maincontent_analyzer"
    }

    返回结果

    {
    "tokens" : [
    {
    "token" : "paragraph",
    "start_offset" : 1,
    "end_offset" : 2,
    "type" : "ENGLISH",
    "position" : 0
    },
    {
    "token" : "java",
    "start_offset" : 3,
    "end_offset" : 7,
    "type" : "ENGLISH",
    "position" : 1
    },
    {
    "token" : "python",
    "start_offset" : 8,
    "end_offset" : 14,
    "type" : "ENGLISH",
    "position" : 2
    },
    {
    "token" : "javascript",
    "start_offset" : 15,
    "end_offset" : 25,
    "type" : "ENGLISH",
    "position" : 3
    },
    {
    "token" : "sentence",
    "start_offset" : 26,
    "end_offset" : 28,
    "type" : "ENGLISH",
    "position" : 4
    },
    {
    "token" : "paragraph",
    "start_offset" : 28,
    "end_offset" : 28,
    "type" : "ENGLISH",
    "position" : 5
    },
    {
    "token" : "paragraph",
    "start_offset" : 30,
    "end_offset" : 31,
    "type" : "ENGLISH",
    "position" : 6
    },
    {
    "token" : "oracle",
    "start_offset" : 32,
    "end_offset" : 38,
    "type" : "ENGLISH",
    "position" : 7
    },
    {
    "token" : "mysql",
    "start_offset" : 39,
    "end_offset" : 44,
    "type" : "ENGLISH",
    "position" : 8
    },
    {
    "token" : "sqlserver",
    "start_offset" : 45,
    "end_offset" : 54,
    "type" : "ENGLISH",
    "position" : 9
    },
    {
    "token" : "sentence",
    "start_offset" : 55,
    "end_offset" : 57,
    "type" : "ENGLISH",
    "position" : 10
    },
    {
    "token" : "paragraph",
    "start_offset" : 57,
    "end_offset" : 57,
    "type" : "ENGLISH",
    "position" : 11
    }
    ]
    }

    ```

    测试查询


  • 同段查询:java python

    <br /> GET sample1/_search<br /> {<br /> "query": {<br /> "span_not": {<br /> "include": {<br /> "span_near": {<br /> "clauses": [<br /> {<br /> "span_term": {<br /> "mainContent": {<br /> "value": "java"<br /> }<br /> }<br /> },<br /> {<br /> "span_term": {<br /> "mainContent": {<br /> "value": "python"<br /> }<br /> }<br /> }<br /> ],<br /> "slop": 12,<br /> "in_order": false<br /> }<br /> },<br /> "exclude": {<br /> "span_term": {<br /> "mainContent": {<br /> "value": "paragraph"<br /> }<br /> }<br /> }<br /> }<br /> }<br /> }<br /> <br /> //结果<br /> {<br /> "took" : 0,<br /> "timed_out" : false,<br /> "_shards" : {<br /> "total" : 1,<br /> "successful" : 1,<br /> "skipped" : 0,<br /> "failed" : 0<br /> },<br /> "hits" : {<br /> "total" : {<br /> "value" : 1,<br /> "relation" : "eq"<br /> },<br /> "max_score" : 0.1655603,<br /> "hits" : [<br /> {<br /> "_index" : "sample1",<br /> "_type" : "_doc",<br /> "_id" : "1",<br /> "_score" : 0.1655603,<br /> "_source" : {<br /> "mainContent" : "<p>java python javascript</p><p>oracle mysql sqlserver</p>"<br /> }<br /> }<br /> ]<br /> }<br /> }<br />

  • 同段查询:java oracle

    ```
    GET sample1/_search
    {
    "query": {
    "span_not": {
    "include": {
    "span_near": {
    "clauses": [
    {
    "span_term": {
    "mainContent": {
    "value": "java"
    }
    }
    },
    {
    "span_term": {
    "mainContent": {
    "value": "oracle"
    }
    }
    }
    ],
    "slop": 12,
    "in_order": false
    }
    },
    "exclude": {
    "span_term": {
    "mainContent": {
    "value": "paragraph"
    }
    }
    }
    }
    }
    }

    结果:没有文档返回

    {
    "took" : 0,
    "timed_out" : false,
    "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
    },
    "hits" : {
    "total" : {
    "value" : 0,
    "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
    }
    }

    ```

    纯文本格式

    纯文本和HTML的区别是段落分割符不同,使用\n.

    创建索引


    <br /> PUT sample2<br /> {<br /> "settings": {<br /> "number_of_replicas": 0,<br /> "number_of_shards": 1,<br /> "analysis": {<br /> "analyzer": {<br /> "maincontent_analyzer": {<br /> "type": "custom",<br /> "char_filter": [<br /> "sentence_paragrah_mapping"<br /> ],<br /> "tokenizer": "ik_max_word"<br /> }<br /> },<br /> "char_filter": {<br /> "sentence_paragrah_mapping": {<br /> "type": "mapping",<br /> "mappings": [<br /> """\n => \u0020sentence\u0020paragraph\u0020 """,<br /> """! => \u0020sentence\u0020 """,<br /> """? => \u0020sentence\u0020 """,<br /> """。 => \u0020sentence\u0020 """,<br /> """? => \u0020sentence\u0020 """,<br /> """! => \u0020sentence\u0020"""<br /> ]<br /> }<br /> }<br /> }<br /> },<br /> "mappings": {<br /> "properties": {<br /> "mainContent": {<br /> "type": "text",<br /> "analyzer": "maincontent_analyzer",<br /> "search_analyzer": "ik_smart"<br /> }<br /> }<br /> }<br /> }<br />

    测试分词

    ```
    POST sample2/_analyze
    {
    "text": ["java python javascript\noracle mysql sqlserver"],
    "analyzer": "maincontent_analyzer"
    }

    结果

    {
    "tokens" : [
    {
    "token" : "java",
    "start_offset" : 0,
    "end_offset" : 4,
    "type" : "ENGLISH",
    "position" : 0
    },
    {
    "token" : "python",
    "start_offset" : 5,
    "end_offset" : 11,
    "type" : "ENGLISH",
    "position" : 1
    },
    {
    "token" : "javascript",
    "start_offset" : 12,
    "end_offset" : 22,
    "type" : "ENGLISH",
    "position" : 2
    },
    {
    "token" : "sentence",
    "start_offset" : 22,
    "end_offset" : 22,
    "type" : "ENGLISH",
    "position" : 3
    },
    {
    "token" : "paragraph",
    "start_offset" : 22,
    "end_offset" : 22,
    "type" : "ENGLISH",
    "position" : 4
    },
    {
    "token" : "oracle",
    "start_offset" : 23,
    "end_offset" : 29,
    "type" : "ENGLISH",
    "position" : 5
    },
    {
    "token" : "mysql",
    "start_offset" : 30,
    "end_offset" : 35,
    "type" : "ENGLISH",
    "position" : 6
    },
    {
    "token" : "sqlserver",
    "start_offset" : 36,
    "end_offset" : 45,
    "type" : "ENGLISH",
    "position" : 7
    }
    ]
    }

    ```

elasticsearch6.8 大宽表该如何设计

locatelli 回复了问题 • 2 人关注 • 1 个回复 • 2755 次浏览 • 2020-03-06 05:25 • 来自相关话题

通过复制lucene segmentsinfo添加索引数据

Charele 回复了问题 • 2 人关注 • 1 个回复 • 969 次浏览 • 2020-03-05 21:23 • 来自相关话题

java.security.AccessControlException

Charele 回复了问题 • 3 人关注 • 2 个回复 • 1402 次浏览 • 2020-03-05 15:30 • 来自相关话题

都2020年了,折叠查询可以返回折叠后的总条数了吗?

匿名用户 回复了问题 • 2 人关注 • 2 个回复 • 2567 次浏览 • 2020-03-05 09:04 • 来自相关话题

mongodb一个db多个collection怎么同步到ES

回复

匿名用户 回复了问题 • 2 人关注 • 2 个回复 • 1610 次浏览 • 2020-03-05 09:01 • 来自相关话题

ES超大数据集群方案请教

匿名用户 回复了问题 • 2 人关注 • 2 个回复 • 2790 次浏览 • 2020-03-05 08:50 • 来自相关话题

如下<<<<<<<<<

回复

unclex 回复了问题 • 1 人关注 • 1 个回复 • 2558 次浏览 • 2020-03-04 23:17 • 来自相关话题

es在搜索无输入关键词下使用boost权重

laoyang360 回复了问题 • 2 人关注 • 1 个回复 • 3163 次浏览 • 2020-03-04 22:34 • 来自相关话题

救救孩子吧 5.5升级6.7后 painless 脚本 空指针 找了好几个小时了谢谢大神!

mmjinf001 回复了问题 • 4 人关注 • 2 个回复 • 1402 次浏览 • 2020-03-04 21:59 • 来自相关话题