SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

はてなエンジニア直伝! はてなのサービスを取り巻く先端技術トピック

検索技術と自然言語処理技術を駆使して話題のトピックをひとまとめ ~はてなブックマークのトピックページの作り方

はてなエンジニア直伝! はてなのサービスを取り巻く先端技術トピック 第2回

  • X ポスト
  • このエントリーをはてなブックマークに追加

トピックキーワードの生成

 まず、はてなブックマークでのインデックス構造について説明します。以下は、はてなブックマークの記事データに対するElasticsearchのインデックスの一部を取り出したものですが、記事のタイトルや本文、時間情報などがインデックスされています。

はてなブックマークの記事データに対するElasticsearchのmapping(一部)
{
  "mapping": {
    "entry": {
      "_id": {
        "path": "id"
      },
      "properties": {
        "id": { "type": "integer" },
        "url": { "type": "string" },
        "title": { "type": "string" },
        "content": { "type": "string" },
        "created": { "type": "date", "format": "date_time_no_millis" }
      }
    }
  }
}

 トピックキーワード生成ではSignificant Terms Aggregationを2層にして利用します。タイトルと本文のそれぞれに対して適用しますが、本稿では、記事のタイトルに対してSignificant Terms Aggregationを適用する例を使って説明します。以下の例では2015年1月13日から2015年1月20日までの1週間に話題となったトピックキーワードを生成しています。

トピックキーワード生成のリクエスト例
{
  "query": {
    "filtered": {
      "filter": {
        "bool": {
          "must": [
            { "range": { "created": {
              "to": "2015-01-20T00:00:00+09:00",
              "from": "2015-01-13T00:00:00+09:00"
            } } }
          ]
        }
      }
    }
  },
  "aggs": {
    "terms": {
      "significant_terms": {
        "field": "title", "size": 5, "jlh": {}
      },
      "aggs": {
        "terms": {
          "significant_terms": {
            "field": "title", "size": 5, "jlh": {}
          }
        }
      }
    }
  }
}
トピックキーワード生成のレスポンス例(一部)
{
   "aggregations": {
      "terms": {
         "doc_count": 13757,
         "buckets": [
            {
               "key": "シャルリ", "doc_count": 55, "score": 2.50210212027157, "bg_count": 400,
               "terms": {
                  "doc_count": 55,
                  "buckets": [
                     { "key": "シャルリ", "doc_count": 55, "score": 129926.04000000001, "bg_count": 400 },
                     { "key": "エブド", "doc_count": 39, "score": 99737.61800012618, "bg_count": 262 },
                     { "key": "赦す", "doc_count": 3, "score": 5522.228146399055, "bg_count": 28 },
                     { "key": "ムハンマド", "doc_count": 7, "score": 4729.318328535611, "bg_count": 178 },
                     { "key": "風刺", "doc_count": 16, "score": 4172.5661787444915, "bg_count": 1054 }
                  ]
               }
            },
            {
               "key": "エブド", "doc_count": 39, "score": 2.277874539496356, "bg_count": 221,
               "terms": {
                  "doc_count": 39,
                  "buckets": [
                     { "key": "エブド", "doc_count": 39, "score": 183722.4660633484, "bg_count": 221 },
                     { "key": "シャルリ", "doc_count": 38, "score": 115756.92828528726, "bg_count": 333 },
                     { "key": "赦す", "doc_count": 3, "score": 8580.414623837702, "bg_count": 28 },
                     { "key": "ムハンマド", "doc_count": 4, "score": 4314.2186626289185, "bg_count": 99 },
                     { "key": "風刺", "doc_count": 10, "score": 3132.9434242977027, "bg_count": 852 }
                  ]
               }
            },
            {
               "key": "スイスフラン", "doc_count": 57, "score": 1.8023504538916841, "bg_count": 596,
               "terms": {
                  "doc_count": 57,
                  "buckets": [
                     { "key": "スイスフラン", "doc_count": 57, "score": 89385.22483221476, "bg_count": 596 },
                     { "key": "急騰", "doc_count": 15, "score": 1992.9018004791897, "bg_count": 1851 },
                     { "key": "上限", "doc_count": 8, "score": 1391.6562517093605, "bg_count": 754 },
                     { "key": "損失", "doc_count": 8, "score": 1118.6385812659432, "bg_count": 938 },
                     { "key": "暴騰", "doc_count": 5, "score": 766.1320808990831, "bg_count": 535 }
                  ]
               }
            },
            {
               "key": "ニュース", "doc_count": 1966, "score": 1.6056558085044332, "bg_count": 732518,
               "terms": {
                  "doc_count": 1966,
                  "buckets": [
                     { "key": "ニュース", "doc_count": 1966, "score": 84.61743192658747, "bg_count": 732518 },
                     { "key": "yahoo", "doc_count": 583, "score": 26.437202086858722, "bg_count": 206296 },
                     { "key": "風刺", "doc_count": 44, "score": 24.771369040835854, "bg_count": 1267 },
                     { "key": "ようじ", "doc_count": 28, "score": 20.977907336532066, "bg_count": 606 },
                     { "key": "nhk", "doc_count": 271, "score": 20.15849908084594, "bg_count": 58713 }
                  ]
               }
            },
            {
               "key": "ようじ", "doc_count": 48, "score": 1.511414342391317, "bg_count": 504,
               "terms": {
                  "doc_count": 48,
                  "buckets": [
                     { "key": "ようじ", "doc_count": 48, "score": 80424.74801587302, "bg_count": 504 },
                     { "key": "つま", "doc_count": 34, "score": 26207.617735627864, "bg_count": 776 },
                     { "key": "混入", "doc_count": 22, "score": 4192.095123673341, "bg_count": 2031 },
                     { "key": "スナック菓子", "doc_count": 4, "score": 2759.6237064270153, "bg_count": 102 },
                     { "key": "手配", "doc_count": 8, "score": 2128.3030350766644, "bg_count": 529 }
                  ]
               }
            }
         ]
      }
   }
}

 以下の5つのトピックキーワード集合が生成されています。

  • シャルリ、エブド、赦す、ムハンマド、風刺
  • エブド、シャルリ、赦す、ムハンマド、風刺
  • スイスフラン、急騰、上限、損失、暴騰
  • ニュース、yahoo、風刺、ようじ、nhk
  • ようじ、つま、混入、スナック菓子、手配

 ただし、ここには以下の2つの問題があります。

重複トピックキーワード集合

 1つ目のトピックキーワードと2つめのトピックキーワードは同じ話題を表しています。この問題については、トピックキーワードの生成時には対処せずに、トピック生成処理の最後のフェーズでトピックをマージすることで解決します。

トピックキーワードの誤生成

 ニュースやyahoo、nhkなど、トピックを表さないキーワードが生成されています。これは記事タイトルにサイト名が含まれていることが要因で、記事タイトルによく出現する単語はスコアが高くなるため、誤ってトピックキーワードとして生成されています。これを解決するために、ニュースなどのよく出現する単語をストップワードとして用意します。ただし、人手で用意するのは管理等も含めコストが高いため、期間を指定せずにSignificant Terms Aggreagationを利用することで自動生成します。

 記事タイトルによく出現する単語はトピックキーワードではないという仮定のもと、これらをストップワードとして利用します。

 ストップワード、および、一定のスコア以下のキーワードを除外し、残ったものをトピックキーワードとして取得します。

トピックキーワードに関連する記事の取得

 生成したトピックキーワードに関連する記事を取得します。ただし、単にキーワードのいずれかを含む記事を取得したのでは、関係ない記事もトピックに含まれてしまいます。そこで、Significant Terms Aggregationで取得したトピックキーワードのスコアを活用します。記事スコアとして、記事に含まれるトピックキーワードのスコアの割合を算出し、スコアが一定以上の記事をトピックの記事とみなして取得します。これはスコアが高いキーワードが含まれていれば、トピックに関する記事である可能性が高いといえるからです。

記事スコア = 記事に含まれるトピックキーワードのスコアの合計 / トピックキーワードのスコアの合計

 この条件を満たす記事の取得方法について説明します。Elasticsearchに対して単純にトピックキーワードを使って検索すると、TF-IDFやBM-25といった手法でスコアリングされた結果が返ってくるため、トピックキーワードのスコアを考慮できません。

 そこで、ElasticsearchのFunction Score Queryを利用します。

 Function Score Queryを用いることで、トピックキーワードのスコアを利用した独自のスコアリングが可能となり、一定のスコア以上の記事を取得できます。以下は、トピックキーワード集合「シャルリ、エブド、赦す、ムハンマド、風刺」に関連する記事を取得する例です。script_scoreparamsにキーワードとそのスコアを指定し、scriptでキーワードのスコアの和を記事のスコアとなるようにしています。そして、min_scoreに記事スコアの閾値(この例ではキーワードスコアの和の8割となる195270)を指定することで、トピックキーワードに関連する記事のみを取得します。

トピックキーワードに関連する記事を取得するリクエスト例
{
  "query": {
    "function_score": {
      "score_mode": "sum",
      "boost_mode": "replace",
      "functions": [
        {
          "script_score": {
            "params": {
              "field": "title",
              "words": ["シャルリ","エブド","赦す","ムハンマド","風刺"],
              "scores": [129926.04,99737.62,5522.23,4729.32,4172.57]
            },
            "lang": "groovy",
            "script": "def score=0;for(i in 0..4){ if (_index[field][words[i]].tf() > 0){ score += scores[i]} }; score;"
          }
        }
      ],
      "query": {
        "bool": {
          "should": [
            { "match": { "title": "シャルリ" } },
            { "match": { "title": "エブド" } },
            { "match": { "title": "赦す" } },
            { "match": { "title": "ムハンマド" } },
            { "match": { "title": "風刺" } }
          ],
          "minimum_should_match": 1
        }
      }
    }
  },
  "min_score": 195270,
}

トピックのマージ

 ここまでに説明した手法によって話題のトピックが作成されます。ただし、同じ内容のトピックが複数作成されるという問題がまだ解決できていません。そのため、重複トピックを判定して、トピックをマージします。

 重複トピック判定にはトピックキーワードのスコア重複率、および、トピックの記事の重複率を利用します。いずれかの重複率が一定以上の場合に同じトピックであると判定し、トピックをマージします。トピックのマージはキーワード、記事ともに、単純に和集合をとります。

次のページ
トピックタイトルの生成

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
はてなエンジニア直伝! はてなのサービスを取り巻く先端技術トピック連載記事一覧
この記事の著者

小澤 俊介(株式会社はてな)(コザワ シュンスケ)

 株式会社はてな アプリケーションエンジニア web:skozawa's home blog:skozawa's blog twitter:@5kozawa

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/8767 2015/07/24 14:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング