MongoDBを使用したNodeBBフォーラムからDiscourseへの移行

ご存知の通り、NodeBB は Redis と MongoDB の 2 つのデータベースバックエンドをサポートしています。Discourse のインポートスクリプトも両方をサポートしています。このチュートリアルでは、データベースバックエンドとして MongoDB を使用した NodeBB の移行方法について学びます。ここでは、mongoアダプターを使用した NodeBB Importer を使用します。NodeBB フォーラムが Redis をバックエンドとして使用している場合は、redisアダプターを紹介する このチュートリアル をご覧ください。

計画

  • 開発環境の準備
  • 本番環境からのデータベースエクスポート
  • 本番環境のデータベースを Discourse インスタンスにインポート
  • インポートスクリプトの実行

移行可能な項目

  • グループ
  • アタッチメント
  • カテゴリ
    • ルートカテゴリ ⇒ ルートカテゴリ
    • サブカテゴリおよびサブのサブカテゴリ ⇒ サブカテゴリ
  • トピックおよび投稿
    • ピン留めされたトピック ⇒ ピン留めされたトピック
    • 固定されたトピック ⇒ 閉鎖されたトピック
    • トピックの閲覧数
    • 高評価したユーザー
    • スタイル、メンション、絵文字、アタッチメント
  • ユーザー(以下の属性を含む)
    • プロフィール背景
    • アバター
    • 禁止ステータス
    • ユーザー名
    • 名前
    • メールアドレス
    • 管理者権限
    • 自己紹介
    • グループ
    • ウェブサイト
    • 所在地
    • 登録日時

ローカル開発環境の準備

計画で述べたように、まず開発環境を準備する必要があります。Discourse 自体をインストールするには、以下のいずれかのガイドに従ってください。

:bulb: Discourse サーバーのセットアップに問題がある場合は、このガイド を参照してください。

次に、MongoDB データベースサーバーをインストールします。

Ubuntu-18-04:

$ wget -qO - https://www.mongodb.org/static/pgp/server-4.0.asc | sudo apt-key add -
$ echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.0.list
$ sudo apt-get update
$ sudo apt-get install -y mongodb-org
$ sudo service mongod status

詳細については、公式ガイド を参照してください。

Mac OS:

$ brew tap mongodb/brew
$ brew install mongodb-community@4.0
$ brew services start mongodb-community@4.0
$ brew services status mongodb-community@4.0

詳細については、公式ガイド を参照してください。

Windows 10:

インストーラーをダウンロードし、MongoDB を Windows サービスとしてインストールします。

https://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2008plus-ssl-4.0.12-signed.msi

管理者権限で cmd を実行し、mongo サーバーが正常に動作しているか確認します。

"C:\Program Files\MongoDB\Server\4.0\bin\mongo.exe"

詳細については、公式ガイド を参照してください。

この環境が私たちの Discourse サーバーとなります。

本番環境データベースダンプのエクスポート

NodeBB フォーラム(本番サーバー)をシャットダウンします。

$ cd /path_to_nodebb
$ ./nodebb stop

データベースをシャットダウンします。

$ sudo service mongodb stop

データベースをバックアップします。

$ mongodump --out ~/my_backup_path/

上記のコマンドの出力は以下のようになります。

2019-08-16T15:17:09.845+0300 done dumping admin.system.users (1 document)
2019-08-16T15:17:09.846+0300 done dumping admin.system.version (2 documents)
2019-08-16T15:17:09.849+0300 done dumping nodebb.sessions (10 documents)
2019-08-16T15:17:09.850+0300 done dumping nodebb.socket.io (215 documents)
2019-08-16T15:17:09.854+0300 done dumping nodebb.objects (1488 documents)

バックアップは単なるファイルではなく、実際にはディレクトリであることを注意してください。

:bulb: mongo CLI 内で use myDatabase を実行し、次に db.stats().dataSize; を実行することで、データベースのサイズを確認できます。返される値はバイト単位です。

フォーラムのアセットをバックアップします。

$ cd /path_to_nodebb_root_directory/
$ tar -czf ./uploads.tar.gz ./public/uploads

データベースとアセットのバックアップが完了したら、それらを Discourse サーバーにコピーします。

データベースのインポート

データベースが準備できたので、ローカルの MongoDB インスタンスにインポートできます。

$ mongorestore ~/path_of_my_backup_directory/

その他のオプションについては、公式ガイド を参照してください。

次に、インポーターがアセットをインポートできるように uploads.tar.gz を展開する必要があります。

$ tar xvzf uploads.tar.gz

インポートスクリプトの実行

データベースとアセットが準備できたので、インポートスクリプトを実行する準備が整いました。その前に、ニーズに合わせて NodeBB インポートスクリプトを編集する必要があります。

NodeBB のアップロードフォルダーのパスは以下の通りです。

ATTACHMENT_DIR = '/absolute_path/uploads'

次に、インポーターに redis アダプターではなく mongo アダプターを使用するように指示します。

adapter = NodeBB::Mongo
@client = adapter.new('mongodb://127.0.0.1:27017/nodebb')

# adapter = NodeBB::Redis
# @client = adapter.new(
# host: "localhost",
# port: "6379",
# db: 14
# )

クリーンな Discourse と mongo gem のサポート付きでインポーターを実行します。

$ cd ~/discourse
$ echo "gem 'mongo'" >> Gemfile
$ bundle install
$ bundle exec rake db:drop db:create db:migrate
$ bundle exec ruby script/import_scripts/nodebb/nodebb.rb

インポーターは MongoDB インスタンスに接続し、すべてのデータを Discourse に移行します。

インポーターが完了したら、Discourse を起動します。

$ bundle exec rails server

移行されたデータを処理するために Sidekiq を起動します。

$ bundle exec sidekiq

進捗状況は http://localhost:3000/sidekiq/queues で監視できます。

この チュートリアル に従って、Discourse のバックアップを実行し、それを Discourse の本番サーバーにアップロードします。

:tada:

プロセスについてご質問がございましたら、喜んでお手伝いいたします。

円滑な移行を祈っています :grinning:

「いいね!」 10

Great work really. While I was trying, I am getting below error. Any idea what I might be doing wrong here?

importing groups
   10 / 10 (100.0%)  [474765 items/min]    
importing top level categories...
    8 / 8 (100.0%)  [437896 items/min]    
importing child categories...
   68 / 68 (100.0%)  [774048 items/min]  
importing users
 5534 / 5534 (100.0%)  [355520 items/min]    
adding users to groups...

importing topics...
Traceback (most recent call last):
	12: from script/import_scripts/nodebb/nodebb.rb:532:in `<main>'
	11: from /home/fsuzer/discourse/script/import_scripts/base.rb:47:in `perform'
	10: from script/import_scripts/nodebb/nodebb.rb:52:in `execute'
	 9: from script/import_scripts/nodebb/nodebb.rb:336:in `import_topics'
	 8: from /home/fsuzer/discourse/script/import_scripts/base.rb:877:in `batches'
	 7: from /home/fsuzer/discourse/script/import_scripts/base.rb:877:in `loop'
	 6: from /home/fsuzer/discourse/script/import_scripts/base.rb:878:in `block in batches'
	 5: from script/import_scripts/nodebb/nodebb.rb:337:in `block in import_topics'
	 4: from /home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:71:in `topics'
	 3: from /home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:71:in `map'
	 2: from /home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:71:in `block in topics'
	 1: from /home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:79:in `topic'
/home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:100:in `post': undefined method `[]' for nil:NilClass (NoMethodError)

Some field from the topic is empty. Or perhaps the topic query returned nothing.

「いいね!」 1

Can someone help me?

Error: Could not find gem ‘mongo’ in any of the gem sources listed in your Gemfile.

After $ bundle install

This will happen if you skipped this:

Make sure you Gemfile has this line:

gem 'mongo'

Then run:

$ bundle install
「いいね!」 1

Gem

Thanks for your help. But this line already exists in my file. I also did the previous steps informed.

I don’t know if that helps, but when I run gem list mongo it’s not listed.

Your Gemfile sounds weird to me. Please, can you share the whole content?

Your Gemfile is just wrong. It should have another content, just like this:

In addition to:

gem 'mongo'

こんにちは、

ガイドをありがとうございます!マイグレーションスクリプトを実行すると、スクリプトがユーザーのインポートに進んだ時点で以下のエラーが表示されます:

Traceback (most recent call last):
        21: from script/import_scripts/nodebb/nodebb.rb:532:in `<main>'
        20: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:47:in `perform'
        19: from script/import_scripts/nodebb/nodebb.rb:50:in `execute'
        18: from script/import_scripts/nodebb/nodebb.rb:130:in `import_users'
        17: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:262:in `create_users'
        16: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:262:in `each'
        15: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:274:in `block in create_users'
        14: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:391:in `create_user'
        13: from /home/odyslam/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.3/lib/active_support/core_ext/object/try.rb:15:in `try'
        12: from /home/odyslam/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.3/lib/active_support/core_ext/object/try.rb:15:in `public_send'
        11: from script/import_scripts/nodebb/nodebb.rb:164:in `block (2 levels) in import_users'
        10: from script/import_scripts/nodebb/nodebb.rb:211:in `import_profile_picture'
         9: from /home/odyslam/forum/discourse/lib/upload_creator.rb:45:in `create_for'
         8: from /home/odyslam/forum/discourse/lib/distributed_mutex.rb:14:in `synchronize'
         7: from /home/odyslam/forum/discourse/lib/distributed_mutex.rb:29:in `synchronize'
         6: from /home/odyslam/forum/discourse/lib/distributed_mutex.rb:29:in `synchronize'
         5: from /home/odyslam/forum/discourse/lib/distributed_mutex.rb:33:in `block in synchronize'
         4: from /home/odyslam/forum/discourse/lib/upload_creator.rb:66:in `block in create_for'
         3: from /home/odyslam/forum/discourse/lib/upload_creator.rb:381:in `optimize!'
         2: from /home/odyslam/forum/discourse/app/models/optimized_image.rb:178:in `ensure_safe_paths!'
         1: from /home/odyslam/forum/discourse/app/models/optimized_image.rb:178:in `each'
/home/odyslam/forum/discourse/app/models/optimized_image.rb:179:in `block in ensure_safe_paths!': Discourse::InvalidAccess (Discourse::InvalidAccess)

お手数ですが、ご確認と修正のほどよろしくお願いいたします。

プロフィール画像のアップロード部分のコメントアウトで回避できました。それは問題ありません。

しかし、トピックをインポートすると nil エラーが発生します。:frowning:

Traceback (most recent call last):
        12: from script/import_scripts/nodebb/nodebb.rb:532:in `<main>'
        11: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:47:in `perform'
        10: from script/import_scripts/nodebb/nodebb.rb:52:in `execute'
         9: from script/import_scripts/nodebb/nodebb.rb:336:in `import_topics'
         8: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:862:in `batches'
         7: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:862:in `loop'
         6: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:863:in `block in batches'
         5: from script/import_scripts/nodebb/nodebb.rb:337:in `block in import_topics'
         4: from /home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:71:in `topics'
         3: from /home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:71:in `map'
         2: from /home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:71:in `block in topics'
         1: from /home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:79:in `topic'
/home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:100:in `post': undefined method `[]' for nil:NilClass (NoMethodError)

将来に関心のある方のために、移行を完了しました。私の経験に関するコメントをいくつか記します。このプロセスにはいくつかの落とし穴があります。

https://odyslam.com/blog/Migrating-from-Nodebb-to-Discourse/

「いいね!」 2
21: script/import_scripts/nodebb/nodebb.rb:532:in `<main>'
        20: from /home/nodebb/discourse/script/import_scripts/base.rb:47:in `perform'
        19: from script/import_scripts/nodebb/nodebb.rb:50:in `execute'
        18: from script/import_scripts/nodebb/nodebb.rb:130:in `import_users'
        17: from /home/nodebb/discourse/script/import_scripts/base.rb:264:in `create_users'
        16: from /home/nodebb/discourse/script/import_scripts/base.rb:264:in `each'
        15: from /home/nodebb/discourse/script/import_scripts/base.rb:276:in `block in create_users'
        14: from /home/nodebb/discourse/script/import_scripts/base.rb:393:in `create_user'
        13: from /home/nodebb/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.1.4.1/lib/active_support/core_ext/object/try.rb:15:in `try'
        12: from /home/nodebb/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.1.4.1/lib/active_support/core_ext/object/try.rb:15:in `public_send'
        11: from script/import_scripts/nodebb/nodebb.rb:164:in `block (2 levels) in import_users'
        10: from script/import_scripts/nodebb/nodebb.rb:211:in `import_profile_picture'
         9: from /home/nodebb/discourse/lib/upload_creator.rb:64:in `create_for'
         8: from /home/nodebb/discourse/lib/distributed_mutex.rb:14:in `synchronize'
         7: from /home/nodebb/discourse/lib/distributed_mutex.rb:29:in `synchronize'
         6: from /home/nodebb/discourse/lib/distributed_mutex.rb:29:in `synchronize'
         5: from /home/nodebb/discourse/lib/distributed_mutex.rb:33:in `block in synchronize'
         4: from /home/nodebb/discourse/lib/upload_creator.rb:83:in `block in create_for'
         3: from /home/nodebb/discourse/lib/upload_creator.rb:501:in `optimize!'
         2: from /home/nodebb/discourse/app/models/optimized_image.rb:181:in `ensure_safe_paths!'
         1: from /home/nodebb/discourse/app/models/optimized_image.rb:181:in `each'
/home/nodebb/discourse/app/models/optimized_image.rb:182:in `block in ensure_safe_paths!': Discourse::InvalidAccess (Discourse::InvalidAccess)

@enigmaty @OdysLam @FreeWorLD さんと同じエラーが発生しています。

こんにちは!

最新のNodeBB 4.9.xから現在のDiscourse開発版への移行を試みた方はいますか?

/home/dev/discourse/script/import_scripts/nodebb/mongo.rb:100:in 'NodeBB::Mongo#post': undefined method '[]' for nil (NoMethodError)

      post["timestamp"] = timestamp_to_date(post["timestamp"])
                                                ^^^^^^^^^^^^^
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'block in NodeBB::Mongo#posts'
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'Array#map'
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'NodeBB::Mongo#posts'
        from script/import_scripts/nodebb/nodebb.rb:413:in 'block in ImportScripts::NodeBB#import_posts'
        from /home/dev/discourse/script/import_scripts/base.rb:943:in 'block in ImportScripts::Base#batches'
        from <internal:kernel>:168:in 'Kernel#loop'
        from /home/dev/discourse/script/import_scripts/base.rb:942:in 'ImportScripts::Base#batches'
        from script/import_scripts/nodebb/nodebb.rb:412:in 'ImportScripts::NodeBB#import_posts'
        from script/import_scripts/nodebb/nodebb.rb:52:in 'ImportScripts::NodeBB#execute'
        from /home/dev/discourse/script/import_scripts/base.rb:47:in 'ImportScripts::Base#perform'
        from script/import_scripts/nodebb/nodebb.rb:568:in '<main>'

開発環境で移行中にこのエラーが発生しました。
何か考えはありますか?

移行スクリプトは2026年になってもコミュニティによってサポートされていますか?
timestampフィールドは存在し、有効なデータを含んでいます。

よろしくお願いします。

問題は post が nil であることです。したがって、設定が正しくありません。ユーザーはインポートされましたか?

試みることはできますが、かなり限定的です。フォーラムで簡単にサポートできる範囲を超える、多くのことがどのように機能するかを理解する必要があるでしょう。

「いいね!」 3
ユーザーをインポート中
      182 / 182 (100.0%)  [437953 items/min]
ユーザーをグループに追加中...

トピックをインポート中...
      132 / 132 (100.0%)  [3567036 items/min]
投稿をインポート中...
/home/dev/discourse/script/import_scripts/nodebb/mongo.rb:100:in 'NodeBB::Mongo#post': undefined method '[]' for nil (NoMethodError)

      post["timestamp"] = timestamp_to_date(post["timestamp"])
                                                ^^^^^^^^^^^^^
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'block in NodeBB::Mongo#posts'
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'Array#map'
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'NodeBB::Mongo#posts'
        from script/import_scripts/nodebb/nodebb.rb:413:in 'block in ImportScripts::NodeBB#import_posts'
        from /home/dev/discourse/script/import_scripts/base.rb:943:in 'block in ImportScripts::Base#batches'
        from <internal:kernel>:168:in 'Kernel#loop'
        from /home/dev/discourse/script/import_scripts/base.rb:942:in 'ImportScripts::Base#batches'
        from script/import_scripts/nodebb/nodebb.rb:412:in 'ImportScripts::NodeBB#import_posts'
        from script/import_scripts/nodebb/nodebb.rb:52:in 'ImportScripts::NodeBB#execute'
        from /home/dev/discourse/script/import_scripts/base.rb:47:in 'ImportScripts::Base#perform'
        from script/import_scripts/nodebb/nodebb.rb:568:in '<main>'


スクリプトの出力によると、ユーザーは正しくインポートされたようです。上記を参照してください。

すみません、あなたは生きている人間ですか、それとも賢いAIアシスタントですか?

それなら、問題は投稿データを読み込めていないことにあるはずです。

私の推測では、問題はここにあって、何らかの理由で投稿が見つかっていないのでしょう。

    def posts(offset = 0, page_size = 2000)
      post_keys = mongo.find(_key: "posts:pid").skip(offset).limit(page_size).pluck(:value)

      post_keys.map { |post_key| post(post_key) }
    end

私は、10年間にわたりDiscourseのサポートで生計を立てており、数多くのインポーターを作成し、さまざまなプラットフォームから100以上のフォーラムをインポートしてきた生身の人間です。

AIアシスタントからの助けが必要な場合は、https://ask.discourse.com/ をご覧ください。

「いいね!」 6

どうにかスクリプトの関数を正しく動作させることができました。

Claude Code の助けも借りて、変更点を以下に示します :slight_smile:

 =>mongo.rb
   
   def posts(offset = 0, page_size = 1000)
      post_keys = mongo.find(_key: "posts:pid").skip(offset).limit(page_size).pluck(:value)
      post_keys
          .map { |pid| post(pid) }
          .compact  # <-- nilの結果をすべて削除(孤立したpid)
      post_keys.map { |post_key| post(post_key) }
    end

    def post(id)
    post = mongo.find(_key: "post:#{id}").first
    return nil if post.nil? # <-- nullチェック
    post["timestamp"] = timestamp_to_date(post["timestamp"])
    if post["upvoted_by"] = mongo.find(_key: "pid:#{id}:upvote").first
        post["upvoted_by"] = mongo.find(_key: "pid:#{id}:upvote").first[:members]
      else
        post["upvoted_by"] = []
      end

      post["pid"] = post["pid"].to_s
      post["deleted"] = post["deleted"].to_s

      post
    end
	
=>nodebb.rb

 create_posts(posts, total: post_count, offset: offset) do |post|
        # postがnullの場合はスキップ
		# merged_postの場合はスキップ
        next if post.nil?
        next if @merged_posts_map[post["pid"]]

        # 削除済みの場合はスキップ
        next if post["deleted"] == "1"

        raw = post["content"]
        post_id = "p#{post["pid"]}"

        next if raw.blank?
        topic = topic_lookup_from_imported_post_id("t#{post["tid"]}")

        unless topic
          puts "Topic with id #{post["tid"]} not found, skipping"
          next
        end	

これで正しく動作しているようです。

Discourse の内部アーキテクチャの観点から見て、これがどれほど正しいかはわかりませんが、一見したところ動作しているようです。

改善点や最適化に関する提案はいつでも歓迎します。

「いいね!」 1

ヒント: 完全にサイレントに失敗させないでください。これを次のように変更します。

if post.nil?
  puts "!!! Could not find post #{id}"
  return nil
end

さもないと、投稿の半分がどこにもないことに気づかず、何時間もイライラした後、原因がこれだったと判明します。

これら2つについても同様です

        next if post.nil?
        next if post["deleted"] == "1"
「いいね!」 5

mongo.rb ファイル内のメソッド posts を書き直しました

def posts(offset = 0, page_size = 1000)
post_keys = mongo.find(_key: “posts:pid”).sort(score: 1).skip(offset).limit(page_size).pluck(:value)
post_keys.map { |pid| post(pid)}.compact
end

ここに引用テキストを入力してください

このアプローチにより、トピック内の投稿が正しい時系列順にソートされることが保証されます。昇順です。
sort(score: 1) の呼び出しに注目してください。