Mehrsprachige Suche unter Verwendung der Elasticsearch-Sprachenerkennung

Wir freuen uns, dass die Version 7.6 von Elasticsearch nicht nur einen Machine-Learning-basierten Inferenz-Ingestionsprozessor mitbringt, sondern auch eine Sprachenerkennung enthält. Das gibt uns die Gelegenheit, einige Anwendungsfälle für die Suche in mehrsprachigen Textkorpora zu beschreiben und zu erläutern, welche Rolle die Sprachenerkennung dabei spielt. Auf einige dieser Themen sind wir bereits in der Vergangenheit eingegangen und wir werden uns bei ausgewählten Beispielen auf die früheren Blogposts beziehen.

Motivation

Die Welt von heute ist hochgradig untereinander vernetzt. Das hat auch zur Folge, dass Dokumente und andere Informationsquellen in den verschiedensten Sprachen vorliegen können. Für viele Suchanwendungen stellt dies ein Problem dar. Wir müssen die Sprache, in denen diese Dokumente verfasst sind, so gut wie möglich verstehen, um sie korrekt analysieren und ein optimales Sucherlebnis bieten zu können. An dieser Stelle kommt die Sprachenerkennung ins Spiel.

Die Sprachenerkennung hilft, die Suchrelevanz für diese mehrsprachigen Textkorpora insgesamt zu verbessern. Wir möchten auch Dokumente, von denen wir noch nicht einmal wissen, in welcher Sprache sie verfasst sind, effizient durchsuchen können. Die Dokumente können dabei in einer oder in mehreren Sprachen verfasst sein. Dokumente in nur einer Sprache sind häufig in Bereichen wie der Informatik anzutreffen, in denen Englisch die vorherrschende Kommunikationssprache ist. Dagegen enthalten zum Beispiel Texte aus den Bereichen Biologie und Medizin häufig neben englischen auch lateinische Begriffe.

Durch Anwendung sprachenspezifischer Analysen können wir die Relevanz (in Bezug auf die Genauigkeit und den Recall) verbessern, denn so wird sichergestellt, dass die Begriffe in den Dokumenten verstanden, indexiert und entsprechend gesucht werden können. Die verschiedenen sprachenspezifischen Analyzer in Elasticsearch (integriert und über zusätzliche Plugins bereitgestellt) helfen, die Tokenisierung, die Tokenfilterung und die Begriffsfilterung zu verbessern:

  • Wort- und Synonymlisten werden überflüssig
  • Wortformnormalisierung: Stammformreduktion und Lemmatisierung
  • Zerlegung von Begriffen in ihre Bestandteile (z. B. für Deutsch, Niederländisch und Koreanisch)

Aus ganz ähnlichen Gründen kommt die Sprachenerkennung auch als einer der ersten Schritte bei der Verarbeitung natürlicher Sprache (Natural Language Processing, NLP) zum Einsatz, um die Anwendung hochgradig genauer sprachenspezifischer Algorithmen und Modelle zu ermöglichen. Vortrainierte NLP-Modelle wie BERT und ALBERT von Google oder GPT-2 von OpenAI werden häufig anhand sprachenspezifischer Textkorpora oder Textkorpora mit einer vorherrschenden Sprache trainiert und anschließend für Aufgaben wie die Dokumentklassifizierung, Sentimentanalyse, Entitätenerkennung (Named Entity Recognition, NER) usw. präzisiert.

Bei den folgenden Beispielen und Strategien gehen wir, sofern nicht anders angegeben, davon aus, dass die Dokumente in nur einer oder einer vorherrschenden Sprache vorliegen.

Vorteile der sprachenspezifischen Analyse

Zur weiteren Motivation werfen wir kurz einen Blick auf einige Vorteile, die sprachenspezifische Analyzer haben.

Zerlegung von Begriffen in ihre Bestandteile: Eine Eigenart des Deutschen ist es, dass Substantive durch Zusammensetzung mehrerer einzelner Substantive gebildet werden können. Das Ergebnis sind schön lange, aber auch manchmal schwer leserliche zusammengesetzte Substantive. Ein einfaches Beispiel sind Zusammensetzungen mit dem Wort „Jahr“, wie z. B. „Jahrhundert“, „Jahreskalender“ oder „Schuljahr“. Ohne einen speziellen Analyzer, der diese Wörter in ihre Bestandteile zerlegen kann, wären wir nicht in der Lage, nach „jahr“ zu suchen und Dokumente zurückzubekommen, in denen es beispielsweise ums „Schuljahr“ geht. Außerdem gelten im Deutschen andere Regeln für die Plural- und Dativbildung als in anderen Sprachen, was zur Folge hat, dass bei einer Suche nach „jahr“ auch Begriffe wie „Jahre“ (Plural) und „Jahren“ (Plural Dativ) gefunden werden sollen.

Gleicher Begriff in mehreren Sprachen: In einigen Sprachen gibt es Begriffe, die so auch in anderen Sprachen verwendet werden, oder fachspezifische Terminologie, die in vielen Sprachen gleich ist. Nehmen wir als Beispiel das englische Wort „Computer“, das in vielen Sprachen in derselben Form verwendet wird. Wenn wir nach „Computer“ suchen möchten, soll der Begriff vielleicht auch in Dokumenten gefunden werden, die nicht in Englisch verfasst sind. Wenn wir in der Lage sind, in Dokumenten zu suchen, die in bestimmten bekannten Sprachen vorliegen, und dabei auch nach gleich verwendeten Begriffen suchen können, eröffnet dies interessante Perspektiven. Ziehen wir auch hier als Beispiel Deutsch heran und gehen davon aus, dass wir Dokumente in mehreren Sprachen haben, in denen es um die Computersicherheit geht. Es bedarf eines Deutsch-Analyzers, um bei Suchen nach „Computer“ entsprechende Treffer sowohl in englisch- als auch in deutschsprachigen Dokumenten zu finden.

Sprachen mit nichtlateinischen Schriftsystemen: Für die meisten Sprachen mit lateinischem Schriftsystem funktioniert der Analyzer standard recht gut. Bei nichtlateinischen Schriftsystemen wie Kyrillisch oder den CJK-Zeichen (Chinesisch/Japanisch/Koreanisch) versagt er jedoch ziemlich schnell. In einer Serie früherer Blogposts haben wir uns angesehen, wie die CJK-Sprachen aufgebaut sind und warum es nötig ist, sprachenspezifische Analyzer zu haben. Im Koreanischen beispielsweise gibt es sogenannte Postpositionen. Das sind Suffixe, die an Substantive und Pronomen angehängt werden und deren Bedeutung verändern. Mit dem Analyzer standard können zwar mitunter Suchbegriffübereinstimmungen gefunden werden, diese lassen sich aber in der Regel nicht gut „scoren“. Das kann zwar zu einem guten Recall bei Dokumenten führen, die Genauigkeit ist jedoch mau. In anderen Fällen findet der Analyzer standard vielleicht gar keine Übereinstimmungen, sodass nicht nur die Genauigkeit, sondern auch der Recall schlecht ausfallen.

Sehen wir uns als Arbeitsbeispiel den Begriff „Olympische Winterspiele“ an. Im Koreanischen wird dafür „동계올림픽대회는“ verwendet. Der Begriff setzt sich zusammen aus „동계“ („Wintersaison“), „올림픽대회“ („Olympische Spiele“) und der Postposition „는“, einem Suffix, der angibt, dass „동계올림픽대회“ als Thema des Satzes fungiert. Wenn wir mit dem Analyzer standard nach genau dieser Zeichenfolge suchen, erhalten wir eine perfekte Übereinstimmung. Eine Suche nach „올림픽대회“ („Olympische Spiele“ ohne „Winter“) erbringt dagegen keine Ergebnisse. Wenn wir hingegen den Koreanisch-Analyzer nori verwenden, erhalten wir eine Übereinstimmung, weil „동계올림픽대회는“ („Olympische Winterspiele“) beim Indexieren korrekt tokenisiert wurde.

Einstieg in die Sprachenerkennung

Demoprojekt

Zur Illustration von Anwendungsfällen und Strategien für die Sprachenerkennung bei der Suche haben wir ein kleines Demoprojekt aufgesetzt. Es enthält alle Beispiele aus diesem Blogpost sowie einige Tools zum Indexieren und Suchen in WiLI-2018, einem mehrsprachigen Textkorpus, der als Referenz und Arbeitsbeispiel für das Experimentieren mit der mehrsprachigen Suche verwendet werden kann. Wenn Sie die Beispiele nachverfolgen möchten, ist es hilfreich (aber nicht unbedingt notwendig), das Demoprojekt mit indexierten Dokumenten zu installieren und auszuführen.

Sie können für diese Experimente Elasticsearch 7.6 lokal installieren oder die kostenlose Probeversion des Elasticsearch Service nutzen.

Erste Experimente

Die Sprachenerkennung arbeitet mit einem vortrainierten Modell, das in der Standarddistribution von Elasticsearch mitgeliefert wird. Es wird zusammen mit dem Inferenz-Ingestionsprozessor (inference) verwendet. Dazu legen Sie beim Einrichten des Inferenz-Prozessors in einer Ingest-Pipeline für model_id den Wert lang_ident_model_1 fest.

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

Die restliche Konfiguration ist mit der anderer Modelle identisch, das heißt, Sie haben die Möglichkeit, Einstellungen wie die Anzahl der auszugebenden Top-Klassen, das Ausgabefeld, das die Vorhersage enthält, und – für unsere Anwendungsfälle am allerwichtigsten – das zu verwendende Eingabefeld festzulegen. Standardmäßig geht das Modell davon aus, dass die Eingabe in einem Feld mit dem Namen „text“ enthalten ist. Im folgenden Beispiel verwenden wir die _simulate-API der Pipeline mit einigen Einzelfelddokumenten. Sie sorgt zu Inferenzzwecken dafür, dass die Eingabeinhaltsfelder dem Textfeld zugeordnet werden, ohne dass sich dieses Mapping auf andere Prozessoren in der Pipeline auswirkt. Anschließend gibt sie die drei Top-Klassen aus, damit diese untersucht werden können.

# Simulieren eines einfachen Inferenz-Setups

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"
      }
    }
  ]
}

Die Ausgabe zeigt uns jedes einzelne Dokument plus einige zusätzliche Informationen im Feld _ml.lang_ident, darunter die Wahrscheinlichkeit für die drei Top-Sprachen und in _ml.lang_ident.predicted_value die Wahrscheinlichkeit für die Top-Sprache.

{
  "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"
        }
      }
    }
  ]
}

Das sieht gut aus! Für das erste Dokument wurde Deutsch und für das zweite und dritte Dokument Englisch erkannt, für das dritte Dokument auch noch ein bisschen Latein.

Strategien für die Sprachenerkennung bei der Suche

Nach diesem einfachen Beispiel für die Sprachenerkennung ist es nun an der Zeit, eine Strategie für das Indexieren und Suchen zu entwickeln.

Wir werden im Folgenden zwei grundlegende Indexierungsstrategien verwenden: die feldbasierte Sprachenerkennung und die indexbasierte Sprachenerkennung. Bei der Strategie der feldbasierten Sprachenerkennung werden wir nur einen Index mit mehreren sprachenspezifischen Feldern erstellen und für jede Sprache einen eigenen Analyzer verwenden. Zum Zeitpunkt der Suche können wir festlegen, ob die Suche nur anhand eines bekannten Sprachenfelds oder anhand aller Sprachenfelder erfolgen soll, und wir können das Feld mit der besten Übereinstimmung auswählen. Bei der Strategie der indexbasierten Sprachenerkennung werden wir mehrere sprachenspezifische Indizes mit verschiedenen Mappings erstellen, wobei das indexierte Feld einen Analyzer für genau diese Sprache hat. Zum Zeitpunkt der Suche können wir ähnlich wie bei der feldbasierten Sprachenerkennung vorgehen und mit einem Indexmuster in der Suchanfrage festlegen, ob die Suche nur anhand eines einzelnen Sprachenindex oder anhand mehrerer Indizes erfolgen soll.

Vergleichen Sie diese beiden Strategien damit, wie Sie bisher vorgegangen sind: Sie mussten ein und dieselbe Zeichenfolge mehrfach indexieren, jedes Mal mit einem Feld oder Index mit einem sprachenspezifischen Analyzer. Dieser Ansatz kann zwar funktionieren, er führt aber zu jeder Menge Dopplungen, was das Abfragetempo ausbremst und deutlich mehr Speicherplatz beansprucht als nötig ist.

Indexieren

Sehen wir uns damit die beiden Indexierungsstrategien an, denn sie bestimmen, welche Suchstrategien wir verwenden können.

Feldbasiert

Bei der Strategie der feldbasierten Sprachenerkennung verwenden wir die Ausgabe der Sprachenerkennung und eine Reihe von Prozessoren in einer Ingest-Pipeline, um den Wert des Eingabefeldes in einem sprachenspezifischen Feld zu speichern. Wir unterstützen dabei nur einen begrenzten Satz von Sprachen (Deutsch, Englisch, Koreanisch, Japanisch und Chinesisch), da wir für jede Sprache einen spezifischen Analyzer einrichten müssen. Alle Dokumente, die in einer nicht von uns unterstützten Sprachen vorliegen, werden mit dem Analyzer standard in einem standardmäßig definierten Feld indexiert.

Eine vollständige Definition der Pipeline ist im folgenden Demoprojekt zu finden: config/pipelines/lang-per-field.json

Ein Mapping zur Unterstützung dieser Indexierungsstrategie würde wie folgt aussehen:

{
  "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"
          }
        }
      }
    }
  }
}

(Hinweis: Die Konfiguration für den Deutsch-Analyzer wurde im Beispiel oben aus Platzgründen weggelassen. Sie ist hier zu finden: config/mappings/de_analyzer.json)

Wie im vorherigen Beispiel verwenden wir zum Erkunden die _simulate-API der Pipeline:

# Simulieren einer feldbasierten Sprachenerkennung und Ausgabe der drei Top-Sprachenklassen für die weitere Untersuchung

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"
      }
    }
  ]
}

Die Ausgabe bei einer feldbasierten Sprachenerkennung sieht wie folgt aus:

{
  "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"
        }
      }
    }
  ]
}

Wie erwartet werden die Felder mit Deutsch in contents.de, die Felder mit Englisch in contents.en, die Felder mit Koreanisch in contents.ko usw. gespeichert. Es sei darauf hingewiesen, dass wir auch ein paar Beispiele für nicht unterstützte Sprachen untergemischt haben: Französisch und Latein. Diese werden nicht als unterstützt („supported“) gekennzeichnet und sie sind nur bei einer Suche anhand des standardmäßigen Feldes verfügbar. Sehen Sie sich auch die Top-Vorhersagen für das Latein-Beispiel an. Das Modell denkt offenbar, es handele sich um Latein, was richtig ist, es ist sich aber unsicher und schlägt mit einer hohen Wahrscheinlichkeit auch noch Französisch vor.

Dies ist nur ein einfaches Beispiel für eine Ingest-Pipeline mit Sprachenerkennung, das aber dennoch zeigt, was möglich ist. Die Flexibilität von Ingest-Pipelines ermöglicht die Verwendung in vielen unterschiedlichen Szenarien. Ein paar Alternativen sehen wir uns noch am Ende des Blogposts an. Einige der Schritte in diesem Beispiel könnten in einer Produktionspipeline kombiniert oder weggelassen werden, denken Sie aber immer daran, dass sich eine gute Datenverarbeitungspipeline nicht durch möglichst wenige Zeilen auszeichnet, sondern dadurch, dass sie sich leicht lesen und verstehen lässt.

Indexbasierte Sprachenerkennung

Unsere Strategie zur indexbasierten Sprachenerkennung verwendet dieselben Grundbausteine wie die Pipeline für die feldbasierte Sprachenerkennung. Der große Unterschied zwischen beiden besteht darin, dass die Speicherung nicht in einem sprachenspezifischen Feld, sondern in einem anderen Index erfolgt. Dies ist möglich, weil wir zum Zeitpunkt des Ingestierens das _index-Feld eines Dokuments festlegen können, was es uns ermöglicht, den Standardwert durch einen sprachenspezifischen Indexnamen zu ersetzen. Wenn wir die Sprache nicht unterstützen, lassen wir diesen Schritt weg, sodass das Dokument im standardmäßigen Index indexiert wird. So einfach ist das!

Eine vollständige Definition der Pipeline ist im folgenden Demoprojekt zu finden: config/pipelines/lang-per-index.json

Ein Mapping zur Unterstützung dieser Indexierungsstrategie würde wie folgt aussehen:

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

Bei diesem Mapping ist zu beachten, dass wir keinen speziellen Analyzer angegeben haben und stattdessen diese Datei als Vorlage verwenden. Den Analyzer für die jeweilige Sprache legen wir fest, wenn wir die einzelnen sprachenspezifischen Indizes erstellen.

Simulieren dieser Pipeline:

# Indexbasiertes Simulieren einer Sprache und Ausgabe der drei Top-Sprachenklassen für die weitere Untersuchung

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"
      }
    }
  ]
}

Die Ausgabe bei einer indexbasierten Sprachenerkennung sieht wie folgt aus:

{
  "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"
        }
      }
    }
  ]
}

Wie zu erwarten ist, sind die Sprachenerkennungsergebnisse identisch mit denen bei der feldbasierten Strategie. Der einzige Unterschied besteht darin, wie wir diese Informationen in der Pipeline nutzen, um ein Dokument an den richtigen Index zu leiten.

Suchen

Was ist nun angesichts der beiden Indexierungsstrategien der beste Weg für die Suche? Wie oben bereits erwähnt stehen uns für jede der beiden Strategien mehrere Möglichkeiten offen. Eine Frage, die in diesem Zusammenhang häufig gestellt wird, lautet: Wie legt man einen sprachenspezifischen Analyzer für die Abfragezeichenfolge so fest, dass sie zum indexierten Feld passt? Darüber brauchen Sie sich keine Sorgen zu machen, denn zum Zeitpunkt der Suche müssen Sie keinen speziellen Analyzer angeben. Sofern Sie in Ihrer Abfrage-DSL keinen search_analyzer angeben, wird die Abfragezeichenfolge vom selben Analyzer analysiert wie das entsprechende Feld. Wenn wir, wie in den Beispielen für die feldbasierte Sprachenerkennung, die Felder „en“ und „de“ haben, wird die Abfragezeichenfolge für das Feld „en“ mit dem Analyzer english und die für das Feld „de“ mit dem Analyzer german_custom analysiert.

Abfragesprache

Bevor wir uns eingehender mit den Suchstrategien befassen, ist es wichtig, etwas Kontext zur Sprachenerkennung in der Abfragezeichenfolge des Nutzers selbst festzulegen. Sie werden vielleicht denken: „Wir kennen ja jetzt die (vorherrschende) Sprache der indexierten Dokumente, dann würde es doch damit getan sein, die Sprachenerkennung auf die Abfragezeichenfolge anzuwenden und eine normale Suche anhand des entsprechenden Feldes oder Index durchzuführen.“ Leider neigen Suchabfragen dazu, kurz zu sein. Also so richtig kurz! Eine Studie [1] von 2001, in der es um die gute alte Websuchmaschine Excite ging, hat gezeigt, dass die durchschnittliche Nutzerabfrage aus lediglich 2,4 Begriffen bestand! Auch wenn das eine Weile her ist und sich durch Conversational Search und Abfragen in natürlicher Sprache („Wie verwende ich Elasticsearch für die Suche in mehrsprachigen Textkorpora?“) seitdem viel geändert hat, sind Suchabfragen tendenziell immer noch zu kurz, um sie für die Sprachenerkennung nutzen zu können. Viele Algorithmen zur Sprachenerkennung funktionieren am besten, wenn mehr als 50 Zeichen vorhanden sind [2]. Dazu kommt noch, dass wir es häufig mit Suchabfragen zu tun haben, bei denen es sich um Personen- oder Eigennamen oder wissenschaftliche Bezeichnungen handelt, zum Beispiel „Justin Trudeau“, „Foo Fighters“ oder „Digitalis purpurea“. Der Nutzer möchte vielleicht, dass Dokumente unabhängig von ihrer Sprache gefunden werden, was aber durch reines Analysieren dieser Art von Abfragezeichenfolgen nicht erkennbar ist.

Daher empfehlen wir, bei Abfragezeichenfolgen allein nicht mit Sprachenerkennung (welcher Art auch immer) zu arbeiten. Wenn Sie für die Auswahl des Suchfelds oder ‑index dennoch die Sprache der Nutzerabfrage verwenden möchten, sollten Sie über andere Ansätze nachdenken, bei denen die impliziten oder expliziten Informationen über den Nutzer verwendet werden. Impliziter Kontext ließe sich beispielsweise aus der Domain der Website (z. B. .com oder .de) oder den Locale-Informationen des App Stores ableiten, aus dem Ihre App heruntergeladen wurde (z. B. ob aus dem US-Store oder dem deutsche Store). In den meisten Fällen besteht der beste Weg allerdings darin, den Nutzer einfach zu fragen! Viele Websites bitten neue Nutzer bei ihrem ersten Besuch um eine Auswahl von Land und Sprache. Auch das Facettieren (mit einer Begriffsaggregation) über die Dokumentsprachen hinweg käme infrage, um den Nutzer zu den Sprachen zu leiten, an denen er interessiert ist.

Feldbasiert

Bei der feldbasierten Strategie haben wir mehrere Unterfelder für die Sprachenfestlegung. Bei der Suche müssen wir diese alle auf einmal berücksichtigen und das Feld mit dem Top-Score auswählen. Das ist relativ unkompliziert, da wir in der Indexierungspipeline nur ein Sprachenfeld festgelegt haben. Bei der felderübergreifenden Suche wird also nur ein Feld davon ausgefüllt. Zu diesem Zweck verwenden wir eine multi_match-Abfrage des Typs best_fields (Standard). Diese Kombination wird als dis_max-Abfrage ausgeführt und wir verwenden sie, weil wir alle übereinstimmenden Begriffe in einem Feld finden möchten, nicht in mehreren Feldern.

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

Wenn wir in allen Sprachen suchen möchten, können wir der multi_match-Abfrage auch das Feld contents.default hinzufügen. Ein Vorteil der feldbasierten Strategie besteht darin, dass wir die erkannten Sprachen auch dazu verwenden können, bestimmte Dokumente nach vorn zu stellen, z. B. solche, die mit der Sprache oder dem Land des Nutzers übereinstimmen (siehe oben). Dadurch können sich Genauigkeit und Recall verbessern, weil auf diese Weise die Relevanz direkt beeinflusst werden kann. Das Gleiche gilt auch, wenn nur in einer Sprache gesucht werden soll, also wenn wir z. B. die Abfragesprache des Nutzers kennen. Wir können dann eine einfache match-Abfrage für das Sprachenfeld dieser Sprache, z. B. contents.de, verwenden.

Indexbasierte Sprachenerkennung

Bei der indexbasierten Strategie haben wir mehrere Sprachenindizes, jeder Index enthält aber dieselben Feldnamen. Das bedeutet, dass wir nur eine einfache Abfrage verwenden können. Beim Erstellen der Suchanfrage geben wir dann ein Indexmuster an:

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

Wenn wir in allen Sprachen suchen möchten, verwenden wir ein Indexmuster, das auch zum standardmäßigen Index passt: lang-per-index* (kein Unterstrich!). Wenn wir nur in einer Sprache suchen möchten, können wir einfach den Index für diese Sprache verwenden, z. B. lang-per-index_de.

Beispiele

Anhand der im Abschnitt „Motivation“ beschriebenen Beispiele können wir probieren, eine Suche in unserem Textkorpus WiLI-2018 durchzuführen. Verwenden Sie diese Befehle mit dem Demoprojekt und sehen Sie sich an, was passiert.

Zerlegung von Begriffen in ihre Bestandteile:

# Gefunden werden nur Textstellen, die genau den Begriff "jahr" enthalten 
bin/search --strategy default jahr
# Gefunden werden "jahr", "jahre", "jahren", "jahrhunderts" usw. 
bin/search --strategy per-field jahr

Gleicher Begriff in mehreren Sprachen:

# Gefunden werden nur Textstellen, die genau den Begriff "computer" enthalten; Suchergebnisse enthalten Übereinstimmungen aus mehreren Sprachen 
bin/search --strategy default computer
# Gefunden werden auch zusammengesetzte Wörter im Deutschen: "Computersicherheit" 
bin/search --strategy per-field computer

Sprachen mit nichtlateinischen Schriftsystemen:

# Anzalyzer "standard" wird ungenau und gibt irrelevante/nicht passende Ergebnisse mit "Netzwerk"/"Internet" zurück: "网络" 
bin/search --strategy default 网络
# ICU- und sprachenspezifische Analyse sorgen für korrekte Ergebnisse, aber beachten Sie die unterschiedlichen Scores 
bin/search --strategy icu 网络 
bin/search --strategy per-field 网络

Vergleich

Welche der beiden Strategien sollte nun verwendet werden? Darauf gibt es keine eindeutige Antwort. Um Ihnen die Entscheidung zu erleichtern, finden Sie in der folgenden Tabelle einige der Vor-und Nachteile der beiden Ansätze.

VorteileNachteile
Feldbasiert
  • Nur ein Index, daher einfache Verwaltung
  • Unterstützung mehrerer Sprachen pro Dokument
  • Nur eine Referenzquelle, auch dann, wenn Dokumente mehrere Sprachen enthalten
  • Sprachenspezifisches Bevorzugen von Dokumenten
  • Komplexere Mappings und Abfragen
  • Wird langsamer, je mehr Sprachen und Felder unterstützt werden
Indexbasierte Sprachenerkennung
  • Einfache Abfrage
  • Schnelles Suchen, da jeder Index eine Abfrage bekommt
  • Indizes können je nach Verwendung der Sprache individuell skaliert werden
  • Es müssen mehrere Indizes verwaltet werden
  • Indexieren eines einzelnen Dokuments, in dem mehrere Sprachen vorhanden sind, in mehrere Indizes ist nicht ohne Weiteres möglich

Wenn Ihnen die Entscheidung immer noch schwer fällt, empfehlen wir, beide Strategien auszuprobieren und zu sehen, wie diese jeweils für Ihre Daten funktionieren. Wenn Sie einen Datensatz mit Relevanz-Labels haben, können Sie auch die Ranking-Auswertungs-API verwenden und sehen, ob es bei den beiden Strategien Unterschiede bei der Relevanz gibt.

Weitere Ansätze

Wir haben uns zwei grundlegende Strategien für die Verwendung der Sprachenerkennung und das Indexieren und Suchen in mehrsprachigen Textkorpora angesehen. Dank der Leistungsfähigkeit von Ingest-Pipelines können wir auch zahlreiche andere Ansätze verfolgen und Modifikationen vornehmen. Dazu ein paar Beispiele:

  • Mapping von Sprachen mit gemeinsamem Schriftsystem in ein einzelnes Feld, z. B. Mapping von Chinesisch, Japanisch und Koreanisch in ein Feld namens cjk und anschließende Verwendung des Analyzers cjk und Mapping von en und fr in ein Feld namens latin und anschließende Verwendung des Analyzers standard (siehe examples/olympics.txt).
  • Mapping von unbekannten Sprachen oder von Sprachen mit nichtlateinischem Schriftsystem in ein Feld namens icu und Verwendung des Analyzers icu (siehe config/mappings/lang-per-field.json).
  • Verwendung einer Prozessorbedingung oder eines Skript-Prozessors, Belegen eines Feldes mit mehreren Top-Sprachen über einem bestimmten Grenzwert (für das Facettieren/Filtern).
  • Verketten mehrerer Felder des Dokuments zu einem einzelnen Feld zur Erkennung der Sprache und optionale Verwendung des Feldes zur Suche (z. B. in einem all_contents-Feld) oder einfaches Weiterverfolgen der feldbasierten Strategie nach Erkennung der Sprache (siehe examples/simulate-concatenation.txt und examples/simulate-concatenation.out.json).
  • Verwendung eines Skript-Prozessors, Auswahl der vorherrschenden Sprache, aber nur dann, wenn die Top-Klasse über dem Grenzwert liegt (z. B. 60 % oder 50 %) oder der Wert deutlich über dem der an zweiter Stelle genannten Klasse ist (z. B. über 50 % und mehr als 10 % höher als die zweite Klasse).

Fazit

Ich hoffe, dass dieser Blogpost Ihnen ein paar Vorstellungen davon vermitteln konnte, wie die Sprachenerkennung für die mehrsprachige Suche erfolgreich eingesetzt werden kann, sodass Sie ihn als Ausgangspunkt für Ihre weitere Arbeit verwenden können. Wir würden uns über Feedback dazu in unserem Diskussionsforum freuen. Berichten Sie uns, ob Sie die Sprachenerkennung erfolgreich einsetzen oder ob es bei Ihnen irgendwelche Probleme damit gibt.

Referenzen

  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. Volume 52, Issue 3, S.226–234.
  2. A. Putsma. 2001. Applying Monte Carlo Techniques to Language Identification.