工程

使用 Elasticsearch 中的语言识别模型进行多语言搜索

我们很高兴地宣布随着 Machine Learning 推理采集处理器的推出,我们在 Elasticsearch 7.6 中推出了语言识别模型。由于推出了这一模型,我们希望借此机会向您提供多语言语料库搜索方面的一些用例和策略,以及语言识别模型可以发挥什么作用。我们之前曾讲过其中一些主题,在随后的一些示例中我们会基于这些内容进行讲解。

设计初衷

在当今高度互联的世界中,我们发现文档和其他信息来源可能以各种语言写成。这给很多搜索应用程序带来了一个难题。我们需要尽可能准确地理解这些文档所用的语言,因为只有这样才能正确分析并提供最优的搜索体验。语言识别模型登场了。

语言识别模型可用来改善这些多语言语料库的整体搜索相关性。假设我们有一系列文档,但并不知道其中所包含的语言,现在我们想高效地对这些文档进行搜索。这些文档可能只包含一种语言,也可能包含多种语言。前一种情况在计算机科学领域很常见,因为英语是这一领域的主要沟通语言;而后一种情况则常见于生物和医药领域,因为在英语中经常会夹杂一些拉丁语术语。

通过应用针对具体语言的分析过程,我们可以确保正确地理解、索引和搜索文档中的词语,从而提高相关性(同时包括精确率和查全率)。通过在 Elasticsearch 中使用一套针对具体语言的分析器(既可使用内置型,也可使用其他插件),我们能够更好地进行词汇切分、分词筛选和词语筛选:

出于类似原因,我们发现要想充分利用高度精准且针对具体语言的算法和模型,在更广泛意义上的自然语言识别 (NLP) 管道中应用语言识别模型是需首先完成的处理步骤之一。举例说明,预训练的 NLP 模型(例如 BERTALBERT,或者 OpenAI 的 GPT-2)通常都会使用单一语言语料库或者包含某一主要语言的语料库进行训练,然后再针对各种任务(例如文档分类、情感分析、命名实体识别 (NER) 等)进行微调。

对于下面所讲到的示例和策略,除非有其他说明,否则我们均假设文档只包含一种语言或主要语言。

针对具体语言进行分析的好处

为了帮助您更深入地了解这一点,我们快速看一下针对具体语言的分析器有哪些好处。

分解:在德语中,名词通常是由其他名词组合而成的,所形成的组合词超长而且很难读。举个简单例子,将“Jahr”(“年”)和其他词组合到一起可以形成“Jahrhunderts”(“世纪”)、“Jahreskalender”(“年历”)或“Schuljahr”(“学年”)。如果不使用定制分析器来分解这些词语,则我们在搜索“jahr”时便不能获得有关“学年”(“jahr”)的文档。更进一步,相比其他拉丁语系的语言,德语在复数和与格形式方面也有不同的规则,即搜索“jahr”时还应当同时匹配到“Jahre”(复数形式)和“Jahren”(复数的与格形式)。

常用词语:某些语言还会使用常用术语或者特定领域的术语。例如,“computer”(计算机)在很多其他语言中均会照原样使用。搜索“computer”时,我们可能也对非英语文档感兴趣。既要能够搜索一系列已知的语言,还要能够匹配到常用词汇,这是一个很有意思的用例。再次拿德语举例,我们可能有以多种语言写成的“计算机安全”方面的文档。在德语中,“计算机安全”对应的词语是“Computersicherheit”(“sicherheit”表示“安全”或“安保”),这种情况下,只有使用德语分析器,才能在搜索“computer”时同时匹配英语和德语。

非拉丁字母:对于大部分使用拉丁字母的语言(西欧语言),标准分析器的效果很好。然而,对于不使用拉丁字母的语言(例如西里尔字母CJK(中文/日语/韩语)),标准分析器的效果会大打折扣。在之前的博文系列中,我们曾探讨了 CJK 语言的组成结构,以及为何有必要使用针对具体语言的分析器。举个例子,韩语会使用助词(添加在名词或代词后面的后缀,能够改变词语的意思)。有时,使用标准分析器能够匹配到搜索词,但在对匹配结果进行评分时效果并不好。这即意味着,您的文档查全率可能还好,但是精确率会受影响。在其他情况下,标准分析器则不会匹配到任何词语,这时精确率和查全率均会受到影响。

我们看一下有关“冬奥会”的真实示例。在韩语中,冬奥会的说法是 동계올림픽대회는,这个词由 동계(表示“冬季”)、 올림픽대회(表示“奥运会”或“奥林匹克竞赛”)和最后的 는(表示主题的助词,即添加到词语后边表示主题)组成。使用标准分析器的话,如果搜索准确的字符串,能够获得完美匹配,但如果只搜索 올림픽대회(意为“奥运会”),则不会返回结果。然而,如果使用 nori 韩语分析器,我们能够获得匹配结果,因为在索引时已对 동계올림픽대회는(冬奥会)正确进行了分词。

开始使用语言识别模型

演示项目

为了帮助演示语言识别模型的搜索用例和策略,我们创建了一个小型演示项目。其中包括本篇博文中的所有示例,也包括一些适用于 WiLI-2018 的索引和搜索工具;WiLI-2018 是一个多语言语料库,在您进行多语言搜索实验时可作为参考和范例。如要按照这些示例操作,建议(但并非必须)您创建并运行一个演示项目,并向其中索引一些文档。

对于这些实验,您既可以在本地安装 Elasticsearch 7.6,也可以轻松免费试用 Elasticsearch Service

首个实验

语言识别模型是一个预训练模型,已涵盖在 Elasticsearch 的默认分发包中。它需要和推理采集处理器结合使用,具体做法是,您在采集管道中设置推理处理器时需要将 model_id 指定为 lang_ident_model_1

{ 
  "inference": { 
    "model_id": "lang_ident_model_1",
    "inference_config": {},
    "field_mappings": {}
  } 
}

配置的剩余部分与其他模型一样,能允许您指定一些设置,例如要输出的排名最前的类别的数量,将包含预测值的输出字段,以及对我们的用例而言最为重要的一项,要使用的输入字段。默认情况下,该模型将会使用一个名为 text 的字段来包含输入内容。在下面的示例中,我们会使用 pipeline _simulate API 和一些单字段文档。它会将输入内容字段映射到 text 字段以进行推理——这一映射不会影响管道中的其他处理器。然后,它会输出前 3 个类别让您检查。

# 模拟一个基本的推理设置

POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "inference": {
          "model_id": "lang_ident_model_1",
          "inference_config": {
            "classification": {
              "num_top_classes":3
            }
          },
          "field_mappings": {
            "contents": "text"
          },
          "target_field": "_ml.lang_ident"
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "contents":"Das leben ist kein Ponyhof"
      }
    },
    {
      "_source": {
        "contents":"The rain in Spain stays mainly in the plains"
      }
    },
    {
      "_source": {
        "contents":"This is mostly English but has a touch of Latin since we often just say, Carpe diem"
      }
    }
  ]
}

输出向我们显示了每个文档,同时在 _ml.lang_ident 字段中还显示了一些其他信息。这些信息包括前三种语言(存储在 _ml.lang_ident.predicted_value 字段中)以及每种语言的概率。

{
  "docs" : [
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" :"Das leben ist kein Ponyhof",
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "de",
                  "class_probability" :0.9996006023972855
                },
                {
                  "class_name" : "el-Latn",
                  "class_probability" :2.625873919853074E-4
                },
                {
                  "class_name" : "ru-Latn",
                  "class_probability" :1.130237050226503E-4
                }
              ],
              "predicted_value" : "de",
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-21T14:38:13.810179Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" :"The rain in Spain stays mainly in the plains",
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" :0.9988809847231199
                },
                {
                  "class_name" : "ga",
                  "class_probability" :7.764148026288316E-4
                },
                {
                  "class_name" : "gd",
                  "class_probability" :7.968926766495827E-5
                }
              ],
              "predicted_value" : "en",
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-21T14:38:13.810185Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" :"This is mostly English but has a touch of Latin since we often just say, Carpe diem",
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" :0.9997901768317939
                },
                {
                  "class_name" : "ja",
                  "class_probability" :8.756250766054857E-5
                },
                {
                  "class_name" : "fil",
                  "class_probability" :1.6980752372837307E-5
                }
              ],
              "predicted_value" : "en",
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-21T14:38:13.810189Z"
        }
      }
    }
  ]
}

看起来不错哦!我们识别出了第一个文档是德语,第二个和第三个文档是英语,尽管第三个文档中还夹杂着一些拉丁语。

搜索时的语言识别策略

已经看到了语言识别模型的一个基本示例,现在我们该开始将这一模型应用到索引和搜索策略中了。

我们将会使用两种基本的索引策略:每个字段一种语言,以及每个索引一种语言。如果采用每个字段一种语言策略,我们会创建单个索引,其中包括一组针对具体语言的字段,并且会使用为每种语言量身定制的分析器。在搜索时,我们既可以选择对某个已知语言的字段进行搜索,也可以对所有语言字段进行搜索并选择最匹配的字段。如果采用每个索引一种语言策略,我们将会通过不同的映射创建一系列针对特定语言的索引,在这些索引中,所索引的字段会采用针对该种语言的分析器。搜索时,我们可以采用与“每个字段一种语言”策略类似的方法,既可以选择对单个语言索引进行搜索,也可以对符合搜索请求中索引模式的多个索引进行搜索。

将这两种策略与您今天必须完成的操作(对同一字符串进行多次索引,且每次都要使用针对特定语言的分析器将每个字符串索引至某个字段或索引)对比一下,您就可以发现:尽管现行方法奏效,但是会产生极其多的重复内容,导致查询速度变慢,并且所使用的存储空间也会远远超出必要水平。

索引过程

我们将这一过程分解一下,看看这两种索引策略,因为索引策略会决定我们所采用的搜索策略。

每个字段一种语言

如果采用“每个字段一种语言”策略,我们将会在采集管道中使用语言识别模型的输出以及一系列处理器,从而将输入字段存储在针对具体语言的字段中。因为需要为每种语言设置具体的分析器,所以我们仅支持有限的语言集合(德语、英语、韩语、日语和中文)。对于不在我们所支持四种语言之内的任何文档,它们都会通过标准分析器索引至一个默认字段。

完整的管道定义详见演示项目:config/pipelines/lang-per-field.json

支持这一索引策略的映射如下面这样:

{
  "settings": {
    "index": {
      "number_of_shards":1,
      "number_of_replicas":0
    }
  },
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "contents": {
        "properties": {
          "language": {
            "type": "keyword"
          },
          "supported": {
            "type": "boolean"
          },
          "default": {
            "type": "text",
            "analyzer": "default",
            "fields": {
              "icu": {
                "type": "text",
                "analyzer": "icu_analyzer"
              }
            }
          },
          "en": {
            "type": "text",
            "analyzer": "english"
          },
          "de": {
            "type": "text",
            "analyzer": "german_custom"
          },
          "ja": {
            "type": "text",
            "analyzer": "kuromoji"
          },
          "ko": {
            "type": "text",
            "analyzer": "nori"
          },
          "zh": {
            "type": "text",
            "analyzer": "smartcn"
          }
        }
      }
    }
  }
}

(请注意,为简单起见,已将德语分析器配置从上面的示例中删除,不过您可以在这里查看:config/mappings/de_analyzer.json

和前面的示例一样,我们会使用管道的 _simulate API 进行探索:

# 模拟“每个字段一种语言”策略,并输出前 3 个语言类别进行检查

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

这里就是“每个字段一种语言”策略的输出结果:

{
  "docs" : [
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "de" :"Das leben ist kein Ponyhof",
            "default" :"Das leben ist kein Ponyhof",
            "language" : "de",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "de",
                  "class_probability" :0.9996006023972855
                },
                {
                  "class_name" : "el-Latn",
                  "class_probability" :2.625873919853074E-4
                },
                {
                  "class_name" : "ru-Latn",
                  "class_probability" :1.130237050226503E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-22T12:40:03.218641Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "en" :"The rain in Spain stays mainly in the plains",
            "default" :"The rain in Spain stays mainly in the plains",
            "language" : "en",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" :0.9988809847231199
                },
                {
                  "class_name" : "ga",
                  "class_probability" :7.764148026288316E-4
                },
                {
                  "class_name" : "gd",
                  "class_probability" :7.968926766495827E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-22T12:40:03.218646Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "オリンピック大会",
            "language" : "ja",
            "ja" : "オリンピック大会",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ja",
                  "class_probability" :0.9993823252841599
                },
                {
                  "class_name" : "el",
                  "class_probability" :2.6448654791599055E-4
                },
                {
                  "class_name" : "sd",
                  "class_probability" :1.4846805271384584E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-22T12:40:03.218648Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "로마는 하루아침에 이루어진 것이 아니다",
            "language" : "ko",
            "ko" : "로마는 하루아침에 이루어진 것이 아니다",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ko",
                  "class_probability" :0.9999939196272863
                },
                {
                  "class_name" : "ka",
                  "class_probability" :3.0431805047662344E-6
                },
                {
                  "class_name" : "am",
                  "class_probability" :1.710514725818281E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-22T12:40:03.218649Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "授人以鱼不如授人以渔",
            "language" : "zh",
            "zh" : "授人以鱼不如授人以渔",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "zh",
                  "class_probability" :0.9999810103320087
                },
                {
                  "class_name" : "ja",
                  "class_probability" :1.0390454083183788E-5
                },
                {
                  "class_name" : "ka",
                  "class_probability" :2.6302271562335787E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-22T12:40:03.21865Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" :"Qui court deux lievres a la fois, n’en prend aucun",
            "language" : "fr",
            "supported" : false
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "fr",
                  "class_probability" :0.9999669852240882
                },
                {
                  "class_name" : "gd",
                  "class_probability" :2.3485226102079597E-5
                },
                {
                  "class_name" : "ht",
                  "class_probability" :3.536708810360631E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-22T12:40:03.218652Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" :"Lupus non timet canem latrantem",
            "language" : "la",
            "supported" : false
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "la",
                  "class_probability" :0.614050940088811
                },
                {
                  "class_name" : "fr",
                  "class_probability" :0.32530021315840363
                },
                {
                  "class_name" : "sq",
                  "class_probability" :0.03353817054854559
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-22T12:40:03.218653Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "en" :"This is mostly English but has a touch of Latin since we often just say, Carpe diem",
            "default" :"This is mostly English but has a touch of Latin since we often just say, Carpe diem",
            "language" : "en",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" :0.9997901768317939
                },
                {
                  "class_name" : "ja",
                  "class_probability" :8.756250766054857E-5
                },
                {
                  "class_name" : "fil",
                  "class_probability" :1.6980752372837307E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-22T12:40:03.218654Z"
        }
      }
    }
  ]
}

和预计的一样,我们发现德语字段存储在了 contents.de 中,英语字段存储在了 contents.en 中,韩语字段存储在了 contents.ko 中,在此不再一一列举。请注意,我们还夹杂了一些不支持的语言示例,分别为法语和拉丁语。我们看到它们并没有受支持旗标,所以只能在默认字段中对这些内容进行搜索。另外,还请看一下拉丁语示例中排名靠前的预测类别。好像该模型认为应该是拉丁语(这一结果也是正确的),但是并不能确认,所以预测中排名第二的法语概率也很高。

这只是采用语言识别模型的采集管道的一个基本示例,但希望能够让您了解可实现什么结果。由于采集管道很灵活,我们能够应对很多不同场景。在本篇博文的末尾,我们会深入探讨几种备选方法。在生产管道中,您可以合并或省略本示例中的某些步骤,但请记住良好数据处理管道的标准是读取和理解起来都很轻松,而不是代码行越少越好。

每个索引一种语言

“每个索引一种语言”策略所使用的基础构建模块与“每个字段一种语言”的管道是一样的。但有一个巨大不同,即不再将内容存储到针对具体语言的字段中,而是使用不同的索引。这一点之所以能够实现,是因为我们在采集时可以设置文档的 _index 字段,这一方法使得我们能够覆盖默认值并将其设置为针对具体语言的索引名称。如果不支持某种语言,我们可以跳过该步骤,这样文档便会索引至默认索引。就这么简单!

完整的管道定义详见演示项目:config/pipelines/lang-per-index.json

支持这一索引策略的映射如下面这样。

{
  "settings": {
    "index": {
      "number_of_shards":1,
      "number_of_replicas":0
    }
  },
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "contents": {
        "properties": {
          "language": {
            "type": "keyword"
          },
          "text": {
            "type": "text",
            "analyzer": "default"
          }
        }
      }
    }
  }
}

请注意,我们在影射中并未指定定制分析器,而是使用此文件作为模板。在创建每个针对具体语言的索引时,我们会为这种语言设置分析器

模拟这个管道:

# 模拟“每个索引一种语言”策略,并输出前 3 个语言类别进行检查

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

这里就是“每个索引一种语言”策略的输出结果:

{
  "docs" : [
    {
      "doc" : {
        "_index" : "_index_de",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "de",
            "text" :"Das leben ist kein Ponyhof"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "de",
                  "class_probability" :0.9996006023972855
                },
                {
                  "class_name" : "el-Latn",
                  "class_probability" :2.625873919853074E-4
                },
                {
                  "class_name" : "ru-Latn",
                  "class_probability" :1.130237050226503E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-21T14:41:48.486009Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_en",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "en",
            "text" :"The rain in Spain stays mainly in the plains"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" :0.9988809847231199
                },
                {
                  "class_name" : "ga",
                  "class_probability" :7.764148026288316E-4
                },
                {
                  "class_name" : "gd",
                  "class_probability" :7.968926766495827E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-21T14:41:48.486037Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_ja",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "ja",
            "text" : "オリンピック大会"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ja",
                  "class_probability" :0.9993823252841599
                },
                {
                  "class_name" : "el",
                  "class_probability" :2.6448654791599055E-4
                },
                {
                  "class_name" : "sd",
                  "class_probability" :1.4846805271384584E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-21T14:41:48.486039Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_ko",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "ko",
            "text" : "로마는 하루아침에 이루어진 것이 아니다"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ko",
                  "class_probability" :0.9999939196272863
                },
                {
                  "class_name" : "ka",
                  "class_probability" :3.0431805047662344E-6
                },
                {
                  "class_name" : "am",
                  "class_probability" :1.710514725818281E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-21T14:41:48.486041Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_zh",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "zh",
            "text" : "授人以鱼不如授人以渔"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "zh",
                  "class_probability" :0.9999810103320087
                },
                {
                  "class_name" : "ja",
                  "class_probability" :1.0390454083183788E-5
                },
                {
                  "class_name" : "ka",
                  "class_probability" :2.6302271562335787E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-21T14:41:48.486043Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "fr",
            "text" :"Qui court deux lievres a la fois, n’en prend aucun"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "fr",
                  "class_probability" :0.9999669852240882
                },
                {
                  "class_name" : "gd",
                  "class_probability" :2.3485226102079597E-5
                },
                {
                  "class_name" : "ht",
                  "class_probability" :3.536708810360631E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-21T14:41:48.486044Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "la",
            "text" :"Lupus non timet canem latrantem"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "la",
                  "class_probability" :0.614050940088811
                },
                {
                  "class_name" : "fr",
                  "class_probability" :0.32530021315840363
                },
                {
                  "class_name" : "sq",
                  "class_probability" :0.03353817054854559
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-21T14:41:48.486046Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_en",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "en",
            "text" :"This is mostly English but has a touch of Latin since we often just say, Carpe diem"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" :0.9997901768317939
                },
                {
                  "class_name" : "ja",
                  "class_probability" :8.756250766054857E-5
                },
                {
                  "class_name" : "fil",
                  "class_probability" :1.6980752372837307E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" :"2020-01-21T14:41:48.48605Z"
        }
      }
    }
  ]
}

和预想的一样,语言识别模型的结果与“每个字段一种语言”策略的结果是一样的,唯一不同是我们如何使用管道中的这一信息来将文档路由至正确的索引。

搜索

鉴于有两种索引策略,搜索时哪种方法最好呢?如上面所提到的,对于每种索引策略,我们都有几个选择。一个常见问题就是,我们如何为查询字符串指定具体语言的分析器,以便将查询字符串匹配到所索引的字段?不用担心,您用不着在搜索时便指定特殊的分析器。除非您在查询 DSL 中指定了 search_analyzer,否则查询字符串将会使用与正在进行匹配的字段相同的分析器进行分析。和“每个字段一种语言”的示例一样,如果您有 en 和 de 这两个字段,当匹配 en 字段时会使用 english 分析器进行分析,当匹配到 de 字段时,会使用 german_custom 分析器进行分析。

查询语言

在我们深入探讨搜索策略之前,您首先需要知道有关对用户的查询字符串应用语言识别模型的一些背景信息,这一点很重要。您可能会想,“好的,既然我们知道了所索引文档的(主要)语言,为什么不直接对查询字符串进行语言识别,然后再对相应的字段或索引执行正常搜索呢?”很遗憾,搜索查询一般很短。有可能非常非常短!早在 2001 年就有一项关于曾十分热门的 Excite 网络搜索引擎的研究 [1],这项研究表明用户查询的内容平均下来只有 2.4 个词!这是很久之前的研究了,尽管这些年由于“对话式搜索”和“自然语言查询”(例如“我怎样使用 Elasticsearch 搜索多语言语料库”)的出现已经发生了巨大变化,但搜索查询仍然太短,并不足以确定语言。很多语言识别算法在多于 50 个字符时才能实现最佳效果 [2]。除此之外还有一个问题,我们的搜索查询内容通常是专有名词、实体名称或者科学术语,例如“Justin Trudeau”(贾斯廷·特鲁多)、“Foo Fighters”(喷火战机乐队)或“plantar fasciitis”(足底筋膜炎)。用户可能希望查找以任意语言写成的文档,但是仅仅通过这些查询字符串根本不可能知道。

因此,我们不推荐仅对查询文本使用语言识别模型(无论哪类模型)。如果真的想使用用户的查询语言来选择搜索字段或索引,您最好考虑通过其他方法来利用有关用户的明示或暗示信息。举例而言,暗示背景信息可能包括使用的网站域名(例如 .com 或 .de),或者应用是从哪个应用商店下载的(例如美国商店或德国商店)。然而,大多数情况下,您最好还是直接询问用户!很多网站在新用户首次访问时都会让用户选择地区。您也可以考虑(使用词聚合)对文档语言进行分面,从而让用户指导您找到他们感兴趣的语言。

每个字段一种语言

如果使用“每个字段一种语言”策略,由于会有多个语言子字段,所以我们需要同时对所有子字段进行搜索并选择分数最高的字段。这一方法十分直接,因为我们在索引管道中只设置了一个语言字段。所以,尽管我们对多个字段进行搜索,但是其中只有一个填充有内容。为了实现这一结果,我们需要将 multi_match 查询和 best_fields(默认)类型结合起来使用。这一组合会作为一个 dis_max 查询予以执行,而且我们之所以使用此组合是因为我们只对在单一字段中(而非跨越多个字段)匹配到所有词感兴趣。

GET lang-per-field/_search 
{ 
  "query": { 
    "multi_match": { 
      "query": "jahr",
      "type": "best_fields",
      "fields": [ 
        "contents.de",
        "contents.en",
        "contents.ja",
        "contents.ko",
        "contents.zh" 
      ] 
    } 
  } 
}

如想搜索所有语言,我们还可以将 contents.default 字段添加到 multi_match 查询中。“每个字段一种语言”策略的另一优势是能够使用已识别到的语言来帮助提高文档权重(例如上面所讨论的与用户的语言或地区进行匹配的方法)。由于这种方法能够直接影响相关性,所以可同时改善准确率和查全率。类似地,如想搜索单种语言,譬如已经知道用户的查询语言,则我们可以对该语言所对应的语言字段(例如 contents.de)使用 match 查询。

每个索引一种语言

使用“每个索引一种语言”策略时,我们拥有多个语言索引,但是每个索引的字段名称都相同。这意味着我们可以使用单个简单查询,提交搜索请求时只需指定索引模式就可以了:

GET lang-per-index_*/_search 
{ 
  "query": { 
    "match": { 
      "contents.text": "jahr" 
    } 
  } 
}

如想搜索所有语言,我们可使用索引模式,此模式要能匹配到默认索引:lang-per-index*(注意没有下划线)。如想搜索单种语言,我们可以直接使用该种语言的索引,例如 lang-per-index_de

示例

使用在“设计初衷”一节所描述的相同示例,我们可以在 WiLI-2018 语料库中进行搜索。在演示项目中试一下这些命令,看看结果怎样。

分解:

# 仅准确匹配“jahr”这个词 
bin/search --strategy default jahr
# 匹配“jahr”、“jahre”、“jahren”、“jahrhunderts”等等
bin/search --strategy per-field jahr

常用词语:

# 仅准确匹配“computer”这个词,结果中包含多种语言 
bin/search --strategy default computer
# 同时匹配德语复合词:“Computersicherheit”(计算机安全) 
bin/search --strategy per-field computer

非拉丁字母:

# 对于“网络”这个词,标准分析器的结果精确度很差,返回了不相关/不匹配的结果 
bin/search --strategy default 网络
# ICU 和针对具体语言的分析过程返回了正确内容,但请注意分数不同 
bin/search --strategy icu 网络 
bin/search --strategy per-field 网络

对比

基于对这两种策略的介绍,您会真正使用哪一种呢?这得看具体情况。下面是每种方法的优势和劣势,希望能帮助您做出决定。

优势劣势
每个字段一种语言
  • 只有一个索引,管理起来很轻松
  • 支持一份文档中有多种语言
  • 即使文档包含多种语言,也能提供单一的数据源
  • 允许基于语言提高文档权重
  • 映射和查询更为复杂
  • 由于支持的语言和字段增加,所以性能较慢
每个索引一种语言
  • 查询简单
  • 由于每个索引会有一个查询请求,所以搜索速度快
  • 能够基于语言的使用情况单独调整索引大小
  • 需要管理多个索引
  • 如果文档中有多种语言,不能很轻松地支持将单份文档索引至多个索引

如果您仍不能决定,我们建议您两种策略都尝试一下,看看对于您的数据集每种策略的效果如何。如果您的数据集有相关性标签,您也可以使用排名评估 API 来看一下不同策略的相关性是否有差别。

更多方法

我们了解了可通过哪两种基本的语言识别策略来对多语言语料库进行索引和搜索。配合采集管道的强大功能,我们能够实现各种各样的其他方法和变型。您不妨探索一下如下示例:

  • 将普遍使用字符的语言映射到单一字段中,例如将中文、日语和韩语映射到一个 cjk 字段并使用 cjk 分析器,然后使用标准分析器将 enfr 映射到 latin 字段(请参见 examples/olympics.txt)。
  • 将不认识的语言或非拉丁字符映射到 icu 字段,并使用 icu 分析器(请参见 config/mappings/lang-per-field.json)。
  • 使用处理器条件字符处理器进行设置,从而将多个排名最前且高于某个阈值的语言写入一个字段(以进行分片/筛选)。
  • 将文档中的多个字段连接起来形成单一字段,从而识别语言,还可以选择用其来进行搜索(例如在 all_contents 字段中),或者也可以在识别出语言后继续遵照“每个字段一种语言”策略(参见:examples/simulate-concatenation.txtexamples/simulate-concatenation.out.json)。
  • 使用字符处理器,且只有在满足下列条件时才选择主要语言:排名第一的类别高出特定阈值(例如 60% 或 50%),或者显著高出所预测的排名第二的类别(例如高于 50% 并且比排名第二的类别至少高 10%)。

总结

希望本篇博文可以让您获得初步认识,了解如何成功使用语言识别模型来进行多语言搜索!我们真诚希望倾听您的意见,欢迎加入我们的论坛并勇敢说出您的想法。无论您成功应用了语言识别模型,还是遇到了任何问题,都请告诉我们哦。

参考资料

  1. Amanda Spink、Dietmar Wolfram、Major B. J. Jansen、Tefko Saracevic。2001。Searching the Web: The Public and Their Queries(网上搜索:公众和他们的查询)。Journal of the American Society for Information Science and Technology。第 52 卷第 3 期第 226-234 页。
  2. A. Putsma。2001。Applying Monte Carlo Techniques to Language Identification(将蒙特卡罗方法应用于语言识别)。