Ворклог по задаче "Переосмыслить структуру сайта, доработать ее и пересобрать контент"
Не буду цитировать всю переписку с чатгпт, но я полностью переосмыслил суть концептов и фактов и все свелось к тому, что нет смысла делать факты отдельными сущностями в нашей реализации. У нас сами концепты могут быть и фактами.
Вот краткая выжимка как я это вижу:
У нас не банковская система, чтобы строить подробные связи. У нас жизненный цикл информации такой:
1. Создается концепт, которого ранее еще не было. К примеру, Some Homestay.
2. В нем прописывается в обычном текстовом виде все, что хочется передать. Это все то, что потом прочитает пользовать. В нашем проекте по сути это само по себе факт, потому что весь контент создаем мы. И мы сразу указываем качественные признаки что есть что. Со временем, когда эта информация окажется некачественная, мы просто понизим ее качественный признак. Условно, нам достаточно сейчас только одной метрики от 0 до 1 флоэт. Чем выше качество, тем выше видимость. К примеру, у меня на сайте полно старых статей, которые уже не нужны, их надо выкинуть, но я не могу их выкинуть, ибо 404 не круто. Мне надо просто их дальше засунуть
3. Связка с другими концептами. То есть мне нужна просто перелинковка. Как и в классических сайтах, я просто могу добавить ссылки на другие концепты. Это чтобы пользователь мог читать и переходить в попутные карточки. Но это связь внешняя. А мне нужна и обратная. Соответственно, у меня не просто связи ссылки анкор тут нужны, а попутная техническая информация связи (почему я на это ссылаюсь и опять-таки, какой-то качественный признак).
4. При сохранении такого концепта пересчитываются все его эти связи. Те, что были удалены из содержимого, удаляются и из списка связей. А добавленные появляются.
Вот тут нам как раз и нужна плоская таблица связей концептов многие-ко-многим. Тут можно тогда посмотреть общую семантику всей базы знаний.
И тут я вот что думаю, если эту идею развивать. Можно сделать полную сементику на уровне HTML-тегов. Мы в тексте просто вставляем ссылки. При этом у них есть innerText, title, rel, data-атрибуты и т.п. Все это можно зеркалить с нашей индекс-таблицей связей. При этом parent и root остаются как жесткие связки
Вот что чатгпт подытожила:
Да, это уже выглядит очень цельно.
Тогда HTML/Markdown-контент становится первичным семантическим графом, а таблица связей — его материализованным индексом для быстрых запросов и обратной перелинковки.
При этом:
parentId— жёсткая непосредственная иерархия;rootId— принадлежность к одному дереву или разделу;- HTML-ссылки — мягкие смысловые связи многие-ко-многим;
- таблица отношений — зеркальное представление ссылок из контента.
Ссылка как полноценная семантическая связь
Например, в редакторе:
<a
href="/places/som-homestay"
data-concept-id="som-homestay-id"
data-relation="overnight"
data-context="Подходящий ночлег перед ранним посещением пещеры"
data-quality="0.9"
title="Som Homestay — ночлег рядом с пещерой"
>
переночевать в Som Homestay
</a>
Здесь уже содержится почти вся нужная семантика:
href— публичный адрес;data-concept-id— стабильная техническая идентичность;innerText— естественный анкор;title— дополнительное пояснение пользователю;data-relation— тип смысловой связи;data-context— почему ссылка появилась;data-quality— сила или полезность связи.
Из неё создаётся индексная запись:
sourceId: cave-id
targetId: som-homestay-id
relation: overnight
anchor: переночевать в Som Homestay
title: Som Homestay — ночлег рядом с пещерой
context: Подходящий ночлег перед ранним посещением пещеры
quality: 0.9
Что хранить в rel
Я бы осторожно использовал стандартный HTML-атрибут rel.
Он уже имеет браузерную и SEO-семантику:
nofollow;ugc;sponsored;noopener;noreferrer;alternate;author;tag.
Свою предметную семантику безопаснее хранить отдельно:
data-relation="overnight-before"
А стандартный rel использовать по назначению:
rel="ugc nofollow"
Иначе позднее можно столкнуться с конфликтом между собственной моделью и HTML-стандартом.
Минимальный формат ссылки
Для ручной работы в редакторе слишком много атрибутов будет мешать. Поэтому обязательными я бы сделал только:
<a
data-concept-id="..."
data-relation="..."
>
текст ссылки
</a>
Остальное можно определить автоматически:
href— получить черезSiteRoute;anchor— взять изinnerText;title— сгенерировать из связанного концепта;quality— использовать значение по умолчанию;context— извлечь из окружающего предложения;position— вычислить парсером.
Расширенный режим нужен только там, где автор хочет явно переопределить семантику:
<a
data-concept-id="..."
data-relation="overnight"
data-quality="0.95"
data-context="Лучший ночлег перед ранним выездом"
>
Som Homestay
</a>
Окружающий текст тоже несёт семантику
Допустим, написано:
Перед ранним посещением пещеры лучше
<a data-concept-id="som-id">переночевать в Som Homestay</a>,
поскольку дорога утром занимает всего около 30 минут.
Даже без явного data-relation агент или парсер может вывести:
relation: overnight_before
context: ночлег перед ранним посещением пещеры
Но я бы различал:
- явную семантику автора;
- автоматически определённую семантику.
Например:
origin: explicit
или:
origin: inferred
confidence: 0.72
Не обязательно показывать это редактору. Это техническая информация индексной таблицы.
Возможная схема таблицы
enum KBConceptRelationOrigin {
explicit
inferred
generated
}
model KBConceptRelation {
id String @id @default(cuid()) @db.VarChar(36)
createdAt DateTime @default(now()) @db.Timestamp(3)
updatedAt DateTime @default(now()) @updatedAt @db.Timestamp(3)
sourceId String
Source KBConcept @relation(
"ConceptRelationSource",
fields: [sourceId],
references: [id],
onDelete: Cascade
)
targetId String
Target KBConcept @relation(
"ConceptRelationTarget",
fields: [targetId],
references: [id],
onDelete: Cascade
)
relation String? @db.VarChar(100)
anchor String? @db.Text
title String? @db.Text
context String? @db.Text
quality Float @default(0.5)
confidence Float?
origin KBConceptRelationOrigin @default(explicit)
// Стабильный идентификатор конкретной ссылки в редакторе
nodeId String? @db.VarChar(100)
// Положение в содержимом
position Int?
// Исходные или дополнительные HTML-атрибуты
data Json?
@@unique([sourceId, nodeId])
@@index([sourceId])
@@index([targetId])
@@index([sourceId, relation])
@@index([targetId, relation])
@@index([quality])
}
quality и confidence здесь имеют разный смысл:
quality— насколько связь полезна для пользователя;confidence— насколько уверенно определена её семантика.
Для явно заданной ссылки:
origin = explicit
confidence = 1
Для автоматически распознанной:
origin = inferred
confidence = 0.74
На первом этапе confidence можно вообще не добавлять.
Нужен стабильный идентификатор HTML-узла
Если просто использовать sourceId + targetId, возникнет проблема: один текст может ссылаться на один концепт несколько раз по разным причинам.
Например:
<a
data-node-id="link-a17"
data-concept-id="som-id"
data-relation="overnight-before"
>
ночлег перед посещением
</a>
<a
data-node-id="link-b24"
data-concept-id="som-id"
data-relation="cold-weather-warning"
>
зимой здесь может быть холодно
</a>
Это две самостоятельные связи с одной и той же целью.
data-node-id должен создаваться редактором автоматически и сохраняться при перемещении ссылки внутри документа.
Тогда синхронизация при сохранении проста:
- ссылка с тем же
nodeId— обновляется; - новый
nodeId— добавляется; - отсутствующий
nodeId— удаляется.
HTML и индекс должны иметь владельца истины
Здесь желательно избежать двустороннего редактирования.
Я бы зафиксировал:
Контент является источником истины для исходящих редакционных связей.
Таблица — индекс.
Значит:
- пользователь редактирует ссылку в тексте;
- после сохранения индекс пересчитывается;
- прямое редактирование строки индексной таблицы запрещено или ограничено;
- обратные блоки формируются из индексной таблицы;
- изменение входящей связи происходит через исходный концепт.
Иначе появится рассинхронизация: таблица говорит одно, HTML — другое.
Автоматические связи можно хранить отдельно от контентных и не зеркалить в HTML до явного принятия.
Обратная связь
Если в карточке пещеры есть:
<a
data-concept-id="som-id"
data-relation="overnight-after"
>
Som Homestay
</a>
То на странице Som Homestay можно автоматически вывести:
Подходит для ночлега после посещения
Пещера X — автор рекомендует этот хоумстей как ночлег после посещения.
Или встроить в текст динамический блок:
<concept-backlinks
data-relation="overnight-after"
data-limit="5"
></concept-backlinks>
Причём обратное представление не обязано повторять исходный анкор. Оно строится из:
- типа отношения;
- контекста;
- исходного концепта;
- качества связи.
parent и root действительно стоит оставить
Они решают другую задачу.
parentId
Жёсткая принадлежность или основной контейнер:
Тхай Нгуен
└── Som Homestay
либо структура контента:
Жильё
└── Хоумстеи
└── Som Homestay
Нужно только выбрать один смысл parentId и не смешивать несколько иерархий.
Например:
parentIdвсегда означает редакционную принадлежность к основному разделу.
rootId
Ускоряет:
- получение всего дерева;
- поиск внутри раздела;
- уникальность кода;
- управление большой веткой;
- удаление или перенос раздела.
Мягкие ссылки
Всё остальное:
- рекомендуемый ночлег;
- часть комплекса;
- альтернатива;
- следующая точка маршрута;
- связано с погодным риском;
- упоминается в маршруте;
идёт через индекс ссылок.
Полная семантика не обязательно требует сложных тегов
Можно начать с обычной ссылки:
<a data-concept-id="som-id">Som Homestay</a>
А тип отношения определять по трём уровням:
- Явный
data-relation. - Контекст родительского блока.
- Анализ окружающего текста агентом.
Например:
<section data-semantic-context="recommended-overnight">
<p>
Перед поездкой лучше остановиться в
<a data-concept-id="som-id">Som Homestay</a>.
</p>
</section>
Здесь каждая ссылка внутри секции наследует:
relation = recommended_overnight
Это может сильно упростить ручную разметку.
То же самое в Markdown-компонентах:
:::semantic{relation="overnight-before"}
Перед ранним посещением лучше остановиться в
[Som Homestay](concept:som-id).
:::
Семантика может жить на разных уровнях DOM
На ссылке
Для точной индивидуальной связи:
<a data-relation="alternative">
На абзаце
Для общего контекста:
<p data-semantic-context="winter-warning">
На секции
Для группы ссылок:
<section data-semantic-context="where-to-stay">
На кастомном компоненте
Для динамической выборки:
<concept-list
data-relation="overnight-before"
data-source="current"
></concept-list>
Парсер может наследовать семантику сверху вниз:
link.relation
?? paragraph.semanticContext
?? section.semanticContext
?? inferredFromText
?? "reference"
Что получится в итоге
У тебя появляется четыре согласованных уровня:
Жёсткое дерево:
parentId / rootId
Авторский смысл:
HTML-контент и ссылки
Быстрый граф:
KBConceptRelation
Динамическое представление:
обратные ссылки, списки и агентские блоки
Это очень практичная архитектура. Она не требует отдельно описывать весь мир в таблицах: семантика возникает прямо в процессе написания нормального человеческого текста, а база лишь материализует её для поиска, анализа и обратного использования.