MongoDB 向 MeiliSearch 同步的踩坑记录

使用 meilisync

感谢 yzqzss 的大力支持

tl;dr: fork 并大改了程序，参见

崩坏的前端

先尝试使用了他的 Admin Console，https://github.com/long2ice/meilisync-admin

只有 AMD64，没有 ARM 的镜像。谁还没有 x86_64 的机器啊，尝试在非数据库的机器上使用。我当时还没意识到从 A 机拉数据到 B 机再塞给 C 机是啥概念，但总之当时犯傻了。

下镜像、运行…… 等等这个数据库同步的 admin 为什么还要 MySQL 和 Redis 的 DBurl 啊？不理解但是当时配置了。

随后……

没有初始账户 ref #7

解决方案是手动写邮箱密码进数据库，还要手搓 bcrypt hash

创建 MongoDB 数据源，报错 Unknown option user #11

原因竟然是，不同数据库在后端配置文件需要的参数不同，网页只按照 PostgreSQL 的 user 进行了传参，而且后端没处理直接塞，sync 程序原地爆炸。

使用抓包改包重放解决。

本来写了 fix 但发现 PostgreSQL 的参就是对的，那爱咋咋地吧我感觉我管不了。也应该没人看到这还想用吧…… 应该吧。

设置好一切了之后还是跑不起来…… 后端有如下报错 (删除一吨内容)：

Copy
2025-04-11 21:38:42.156 | INFO | uvicorn.protocols.http.httptools_impl:send:496 - 10.0.1.1:64000 - "POST /api/sync HTTP/1.1" 500
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "/meilisync_admin/meilisync_admin/models.py", line 64, in meili_client
self.meilisearch.api_url,
^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'QuerySet' object has no attribute 'api_url'

额…… 看起来不是简单配置文件的问题了……

于是尝试使用 cli，避免是那鬼畜的 Admin Console 导致的问题以为马上就是终结的开始，原来只是开始的终结。

滞后的 docker

找到实际进行同步的程序，https://github.com/long2ice/meilisync

我还是尝试了 Docker，毕竟是”Recommended“的方法。但是按照他 Readme 写的 compose，

Copy
version: "3"
services:
meilisync:
image: long2ice/meilisync
volumes:
- ./config.yml:/meilisync/config.yml
restart: always

拉下来的镜像有问题。

我遇到的是 TypeError: 'async for' requires an object with aiter method, got list #94
但对应还有 TypeError: 'async for' requires an object with aiter method, got coroutine #76

嗯，得用 dev 呢。

啊对和上文一样，他 MongoDB 的 user 的字段是 username，和模板不一样。鬼知道当时我怎么能灵光一现想出是 username 的。

后面配置文件来回几次之后不想搞 docker 了，故转为本地 cli。

本地 Python

本地 cli 阶段，一切似乎向着好的方向好起来了。

为数不多的几个问题就是，虽然有 pip install meilisync[mongo] for MongoDB，但是只安装这个是不够的。实际运行任何命令都会狠狠的告诉你，缺这缺那。你只能 pip install meilisync[all] for all.

还有小 bug 要打 zsh。

Copy
$ pip install meilisync[mongo]
zsh: no matches found: meilisync[mongo]

终于配置好 config，一切似乎在向好发展…… 或者是么？

爆炸的进度

参考 #17 中的回复，当以 MongoDB 作为数据源时，progress.json 可能不会自动生成，导致一堆一堆的TypeError: meilisync.progress.file.File.set() argument after ** must be a mapping, not NoneType。

解决方案，先 touch 一下progress.json，再写进案例……

Copy
{"resume_token": {"_data": "8267FBA647000000022B042C0100296E5A10046F963A9EB7AB4D14B8CF191E8E5E8D67463C6F7065726174696F6E54797065003C696E736572740046646F63756D656E744B65790046645F6964006467FBA6470D168B18625CC73E000004"}}#

狠狠的 log

测试没发现大问题之后，改配置文件关了 debug，使用 nohup 运行起来之后我就去干别的事了，直到硬盘告警把我拉回到 shell。同步数据库到新地方的确很耗空间，我也准备好了。但我实在没想到是中间的这位硬盘先爆炸了 —— 增速甚至大于 Meili 数据库的机器 —— 人畜无害的同步器给我摔了 6 个 G 的日志到我脸上。

这不可能啊，我明明在配置文件中写了 debug=false 啊 ——

tail 了一下巨大的 log，发现他把每一条同步的内容全纯文本的记录下来了……

其实是默认的插件实例导致的。

在配置文件中存在如下的内容：

Copy
debug: false
plugins:
- meilisync.plugin.Plugin

其中 plugin 部分实际引用的是 https://github.com/long2ice/meilisync/blob/dev/meilisync/plugin.py

Copy
class Plugin:
is_global = False

async def pre_event(self, event: Event):
logger.debug(f"pre_event: {event}, is_global: {self.is_global}")
return event

async def post_event(self, event: Event):
logger.debug(f"post_event: {event}, is_global: {self.is_global}")
return event

在这个 plugin 中，不管配置文件中 debug 的设置为何值，都会写入 debug。

解决方案有三种：

不引用这个 plugin

修改 plugin 内容

修改全局的 log 级别：

Meilisync 使用 loguru，参考其文档可以通过设置 level 实现，再根据其环境变量的相关文档，可以设置LOGURU_LEVEL，可采用值如下表：

Level name | Severity value | Logger method
TRACE | 5 | logger.trace()
DEBUG | 10 | logger.debug()
INFO | 20 | logger.info()
SUCCESS | 25 | logger.success()
WARNING | 30 | logger.warning()
ERROR | 40 | logger.error()
CRITICAL | 50 | logger.critical()

然后设置环境变量：

Unix 下：

Copy
export LOGURU_LEVEL=INFO

Windows 下：

PowerShell

Copy
$env:LOGURU_LEVEL="INFO"

CMD

Copy
set LOGURU_LEVEL=INFO

后续来看节省了一吨的空间……

在各种 debug 中，把这个服务从 B 迁移到了 C，也就是 Meili Search 所在的地方。这在后来被证明提升了非常多的速度。

Index 的疑虑

我的数据中有 id 一项，但实际使用中会冒出各种问题，还是使用_id 作为主键。

动手改脚本

矫正了类型

似乎一切正常的运行了一阵子之后，程序自己就死掉了。几次检查之后发现是 TypeError: Object of type ObjectId is not JSON serializable。此时的进度大概都是 1,140,000 条数据。

同样有 GitHub Issue，#16，说是”fixed“。检查了一下本地代码，的确包含了 fix 的内容。但似乎还有出现在 #102，这次就没有任何回复了。

最难搞的不是修代码，而是让它再出错。由于出错之后进度就 g 了，每次都是从头开始，而每次跑到错误地方需要 20 分钟，消耗了几个小时在这个上面…… 还有，运行期间 CPU 全都拉满…… 我还应该庆幸不是用的小服务商机器会被拉闸……

对了，这段时间，用上了 sentry.io。很奇怪作者在这个同步工具中特别留了 sentry.io 的口子，但它真的非常有用。也许作者知道会在各种地方出 bug？

本地的修改

于是再实现了一下检测与修复。一开始尝试改造那个 plugin，但一开始实在没理顺内容，最后决定硬改源码！添加了额外的类型检查。不想一次次的 pip install，直接进site-packages改文件，又快又好。

修复完 ObjectId 不久，看超过半小时都没问题，进度也在一点点的走，正准备去睡觉，结果又有报错，这次是Object of type datetime is not JSON serializable，类似 #31 。同样的，加检查。这次是在 5,270,000 的位置，大概三分之一。修好之后能接着同步，也过了一半，于是我放心的去睡觉了。

然后早上醒来又是晴天霹雳，在差不多三分之二的时候，就会冒出Client error '408 Request Timeout' for url 'http://127.0.0.1:7700/tasks/xxxx。实在没办法，索引的东西太多太多，算不过来也就越积压越多，直至彻底 boom。但不正常的是，这个问题也被修过，在 #13 有提起过，但不知道啥原因，还是爆炸了。此时我检查了一下积压了多少，发现运行一个小时就会积压半个小时…… 我应该庆幸没有发生 Too many open files 的问题……

索引慢是没办法的…… 欸等等，说到底这索引就不应该这么快加才对吧！

延迟的索引

我决定尝试延迟，然后发现可怕的事实：在创建 index 的时候，meilisync 没有指定任何的字段索引选项。所以文档中的每个字段都会显示并可搜索，这耗费了超级多的资源，也造成可怕的浪费。我们完全没必要一开始就索引全部的内容。相反，我们在从远端同步数据的时候，不应该建立任何的索引，而应该等到直到所有来自源的内容全都都成功被插入了之后，再做索引的处理，而且应该能在 config file 中指定哪些字段被索引，包括索引的类型（searchable, sortable, filterable, none )

于是写了。目前写了从远端同步数据的时候，不应该建立任何的索引的逻辑，同步速度提升了一万倍。

然后写了在同步完之后通过改 setting 设置索引的功能，一切看似非常正常，直到一觉睡醒还是没有任何索引。

这不应该啊。检查之后发现，虽然提交了修改索引的任务，但跑到一大半爆炸了：

Copy
Index `nmbxd`: internal: MDB_TXN_FULL: Transaction has too many dirty pages - transaction too big.

终于不是 meilisync 的问题了！

优化的内存

仔细确认之后发现，其实不是跑了几个小时。它跑了几十分钟就 transaction too big 了，然后缩小 batch 重试，直到咋都试不出来。

锻炼！

经 @yzqzss 提醒了最佳实现是先设定 attribution，再同步 index，遵循最佳实现，但那样的话可能再出现 408。要解决 408 得实现队列，我比较懒不想再写代码了。

随后找了下竟然找到了能减少 index 时内存占用的 flag，参考 https://github.com/meilisearch/meilisearch/issues/3603 ，--experimental-reduce-indexing-memory-usage，的确一用就灵，一次成功。

以及关于更新，运行 meilisync refresh 即可。

再之后就是写 systemd 来定时进行同步了，如下。

Copy
# /etc/systemd/system/meilisync.timer
[Unit]
Description=Run meilisync refresh nmbxd weekly on Monday at 5 AM

[Timer]
# Run every Monday at 5:00 AM local time
OnCalendar=Mon *-*-* 05:00:00
Persistent=false

[Install]
WantedBy=timers.target

Copy
# /etc/systemd/system/meilisync.service
[Unit]
Description=Meilisync Refresh nmbxd
#After=network.target

[Service]
Type=oneshot
WorkingDirectory=/path/to/meilisync/config/
Environment='LOGURU_LEVEL=DEBUG'
ExecStart=/etc/meilisync/meilisync/bin/meilisync refresh

配置文件/path/to/meilisync/config/config.yml

Copy
debug: false
progress:
type: file
source:
type: mongo
host: REDACTED
port: REDACTED
username: 'REDACTED'
password: 'REDACTED'
database: REDACTED
meilisearch:
api_url: http://127.0.0.1:REDACTED
api_key: REDACTED
insert_size: 10000
insert_interval: 10
sync:
- table: REDACTED
index: REDACTED
full: true
pk: _id
attributes:
id: [filterable, sortable]
fid: [filterable]
img: [filterable]
ext: [filterable, sortable]
now: [filterable, sortable]
name: [searchable]
title: [searchable]
content: [searchable]
parent: [filterable, sortable]
type: [filterable]
userid: [filterable]
sentry:
dsn: ''
environment: 'production'