惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
罗磊的独立博客
H
Help Net Security
I
Intezer
G
Google Developers Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
T
Troy Hunt's Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
U
Unit 42
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
N
News and Events Feed by Topic
J
Java Code Geeks
S
Security Affairs
T
The Blog of Author Tim Ferriss
Recent Commits to openclaw:main
Recent Commits to openclaw:main
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
D
Docker
The GitHub Blog
The GitHub Blog
F
Full Disclosure
N
News and Events Feed by Topic
Webroot Blog
Webroot Blog
S
Security @ Cisco Blogs
腾讯CDC
人人都是产品经理
人人都是产品经理
M
MIT News - Artificial intelligence
Blog — PlanetScale
Blog — PlanetScale
T
Threatpost
D
DataBreaches.Net
Recent Announcements
Recent Announcements
博客园 - 三生石上(FineUI控件)
MongoDB | Blog
MongoDB | Blog
博客园 - 【当耐特】
L
LINUX DO - 最新话题
Google Online Security Blog
Google Online Security Blog
S
Schneier on Security
S
Securelist
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Help Net Security
Help Net Security
P
Proofpoint News Feed
Project Zero
Project Zero
S
SegmentFault 最新的问题
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MyScale Blog
MyScale Blog
Google DeepMind News
Google DeepMind News
宝玉的分享
宝玉的分享
Y
Y Combinator Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - 叶小钗

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
# pg-smart-search: Путь от 8 секунд до 40 мс — Часть 2. Масштабирование до миллиона строк и производственная архитектура
Muminov Abubakr · 2026-06-16 · via Все публикации подряд на Хабре

Средний

6 мин

0

PostgreSQL vs Elasticsearch — путь от 8 секунд до 40 мс

PostgreSQL vs Elasticsearch — путь от 8 секунд до 40 мс

Привет, Хабр!

В первой части мы разобрали архитектуру pg-smart-search изнутри: параллельный Promise.race, механизм Zombie Prevention через AbortSignal, адаптерный паттерн и CLI-инструмент. Если не читали -- рекомендую начать оттуда, здесь я буду отсылать к тем концепциям.

В этой части речь пойдет о том, что происходит, когда ты запускаешь всё это на реальных данных.

На объемах до 100K строк система работала именно так, как задумано: FTS побеждал в Promise.race первым, кэш давал sub-1ms на горячих запросах, пропускная способность — 90 req/sec. Картина мечты.

Потом пришли данные. Много данных. 1 000 000 строк с нормальным, реальным словарем — и всё сломалось. Время поиска улетело к 8-ми секундам.

В этой статье - честный разбор двух этапов спасения: сначала архитектурные фиксы, потом бой с «ошибками выжившего» в бенчмарках. И в конце — production-grade микрооптимизации, которые мы сделали в v1.4.1, вылизав каждую миллисекунду.


Архитектура pg-smart-search: гибридный поиск с Promise.race и Zombie Prevention

Архитектура pg-smart-search: гибридный поиск с Promise.race и Zombie Prevention

ЭТАП 1: Архитектурные грабли на 1М строк

Изначально я сделал всё «по классике»: OFFSET пагинация, триггеры на апдейт FTS-векторов и COUNT(*) OVER() на каждой странице. На тестовых 10 тысячах строк -- летало. На 1 миллионе -- 8 гребаных секунд.

1. Пагинация через OFFSET: скрытый убийца

LIMIT 20 OFFSET 200000 -- база честно сканирует 200К строк и выбрасывает их. На миллионе это катастрофа.

Перешли на Keyset Pagination: WHERE id > $cursor. База прыгает к нужному месту по B-Tree индексу мгновенно. Глубина страницы не имеет значения.

2. COUNT(*) OVER() на каждой странице

Оконная функция для подсчета total_count проходит по всем подходящим строкам при каждом запросе. Ввели флаг skipTotalCount -- считаем только на первой странице, дальше отдаём 0. Нагрузка на пагинации упала на 90%.

3. Триггеры на to_tsvector

3. Триггеры на to_tsvector

Массовый COPY миллионов строк с триггерами, пересчитывающими FTS-векторы, просто вешал базу. Решение -- Generated Columns:

GENERATED ALWAYS AS (to_tsvector('config', column)) STORED

Вектор строится при вставке, один проход, в 10 раз быстрее.

Результат Этапа 1: задержка упала с 8 секунд до ~80мс. Мы праздновали. Зря.


ЭТАП 2: Ошибка выжившего и настоящий High Cardinality

Почему 19 мс было ложью

Первые бенчмарки показывали 19 мс на 1M строк. Красиво. Но скрипт-генератор тестовых данных использовал всего 40 слов в лексиконе. PostgreSQL сжимал GIN-индекс так, что он целиком влезал в кэш L3 процессора.

В реальном тексте кардинальность -- сотни тысяч слов. Когда мы перегенерировали данные из 100 000 уникальных слов, тесты упали с Out-Of-Memory в Docker. Пришлось ограничить maintenance_work_mem до 512MB и делать настоящую оптимизацию.

GIN -> GiST: магия оператора <->

В первой статье мы описали, как используем pg_trgm как fallback когда FTS пустой. Там же у нас висел стандартный GIN-индекс:

CREATE INDEX idx_trigram ON my_table USING GIN (text gin_trgm_ops);

На 1M строк с real-world данными это работало так: GIN вытаскивал все строки по условию похожести, а база последовательно высчитывала word_similarity для каждой из них в памяти, чтобы найти топ-20. На миллионе это -- полный скан.

Переход на GiST изменил всё:

```

CREATE INDEX idx_trigram_gist ON my_table USING GIST (text gist_trgm_ops);

-- KNN-запрос — база сразу находит 20 ближайших соседей
SELECT text FROM my_table ORDER BY text <-> $1 ASC LIMIT 20;

GiST нативно поддерживает оператор дистанции <->. База не сканирует ничего лишнего -- по дереву она мгновенно находит K ближайших соседей (KNN). Fallback-задержка упала до миллисекунд.

Эволюция ts_rank: честный разговор

В первой статье я показывал код с ts_rank_cd (Cover Density) как фичу взвешенного ранжирования. Тогда на 15K строк это выглядело нормально.

На миллионе строк ts_rank_cd стал узким горлышком. Алгоритм вычисляет позиционную близость лексем -- для этого база спускается в каждую запись и читает позиции слов. На Bitmap Scan по 1M строк это катастрофа.

Замена на ts_rank (только частота слов, без позиций) срезала холодную задержку ещё на 30%. Качество результатов -- практически идентично для большинства запросов. Для поиска по ID или SKU разницы ноль. Для академических статей -- может быть заметно, но это уже другой usecase.


Зомби-запросы: до и после внедрения cancelPool

Зомби-запросы: до и после внедрения cancelPool

Про механику Zombie Prevention с AbortSignal и cancelPool подробно писал в первой части. Если коротко: при 1000 RPS на миллионной базе это разница между стабильной работой и Connection Starvation.


Итог: Честные O(log N)

После всех оптимизаций -- финальный бенчмарк с High Cardinality словарем (100K уникальных слов):

Кол-во строк

Cold Latency

10 000

2.60 мс

50 000

6.40 мс

100 000

7.08 мс

500 000

10.33 мс

1 000 000

39.33 мс

График: логарифмическое масштабирование Cold Latency — O(log N)

График: логарифмическое масштабирование Cold Latency — O(log N)

Данных в 100 раз больше, время увеличилось лишь в 15 раз. Строгий O(log N). С кэшированием (Hot Cache) -- ~6 мс при тысячах RPS.


ЭТАП 3: Производственная архитектура (v1.4.1)

Хорошо работать на бенчмарке -- одно. Работать на продакшене при 1000 RPS -- другое. В v1.4.1 мы прошлись по коду с профилировщиком и вырезали всё лишнее.

1. Кэширование стратегий -- ноль аллокаций на запрос

До этого каждый вызов search() создавал новый объект стратегии:

// БЫЛО: новый объект на каждый запрос

results = await new LiteStrategy(this.adapter, this.config).search(

  query,

  options,

);

При 1000 RPS — тысячи бессмысленных аллокаций в секунду. GC в ответ начинает делать Stop-The-World паузы, которые незаметны на синтетике и очень заметны на проде.

Стратегии stateless -- создаём один раз в конструкторе:

// СТАЛО: 0 аллокаций на запрос

constructor(adapter, config) {

    this.liteStrategy = new LiteStrategy(adapter, config);

    this.ftsStrategy  = new FTSStrategy(adapter, config);

    this.advancedStrategy = new AdvancedStrategy(adapter, config);

    this.vectorStrategy   = new VectorStrategy(adapter, config);

}

2. Ring Buffer для метрик -- O(1) вместо O(n)

MetricsCollector собирал историю латентности в обычный массив. При достижении лимита -- Array.shift(), который копирует весь массив. На каждой записи метрики. O(N).

// СТАЛО: Ring Buffer на Float64Array

private latencyBuffer = new Float64Array(1000);

private latencyIdx = 0;

recordDbLatency(ms: number) {

    this.latencyBuffer[this.latencyIdx % 1000] = ms;

    this.latencyIdx++;

    // O(1), 0 аллокаций, фиксированный размер

}

3. Статический маппинг раскладки

В первой статье я показывал Smart Layout Correction -- конвертацию ghbdtn -> привет. Но маппинг EN→RU пересоздавался на каждый вызов:

// БЫЛО: 130-символьный маппинг × N запросов = бессмысленная работа

static convertLayout(query: string): string {

    const en = "qwerty...".split('');

    const ru = "йцукен...".split('');

    const map = {};

    en.forEach((c, i) => map[c] = ru[i]); // Каждый. Раз.

    ...

}
// СТАЛО: вычисляется одним разом при загрузке модуля

private static readonly layoutMap = (() => {

    const en = "qwerty...".split('');

    const ru = "йцукен...".split('');

    const m: Record<string, string> = {};

    en.forEach((c, i) => m[c] = ru[i]);

    return m;

})();

static convertLayout(query: string): string {

    return query.split('').map(c => this.layoutMap[c] || c).join('');

}

### 4. Memory Leak в MemoryCache -- sweep timer

MemoryCacheProvider удалял записи только при обращении (lazy eviction). Если ключ больше никогда не запрашивается -- он живёт в Map вечно. Классическая утечка, которую вы не увидите в краткосрочном тесте.

constructor(sweepIntervalMs = 60_000) {

    this.sweepTimer = setInterval(() => {

        const now = Date.now();

        for (const [key, item] of this.cache) {

            if (now > item.expires) this.cache.delete(key);

        }

    }, sweepIntervalMs);

    this.sweepTimer.unref(); // Не блокирует exit Node.js

}

5. AbortSignal в транзакциях — закрываем последнюю дыру

В первой статье мы описали Zombie Prevention. Но там был пробел: AbortSignal прокидывался только в обычные запросы. Внутри транзакций (BEGIN...COMMIT) сигнал терялся, и транзакция продолжала работать даже когда клиент уже ушёл.

Расширили PgClientAdapter: при получении сигнала abort он немедленно идёт в cancelPool и отправляет pg_cancel_backend(pid). Теперь зомби-запросы невозможны ни в одном сценарии.

6. Детерминированные кэш-ключи

Тонкий баг: {status: 'active', lang: 'ru'} и {lang: 'ru', status: 'active'} генерировали разные кэш-ключи через JSON.stringify. Cache miss на ровном месте.

const filterStr = JSON.stringify(

  Object.keys(filters)

    .sort()

    .reduce((acc, k) => {

      acc[k] = filters[k];

      return acc;

    }, {}),

);

Мелочь. Но на Redis-кэше это разница между 0.5 мс и 40 мс на каждом дублирующемся запросе.


TL;DR для тех, кто листает в конец

Проблема

Решение

OFFSET пагинация на 1M строк

Keyset (Cursor) Pagination

COUNT(*) OVER() на каждой странице

skipTotalCount

Триггеры на to_tsvector при COPY

Generated Columns

GIN + ORDER BY similarity -- полный скан

GiST + KNN оператор <->

ts_rank_cd -- тормоз на 1M+

Заменили на ts_rank

Стратегии создаются на каждый запрос

Кэширование в конструкторе

Array.shift() в метриках -- O(n)

Ring Buffer -- O(1)

Маппинг раскладки пересоздаётся

Статическое поле

MemoryCache без очистки

Background sweep timer

AbortSignal теряется в транзакциях

Проброс + EventListener

JSON.stringify без сортировки ключей

Детерминированная сортировка


Если вы строите поиск на PostgreSQL и не хотите тащить внешние движки — код и документация здесь: pg-smart-search на GitHub🌟

Если сталкивались с «ошибками выжившего» в своих бенчмарках или есть другие трюки для PostgreSQL -- пишите в комментариях, интересно сравнить опыт!

#PostgreSQL #NodeJS #Search #Backend #OpenSource #Performance #Database