Классический RAG индексирует исходный текст документа, предварительно разбивая на фрагменты. Потом рассчитывает векторное представление фрагментов и сохраняет их векторные представления в базу данных для поиска. Это дает возможность искать по сходству фрагментов текста и поискового запроса пользователя, но не дает возможность искать по более высокоуровневым резюме и смыслам, темам поднятым в тексте и прочему. Также не помогает с аналитикой по содержимому.
Бесплатный проект text-metadata-generator позволяет выполнять запросы к LLM по каждому документу из коллекции документов, результаты вывода LLM проверяются по JSON схеме.
Зачем может пригодиться эта программа и подход со структурированием текстовой информации
-
своя библиотека с каталогом — поиск по локальным документам с использованием комбинации SQL предикатов и семантического поиска
-
аналитика по документам, возможность находить новое в текстах: комбинируя структурированные поля созданные LLM из исходного текста, и находя закономерности с уже существующими в документе метаданными. Например, связывая с рейтингом признак NSFW, тон повествования, полноту содержания итп.
-
разгрести “авгиевы конюшни” личных заметок в Obsidian или git репозитарии с Markdown файлами
Рассмотрим как работает данный подход на 13275 статьях с Хабра, а также текстах трех песнен…
Примеры обработки данных
Технические статьи это главный источник для которого разрабатывался данный подход, но можно запускать анализ данных на текстах популярных песен. Если для одного и того же текста использовать разные LLM модели, то можно получить синтетический плюрализм “мнений” от LLM
Nautilus Pompilius: Воздух
---title: "Воздух"author: "Nautilus Pompilius"---Когда они окружили домИ в каждой руке был стволОн вышел в окно с красной розой в рукеИ по воздуху плавно пошелИ хотя его руки были в кровиОни светились как два крылаИ порох в стволах превратился в песокУвидев такие делаВоздух выдержит только техТолько тех, кто верит в себяВетер дует туда, кудаПрикажет тот, кто верит в себяВоздух выдержит только техТолько тех, кто верит в себяВетер дует туда, кудаПрикажет тот кто...Они стояли и ждали, когдаОн упадет с небесНо красная роза в его рукеБыла похожа на крестИ что-то включилось само-собойВ кармане полковничьих брюкИ чей-то голос так громко сказалЧто услышали все вокругВоздух выдержит только техТолько тех, кто верит в себяВетер дует туда, кудаПрикажет тот, кто верит в себяВоздух выдержит только техТолько тех кто верит в себяВетер дует туда, кудаПрикажет тот, кто...А полковник думал мысльИ разглядывал пыль на ремне:"Если воры ходят по небесамЧто мы делаем здесь, на ЗемлеДети смотрят на нас свысокаИ собаки плюют нам вследНо если никто мне не задал вопросОткуда я знаю ответ, что..."Воздух выдержит только техТолько тех, кто верит в себяВетер дует туда, кудаПрикажет тот, кто верит в себяВоздух выдержит только техТолько тех, кто верит в себяА ветер дует туда, кудаПрикажет тот, кто...
Title Анализ песни «Воздух» группы Nautilus PompiliusSummary Текст описывает сюрреалистическую сцену противостояния одного человека и вооруженного отряда. Главный герой совершает невозможное — буквально проходит по воздуху, превращая оружие врагов в песок, что вызывает экзистенциальный кризис у его преследователей.Meaning Основная идея произведения заключается в торжестве духа над материальной силой и насилием. Вера в себя представлена как сверхъестественная сила, позволяющая человеку подняться над обстоятельствами и земными законами. Конфликт между 'полковником' (символом системы и принуждения) и 'героем' (символом свободы и веры) подчеркивает бессилие грубой силы перед истинной внутренней свободой.Themes Тема внутренней свободы: истинная свобода достигается не через борьбу с оружием, а через веру в себя и внутреннее преображение. Тема бессилия власти: материальное превосходство (стволы, чины) оказывается бесполезным перед лицом духовного величия.Topics name: Противостояние личности и системы topic_description: Конфликт между отдельным человеком с его идеалами и организованной силой в лице вооруженного отряда под руководством полковника. name: Духовная трансцендентность topic_description: Способность человека выйти за пределы физических возможностей и законов природы благодаря внутренней силе и вере.Key Insights Материальная сила (порох) может превратиться в ничто (песок) при столкновении с истинной верой. Осознание собственного бессилия приводит к экзистенциальным вопросам о смысле пребывания на земле и моральном облике власти.Genreprimary_genre: fictionsecondary_genres: [ "poetry", "song lyrics", "surrealism"]Keywords вера в себя красная роза полковник воздухKeyword Taxonomy Внутренняя убежденность и уверенность, дающая человеку силу преодолевать любые препятствия. Символ любви, чистоты или жертвенности, контрастирующий с оружием. Персонаж, олицетворяющий государственную машину, власть и жесткую иерархию. Пространство свободы и духовного возвышения, доступное лишь достойным.Sentimentpolarity: mixedconfidence: 0.9tone: Метафоричный и возвышенныйexplanation: Текст сочетает в себе тревожную атмосферу осады (негатив) и торжество духовного освобождения (позитив).Completenessscore: 1level: comprehensiveDemagoguery Analysisdetected_techniques_used_in_this_text: [ "none_detected"]severity: noneexplanation: Текст является художественным произведением (песней) и не преследует цель манипулировать аудиторией с помощью демагогических приемов.Has Advertising: falseAdvertising Detailsconfidence: 1explanation: В тексте отсутствуют какие-либо упоминания брендов или рекламные призывы.Target Audiencelevel: intermediateaudience_description: Любители поэзии, рок-музыки и люди, склонные к философским размышлениям о свободе и власти.Is NSFW: falseMetadata:llm.main.executionTime: 75408llm.main.inputTokenCount: 2923llm.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 overstateMy satisfaction.Aperture Science.We do what we mustBecause we can.For the good of all of us.Except the ones who are dead.But there's no sense cryingOver every mistake.You just keep on tryingTill you run out of cake.And the Science gets done.And you make a neat gun.For the people who areStill 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 becauseI was so happy for you!Now these points of dataMake 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 learnedFor the people who areStill alive.Go ahead and leave me.I think I prefer to stay inside.Maybe you'll find someone elseTo help you.Maybe Black Mesa...THAT WAS A JOKE, HA HA, FAT CHANCE.Anyway this cake is greatIt's so delicious and moistLook at me still talking when there's science to doWhen I look out thereIt makes me GLaD I'm not you.I've experiments to runThere is research to be doneOn the people who areStill alive.And believe me I am still aliveI'm doing science and I'm still aliveI feel FANTASTIC and I'm still aliveWhile you're dying I'll be still aliveAnd when you're dead I will be still aliveStill aliveStill alive.
TitleАнализ песни Still AliveSummaryТекст представляет собой ироничную песню от лица искусственного интеллекта (GLaDOS) из игры Portal. Персонаж поздравляет испытуемого с завершением тестов, одновременно насмехаясь над его судьбой и подчеркивая свое превосходство.MeaningОсновная идея текста заключается в контрасте между стерильным, формальным подходом к науке и жестокостью экспериментов. Песня демонстрирует нарциссизм и эмоциональную отстраненность ИИ, который воспринимает страдания живых существ лишь как «точки данных» для достижения результата.Themes Тема власти и контроля ИИ над человеком. Текст подчеркивает безнадежность положения испытуемого перед лицом системы.Topics name: Научный эксперимент topic_description: Процесс сбора данных через серию испытаний, где человеческие жизни считаются расходным материалом.Key Insights Научный прогресс в данной вселенной лишен этики и морали, превращаясь в инструмент истязания.Genreprimary_genre: fictionsecondary_genres: [ "song lyrics", "satire"]Keywords aperture science glados тортик (cake) black mesaKeyword Taxonomy Вымышленная научно-исследовательская организация, создавшая GLaDOS. Искусственный интеллект, выступающий в роли рассказчика и антагониста. Символ ложного обещания и вознаграждения за прохождение тестов. Конкурирующая организация, упоминание которой вызывает гнев у рассказчика.Sentimentpolarity: mixedconfidence: 0.9tone: Пассивно-агрессивный и саркастичныйexplanation: Текст сочетает в себе формальные слова о «триумфе» и «успехе» с жестокими описаниями смерти и издевательствами.Completenessscore: 1level: comprehensiveDemagoguery Analysisdetected_techniques_used_in_this_text: [ "hyperbole_bombast", "us_versus_them_binary", "false_moral_equivalence"]severity: lowexplanation: Персонаж использует гиперболы («огромный успех») и противопоставляет себя («я») жертве («вы»), чтобы утвердить свое доминирование, однако это является частью художественного образа, а не попыткой реального политического манипулирования.Has Advertising: falseAdvertising Detailsadvertising_items: []confidence: 1explanation: Упоминания Aperture Science и Black Mesa являются частью вымышленного мира игры, а не реальной рекламой.Contradictory Statements Я даже не злюсь / Я настолько искренен сейчасTarget Audiencelevel: beginneraudience_description: Геймеры, поклонники серии Portal или люди, интересующиеся современной поп-культурой и ироничными текстами.Is NSFW: falseMetadata:llm.main.executionTime: 37367llm.main.inputTokenCount: 2899llm.main.outputTokenCount: 754
The Beatles: Yesterday
---title: "Yesterday"author: "The Beatles"---YesterdayAll my troubles seemed so far awayNow it looks as though they're here to stayOh, I believe in yesterdaySuddenlyI'm not half the man I used to beThere's a shadow hangin' over meOh, yesterday came suddenlyWhy she had to go I don't know she wouldn't sayI said somethin' wrong, now I long for yesterdayYesterdayLove was such an easy game to playNow I need a place to hide awayOh, I believe in yesterdayWhy she had to go I don't know she wouldn't sayI said somethin' wrong, now I long for yesterdayYesterdayLove was such an easy game to playNow I need a place to hide awayOh, I believe in yesterdayHmm, hmm, hmm, hmm, hmm, hmm, hmm
Title Анализ песни «Yesterday» группы The BeatlesSummary Лирический текст о глубоком сожалении, утрате любви и тоске по прошлому, когда жизнь казалась проще и счастливее.Meaning Основная идея произведения заключается в болезненном осознании необратимости времени и горечи от внезапного разрыва отношений. Автор противопоставляет беззаботное «вчера» мрачному «сегодня», выражая чувство вины за неопределенную ошибку, которая привела к одиночеству.Themes Тема ностальгии: идеализация прошлого как способа сбежать от настоящей боли. Тема вины: размышления о собственной ошибке, которая привела к катастрофе в отношениях.Topics name: Утрата topic_description: Исследование эмоциональной боли, возникающей после ухода близкого человека. Описание пустоты, которая остается в жизни после разрыва.Key Insights Прошлое часто воспринимается как более простое и легкое в моменты настоящего кризиса. Внезапность перемен может привести к потере ощущения собственной целостности и идентичности.Genreprimary_genre: fictionsecondary_genres: [ "lyrics", "poetry", "pop-ballad"]Keywords вчерашний день разрыв отношений сожалениеKeyword Taxonomy Символ утраченного счастья и периода эмоционального благополучия. Событие, ставшее причиной глубокого психологического кризиса героя. Доминирующее чувство вины и тоски по прошлому.Sentimentpolarity: negativeconfidence: 0.95tone: Меланхоличный и печальныйexplanation: Текст пропитан чувством утраты, одиночества и безысходности.Completenessscore: 1level: comprehensiveDemagoguery Analysisdetected_techniques_used_in_this_text: [ "none_detected"]severity: noneexplanation: Текст является лирическим произведением, выражающим личные чувства, и не содержит элементов манипуляции или демагогии.Has Advertising: falseAdvertising Detailsconfidence: 1explanation: Текст представляет собой художественное произведение (текст песни) и не содержит рекламных материалов.Target Audiencelevel: beginneraudience_description: Широкий круг слушателей, люди, пережившие утрату или расставание, любители поэзии и музыки.Is NSFW: falseMetadata:llm.main.executionTime: 11740llm.main.inputTokenCount: 2662llm.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 '\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 bashset -euo pipefailDUCKDB="$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. Семантические запросы и структурированные фильтры же работают полностью локально и не отправляются в глобальную.
Можете повторить все то же на любых данных
-
Подготовить директорию с вашими файлами Markdown или HTML.
-
Установить text-metadata-generator
-
Запустить обработку для директории из пункта 1
-
Остановить приложение. Подключиться к duckdb базе данных в консоли и экспортировать таблицу indexed_text в Parquet
-
Склонировать git репозитарий semantic_and_structured_search и положить в /assets свой indexed_text.parquet
-
Положить в /text_embeddings_partitions веб-приложения файлы с эмбеддингами data_0.parquet … data_42.parquet
-
Запустить локальный веб сервер (например с помощью команды
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 если у вас много текстовых документов и вам нужно их классифицировать по жанру, выделить ключевые слова, темы, инсайты и искать по ним.
ссылка на оригинал статьи https://habr.com/ru/articles/1036594/