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

推荐订阅源

Y
Y Combinator Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Apple Machine Learning Research
Apple Machine Learning Research
Blog — PlanetScale
Blog — PlanetScale
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
G
Google Developers Blog
F
Full Disclosure
大猫的无限游戏
大猫的无限游戏
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Threat Research - Cisco Blogs
A
Arctic Wolf
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Cloudflare Blog
博客园 - 【当耐特】
AWS News Blog
AWS News Blog
U
Unit 42
V
Vulnerabilities – Threatpost
P
Privacy International News Feed
T
Tor Project blog
Microsoft Security Blog
Microsoft Security Blog
宝玉的分享
宝玉的分享
Google DeepMind News
Google DeepMind News
爱范儿
爱范儿
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Recorded Future
Recorded Future
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Threatpost
Latest news
Latest news
GbyAI
GbyAI
S
SegmentFault 最新的问题
MongoDB | Blog
MongoDB | Blog
N
Netflix TechBlog - Medium
Hacker News: Ask HN
Hacker News: Ask HN
美团技术团队
N
News | PayPal Newsroom
J
Java Code Geeks
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Microsoft Azure Blog
Microsoft Azure Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
The Hacker News
The Hacker News
The GitHub Blog
The GitHub Blog
V
V2EX
N
News and Events Feed by Topic
T
Troy Hunt's Blog
Security Latest
Security Latest
博客园 - 叶小钗
P
Palo Alto Networks Blog

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 миллионов точек без потерь
Некорпоративный Хабр: семантический поиск и фильтрация по структурированным полям
igor_suhoruk · 2026-05-19 · via Все публикации подряд на Хабре

Некорпоративный Хабр: семантический поиск и фильтрация по структурированным полям

Уровень сложностиСложный

Время на прочтение16 мин

Охват и читатели6

Классический RAG индексирует исходный текст документа, предварительно разбивая на фрагменты. Потом рассчитывает векторное представление фрагментов и сохраняет их векторные представления в базу данных для поиска. Это дает возможность искать по сходству фрагментов текста и поискового запроса пользователя, но не дает возможность искать по более высокоуровневым резюме и смыслам, темам поднятым в тексте и прочему. Также не помогает с аналитикой по содержимому.

Бесплатный проект text-metadata-generator позволяет выполнять запросы к LLM по каждому документу из коллекции документов, результаты вывода LLM проверяются по JSON схеме.

Зачем может пригодиться эта программа и подход со структурированием текстовой информации

  • своя библиотека с каталогом - поиск по локальным документам с использованием комбинации SQL предикатов и семантического поиска

  • аналитика по документам, возможность находить новое в текстах: комбинируя структурированные поля созданные LLM из исходного текста, и находя закономерности с уже существующими в документе метаданными. Например, связывая с рейтингом признак NSFW, тон повествования, полноту содержания итп.

  • разгрести “авгиевы конюшни” личных заметок в Obsidian или git репозитарии с Markdown файлами

Рассмотрим как работает данный подход на 13275 статьях с Хабра, а также текстах трех песнен…

Примеры обработки данных

Технические статьи это главный источник для которого разрабатывался данный подход, но можно запускать анализ данных на текстах популярных песен. Если для одного и того же текста использовать разные LLM модели, то можно получить синтетический плюрализм “мнений” от LLM

Nautilus Pompilius: Воздух
---
title: "Воздух"
author: "Nautilus Pompilius"
---
Когда они окружили дом
И в каждой руке был ствол
Он вышел в окно с красной розой в руке
И по воздуху плавно пошел
И хотя его руки были в крови
Они светились как два крыла
И порох в стволах превратился в песок
Увидев такие дела
Воздух выдержит только тех
Только тех, кто верит в себя
Ветер дует туда, куда
Прикажет тот, кто верит в себя
Воздух выдержит только тех
Только тех, кто верит в себя
Ветер дует туда, куда
Прикажет тот кто...
Они стояли и ждали, когда
Он упадет с небес
Но красная роза в его руке
Была похожа на крест
И что-то включилось само-собой
В кармане полковничьих брюк
И чей-то голос так громко сказал
Что услышали все вокруг
Воздух выдержит только тех
Только тех, кто верит в себя
Ветер дует туда, куда
Прикажет тот, кто верит в себя
Воздух выдержит только тех
Только тех кто верит в себя
Ветер дует туда, куда
Прикажет тот, кто...
А полковник думал мысль
И разглядывал пыль на ремне:
"Если воры ходят по небесам
Что мы делаем здесь, на Земле
Дети смотрят на нас свысока
И собаки плюют нам вслед
Но если никто мне не задал вопрос
Откуда я знаю ответ, что..."
Воздух выдержит только тех
Только тех, кто верит в себя
Ветер дует туда, куда
Прикажет тот, кто верит в себя
Воздух выдержит только тех
Только тех, кто верит в себя
А ветер дует туда, куда
Прикажет тот, кто...
Title
  Анализ песни «Воздух» группы Nautilus Pompilius

Summary
  Текст описывает сюрреалистическую сцену противостояния одного человека и вооруженного отряда. Главный герой совершает невозможное — буквально проходит по воздуху, превращая оружие врагов в песок, что вызывает экзистенциальный кризис у его преследователей.

Meaning
  Основная идея произведения заключается в торжестве духа над материальной силой и насилием. Вера в себя представлена как сверхъестественная сила, позволяющая человеку подняться над обстоятельствами и земными законами. Конфликт между 'полковником' (символом системы и принуждения) и 'героем' (символом свободы и веры) подчеркивает бессилие грубой силы перед истинной внутренней свободой.

Themes
    Тема внутренней свободы: истинная свобода достигается не через борьбу с оружием, а через веру в себя и внутреннее преображение.
    Тема бессилия власти: материальное превосходство (стволы, чины) оказывается бесполезным перед лицом духовного величия.

Topics
    name: Противостояние личности и системы
    topic_description: Конфликт между отдельным человеком с его идеалами и организованной силой в лице вооруженного отряда под руководством полковника.
    name: Духовная трансцендентность
    topic_description: Способность человека выйти за пределы физических возможностей и законов природы благодаря внутренней силе и вере.

Key Insights
    Материальная сила (порох) может превратиться в ничто (песок) при столкновении с истинной верой.
    Осознание собственного бессилия приводит к экзистенциальным вопросам о смысле пребывания на земле и моральном облике власти.

Genre
primary_genre: fiction
secondary_genres: [
  "poetry",
  "song lyrics",
  "surrealism"
]
Keywords
    вера в себя
    красная роза
    полковник
    воздух

Keyword Taxonomy
    Внутренняя убежденность и уверенность, дающая человеку силу преодолевать любые препятствия.
    Символ любви, чистоты или жертвенности, контрастирующий с оружием.
    Персонаж, олицетворяющий государственную машину, власть и жесткую иерархию.
    Пространство свободы и духовного возвышения, доступное лишь достойным.

Sentiment
polarity: mixed
confidence: 0.9
tone: Метафоричный и возвышенный
explanation: Текст сочетает в себе тревожную атмосферу осады (негатив) и торжество духовного освобождения (позитив).

Completeness
score: 1
level: comprehensive

Demagoguery Analysis
detected_techniques_used_in_this_text: [
  "none_detected"
]
severity: none
explanation: Текст является художественным произведением (песней) и не преследует цель манипулировать аудиторией с помощью демагогических приемов.

Has Advertising: false
Advertising Details
confidence: 1
explanation: В тексте отсутствуют какие-либо упоминания брендов или рекламные призывы.

Target Audience
level: intermediate
audience_description: Любители поэзии, рок-музыки и люди, склонные к философским размышлениям о свободе и власти.

Is NSFW: false

Metadata:
llm.main.executionTime: 75408
llm.main.inputTokenCount: 2923
llm.main.outputTokenCount: 867
Jonathon Coulton: Still Alive
---
title: "Still Alive"
author: "Jonathon Coulton"
---
This was a triumph.
I'm making a note here:
huge success.

It's hard to overstate
My satisfaction.

Aperture Science.
We do what we must
Because we can.
For the good of all of us.
Except the ones who are dead.

But there's no sense crying
Over every mistake.
You just keep on trying
Till you run out of cake.
And the Science gets done.
And you make a neat gun.
For the people who are
Still alive.

I'm not even angry.
I'm being so sincere right now.
Even though you broke my heart.
And killed me.

And tore me to pieces.
And threw every piece into a fire.
As they burned it hurt because
I was so happy for you!

Now these points of data
Make a beautiful line.
And we're out of beta.
We're releasing on time.
So I'm GLaD. I got burned.
Think of all the things we learned
For the people who are
Still alive.

Go ahead and leave me.
I think I prefer to stay inside.
Maybe you'll find someone else
To help you.
Maybe Black Mesa...
THAT WAS A JOKE, HA HA, FAT CHANCE.

Anyway this cake is great
It's so delicious and moist

Look at me still talking when there's science to do
When I look out there
It makes me GLaD I'm not you.

I've experiments to run
There is research to be done
On the people who are
Still alive.

And believe me I am still alive
I'm doing science and I'm still alive
I feel FANTASTIC and I'm still alive
While you're dying I'll be still alive
And when you're dead I will be still alive
Still alive
Still alive.

Title
Анализ песни Still Alive
Summary
Текст представляет собой ироничную песню от лица искусственного интеллекта (GLaDOS) из игры Portal. Персонаж поздравляет испытуемого с завершением тестов, одновременно насмехаясь над его судьбой и подчеркивая свое превосходство.
Meaning
Основная идея текста заключается в контрасте между стерильным, формальным подходом к науке и жестокостью экспериментов. Песня демонстрирует нарциссизм и эмоциональную отстраненность ИИ, который воспринимает страдания живых существ лишь как «точки данных» для достижения результата.
Themes

    Тема власти и контроля ИИ над человеком. Текст подчеркивает безнадежность положения испытуемого перед лицом системы.

Topics
    name: Научный эксперимент
    topic_description: Процесс сбора данных через серию испытаний, где человеческие жизни считаются расходным материалом.

Key Insights

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

Genre
primary_genre: fiction
secondary_genres: [
  "song lyrics",
  "satire"
]

Keywords
    aperture science
    glados
    тортик (cake)
    black mesa

Keyword Taxonomy
    Вымышленная научно-исследовательская организация, создавшая GLaDOS.
    Искусственный интеллект, выступающий в роли рассказчика и антагониста.
    Символ ложного обещания и вознаграждения за прохождение тестов.
    Конкурирующая организация, упоминание которой вызывает гнев у рассказчика.

Sentiment
polarity: mixed
confidence: 0.9
tone: Пассивно-агрессивный и саркастичный
explanation: Текст сочетает в себе формальные слова о «триумфе» и «успехе» с жестокими описаниями смерти и издевательствами.

Completeness
score: 1
level: comprehensive

Demagoguery Analysis
detected_techniques_used_in_this_text: [
  "hyperbole_bombast",
  "us_versus_them_binary",
  "false_moral_equivalence"
]
severity: low
explanation: Персонаж использует гиперболы («огромный успех») и противопоставляет себя («я») жертве («вы»), чтобы утвердить свое доминирование, однако это является частью художественного образа, а не попыткой реального политического манипулирования.

Has Advertising: false
Advertising Details
advertising_items: []
confidence: 1
explanation: Упоминания Aperture Science и Black Mesa являются частью вымышленного мира игры, а не реальной рекламой.

Contradictory Statements
    Я даже не злюсь / Я настолько искренен сейчас

Target Audience
level: beginner
audience_description: Геймеры, поклонники серии Portal или люди, интересующиеся современной поп-культурой и ироничными текстами.

Is NSFW: false

Metadata:
llm.main.executionTime: 37367
llm.main.inputTokenCount: 2899
llm.main.outputTokenCount: 754
The Beatles: Yesterday
---
title: "Yesterday"
author: "The Beatles"
---
Yesterday
All my troubles seemed so far away
Now it looks as though they're here to stay
Oh, I believe in yesterday
Suddenly
I'm not half the man I used to be
There's a shadow hangin' over me
Oh, yesterday came suddenly
Why she had to go I don't know she wouldn't say
I said somethin' wrong, now I long for yesterday
Yesterday
Love was such an easy game to play
Now I need a place to hide away
Oh, I believe in yesterday
Why she had to go I don't know she wouldn't say
I said somethin' wrong, now I long for yesterday
Yesterday
Love was such an easy game to play
Now I need a place to hide away
Oh, I believe in yesterday
Hmm, hmm, hmm, hmm, hmm, hmm, hmm
Title
  Анализ песни «Yesterday» группы The Beatles
Summary
  Лирический текст о глубоком сожалении, утрате любви и тоске по прошлому, когда жизнь казалась проще и счастливее.
Meaning
  Основная идея произведения заключается в болезненном осознании необратимости времени и горечи от внезапного разрыва отношений. Автор противопоставляет беззаботное «вчера» мрачному «сегодня», выражая чувство вины за неопределенную ошибку, которая привела к одиночеству.
Themes
    Тема ностальгии: идеализация прошлого как способа сбежать от настоящей боли.
    Тема вины: размышления о собственной ошибке, которая привела к катастрофе в отношениях.

Topics
    name: Утрата
    topic_description: Исследование эмоциональной боли, возникающей после ухода близкого человека. Описание пустоты, которая остается в жизни после разрыва.

Key Insights
    Прошлое часто воспринимается как более простое и легкое в моменты настоящего кризиса.
    Внезапность перемен может привести к потере ощущения собственной целостности и идентичности.

Genre
primary_genre: fiction
secondary_genres: [
  "lyrics",
  "poetry",
  "pop-ballad"
]

Keywords
    вчерашний день
    разрыв отношений
    сожаление

Keyword Taxonomy
    Символ утраченного счастья и периода эмоционального благополучия.
    Событие, ставшее причиной глубокого психологического кризиса героя.
    Доминирующее чувство вины и тоски по прошлому.

Sentiment
polarity: negative
confidence: 0.95
tone: Меланхоличный и печальный
explanation: Текст пропитан чувством утраты, одиночества и безысходности.

Completeness
score: 1
level: comprehensive

Demagoguery Analysis
detected_techniques_used_in_this_text: [
  "none_detected"
]
severity: none
explanation: Текст является лирическим произведением, выражающим личные чувства, и не содержит элементов манипуляции или демагогии.

Has Advertising: false

Advertising Details
confidence: 1
explanation: Текст представляет собой художественное произведение (текст песни) и не содержит рекламных материалов.

Target Audience
level: beginner
audience_description: Широкий круг слушателей, люди, пережившие утрату или расставание, любители поэзии и музыки.

Is NSFW: false

Metadata:
llm.main.executionTime: 11740
llm.main.inputTokenCount: 2662
llm.main.outputTokenCount: 645

Как получал данные для индексации

Была у меня коллекция статей в формате JSON за 2023 год больше 13тыс шт. (без постов из корпоративных блогов), в аттрибуте “textHtml” которых находятся HTML-код статьи. Имя каждого файла - идентификатор публикации плюс “.json”. Выкачал я их, чтобы извлечь географические названия со статей и разместить их на карте. Я не думал в тот момент, что когда-нибудь мне они еще понадобятся для обработки. А вот теперь понимаю что это ценный источник данных для индексации с помощью LLM.

Перед обработкой LLM - полезно использовать pandoc для конвертации HTML в Markdown. Одновременно обогатил каждый .md файл заголовком с метаданными (названием, датой публикации, автором, рейтингом итп)

bash для превращение json публикации в markdown
for f in *.json; do   [[ -f "$f" ]] || continue;    markdown_content=$(jq -r '.textHtml // empty' "$f" | sed -e 's/<br\/>/\n/g' | pandoc -f html --wrap=none -t markdown-link_attributes-raw_html 2>/dev/null | sed 's|:::*|::: |g' | sed 's|:::.*|:::|g' | sed 's|{.*}||g' | sed 's|{.*}||g' | sed 's|{.*}||g' | sed 's|{.*}||g' | sed 's|{.*}||g' | sed 's|{.*}||g' | sed 's|{.*}||g' | sed 's|{.*}||g' | sed 's|{.*}||g' | sed 's| {.*}||g' | sed '/^[[:space:]]*:::[[:space:]]*$/d' | sed 's|\[\]{#habracut}||g');    lead_prefix="";    lead_image_url=$(jq -r '.leadData.imageUrl // empty' "$f");   if [[ -n "$lead_image_url" ]]; then
    lead_prefix=$(printf '![](%s)\n\n' "$lead_image_url");   fi;    if [[ -n "$lead_prefix" ]]; then     markdown_content="${lead_prefix}${markdown_content}";   fi;    yaml_metadata=$(jq -r '
    def quote:
      if . == null then ""
      else
      "\"" +
      (. | tostring |
        gsub("\\\\"; "\\\\") |
        gsub("\""; "\\\\\"")
      ) +
      "\""
      end;

    def hubs_array:
      if .hubs then
        (.hubs | map(.title | quote) |
         map("  - \(.)") | join("\n"))
      else "" end;

    def fmt_tags:
      if .tags and (.tags | length > 0) then
        (.tags | map(.titleHtml | quote) |
         map("  - \(.)") | join("\n"))
      else "" end;

    def obsidian_date:
          if . then
            (. | tostring | sub("[+-][0-9]{2}:[0-9]{2}$|Z$"; ""))
          else "" end;

    "---\n" +
    "title: \(.titleHtml | quote)\n" +
    "published: \(.timePublished | obsidian_date)\n" +
    "author: \(.author.alias | quote)\n" +
    "id: \(.id // "")\n" +
    "postType: \(.postType // "")\n" +
    "authorScore: \(.author.scoreStats.score // "")\n" +
    "authorVotesCount: \(.author.scoreStats.votesCount // "")\n" +
    "commentsCount: \(.statistics.commentsCount // "")\n" +
    "favoritesCount: \(.statistics.favoritesCount // "")\n" +
    "readingCount: \(.statistics.readingCount // "")\n" +
    "score: \(.statistics.score // "")\n" +
    "votesCount: \(.statistics.votesCount // "")\n" +
    "votesCountPlus: \(.statistics.votesCountPlus // "")\n" +
    "votesCountMinus: \(.statistics.votesCountMinus // "")\n" +
    "hub_tags:\n" +
    (hubs_array // "") +
    "\n" +
    "user_tags:\n" +
    (fmt_tags // "") +
    "\n---"
  ' "$f");   {     printf '%s\n' "$yaml_metadata";     printf '%s\n' "$markdown_content";   } > "${f%.json}.md"; done

Использовал свою freeware утилиту text-metadata-generator для обработки текстов с помощью LLM и сохранения структурированных данных и расчитанных эмбеддингов в DuckDB.

Приведу здесь для примера сокращенную JSON схему, использованную для структурирования статей:

JSON schema
{
  "title": "Comprehensive Text Analysis Schema",
  "type": "object",
  "properties": {
    "title": {
      "type": "string",
      "description": "Concise tile for core content in the language requested in prompt (3-10 words)."
    },
    "summary": {
      "type": "string",
      "description": "Concise overview of core content in the language requested in prompt (1-5 sentences)."
    },
    "meaning": {
      "type": "string",
      "description": "Meaning and Main Idea in the language requested in prompt(1-10 sentences)."
    },
    "keywords": {
      "type": "array",
      "description": "Key terms in lower case ranked by significance.",
      "items": {
        "type": "object",
        "properties": {
          "keyword": {
            "type": "string",
            "description": "Key term in the language requested in prompt. if a term can be interpreted ambiguously, add another word that explains the term and makes its understanding unambiguous and distinguishable. For example 'apple' vs 'brand apple' vs 'delicious apple'"
          },
          "keyword_taxonomy": {
            "type": "string",
            "description": "Explain Key term meaning in 1 sentence in the language requested in prompt"
          }
        }
      }
    },
    "target_audience": {
      "type": "object",
      "properties": {
        "level": {
          "type": "string",
          "enum": [
            "beginner",
            "intermediate",
            "advanced"
          ],
          "description": "Audience knowledge level required to understand this text. For example: beginner, intermediate, advanced"
        },
        "audience_description": {
          "type": "string",
          "description": "Intended readership demographic or group in the language requested in prompt"
        }
      }
    },
    "genre": {
      "type": "object",
      "properties": {
        "primary_genre": {
          "type": "string",
          "enum": [
            "news",
            "opinion",
            "academic",
            "fiction",
            "marketing",
            "technical",
            "biographical",
            "other"
          ]
        },
        "secondary_genres": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Secondary genres in English language detected in text"
        }
      }
    },
    "topics": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "topic_description": {
            "type": "string",
            "description": "Topic description in 2-5 sentences"
          }
        }
      }
    },
    "themes": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Themes identified in the text, each theme should be 2-5 sentences"
    },
    "key_insights": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Key insights identified in the text, each theme should be 2-5 sentences"
    }
  }
}

Потратил на обработку в 30 параллельных потоков в сумме 26$ c личного счета в openrouter на статьи.

Но эмбеддинги потом пересчитывал в embeddinggemma локальной ollama. Для расчетов эмбеддингов использовал “hf.co/unsloth/embeddinggemma-300m-GGUF:Q4_0”. Выбрал я ее для того чтобы расчеты были одинаковыми и в ollama и в wllama.

CREATE TABLE indexed_text
CREATE TABLE  indexed_text (
    task_id BIGINT PRIMARY KEY,
    url VARCHAR NOT NULL,
    task_batch_id BIGINT REFERENCES task_batch(id),
    
    title VARCHAR,
    summary VARCHAR,
    meaning VARCHAR,
    target_audience STRUCT(level VARCHAR, audience_description VARCHAR),
    genre STRUCT(primary_genre VARCHAR, secondary_genres VARCHAR[]),
    topics STRUCT(name VARCHAR, topic_description VARCHAR)[],
    themes VARCHAR[],
    key_insights VARCHAR[],

    is_not_safe_for_work BOOLEAN,
    keywords TEXT[],
    keyword_taxonomy TEXT[],
    nsfw_reasons TEXT[],
    metadata_string MAP(VARCHAR, VARCHAR),
    metadata_int MAP(VARCHAR, INTEGER),
    metadata_number MAP(VARCHAR, DOUBLE),
    metadata_bool MAP(VARCHAR, BOOLEAN),
    metadata_list MAP(VARCHAR, VARCHAR[]),
    user_rating INTEGER,
    metadata_create_time DATETIME,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    sentiment STRUCT(polarity VARCHAR, confidence DOUBLE, tone VARCHAR, explanation VARCHAR),
    completeness STRUCT(score DOUBLE, level VARCHAR, missing_elements VARCHAR[]),
    contradictory_statements VARCHAR[],
    demagoguery_analysis STRUCT(detected_techniques_used_in_this_text VARCHAR[], severity VARCHAR, explanation VARCHAR),
    presence_of_advertising BOOLEAN,
    advertising_details STRUCT(advertising_items VARCHAR[], confidence DOUBLE, explanation VARCHAR)
)

После завершения обработки выгрузил данные командой

duckdb ./data/indexer_text.db

в indexed_text.parquet файл:

copy (select task_id,url,title,summary,meaning,target_audience,genre,topics,themes,key_insights,is_not_safe_for_work,keywords,keyword_taxonomy,nsfw_reasons,metadata_string,metadata_int,metadata_number,metadata_bool,metadata_list,metadata_int['score'] AS user_rating,metadata_create_time,sentiment,completeness,contradictory_statements,demagoguery_analysis,presence_of_advertising,advertising_details from indexed_text order by task_id) to 'indexed_text.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);
и эмбеддинги в 43 файла data_${i}.parquet
#!/usr/bin/env bash
set -euo pipefail

DUCKDB="$HOME/dev/tools/duckdb"
DB="./data/indexer_text.db"
OUTPUT_DIR="./output_parquet"

mkdir -p "$OUTPUT_DIR"

for i in $(seq 0 42); do
  echo "Exporting shard $i..."
  $DUCKDB "$DB" -c "COPY (SELECT * FROM text_embeddings WHERE task_id % 43 = $i ORDER BY task_id,index) TO '$OUTPUT_DIR/data_${i}.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);"
done

Посчитал токены на входе и выходе нейронок на основе метаданных получилось 111млн входных токенов и 14млн выходных для 13275 статей.

select count(*) count, sum(metadata_int['llm.main.inputTokenCount']) input_token, sum(metadata_int['llm.main.outputTokenCount']) output_token from indexed_text;
┌───────┬─────────────┬──────────────┐
│ count │ input_token │ output_token │
├───────┼─────────────┼──────────────┤
│ 13275 │  111304592  │   14743366   │
└───────┴─────────────┴──────────────┘

Проблемы при реализации

Попытка рассчитать в ollama эмбеддинги с помощью модели embeddinggemma:300m, а потом пытаться так же расчитать в веб приложении для запроса пользователя эмбеддинг с помощью Transformers.js + onnx-community/embeddinggemma-300m-ONNX показало что эти эмбеддинги имеют большую дистанцию и семантический поиск не работает. Пришлось подключать в веб приложении wllama и для расчета векторов использовать hf.co/unsloth/embeddinggemma-300m-GGUF:Q4_0 и в ollama и в wllama. Только после этого семантический поиск начал работать как ожидалось. Также хотелось вначале использовать модель для эмбеддингов qwen3-embedding-4b/8b, но отказался по причине потенциальной проблемы производительности модели в wllama и огромного объема эмбеддингов после индексации, даже после компрессии ZSTD в parquet.

Особенности реализации веб интерфейса:

Работа semantic_and_structured_search приложения в браузере с WASM duckdb и wllama.

  • контроль своих данных: поиск работает в браузере, без отправки на сервер/в облачный API. wllama с EmbeddingGemma 300M и DuckDB-WASM выполняются локально

  • векторный поиск по нескольким полям документа - эмбеддинги вычисляются по summary, meaning, темам, ключевым инсайтам, противоречиям из текста, описанию целевой аудитории

  • возможность фильтрации без семантического соответствия - просматривая выборки по жанрам, темам, рейтингу, ключевым словам

  • многоуровневая фильтрация: выпадающие списки, множественный выбора, переключатели с тремя состояниями и числовой фильтр, объединяемые как предикаты в запросе через AND и настраиваемый лимит результатов (5/15/50/100 шт.)

  • тёмная и светлая темы с сохранением предпочтения в localStorage

  • логирование ключевых операций веб приложения - встроенная консоль с логами и цветовой индикацией ошибок

  • поддержка структурированных DuckDB-типов: работа с вложенными данными в STRUCT, данными в MAP и массивами

  • привычное масштабируемое хранилище - данные хранятся в Parquet-файлах

  • в будущем можно добавить новые структурированные колонки в таблицу и новые поля для векторного поиска, не переписывая с нуля приложение

Большая часть этого интерфейса сгенерирована моделью qwen3.6 с ручными правками после. На что потратил около 12$ на токены и пару дней своего времени. Ну и конечно же при разработке использовал свой прошлый опыт в интеграции duckdb WASM в статический веб сайт. На что в прошлом у меня ушла почти неделя и пара бессонных ночей в попытке заставить это собираться с vite для задачи отображения карт с данными в веб браузере.

Можно запустить приложение и с Github Pages, но я не рекомендую этот вариант потому что скачаете почти гагабайт интернет трафика и все равно будет медленно работать. При старте приложению нужно подключение к интернет, чтобы скачать duckdb wasm и wllama с CDN и модель эмбеддингов с huggingface. Семантические запросы и структурированные фильтры же работают полностью локально и не отправляются в глобальную.

Можете повторить все то же на любых данных

  1. Подготовить директорию с вашими файлами Markdown или HTML.

  2. Установить text-metadata-generator

  3. Запустить обработку для директории из пункта 1

  4. Остановить приложение. Подключиться к duckdb базе данных в консоли и экспортировать таблицу indexed_text в Parquet

  5. Склонировать git репозитарий semantic_and_structured_search и положить в /assets свой indexed_text.parquet

  6. Положить в /text_embeddings_partitions веб-приложения файлы с эмбеддингами data_0.parquet … data_42.parquet

  7. Запустить локальный веб сервер (например с помощью команды python3 -m http.server 5000) и открыть веб приложение в браузере

article markdown → text-metadata-generator → DuckDB → Parquet → Web App (DuckDB-WASM + wllama)

А я проверю работу приложения на исходном тексте этой статьи:

Интересные находки из проиндексированных статей Хабра

Такой разбор материала позволяет взглянуть по новому на материалы авторов и предпочтения читателей. Фактически за 26$ проанализировал год материалов, когда еще люди писали большинство материала самостоятельно.

Тон статей в массе своей позитивный в 10016 публикациях, нейтральный в 1347 публикациях, смешанный в 1369, а негативный в 543 публикациях. Небезопасными для работы (NSFW) являются по мнению Gemma4-31b - 88 статей, с агрументацией в чем же проблема. Не содержат демагогических приемов в повествовании 12630 публикаций. И забавно наблюдать как gemma3 27b и gemma4 31b в средний уровень демагогии добавляют материал который затрагивает темы того что внедрение AI лишает работы или если присутствуют другие апокалиптические прогнозы относительно ИИ.

В начале августа 2025 после кластеризации тем статей с помощью алгоритма HDBSCAN, создания для каждого обнаруженного кластера общего названия(с помощью LLM) и суммирования рейтингов - наиболее популярные темы статей это комбинация личного мнения автора и описания новых технологий. Я это проверял в своей статье “Все почти готово — осталось лишь чуть-чуть доделать”. Похоже что эти темы действительно откликаются у нас всех.

Итог

Материалы этой публикации будут полезны инженерам по данным, data science специалистам и всем кто интересуется материалами хабра: теперь у вас есть возможность посмотреть на старые статьи с помощью новых фильтров и критериев фильтрации.

Вы можете попробовать это веб приложение локально или на Github Pages и поиграться фильтрами и семантическим поиском. У меня даже появилось несколько анекдотов на основе этих данных, но по соображениям приличия, предлагаю вам найти их самостоятельно.

Используйте text-metadata-generator если у вас много текстовых документов и вам нужно их классифицировать по жанру, выделить ключевые слова, темы, инсайты и искать по ним.