250 lines
12 KiB
Markdown
Executable File
250 lines
12 KiB
Markdown
Executable File
# Zulip Message History — SQLite (поиск истории переписки)
|
||
|
||
_Создано: 2026-09-29_
|
||
|
||
## Что это
|
||
|
||
Вся история переписки Zulip (134 734 сообщения, 197 тредов, 11 стримов) выгружена в
|
||
**один SQLite-файл**. Zulip как транспорт выводится из эксплуатации — история остаётся
|
||
в этом файле.
|
||
|
||
| Параметр | Значение |
|
||
|---|---|
|
||
| **Файл** | `~/Documents/zulip_message_history.sqlite` |
|
||
| Размер | 81.9 MB на диске (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/` (см. §Бэкапы) |
|
||
|
||
## Схема
|
||
|
||
```sql
|
||
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` | треды одного стрима |
|
||
|
||
## Как искать историю (главное)
|
||
|
||
```bash
|
||
DB=~/Documents/zulip_message_history.sqlite
|
||
```
|
||
|
||
### 1. Поиск по тексту
|
||
|
||
```bash
|
||
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. Список тредов с количеством сообщений
|
||
|
||
```bash
|
||
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. Прочитать тред целиком
|
||
|
||
```bash
|
||
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. Найти сообщения конкретного человека по ключевым словам
|
||
|
||
```bash
|
||
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`.
|
||
|
||
```bash
|
||
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 — чек-лист
|
||
|
||
⚠️ **Пока не выполнено.** Файл сделан как страховка перед сносом.
|
||
|
||
```bash
|
||
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 |
|
||
| Bot webhook-роутинг (Eagle/Кит/Валера/Клавдий, `host.docker.internal:8644-8645`) | маршрутизация в Zulip перестанет работать |
|
||
| Hermes `platforms: zulip` в `~/.hermes/config.yaml` | платформа отвалится; транспорт уже переехал на Mattermost |
|
||
|
||
ℹ️ Скиллы на Zulip больше не ссылаются — этап 3 закрыт, битых инструкций после сноса не будет
|
||
(кроме двух файлов `docker-balda-service`, см. §«Оставлено как есть»).
|
||
|
||
## Связанные документы
|
||
|
||
- [[plans/zulip-decommission]] — план и статус вывода Zulip из эксплуатации (этап 3 = этот файл)
|
||
- [[projects/zulip-router]] — архитектура event-роутера (Go), конфиг ботов
|
||
- [[tech/hermes-whale-mattermost-transport]] — текущий транспорт Hermes
|
||
- [[tech/hermes-self-improvement-review]] — фоновый review скиллов/памяти (в т.ч. как его выключить)
|
||
- [[how-to/hermes-eagle-mac]] — Hermes на Mac, Zulip Docker стек, RabbitMQ/Khepri pitfall
|
||
- [[tech/docker-mac-disk-issues]] — почему Zulip тормозил (переполнение диска VM)
|
||
|
||
## Как это используется агентами (скилл вместо скиллов)
|
||
|
||
Три скилла, работавшие с Zulip Postgres, **снесены** — их место занял один:
|
||
|
||
| Было | Стало |
|
||
|---|---|
|
||
| `zulip-db-forensics` | `message-history-search` |
|
||
| `zulip-thread-handoff` | ↑ |
|
||
| `zulip-bot-routing` | ↑ |
|
||
|
||
`~/.hermes/skills/note-taking/message-history-search/SKILL.md` — единственная точка входа
|
||
для «ищи в базе», «вспоминай», «читай историю». На неё ссылаются `SOUL.md` (оба файла!) и
|
||
`USER.md`, поэтому агент находит её без подсказки.
|
||
|
||
**Триггер в SOUL/USER:** «ищи в базе» / «чё сделано» → `sqlite3` СРАЗУ, без `session_search`
|
||
и без теории. `session_search` индексирует файлы сессий Hermes, **не** историю чата —
|
||
для старых тредов он пуст.
|