给 Matrix Synapse 添加中文搜索

Matrix很好很强大,一般服务端都用Synapse,支持的协议最完善,然而它的中文搜索很难用,原因在于PostgreSQL未能正确的给中文分词。另一个服务端项目dendrite 支持CJK (中日韩)分词,也只是略好一些,并且那个项目开发也几乎停滞了。 开源IM软件中原生支持中文搜索的有Mattermost,我参考它给Synapse开发了一个方案,具体的做法是 使用 Zhparser 插件版 Postgres,给数据表添加一个字段,改少量Synapse代码。通过文件映射的方式,尽可能减少后期维护成本。

Matrix是一种开源的通信协议,Synapse则是基于Matrix的最流行服务端,遗憾的它对中文搜索支持得不好(Japanese, Chinese and other non-ASCII searching not quite working · Issue #901 · element-hq/synapse),因为它的搜索是通过数据库实现的,通过向数据库传入sql语句to_tsvector('english', ?)to_tsquery('english', ?)来进行全文搜索,而生产部署依赖的 PostgreSQL 数据库默认不支持中文分词。

要实现中文搜索,有两种方式,一种是单独在外部创建搜索引擎(例如:meilisearch),这无法直接在 Element 等客户端使用;另一种是在 PostgreSQL 中安装插件 zhparser,这也是 Mattermost (另一种开源的通信软件)的做法 ,参考:Chinese, Japanese and Korean search - Mattermost documentation

下面是后一种方案的实现,要点在于尽可能的减少对源代码的改动及后期维护成本。

方案的思路是使用 Docker 部署附带 Zhparser 插件的 Postgres 镜像,在 Synapse 数据库中安装插件,然后在搜索相关的表中添加一个用来存放中文分词向量的字段,修改Synapse的 search 代码脚本,将默认的英文搜索模式改为中文搜索,然后在主机目录中映射给同样 Docker 部署的 Synapse 容器。

如果已经安装了Synapse,则需要进行数据库迁移,下面演示步骤。

步骤一:备份并迁移旧数据

一般情况下 Docker 部署是 托管 Homeserver 的最佳方式, 这儿有一个托管在Docker HUb上的 Zhparser 镜像 abcfy2/zhparser, 或者可以自己编译附带 Zhparser 的 Postgres 镜像,又或者在运行中的 Postgres 容器中安装这个插件。

首先 SSH 连接服务器终端,sudo -i 切换到 root 账号。

停止Synapse容器,进入到旧的Synapse 依赖的 Postgres 容器,导出数据库:

# 分别执行以下命令
# 进入 postgres 容器,postgres是数据库容器名称,通过docker ps 可查询
docker exec -it postgres bash
# 在容器内创建数据库备份目录,,如果你没有显式设置的话,这里假设 postgres 的数据目录是 /var/lib/postgresql/data,并且已经映射到主机目录
mkdir /var/lib/postgresql/data/dump_backup
# 使用postgres自带的pg_dump工具导出数据库,database_user database_name分别是数据库用户名和synapse的数据库名
pg_dump -U database_user database_name -f /var/lib/postgresql/data/dump_backup/synapse_backup.sql

如果你想要删除旧数据库则可以使用命令(不建议这么做):

-- 切换到默认的postgres数据库
psql -d postgres -U database_user
-- 断开所有synapsedb数据库连接
SELECT pg_terminate_backend(pg_stat_activity.pid)
FROM pg_stat_activity
WHERE pg_stat_activity.datname = 'synapsedb' AND pid <> pg_backend_pid();
-- 删除旧数据库
DROP DATABASE synapsedb;

Ctrl+D 退出postgres容器虚拟交互终端。

迁移旧数据库数据:

# 创建新数据库映射的目录,path为你自己的路径
mkdir path/pgzhparser/data
# 将旧数据库导出到主机目录的的数据复制到上一步创建的zhparser容器映射的数据目录
cp  path/postgres/data/dump_backup -a path/pgzhparser/data
# 将旧数据导出脚本中的数据库用户名改为将为新数据库创建的用户名(如果两者名称一致的话这一步可以省略)
sed -i 's/old_database_user/new_database_user/g' path/pgzhparser/data/dump_backup/synapse_backup.sql

步骤二:创建zhparser项目

运行docker run 创建容器 :

# port、database_user、database_password、path填入自己新数据库的设置
docker run -d --restart=always --name pgzhparser -p port:5432 -e POSTGRES_USER=database_user -e POSTGRES_PASSWORD=database_password -e POSTGRES_DB=postgres -e PGDATA=/var/lib/postgresql/data -v path/pgzhparser/data:/var/lib/postgresql/data abcfy2/zhparser:latest

步骤三:导入旧数据

进入新数据库容器:

# pgzhparser是步骤二创建的容器名
docker exec -it pgzhparser bash
# 使用 `psql` 连接到 默认的 PostgreSQL 数据库
psql -U database_user -d postgres

创建数据库:

-- synapsedb是新的数据库名称,database_user是之前使用的数据库用户名
CREATE DATABASE synapsedb
    WITH OWNER = database_user
    ENCODING = 'UTF8'
    LC_COLLATE = 'C'
    LC_CTYPE = 'C'
    TEMPLATE = template0;

注:Synapse 项目要求数据库排序规则是 C 编码(Synapse - database),而postgres默认创建数据库的排序规则是 en_US.utf8,一经创建不可修改,因此需要显式指定。

\q退出 psql 提示符。你也可以使用Navicat、DBeaver等图形管理工具来完成在数据库执行SQL语句的步骤。

导入旧数据:

# 在pgzhparser容器中将旧数据库导入到新数据库(前提完成上述迁移旧数据库数据步骤)
psql -U database_user -d synapsedb -f /var/lib/postgresql/data/dump_backup/synapse_backup.sql

启用 Zhparser 分词搜索

# 再次进入 `psql` 提示符,并切换到新创建的数据库:
psql -U database_user -d synapsedb

在数据库中安装 zhparser 插件并配置文本搜索配置:

-- 在 `psql` 提示符下,执行以下 SQL 命令来创建zhparser扩展和配置:
CREATE EXTENSION IF NOT EXISTS zhparser;
-- “chinese”是一个自定义名称,后续将用到
CREATE TEXT SEARCH CONFIGURATION chinese (PARSER = zhparser);
ALTER TEXT SEARCH CONFIGURATION chinese ADD MAPPING FOR n,v,a,i,e,l WITH simple;
-- 在 event_search 表中添加 chinese_vector 列,用于存储中文分词后的向量
ALTER TABLE event_search ADD COLUMN IF NOT EXISTS chinese_vector tsvector;
-- 对已有数据进行中文分词处理
UPDATE event_search SET chinese_vector = to_tsvector('chinese', vector::text);
-- 为中文列创建 GIN 索引
CREATE INDEX CONCURRENTLY event_search_chinese_vector_idx ON event_search USING GIN (chinese_vector)

步骤四:修改synapse搜索源码

synapse搜索相关的python脚本:search.py · element-hq/synapse

下载该脚本并用编辑器打开,修改这几处(查找vector和english字段):

  1. 插入数据表时添加chinese_vector字段。
  2. 添加chinese_vector索引(前面步骤已经在数据库中手动添加了,这个代码适用于从新创建的synapse项目)。
  3. 将英文搜索改为中文搜索。

这儿是一个修改过的脚本,可以直接使用:search.py

步骤五:将修改过的python脚本映射给synapse容器

将修改后的python脚本上传到一个映射到synapse容器的目录,例如新建一个文件夹:./py_config

修改synapse容器的homeserver.yaml,将database连接切换到新数据库:

database:
  name: psycopg2
  txn_limit: 1000
  args:
    user: database_user
    password: password
    dbname: synapsedb
    host: 172.17.0.1
    port: port
    cp_min: 5
    cp_max: 10

修改synapse容器的volumes配置,添加:

volumes:
  # ./py_config是主机上传python脚本的目录
  - ./py_config/search.py:/usr/local/lib/python3.12/site-packages/synapse/storage/databases/main/search.py

启用synapse容器 docker start synapse

然后就可以用中文关键词搜索了。

英文也没问题。

总结

此方案主要避免中文分词的错误,但 postgresql 的全文搜索本身比较羸弱,诸如中英文混合、长句、多关键词仍然准确较低。并且不支持加密房间。

经过测试 安卓版 element 客户端仍然多数显示不到中文关键词搜索的结果,而IOS和Windows版则没有这个问题。

synapse升级有可能代码变更导致这种自定义配置失效。为了监听这种变化,最简单的方式是 RSS 订阅 Releases · element-hq/synapse,其次可以定时请求GitHub API获取项目单独文件的提交日志。还有一种方式:将GitHub仓库镜像到gitea,gitea可以用rss订阅单个文件的提交日志。

对于全新安装的 synapse,下面的docker-compose方便一键部署和更新(仍然需要安装数据库插件和修改项目源码的操作):

version: '3'
services:
  pgzhparser:
    image: abcfy2/zhparser:latest
    container_name: pgzhparser
    restart: unless-stopped
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: user_name
      POSTGRES_PASSWORD: password
      POSTGRES_DB: postgres
      PGDATA: /var/lib/postgresql/data
    volumes:
      - ./data:/var/lib/postgresql/data
    network_mode: bridge
  synapse:
    image: docker.io/matrixdotorg/synapse:latest
    container_name: synapse
    restart: unless-stopped
    environment:
      - SYNAPSE_CONFIG_PATH=/data/homeserver.yaml
    volumes:
      - ./files:/data
      - ./py_config/search.py:/usr/local/lib/python3.12/site-packages/synapse/storage/databases/main/search.py
    network_mode: bridge
    ports:
      - 8448:8448/tcp
    healthcheck:
      disable: true

注意:
Synapse 1.119.0 版之后使用/usr/local/lib/python3.12这个路径,而之前的版本需要将python包路径改为python3.11