Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

20151028 第17回 solr勉強会 U-NEXTにおけるSolr活用事例

8,111 views

Published on

2015/10/28に開催された17回目のSolr勉強会での発表資料です

Published in: Software
  • Be the first to comment

20151028 第17回 solr勉強会 U-NEXTにおけるSolr活用事例

  1. 1. U-NEXTにおけるSolr活用事例 〜インクリメンタルサーチとレコメンドへの挑戦〜 株式会社U-NEXT システム開発部 秋穂 賢
  2. 2. 自己紹介 氏名 秋穂 賢(あきほ すぐる) 所属 株式会社U-NEXT システム開発部 リリース管理や開発用ミドルウェア管理 DBFluteを使ったJava8でのAPI実装 など (前職ではChef / Zabbix / JobScheduler 等) 仕事
  3. 3. まずはじめに Solr勉強会での登壇という貴重な機会を与えて 頂いた ロンウイット様、リクルートテクノロジーズ 様、誠にありがとうございます。 特にロンウイット様にはコンサルタントとしてSolr を活用する上で様々なアドバイスをいただきまし た。 Solr歴半年程度の自分が今、ここに立てている のもロンウイット様のご支援が大きいと感じており ます。
  4. 4. そもそもU-NEXT? 最新情報はWebで http://video.unext.jp/introduction 10/8に全面リニューアルを実 施!
  5. 5. そもそもU-NEXT? 最新情報はWebで http://video.unext.jp/introduction/device
  6. 6. そもそもU-NEXT? 最新情報はWebで http://video.unext.jp/introduction/family
  7. 7. そもそもU-NEXT? 最新情報はWebで http://video.unext.jp/introduction/fee
  8. 8. U-NEXTでの Solr利用箇所 U-NEXTではSolrを以下のシーンで活用 ○ フリーワード検索(インクリメンタルサーチ) ○ ユーザ毎のレコメンドデータ取得 Solrを使ったバックエンド API の開発が主な役割
  9. 9. インクリメンタルサーチでの事例
  10. 10. インクリメンタルサーチ での事例 ○ インクリメンタルサーチ? □ 検索文字を全て入力してから検索を行うのではなく、 入力するたびに検索結果が返ってくる検索方法のこと □ Googleも今はインクリメンタルサーチ ○ Why インクリメンタルサーチ? □ すべての文字を打つことなく結果に辿れる □ 検索中に新しい気づきを得られる可能性 ○ デモ □ http://video.unext.jp
  11. 11. システム構成(全体) Webクライアント (Knockout.js) WebFrontAPI (PHP) Portal API (PHP) CMS API (Java) SolrCloud WebブラウザのJSクライアントとしてKnockout.jsを利用 Web特有の要件を満たすための専用 APIサーバ 各バックエンド系APIをまとめてクライアントへ返答するための API サーバ CMSDB コンテンツ周りの情報を返却する APIサーバ Solrは運用面を考慮して SolrCloudを利用 Varnish (Cache) 検索ワード毎にキャッシュを保持
  12. 12. システム構成(詳細) CMS API (Java) Solr CMSDB CMS API (Java)CMS API (Java)CMS API (Java) Solr SolrCloud LB JavaのAPIクライアントからSolrCloudへのア クセスはLB経由でHttpSolrClientを利用 => 直前までmaster/slaveかSolrCloudのどち らにするか、決められなかったため バッチ Solrインポート用 TSV 定期的にCMSDBからデータ抽出 &TSVファイル生成 SolrへTSVをインポート
  13. 13. インクリメンタルサーチ実現 における課題 ○ ユーザ体験として違和感のない応答速度を担 保する必要がある スキーマ定義を工夫 メタデータに作品名のカナがあるので、有効活用 ○ 一文字打つたびに検索結果を返す必要がある □ 検索文字は変換前の英字(ローマ字)の可能性も考慮 レスポンスに必要なデータを全てSolrに入れ込み UI上、即時反映が必要な項目は含めないように (詳細は省きますが)本番同様の負荷テストの結果、Solrからのレスポン スは2,30msをマーク!!
  14. 14. schema定義 ○ インクリメンタルサーチを実現するために大きく4つ のパターンでanalyzerを定義 □ ※ 全てautoGeneratePhraseQueriesはtrueを設定 1. 日本語タイトル向けの設定(単純検索) ○ (charFilter)solr.MappingCharFilterFactory □ 記号などの不要な文字を削除 ○ (charFilter)solr.ICUNormalizer2CharFilterFactory □ 日本語の正規化 ○ (tokenizer)solr.JapaneseTokenizerFactory □ 形態素解析による日本語の文字分割 ○ (filter)solr.LowerCaseFilterFactory □ 英字の大文字小文字を区別しない
  15. 15. 2. 日本語タイトル向けの設定(インクリメンタル) ○ トークナイザまでは 1 と同じ ○ (filter)solr.JapaneseReadingFormFilterFactory □ 漢字を読みに変換 ○ (filter)solr.ICUTransformFilterFactory(Hiragana-Katakana) □ ひらがなをカタカナに変換 ○ (filter)solr.ICUTransformFilterFactory(Katakana-Latin) □ カタカナをローマ字に変換 ○ (filter)solr.LowerCaseFilterFactory □ 小文字に統一 ○ (filter)solr.EdgeNGramFilterFactory □ 形態素解析結果毎に1gram ○ ※ クエリサイドにはEdgeNGramを含まないように設定 □ => 結果的にはインデックスサイドのEdgeNGramにヒットする schema定義
  16. 16. 3. カナタイトル向けの設定(インクリメンタル) ○ charFilterまでは 1 と同じ ○ (tokenizer)solr.KeywordTokenizerFactory □ カタカナはフィールド全体を1つのトークンとみなす ○ 残りは 2 と同じ ※ クエリサイドは 2 と同じ。ローマ字に直して前方からの部分一致で検索する 例) 「アノヒ」というタイトルの場合、indexは a / an / ano / anoh / anohi が生 成 「あのh」で検索すると「あのh」で形態素解析されて「anoh」でフレーズクエリとして 検索される schema定義 4. 1〜3までで検索にかからなかった向けの2gram設定 ○ solr.NGramTokenizerFactoryで2gramの設定
  17. 17. ○ 1〜4までをdismaxでの横断検索を実行 ○ 横断検索する際には別途、設定してあるシノニムにも同時に検索 を実行 □ 例) 「あの日見た花の名前を僕達はまだ知らない」を「あのはな」と略す ○ 日本語タイトル向けの設定(単純検索) □ => このプライオリティを一番高く設定(400) ○ 日本語タイトル向けの設定(インクリメンタル) ○ カナタイトル向けの設定(インクリメンタル) □ => これらプライオリティを2番目に(200) ○ 2gram設定 □ => 最低のプライオリティに(50) ○ ※ シノニムについては通常ワードの半分にプライオリティを設定 schema定義
  18. 18. 残課題 ○ 検索精度が悪い部分がある □ 作品は存在しているのに検索にひっかからない □ おそらく、漢字かな変換や形態素解析あたりが原因と 想像 □ こういった対象を地道に潰していく必要がある ○ 同着スコア時の並び順の最適化をしたい(短 い単語で検索された時など) □ 現状はscore順となるため、検索する人の傾向によっ て、アニメを優先させたり、ドラマを優先させたりなど、 検索のパーソナライズ化も目指す
  19. 19. レコメンドでの事例
  20. 20. ○ レコメンド要件として、↓がありました □ トップ画面で表示される特集をユーザ毎のオススメを表示したい □ 各ジャンルトップではジャンルに絞り込んだレコメンドを表示した い □ モバイルアプリの場合、最初に好きなジャンルや映画を聞いて、 好きなものを優先して出したい ○ レコメンド自体の計算は別環境で行われていて、計算結果のキャッ シュを何かしらでもたせたかった レコメンドでの事例 インクリメンタルサーチでの高速レスポンスの実績 + RDBに似た絞り込み検索が行える = Solr ※ SolrはRedis等のキャッシュとRDBの間のようなイメージで使える
  21. 21. システム構成 レコメンド 計算サーバ Solrインポート 用TSV レコメンド SolrCloud レコメンド計算結果を計算後、レコメ ンドキャッシュ用SolrCloudにデータ インポートを実行 レコメンド SolrCloudレコメンド SolrCloud CMS API (Java)CMS API (Java)CMS API (Java)CMS API (Java) LB CMSDB 最終的にクライアントへ返す・返さな いという制御はDBで実施 並び順はSolrに格納されたレコメン ド計算結果順になる レコメンド用Solrには計算結果の各 ID情報とスコア情報を持つ
  22. 22. ○ シンプルな絞り込み検索だけ(ファセットは使わない) ○ データとしては以下のような形式(一部略) schema定義 uid ユーザID ジャンルID カテゴリID 特集ID 特集スコア 作品ID 1 user1 anime SF feature1 0.998 s1,s2,s3... 2 user1 houga SF feature10 0.95 s10,s11,s12.. 3 user1 anime Drama feature11 0.93 s20,s21,s22.. 4 user1 youga Action feature18 0.89 s30,s31,s32.. 5 coldstart anime Drama feature4 0.98 s4,s5,s6... 6 coldstart houga SF feature5 0.96 s14,s15,s16.. 7 coldstart anime SF feature15 0.94 s24,s25,s26.. 8 coldstart youga Action feature22 0.92 s34,s35,s36.. ジャンル単位でのレコメンド表示の 際にジャンルで絞り込む 初期のカテゴリ選択の フィルタリングに利用 ユーザ毎のオススメ順 にスコアが並んでいる 特集の中の作品 の並び順もユー ザ毎のオススメ 順になっている
  23. 23. ○ user1が好きなカテゴリにDramaとActionを選択した場合 □ => レコメンド結果はフィルタしない □ => coldstart向けの結果はフィルタする schema定義 uid ユーザID ジャンルID カテゴリID 特集ID 特集スコア 作品ID 1 user1 anime SF feature1 0.998 s1,s2,s3... 2 user1 houga SF feature10 0.95 s10,s11,s12.. 3 user1 anime Drama feature11 0.93 s20,s21,s22.. 4 user1 youga Action feature18 0.89 s30,s31,s32.. 5 coldstart anime Drama feature4 0.98 s4,s5,s6... 6 coldstart houga SF feature5 0.96 s14,s15,s16.. 7 coldstart anime SF feature15 0.94 s24,s25,s26.. 8 coldstart youga Action feature22 0.92 s34,s35,s36.. 選択し ていな い対象 は下に おいや られる
  24. 24. レコメンドでの課題 ○ やること自体はただのfq(score関係ない) ○ データ件数が膨大 □ アクティブユーザ数 × 特集数 が最大のレコメンド結果のデー タ数 □ ○ さすがに多すぎる... & 特集も見る人はそうそういない □ => 1人あたり最大100特集をレコメンド □ □ ※ フリーワード検索は数十万件程度 Solrからはせいぜい100ms程度で応答して欲しい => 性能検証を実施 mask mask mask
  25. 25. 性能検証 ○ AWS上に本番相当スペックのSolrCloudを構築してテスト ○ Solrのシャード数:2 □ 2台のSolrに2シャード作ったので、1Solrあたり、1シャード ○ jmeterで実際の疑似クエリを生成し、ランダムにユーザを選択 し、Solrへの検索を行う ○ 10スレッド * 1000リクエストを実行 ○ 10万ユーザの中からランダムで1人のユーザを選択してレコメンド データをリクエスト ○ 事前に1000リクエスト投げてウォームアップを行う ○ 平均、90パーセンタイル値、最小、最大、エラー数を比較
  26. 26. 性能検証結果 件数(万件) 平均(ms) 90%(ms) 最小(ms) 最大(ms) エラー件数 60 22 30 8 503 0 180 20 24 8 598 0 300 23 30 8 513 0 420 24 32 9 755 0 540 25 32 8 509 0 660 25 33 9 497 0 780 24 32 8 499 0 900 27 31 8 715 0 1020 26 35 8 520 0
  27. 27. 性能検証結果 件数(万件) 平均(ms) 90%(ms) 最小(ms) 最大(ms) エラー件数 60 22 30 8 503 0 180 20 24 8 598 0 300 23 30 8 513 0 420 24 32 9 755 0 540 25 32 8 509 0 660 25 33 9 497 0 780 24 32 8 499 0 900 27 31 8 715 0 1020 26 35 8 520 0 件数が60万件〜1020万 件までは平均・90パーセ ンタイル値共に大きな変 化はなく、良好な性能
  28. 28. 性能検証結果 件数(万件) 平均(ms) 90%(ms) 最小(ms) 最大(ms) エラー件数 60 22 30 8 503 0 180 20 24 8 598 0 300 23 30 8 513 0 420 24 32 9 755 0 540 25 32 8 509 0 660 25 33 9 497 0 780 24 32 8 499 0 900 27 31 8 715 0 1020 26 35 8 520 0 最小は10ms以下と驚異 の性能を発揮 最大はいずれも500ms以 上のパターンがある => ハズレを引く人がたま にいる可能性あり エラー件数はいずれも0と なっており発生していない 状況
  29. 29. 性能検証結果 ○ 1 Solrあたり1シャードで 1 シャードあたり 1020 / 2 = 510万件ま では性能的にはほとんど問題ない結果 ○ 本番では6台のSolrCloudで6シャード2レプリカとした □ 1Solrあたり2シャード □ □ 現状は特に問題がないため、暫くこの構成 mask
  30. 30. その他の工夫点
  31. 31. ○ 本事例ではSolrのクライアントにJava(solrj)を利用 ○ solrjをそのまま使うとタイプセーフではない... タイプセーフに クエリを実行 try (HttpSolrClient httpSolrClient = new HttpSolrClient(“http://10. 105.21.28:8983/solr/general”)) { httpSolrClient.setParser(new XMLResponseParser()); ModifiableSolrParams params = new ModifiableSolrParams(); params.add("q", "あの日"); params.add("defType", "dismax"); params.add("qf", "name kana^10 name_general^20 synonym synonym_general synonym_kana"); QueryResponse response = httpSolrClient.query(params); SolrDocumentList results = response.getResults(); LOG.debug("count={}", results.size()); LOG.debug("list={}", results); } catch (IOException | SolrServerException e) { throw new SystemException("error", e); }
  32. 32. タイプセーフに クエリを実行 ○ ORMにDBFluteを採用 ○ DBFluteの自動生成機能を使ってschema.xmlから各種クラス を自動生成 SolrPagingResultBean<General> list = generalBhv.selectPage(cb -> { cb.query().dismax("あの日", queryField -> { queryField.put(GeneralMeta.Name, null); queryField.put(GeneralMeta.Kana, 10); queryField.put(GeneralMeta.NameGeneral, 20); queryField.put(GeneralMeta.Synonym, null); }); cb.specify().fieldUid(); cb.paging(10, 2); }); LOG.debug("list={}", list); DBFluteの機能で ページング検索が 出来る dismax検索出来る対象 のフィールドがタイプ セーフに指定可能 specifyでflをタイプセー フに指定可能
  33. 33. タイプセーフに クエリを実行 ○ タイプセーフにすることでミスがあるとコンパイルエラー ○ 単純なミスがなくなる □ なぜか動かなくて色々とエラー原因を探し回った結果、結 局ただのtypoだった...なんてことを防げる ○ 変更に強くなる □ スキーマ定義を変えたけど、クエリの指定を変えるのを忘れ た...なんてことを防げる タイプセーフにした結果、本当に必要な ロジックの実装に集中できる
  34. 34. まとめ
  35. 35. ○ Solrの機能を有効活用すれば日本語でのイ ンクリメンタルサーチも実現できる □ ただし、一部精度が悪い部分もあり ○ Solrを使えばレコメンド結果のような大量デー タを高速に検索可能 □ RedisなどのKVSキャッシュでは出来ない絞り込み検 索が高速に可能 □ 特に単純な検索であれば高速なレスポンスが期待で きる ○ タイプセーフは大事 □ 無駄な時間をかけずに済む まとめ
  36. 36. 最後に
  37. 37. U-NEXTでは人材を募集しています 映画やアニメが好きなひと エンターテイメントが好きなひと 配信プラットフォームに興味のあるひと 技術的なことが好きなひと 一緒に仕事をしましょう http://unext.co.jp/recruit/
  38. 38. ご静聴ありがとうございました

×