🔍

オートコンプリートでの日本語入力の問題を解決する

2024/12/01に公開

はじめに

本記事は「情報検索・検索技術 Advent Calendar 2024」の1日目の記事です。
こんにちは、ナレッジワークでエンジニアをしている@togatogaです。興味は「検索システム」、「自然言語処理」、「Rust」です。ナレッジワークでは検索機能の開発・運用を担当しています。
ナレッジワークでお客様がアップロードした営業資料を検索できる機能を提供しています。しかし何の検索キーワードを入力すれば良いかわからないという声があり、検索補助としてQuery Auto Completion(QAC、クエリオートコンプリート)を実装する事にしました。
ナレッジワークのQAC
画像はナレッジワークのQACの画面です。「ナレッジ」と入力した場合に表示されるサジェストキーワードです。
一般的にQACで表示されるサジェストキーワードはユーザーの検索ログから生成されますが、MVPを提供するという事で営業資料の「タイトル」と「説明文(ナレッジワーク内では活用ガイドと呼ぶ)」のテキストからサジェストキーワードを生成する事にしました。
ナレッジワークの画面
「商品A FAQ集」がタイトル、その真下に書いてあるのが「説明文(活用ガイド)」

本記事ではナレッジワークでのクエリオートコンプリート(QAC)の実装について説明します。具体的には日本語入力の問題を述べ、それぞれどのように解決したかを説明します。

オートコンプリート(QAC)の実装

ナレッジワークでは検索基盤をElastic Cloud上のElasticsearchを採用しており、今回のオートコンプリート機能もElastic Cloud上にオートコンプリート専用のインスタンスとインデックスを作成しました。

サジェストキーワードの抽出と生成

ElasticsearchのAnalyzer APIelasticsearch-sudachiを用いてテキストからサジェストキーワードを抽出しました。Sudachiを採用するメリットの一つとしてはSudachiは正規化による表記揺れの対応が挙げられます。例えば、「グーグル」と「google」を同じ単語として扱うことができます。これを用いると「グーグル」と入力しても「google」というサジェストキーワードが表示されるようになります。

グーグル
「グーグル」と入力したときに表示されるサジェストキーワード一覧

形態素解析で得られた単語をそのまま使ってしまうと、助詞(の、は)などがサジェストキーワードとして表示されてしまったり、サジェストキーワードが短すぎて使用感が良くないことが分かりました。(e.g. 新卒採用 -> 「新卒」「採用」)
そこでサジェストキーワードとして採用するのは「名詞」のみに限定し、隣接する単語を結合してある程度の長さのサジェストキーワードを生成するようにしました。また前処理としてはURLを削除するなどノイズになりえそうな文字列を削除する処理を行いました。

読み仮名を付与

サジェストキーワードの読み仮名をインデックスのマッピングに含めることでユーザーは「ローマ字」や「ひらがな(カタカナ)」で入力してもサジェストキーワードを表示することができます。
ふどうさんfudousan
「ふどうさん」「fudousan」と入力したときに表示されるサジェストキーワード一覧

これはsudachi_readingformを使うことで読み仮名を取得しています。
注意点としてはSudachiでは辞書にない単語の読み仮名に対しては空文字列を返す実装になっています。Readingform filter for OOVs

日本語入力の問題を解決する

日本語入力の問題を述べ、それぞれどのように解決したかを説明します。

ローマ字入力の課題

PCで日本語入力する為には一般的にはローマ字入力を使うことが多いです。しかしローマ字入力には表記揺れが存在し、ユーザーが検索した際に表記揺れによってヒットしない問題があります。
ローマ字入力には「ヘボン式」と「訓令式」があります、また「MS-IME形式」があります。例えば、「ナレッジ」という単語のローマ字入力には以下のようないくつかのパターンが存在します。

  • narezzi
  • narejji
  • nareltuzi
  • nareltuji
  • narextuji

この場合「ナレッジ」という単語に対してもし読み仮名のローマ字が「narezzi」しか登録されていない場合、「narejji」や「nareltuzi」、「narextuji」で検索しても「ナレッジ」がヒットしない問題があります。
これはユーザー体験を損なうため、これらのローマ字の違いを吸収するためにローマ字をカタカナに変換する処理を実装して解決しました。
これにより「narezzi」「narejji」「nareltuzi」「nareltuji」「narextuji」のようなローマ字入力に対して一括して「ナレッジ」と変換することでユーザーが検索した際に「ナレッジ」がヒットするようになりました。

narezzinareltuji
「narezzi」「nareltuji」と入力したときに「ナレッジ」に関連したサジェストキーワードが表示される

ローマ字カタカナ変換処理

実装したローマ字カタカナ変換処理について説明します。Google日本語入力とMS-IMEのローマ字入力の変換のルールをサーバー側(Go言語実装)に打ち込んで変換処理を実装しました。(重複子音(「tt」->「っt」)は別の変数として管理しています)

Google日本語入力の画面
Google日本語入力のローマ字変換テーブル

// ローマ字カタカナ変換テーブル
type romaKatakana struct {
 roma     []string
 katakana string
}

// mixing Google IME and MS-IME 2000
var romaKatakanaTable = []romaKatakana{
 // あ行
 {
  roma:     []string{"a"},
  katakana: "ア",
 },
 {
  roma:     []string{"i", "yi"},
  katakana: "イ",
 },
 {
  roma:     []string{"u", "wu", "whu"},
  katakana: "ウ",
 },
 {
  roma:     []string{"e"},
  katakana: "エ",
 },
 {
  roma:     []string{"o"},
  katakana: "オ",
 },
  ....
 // わ行
 {
  roma:     []string{"wa"},
  katakana: "ワ",
 },
 {
  roma:     []string{"wo"},
  katakana: "ヲ",
 },
 {
  roma:     []string{"nn", "n", "xn", "n'"},
  katakana: "ン",
 },
 {
  roma:     []string{"lwa", "xwa"},
  katakana: "ヮ",
 },
}

var geminateConsonantTable = map[string]bool{
 "qq": true,
 "vv": true,
 "ll": true,
 "xx": true,
 "kk": true,
 "gg": true,
 "ss": true,
 "zz": true,
 "jj": true,
 "tt": true,
 "dd": true,
 "hh": true,
 "ff": true,
 "bb": true,
 "pp": true,
 "mm": true,
 "yy": true,
 "rr": true,
 "ww": true,
 "cc": true,
}

ローマ字カタカナ変換処理の実装は省略しますが、シンプルにqueueを使って実装しています。
これにより「narejji」と入力した場合は以下のようなElasticsearchのクエリを生成することができます。

{
  "queries": [
    {
      "multi_match": {
        "query": "narejji",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "romaji_readingforms^0.900",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "ナレッジ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    }
  ]
}

日本語+入力中のローマ字に対してクエリを生成

一般的な日本語入力の場合、ローマ字を入力しているとリアルタイムにひらがなに変換されます。例えば、Google日本語入力を有効化した状態(Hiragana)では「touky」「toukix」と入力していると「とうky」「とうきx」と自動的に変換されます。この文字列で検索しても「東京(toukyo)」や「東急(toukyu)」などのサジェストキーワードは表示されません。このような入力に対してもサジェストキーワードを表示するために、日本語+入力中のローマ字から有効なクエリを生成するようにしました。
具体的には2つのステップからなります。例は「とうky(touky)」

  1. 文字列をひらがなからカタカナに変換
    1. 「とうky」は「トウky」に変換される
  2. 末尾の文字列からローマ字カタカナ変換を用いて複数の文字列を生成
    1. 「トウky」の「ky」を取り出す
    2. 「ky」に続くことができるローマ字カタカナ変換の文字を生成
      1. 「キャ(kya)」「キィ(kyi)」「キュ(kyu)」「キェ(kye)」「キョ(kyo)」の文字列を生成

以下は「とうky」と入力した場合、生成されるElasticsearchのクエリです。

{
  "queries": [
    {
      "multi_match": {
        "query": "touky",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "romaji_readingforms^0.900",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキャ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキィ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキュ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキェ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキョ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    }
  ]
}

以下は「とうきx(toukix)」と入力した場合、生成されるElasticsearchのクエリです。

{
  "queries": [
    {
      "multi_match": {
        "query": "とうきx",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "romaji_readingforms^0.900",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキx",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキァ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキィ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキゥ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキェ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキォ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキヵ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキヶ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキッ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキャ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキュ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキョ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキン",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキヮ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキッァ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキッィ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキッゥ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキッェ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキッォ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキッヵ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキッヶ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキッッ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキッャ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキッュ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキッョ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキッン",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    },
    {
      "multi_match": {
        "query": "トウキッヮ",
        "fields": [
          "suggestions",
          "normalized_suggestion",
          "katakana_readingforms^0.900"
        ],
        "type": "phrase_prefix",
        "max_expansions": 1
      }
    }
  ]
}

このような検索クエリを生成することで「とうky(touky)」「とうきx(toukix)」と入力した場合でも「東京(トウキョウ)」や「東急(トウキュウ)」といったサジェストキーワードを表示することができました。
とう(tou)
「とう(tou)」と入力したときに表示されるサジェストキーワード一覧
とうky(touky)とうきz
「とうky(touky)」「とうきx(toukix)」と入力したときに表示されるサジェストキーワード一覧、「東大(toudai)」「東海(toukai)」が一覧から消えている

さいごに

QACのリリース後に検索指標を確認すると、「0件ヒット率(検索後に表示される検索結果が0件)」と「0件Click率(検索後にユーザーが何も選択しない)」の指標が改善しました。
今後は検索ログを使ったサジェストキーワードの生成やランキング改善に取り組んでいきます。

ナレッジワークでは検索・MLエンジニアを募集しています。検索、推薦、LLMに興味がある方はお気軽にご応募(カジュアル面談含む)ください。

https://herp.careers/v1/kwork/KEPz0YnrR_QC
https://herp.careers/v1/kwork/jisLC1gcXO-x
https://herp.careers/v1/kwork/e_ZqKSFd2LdQ

GitHubで編集を提案
株式会社ナレッジワーク

Discussion