Upgrade to Pro — share decks privately, control downloads, hide ads and more …

note の Elasticsearch 更新系を支える技術

note の Elasticsearch 更新系を支える技術

2025年4月24日に開催した
【日経×Helpfeel×note】検索体験の裏側〜知りたい情報を最短で届けるための取り組み紹介〜
にて発表した資料です
https://nikkei.connpass.com/event/347879/

chov(Kohei T)

April 25, 2025
Tweet

Other Decks in Programming

Transcript

  1. ⽬次 note inc. • 05 …… 会社概要 • 10 ……

    検索の更新にまつわる課題 • 15 …… 同期ズレへの対処 • 20 …… コンバート失敗への対処 • 23 …… 全件インデックスへの対処 • 29 …… 2年間運⽤しての感想と反省 • 32 …… まとめ 4
  2. ⽬次 note inc. • 05 …… 会社概要 • 10 ……

    検索の更新にまつわる課題 • 15 …… 同期ズレへの対処 • 20 …… コンバート失敗への対処 • 23 …… 全件インデックスへの対処 • 29 …… 2年間運⽤しての感想と反省 • 32 …… まとめ 5
  3. note inc. 7 事業概要
 だれもがインターネット上で自由にコンテンツを発表・販売できるメディアプラットフォーム「note」と、noteを基盤に企業の情 報発信をかんたんかつ効果的に行うための高機能プラン「note pro」を中心に事業を展開。
 
 
 noteはクリエイターが文章や画像、音声、動

    画を投稿して、ユーザーがそのコンテンツを楽 しんで応援できるメディアプラットフォームで す。だれもが創作を楽しんで続けられるよう、 安心できる雰囲気や、多様性を大切にしてい ます。個人も法人も混ざり合って、好きなもの を見つけたり、おもしろい人に出会えたりする チャンスが広がっています。
 法人向け高機能プラン。多くのひとが集まる noteの街でメディアをかんたんにつくり、情報 を届けることができます。「ブランディング」「リ クルーティング」「ファンコミュニティ作り」「サブ スクリプション」など目的はさまざま。届ける仕 組みと充実したサポートで、企業がポジティブ なユーザーとつながって関係を深めるお手伝 いをします。
 法人向けサービス
 ・コンテスト
 企業とコラボレーションし、note上でクリエイ ターから作品を募集する企画を開催
 ・イベント
 note連動イベント等のため、イベントスペー ス”note place”を貸出

  4. ⽬次 note inc. • 05 …… 会社概要 • 10 ……

    検索の更新にまつわる課題 • 15 …… 同期ズレへの対処 • 20 …… コンバート失敗への対処 • 23 …… 全件インデックスへの対処 • 29 …… 2年間運⽤しての感想と反省 • 32 …… まとめ 10
  5. note inc. 本⽂データ 読み込み noteの検索システム更新系 この図の解説は後半でします 12 Job Worker <Sidekiq>

    Job Queue <SQS> Object Storage <S3> Updater <k8s cronjob/go> クリエイター Application <Ruby on Rails> データ 更新 ジョブ 送信 メッセージ 送信 Queue Reader <Lambda/go> 更新ログ 読み込み 直列化して 書き込み Database <MySQL> 更新ログ 読み込み Search Engine <Elasticsearch> データ 更新 データ保存 DWH <Snowflake> ⾏動ログなどの読み込み(今回は解説しない)
  6. ⽬次 note inc. • 05 …… 会社概要 • 10 ……

    検索の更新にまつわる課題 • 15 …… 同期ズレへの対処 • 20 …… コンバート失敗への対処 • 23 …… 全件インデックスへの対処 • 29 …… 2年間運⽤しての感想と反省 • 32 …… まとめ 15
  7. note inc. 同期ズレを完全になくすことは不可能 • 前提として、データソースと検索インデックスの同期ズレを完全になくすことは不可能 ◦ RDBの最新状態と、検索インデックスの状態には必ず差分が⽣じる ◦ どれだけ遅延しても良いかを合意することが⼤事 ▪

    noteの本番環境の場合は最⼤5分程度の遅延を許容している • 同期を⾏う際にとりうるオプション ◦ 定期的に、全件に対して検索インデックス更新を⾛らせる ◦ データ更新時に、アプリケーションが検索エンジンの更新APIを直接実⾏する ◦ ジョブキューに更新ログを送信し、⼀定時間ごとに直列化した更新内容をまとめて反映する ▪ noteではこの⽅式を採⽤ 16
  8. note inc. 定期的な全件インデックス更新のPros/Cons • Pros ◦ 更新時点での正しいデータが保証できる ▪ 考えることが減る ◦

    静 的なデータ配 信になるのでコンテナと 相性が良い • Cons ◦ ⼤量データでは更新に数⽇かかる ◦ 最新データへの追随に時間がかかる ⼩規模データや、ほぼ静的なデータで利⽤したい 地理検索ではよく利⽤するパターン 17 検索エンジン App 定期 更新 RDB データ読み込み
  9. note inc. アプリケーションが直接更新する際のPros/Cons • Pros ◦ データソースと 検 索 インデックスの

    同期ズレを短くできる ◦ 構築するものが少なくて済む ◦ 挙動が理解しやすい • Cons ◦ ⼀度に⼤量のデータ更新があった場合の 挙動が保証しにくい ◦ 全件更新後の最新化と相性が悪い ◦ 耐障害性が低い 更新頻度が低めのデータで利⽤したい 物語投稿サイトTalesでは定期更新+この⽅式を採⽤ 18 検索エンジン App 購⼊ スキ マガジン 追加 スキ 購⼊ スキ 記事A 記事B 記事C 更新 イベントごとに送って うまく更新 できるのか…?
  10. note inc. ジョブキュー+定期更新のPros/Cons • Pros ◦ ⼤量データ更新時でもデバッグ可能 ◦ 任意タイミングでの最新化が可能 ◦

    スケーラブルで耐障害性が⾼い • Cons ◦ データソースと 検 索 インデックスの 同期ズレは⼤きくなる ◦ 構築するものが増える ◦ 挙動が理解しにくい ⼤量データに頻繁な更新が⼊る場合で利⽤したい 5,000万件超のコンテンツが常にupdateされ続ける noteではこの⽅式で堅牢に運⽤できる 19 検索エンジン App 購⼊ スキ マガジン 追加 スキ 購⼊ スキ 記事A 記事B 記事C Queue メッセージ送信 Object Storage 直列化して保存 Converter 更新
  11. ⽬次 note inc. • 05 …… 会社概要 • 10 ……

    検索の更新にまつわる課題 • 15 …… 同期ズレへの対処 • 20 …… コンバート失敗への対処 • 23 …… 全件インデックスへの対処 • 29 …… 2年間運⽤しての感想と反省 • 32 …… まとめ 20
  12. note inc. データの振る舞いはときどき変わる • サービスの仕様変更などでデータの振る舞いが 変化すると、コンバートが失敗する場合がある ◦ 開発者全員が検索エンジンを知っている わけではないので、変更が抜けがち •

    アプリケーションから直接更新する アーキテクチャでコンバート失敗すると、 復旧が難しい ◦ ⼤規模データで採⽤しにくい理由の⼀つ note では更新メッセージを Protocol Buffers で定義 変換を単体テストで担保することで失敗を抑⽌ # code def self.message(action_type:, event_at:, ar_obj:) body_hash = case action_type # 略 when :update_status { 'update_note_status_body' => { 'id' => ar_obj.id, 'status' => ar_obj.status.upcase } } # 略 msg = Messages::UpdateLog.new(body_hash) Messages::UpdateLog.encode_json(msg) end # test context 'statusが更新(update)されたとき' do let!(:action_type) { :update_status } it 'メッセージの形式が正しいこと' do msg = JSON.parse(subject) expect(body['id']).to eq ar_obj.id expect(body['status']).to eq ar_obj.status.upcase end end 21
  13. note inc. Protocol Buffers を経由する理由 • 型が保証でき、汎⽤的な形式にシリアライズできるなら何でも良かった ◦ Ruby だけでは型が保証できないので、Rubyの外からガードレールを作りたかった

    • できるだけ⾔語中⽴にしたかった ◦ Ruby on Railsで完結しなかったシステムなので、複数⾔語でメッセージを同じように扱いたかった ➡ 採⽤から2年、更新メッセージ変換関係のエラーは未発⽣ 22
  14. ⽬次 note inc. • 05 …… 会社概要 • 10 ……

    検索の更新にまつわる課題 • 15 …… 同期ズレへの対処 • 20 …… コンバート失敗への対処 • 23 …… 全件インデックスへの対処 • 29 …… 2年間運⽤しての感想と反省 • 32 …… まとめ 23
  15. note inc. 要件によって運⽤難易度は⼤きく変わる 全件インデックスをした後、最新のデータに追随する必要がある 以下の3つの質問への答え次第で、運⽤難易度は⼤きく変わる • データを全件インデックスするのにどれくらい時間がかかるか? ◦ 1営業⽇以上必要であれば、1回の全件インデックスが⼤仕事になる •

    最新性と情報の正しさはどれくらい重要か? ◦ 要求が厳しければ厳しいほど、運⽤難易度は⾼くなる • 更新履歴の取得は容易か? ◦ たとえば毎秒100更新あっても、シーケンシャルな取得が容易なら再構成できる 24
  16. note inc. note の場合はどうだったか • データを全件インデックスするのにどれくらい時間がかかるか : ⼤仕事 ◦ 記事

    ……… 2⽇ ◦ それ以外 … 最⼤2時間 • 最新性と情報の正しさはどれくらい重要か : 厳しくはない ◦ 5分程度の遅延は許容可能 ◦ 情報の正しさについては、間違っていても指摘ベースで修正すれば基本的にはOK • 更新履歴の取得は容易か : 難しい ◦ 更新履歴を簡単にクエリできるシステムが存在しない ➡ 2⽇分の更新履歴を取り扱うシステムが必要 25
  17. note inc. 2⽇分の更新履歴を反映する技術(1/3) コンテキスト図 26 本⽂データ 読み込み 26 Job Worker

    <Sidekiq> Job Queue <SQS> Object Storage <S3> Updater <k8s cronjob/go> クリエイター Application <Ruby on Rails> データ 更新 ジョブ 送信 メッセージ 送信 Queue Reader <Lambda/go> 更新ログ 読み込み 直列化して 書き込み Database <MySQL> 更新ログ 読み込み Search Engine <Elasticsearch> データ 更新 データ保存 DWH <Snowflake> ⾏動ログなどの読み込み(今回は解説しない)
  18. note inc. 2⽇分の更新履歴を反映する技術(2/3) S3/更新ログの設計 27 • s3://<bucket-name>/logs/<index_name>/YYYY-MM-DDTHH:mm:ss.nsZ.log の形式で保存 ◦ タイムスタンプ順にしておくことで、s3

    ls を使えば時系列順の取得が容易 • 中⾝は更新ログの Protocol Buffers から JSON シリアライズした、1⾏1イベントの jsonl ◦ 1⾏ごとに読み込んでいけば良く、更新処理プログラムは省メモリで動作する • 更新ログはオペミスで複数回最新化処理を施してしまっても良いように定義 {"Index":"USERS","EventAt":"2025-04-14T01:08:39.068362808Z","updateIntBody":{"id":653,"Field":"following_count","value":1}} {"Index":"USERS","EventAt":"2025-04-14T01:08:39.069777626Z","updateIntBody":{"id":585,"Field":"follower_count","value":1}} {"Index":"USERS","EventAt":"2025-04-14T01:09:41.856327539Z","updateIntBody":{"id":653,"Field":"following_count","value":2}} {"Index":"USERS","EventAt":"2025-04-14T01:09:41.857213623Z","updateIntBody":{"id":644,"Field":"follower_count","value":5}}
  19. note inc. 2⽇分の更新履歴を反映する技術(3/3) 反映状況の管理 28 • s3://<bucket-name>/last_update/<index_name>.last_updated_at.json に更新状況の状態を保存している • updater

    はファイルから最終更新⽇時を読み取り、s3 ls で次に更新するファイルを知る • 疎結合を指向していたが、素直にRDB管理にしておけばよかった • 更新失敗がもっと起こるかと思っていたが、データ形式由来での更新失敗が⼀度も起こっていないので 備えが有効に機能しているかは不明 { "filename": "logs/users/2025-04-18T02:10:01.306546641Z.log", // 最後に処理したファイル名 "status": true, // 更新の成否 "reason": "" // 更新失敗していた場合の理由 }
  20. ⽬次 note inc. • 05 …… 会社概要 • 10 ……

    検索の更新にまつわる課題 • 15 …… 同期ズレへの対処 • 20 …… コンバート失敗への対処 • 23 …… 全件インデックスへの対処 • 29 …… 2年間運⽤しての感想と反省 • 32 …… まとめ 29
  21. note inc. メッセージ指向で全てを賄うのは難しい • 最⼤2⽇分の更新を適⽤する必要があるので、メッセージ指向⾃体には必然性がある ◦ 運⽤開始から 2,000 万件以上コンテンツが増加しており、全件インデックスも複数回実⾏しているが 全件インデックスでの障害は発⽣していない

    • ただし、ときおり更新ログが送られておらず、更新が抜ける場合がある ◦ noteは10年以上運⽤された、実装上も運⽤上も⾮常に複雑な Ruby on Rails アプリケーション ▪ ログが抜ける原因も単純ではない ◦ どこかの分岐で送信処理が抜けたり、実装が忘れられるのを⾒越して、 あとから辻褄を合わせる機構のほうが重要だった 30
  22. note inc. 素直に全部Rubyで書いておけば良かった • 疎結合や型安全を実現したく、当時所属していたデータ基盤チームがGoを利⽤していたことから Goで実装したが、素直に全部Rubyで書くほうが経済的だった ◦ 他の開発者が参⼊しやすい ◦ 開発環境やローカル環境でのセットアップが楽になる

    ◦ 今は Packwerk が note 内にかなり普及しており、ある程度疎結合にできる ◦ 実装当時はRails→Snowflakeのルートがなかったことからシステムを分ける必然性があったが、 Reverse ETL のフローが確⽴したことでその制約もなくなった ◦ 本体の Ruby on Rails より堅牢であってもオーバーエンジニアリングなので、無理しなくて良かった ▪ ⾔語中⽴なアーキテクチャなので、⼯数さえかければ移⾏可能なのは救い • 物語投稿サイトTales はこの反省を⽣かして、バックエンドと⾔語を揃えて実装している 31
  23. ⽬次 note inc. • 05 …… 会社概要 • 09 ……

    Introduction: 検索の更新にまつわる課題 • 14 …… 同期ズレへの対処 • 19 …… コンバート失敗への対処 • 22 …… 全件インデックスへの対処 • 29 …… 2年間運⽤しての感想と反省 • 32 …… まとめ 32
  24. note inc. まとめ • データソースと検索インデックスの同期ズレを完全になくすことはできない ◦ どれくらい遅延と正しさを担保するのか、合意を得る必要がある ◦ この制約次第で難易度は⼤きく変わるが、noteは⽐較的緩め •

    全件インデックスをした後最新のデータに追随する必要があるので、更新履歴を保存するシステムを実装 ◦ データの振る舞い変更に対して頑強にするべく、Protocol Buffers を⽤いて Ruby に型を強制 • スケーラビリティと耐障害性については⽬論⾒通り実現できたが、2年間運⽤すると課題も⾒えてくる ◦ メッセージ指向で全てを賄うのは難しい ▪ 結果整合性を第⼀に構築すればよかった ◦ note本体より堅牢である意味が薄いので、素直に全部Rubyで書いておけばよかった ▪ 時間経過で制約が変化するので、⾔語中⽴にしておくことは重要 33