Files
obsidian-vault/personal/tech/zulip-message-history.md
T

11 KiB
Executable File
Raw Blame History

Zulip Message History — SQLite (поиск истории переписки)

Создано: 2026-09-29

Что это

Вся история переписки Zulip (134 734 сообщения, 197 тредов, 11 стримов) выгружена в один SQLite-файл. Zulip как транспорт выводится из эксплуатации — история остаётся в этом файле.

Параметр Значение
Файл ~/Documents/zulip_message_history.sqlite
Размер 78.1 MB
Сообщений 134 734 (134 688 в стримах + 46 личек)
Стримов 11
Тредов (topics) 197
Источник zulip-database-1 (Postgres 14, ~/Developer/zulip/docker-compose.yml)
Снимок на 2026-09-29
Бэкап ~/backup/zulip-final/ (см. §Бэкапы)

Схема

streams  (id, name)                                    -- 11 каналов
topics   (id, stream_id → streams.id, name)            -- 197 тредов
messages (id, topic_id → topics.id, kind, sender,
          sender_email, date_sent, content)            -- 134 734
messages_fts (fts5: content, content='messages')       -- полнотекстовый поиск
  • kind = stream | dm
  • content — markdown-исходник Zulip (не HTML-рендер); HTML был в rendered_content и в дамп не попал осознанно
  • date_sent — ISO-8601 UTC (2026-09-29 06:29:41.829084+00)

Индексы

Индекс Назначение
messages_fts (FTS5) поиск по тексту через MATCH
idx_messages_topic сообщения одного треда
idx_messages_date диапазон по датам
idx_topics_stream треды одного стрима

Как искать историю (главное)

DB=~/Documents/zulip_message_history.sqlite

1. Поиск по тексту

sqlite3 "$DB" "
SELECT s.name AS stream, t.name AS topic, m.sender, m.date_sent,
       substr(m.content, 1, 3000) AS content
FROM messages_fts f
JOIN messages m ON m.id = f.rowid
JOIN topics   t ON t.id = m.topic_id
JOIN streams  s ON s.id = t.stream_id
WHERE messages_fts MATCH 'docker jellyfin'
ORDER BY m.date_sent DESC LIMIT 30;"

2. Список тредов с количеством сообщений

sqlite3 -column -header "$DB" "
SELECT s.name AS stream, t.name AS topic, count(*) AS msgs
FROM messages m
JOIN topics  t ON t.id = m.topic_id
JOIN streams s ON s.id = t.stream_id
GROUP BY t.id ORDER BY msgs DESC;"

3. Прочитать тред целиком

sqlite3 "$DB" "
SELECT m.date_sent || ' | ' || m.sender || char(10) || m.content || char(10) || '==='
FROM messages m
WHERE m.topic_id = (SELECT id FROM topics WHERE name = 'Home assistant setup' LIMIT 1)
ORDER BY m.date_sent;"

4. Найти сообщения конкретного человека по ключевым словам

sqlite3 "$DB" "
SELECT t.name, m.date_sent, substr(m.content,1,500)
FROM messages m JOIN topics t ON t.id = m.topic_id
WHERE m.sender = 'Alex' AND m.content LIKE '%план%'
ORDER BY m.date_sent DESC LIMIT 20;"

⚠️ FTS5-синтаксис

Токенизатор — unicode61 (дефолт): поиск по целым словам, стемминга нет.

Хочу Запрос
слово docker MATCH 'docker'
префикс dock… MATCH 'dock*'
фраза MATCH '"zulip router"'
AND MATCH 'docker jellyfin'
OR MATCH 'docker OR jellyfin'
НЕ MATCH 'docker NOT podman'

собрал не найдёт собрать — стемминга нет. Для поиска по подстроке внутри слова нужен trigram-токенизатор (не включён; пересборка FTS-таблицы).

Стримы и треды (снимок)

Стрим Сообщений Тредов
personal 111 534 95
master 7 630 29
projects 7 524 20
executor 7 355 32
Zulip 432 1
daily-brief 182 12
general 15 1
inbox 14 3
journal 1 1
focus 1 1
Direct messages 46 2

Крупнейшие треды (personal): Home assistant setup 12 196 · truenas jellyfin 12 059 · Балда/валера 9 454 · ZONT Config compiler 6 585 · ✔ media toolbox setup 6 198.

🔴 Ограничение — база не живая

Файл — снимок на 2026-09-29. После сноса Zulip новые сообщения сюда не попадают. Живой поиск свежей переписки — только в Mattermost (hermes-maintenance, a6nyh77eyjbn5c1owh1i8eebee). Разделять явно: «историческое (до 29.09)» → SQLite; «что обсуждали вчера» → Mattermost.

Как это было сделано (воспроизводимо)

  1. Экспорт из Postgres в CSV. Ключ — \copy (не COPY), потому что COPY через docker exec требует прав на файл внутри контейнера, а \copy пишет в stdout. Колонки: id, kind, stream, topic, dm_user_id, sender, sender_email, date_sent, content.

    docker exec zulip-database-1 psql -U zulip -d zulip -c "\copy (
      SELECT m.id, 'stream' AS kind, s.name AS stream, m.subject AS topic, 0 AS dm_user_id,
             u.full_name AS sender, u.email AS sender_email, m.date_sent, m.content
      FROM zerver_message m
      JOIN zerver_recipient r ON r.id = m.recipient_id
      JOIN zerver_stream s ON s.id = r.type_id
      JOIN zerver_userprofile u ON u.id = m.sender_id
      WHERE r.type = 2 ORDER BY m.id
    ) TO STDOUT WITH (FORMAT csv, HEADER false)" > /tmp/zulip-export/messages.csv
    

    Личные сообщения (r.type = 1) — отдельным запросом в dms.csv, топик = DM user #<id>. В БД лички в zerver_recipient.type_id = 1, у них нет subject — отсюда синтетический топик.

  2. Сборка SQLite — скрипт build_sqlite.py (Python stdlib csv + sqlite3), создаёт таблицы, наполняет streams/topics/messages, строит индексы и наполняет FTS5.

  3. Верификация — PRAGMA integrity_check, PRAGMA foreign_key_check, count(*) против Postgres, FTS-запрос по реальному слову.

🔴 Питфоллы, которые стоили времени

  • TSV рвётся на многострочном контенте. Первый заход был с -A -F$'\t' → вместо 134 688 строк получилось 486 751 (переносы строк внутри сообщений режут TSV). Решение: WITH (FORMAT csv) — кавычки экранируют переносы корректно. wc -l на CSV всё равно покажет завышенное число переносов — это ожидаемо, считать строки нужно парсером, а не wc.
  • rendered_content — это HTML. Для читаемой истории нужен content (markdown). HTML-дамп бесполезен без рендера.
  • content может содержать что угодно — включая NUL и гигантские блоки кода. В build_sqlite.py нужен csv.field_size_limit(50MB), иначе падает на больших сообщениях.
  • CSV-колонок было 9, а проверял на 8 — первая сборка дала 0 сообщений и 134 734 ошибки «bad field count». Читать позиционно по факту заголовка, не по памяти.

Бэкапы

Что Где
SQLite (история) ~/Documents/zulip_message_history.sqlite
CSV-дампы /tmp/zulip-export/messages.csv (56 MB), dms.csv (18 KB)
Compose + env Zulip ~/Developer/zulip/docker-compose.yml, .env.zulip

Что НЕ выгружалось (осознанно, по указанию Alex)

Только переписка + структура тредов. Вне выгрузки:

  • вложения/аплоады (volume zulip_zulip-data, 38.1 MB — uploads/, certs/)
  • аватары, реакции, подписки, метаданные организаций
  • настройки стримов, права, история изменений сообщений

Если понадобится картинка из старого треда — её в SQLite нет, она была в volume.

Снос Zulip — чек-лист

⚠️ Пока не выполнено. Файл сделан как страховка перед сносом.

cd ~/Developer/zulip
docker compose down
docker volume rm zulip_zulip-data zulip_zulip-pgdata zulip_zulip-rabbitmq zulip_zulip-redis
docker rmi zulip/docker-zulip:11.6-0        # ~3.84 GB

Стек: zulip-zulip-1 (zulip/docker-zulip:11.6-0, порты 8000/8443), zulip-database-1 (zulip/zulip-postgresql:14), zulip-rabbitmq-1, zulip-memcached-1, zulip-redis-1. Объём volumes ≈ 1.1 GB (pgdata 503 MB на диске, БД 439 MB; data 38 MB).

⚠️ Зависимости, которые сломаются

Компонент Что будет
zulip-router (Go, ~/Developer/zulip-router/) мёртвый — поллит Zulip Events API. Убрать из compose, иначе restart-loop
zulip-bot-routing (Еagle/Кит/Валера/Клавдий webhook) маршрутизация в Zulip перестанет работать
Hermes platforms: zulip в ~/.hermes/config.yaml платформа отвалится; транспорт уже переехал на Mattermost

Связанные документы