Пошук у пошті за десятиліття: спочатку ключові слова, потім зміст
AI-пошук по пошті провалюється на брудних даних. Як очистити, зв’язати в ланцюжки та проіндексувати архів, щоб пошук за словами і за змістом працювали разом.

Спрямувати мовну модель на поштову скриньку і ставити їй запитання звучить як проєкт на вихідні. Потім вона впевнено називає вам неправильну дату контракту, бо прочитала цитовану копію старого листа всередині новішої відповіді, або не може знайти рахунок 10442, бо число не має «змісту», за який міг би зачепитися векторний пошук. Хороший пошук по архіву пошти здебільшого не про модель. Він про дані, якими ви її годуєте, і про спосіб, яким ви їх дістаєте. Ось як до цього підходимо ми.
Пошта брудніша, ніж здається
Архів пошти, що охоплює роки, кілька поштових систем і кілька міграцій, має проблеми, які руйнують пошук ще до того, як у справу вступить будь-який AI:
- Дублікати. Той самий лист лежить у Вхідних, у теці проєкту і в старому експорті. Пошук повертає його тричі; AI рахує це як три джерела, що погоджуються.
- Цитована історія. Кожна відповідь несе під собою весь ланцюжок. Проіндексуйте це наївно, і найстаріший лист ланцюжка з’являється в кожній відповіді, поза контекстом і з неправильною датою.
- Кодування. Старіша пошта повна застарілих наборів символів, закодованих заголовків і неправильно позначених частин. Декодуйте це неправильно, і імена та неанглійський текст перетворюються на сміття.
- Вкладення. Контракт є PDF, а не листом, у якому написано «у вкладенні». Якщо ви не витягуєте текст вкладень, ви не можете знайти найважливіші документи.
- Видалення. Пошта, видалена зі скриньки з причин зберігання, має зникнути і з кожного індексу.
Очищуйте, перш ніж індексувати
Дедуплікуйте за Message-ID, з резервним варіантом у вигляді хешу ключових заголовків і тіла, коли його немає. Зберігайте один документ на лист і тримайте список тек, у яких він з’являвся, як метадані.
Зв’язуйте в ланцюжки за References та In-Reply-To, а не за темами. Теми змінюються, і «Re: рахунок» не є унікальним. З належним зв’язуванням ви можете показати цілу розмову по порядку і дати кроку пошуку витягнути ланцюжок цілком замість окремих фрагментів.
Відокремлюйте новий текст від цитованого. Для кожного листа індексуйте все тіло для пошуку за ключовими словами, але вбудовуйте лише ту частину, яку автор справді написав. Простий початок:
import re
from email import policy
from email.parser import BytesParser
QUOTE_MARKERS = [
re.compile(r"^On .+ wrote:$", re.M),
re.compile(r"^-{2,}\s*Original Message\s*-{2,}", re.M | re.I),
re.compile(r"^From: .+\nSent: .+", re.M),
]
def new_text(raw: bytes) -> str:
msg = BytesParser(policy=policy.default).parsebytes(raw)
part = msg.get_body(preferencelist=("plain", "html"))
text = part.get_content() if part else ""
cut = len(text)
for rx in QUOTE_MARKERS:
m = rx.search(text)
if m:
cut = min(cut, m.start())
lines = [l for l in text[:cut].splitlines() if not l.startswith(">")]
return "\n".join(lines).strip()
Справжній поштовий трафік потребує більше маркерів, ніж тут, кожною мовою, якою пише ваша компанія, але принцип зберігається: модель має читати те, що сказала кожна людина, один раз, з правильною датою і відправником.
Витягуйте текст вкладень стандартними інструментами (pdftotext для PDF, бібліотечні парсери для офісних форматів) і індексуйте його як частину листа, зберігаючи посилання на оригінальний файл.
Два індекси, бо вони відповідають на різні запитання
Повнотекстовий індекс відповідає на запитання з точними зачіпками: відправник, домен, діапазон дат, номер рахунку, фраза, «має PDF». Він швидкий, точний і пояснюваний. Використовуйте такий з мовно-обізнаним стемінгом, щоб «рахунки» знаходило «рахунок», і з синонімами між мовами, якими користується ваша компанія, щоб пошук однією мовою знаходив пошту, написану іншою.
Векторний індекс відповідає на запитання про зміст: «коли постачальник уперше заперечив проти графіка постачання?» На це може не бути жодного ключового слова. Модель embedding перетворює кожен очищений лист на вектор, і пошук знаходить листи, близькі за змістом до запитання.
Жодного з них самого по собі не досить. Вектори слабкі в ідентифікаторах і числах; ключові слова слабкі в перефразуванні. Запускайте обидва і поєднуйте:
- Спочатку виділіть з запитання фільтри: людей, домени, дати, «з вкладенням».
- Застосуйте ці фільтри до обох пошуків, щоб векторний пошук ніколи не блукав поза періодом або людьми, про яких запитання.
- Злийте два списки результатів. Reciprocal rank fusion простий і працює добре: оцініть кожен лист сумою
1 / (k + rank)за обома списками. - Розгорніть найкращі результати до їхніх ланцюжків, щоб модель читала розмови, а не фрагменти.
Змусьте модель показувати свою роботу
Лише тоді в справу вступає мовна модель. Дайте їй знайдені листи, кожен з ID, датою і відправником, і доручіть відповідати лише з них і цитувати ID для кожного твердження. В інтерфейсі перетворіть кожне цитування на посилання, що відкриває оригінальний лист.
Цитування виконують дві роботи. Користувач може перевірити кожне твердження одним кліком, що є єдиним відповідальним способом використовувати модель на ділових записах. І ви одразу бачите, коли пошук провалився: відповідь, що цитує не ті листи, є проблемою пошуку, а не моделі, і вона каже вам, куди дивитися.
Кажіть користувачам прямо, що модель може прочитати неправильно. З джерелами за один клік це керований ризик.
Тримайте дані в будівлі
Кожен компонент у цьому дизайні зручно працює на вашому власному обладнанні: повнотекстовий індекс на звичайних серверах, векторна база даних поруч, а модель embedding і мовна модель на GPU-серверах, які ви контролюєте. Пошта, запитання і відповіді ніколи не повинні залишати вашу мережу.
Тестуйте це як систему, а не як демо
Перш ніж довіряти, зберіть невеликий набір запитань з відомими відповідями: конкретний рахунок, перший лист у відомій суперечці, лист, написаний вашою другою мовою, лист, видалений минулого місяця (його не повинно бути знайдено). Запускайте їх після кожної зміни очищення, індексування або промптів і зберігайте результати. Якщо зміна погіршує відомі запитання, ви це побачите.
І стежте за самим конвеєром. Синхронізація, що тихо зупинилася, індекс, який не наздогнав, або черга векторів після перезавантаження виглядають як «пошук працює», доки комусь не знадобиться пошта минулого тижня. Перевіряйте весь ланцюг наскрізь, за розкладом, і сповіщайте, коли будь-який етап відстає.
Коротко
- Дедуплікуйте, зв’язуйте в ланцюжки, декодуйте і прибирайте цитований текст, перш ніж щось індексувати.
- Індексуйте вкладення; саме там контракти.
- Запускайте пошук за ключовими словами і векторний пошук разом, з фільтрами, застосованими до обох.
- Відповідайте лише зі знайденої пошти, з цитуванням на кожне твердження.
- Тримайте кожен компонент на обладнанні, яке ви контролюєте, і тестуйте на відомих запитаннях.
Ми будуємо приватні архіви пошти саме так, з AI, що працює на ваших власних приватних GPU. Якщо ви плануєте такий, ми радо допоможемо.
Related (rows[1])
Пов’язане: Приватний архів пошти з локальним AI-пошуком · Managed офісні документи на Nextcloud
Опубліковано 15 жовтня 2026 р..
Усі нотатки з практики