Skip to content

Data Model: Tag Management (005)

Date: 2026-05-29 Type: Brownfield — documents existing data model

Entity Relationship Diagram

┌──────────────────┐     ┌──────────────────────┐
│     Topic        │     │  TagGroupDefinition   │
│──────────────────│     │──────────────────────│
│ id (UUID) PK     │◄──┐ │ id (UUID) PK          │
│ tag_mode (VARCHAR)│   │ │ name (VARCHAR 100)    │
└──────────────────┘    │ │ display_name (VARCHAR)│
                        │ │ description (TEXT)    │
                        │ │ color_hex (VARCHAR 7) │
                        │ │ sort_order (INT)      │
                        │ │ topic_id (UUID) FK    │──┐
                        │ │ embedding (VECTOR 768)│  │
                        │ └──────────────────────┘  │
                        │                            │
                        │  ┌──────────────────────┐ │
                        │  │  Tag                  │ │
                        │  │──────────────────────│ │
                        ├──│ tag_group_id (UUID) FK│─┘
                        │  │ id (UUID) PK          │
                        │  │ name (TEXT)           │
                        │  │ embedding (VECTOR 768)│
                        │  └──────────────────────┘
                        │            │
                        │            │ M:N via article_tags
                        │            ▼
                        │  ┌──────────────────────┐
                        │  │  Article              │
                        │  │  id (UUID) PK         │
                        │  └──────────────────────┘

                        │  ┌─────────────────────────────┐
                        │  │  TagNormalizationSuggestion  │
                        │  │─────────────────────────────│
                        │  │ id (UUID) PK                 │
                        └──│ existing_tag_id (UUID) FK    │
                           │ new_tag_id (UUID) FK         │
                           │ similarity_score (FLOAT)     │
                           │ article_id (UUID) FK         │
                           │ status (VARCHAR)             │
                           │ resolved_at (TIMESTAMP)       │
                           │ resolved_by (UUID)           │
                           └─────────────────────────────┘

┌──────────────────────────┐     ┌──────────────────────────────────┐
│ TagsTranslation          │     │ TagGroupDefinitionsTranslation   │
│──────────────────────────│     │──────────────────────────────────│
│ id (UUID) PK             │     │ id (UUID) PK                     │
│ tag_id (UUID) FK         │     │ tag_group_definition_id (UUID) FK│
│ language (VARCHAR 10)    │     │ language (VARCHAR 10)            │
│ name (TEXT)              │     │ display_name (VARCHAR 200)       │
│ created_at (TIMESTAMP)   │     │ description (TEXT)               │
└──────────────────────────┘     │ created_at (TIMESTAMP)           │
                                 └──────────────────────────────────┘

Entities

Tag

FieldTypeConstraintsNotes
idUUIDPK, auto-generated
nameTEXTNOT NULLTag label
tag_group_idUUIDFK → tag_group_definitions.id, ON DELETE SET NULL, nullableUngrouped when null
embeddingVECTOR(768)nullableFor cosine similarity

Unique constraint: uq_tag_name_group — partial index on (name, tag_group_id) WHERE tag_group_id IS NOT NULL

Indexes: idx_tags_group on tag_group_id, idx_tags_embedding (HNSW with vector_cosine_ops)

TagGroupDefinition

FieldTypeConstraintsNotes
idUUIDPK, auto-generated
nameVARCHAR(100)NOT NULLSnake_case slug
display_nameVARCHAR(100)NOT NULLTitle case
descriptionTEXTnullable
color_hexVARCHAR(7)nullablee.g. "#6366f1"
sort_orderINTEGERnullableFor reordering
topic_idUUIDFK → topics.id, NOT NULLTopic-scoped
embeddingVECTOR(768)nullableFor group similarity

Unique constraint: (name, topic_id) — group names unique per topic

Indexes: idx_tag_group_defs_embedding (HNSW with vector_cosine_ops)

TagNormalizationSuggestion

FieldTypeConstraintsNotes
idUUIDPK, auto-generated
existing_tag_idUUIDFK → tags.idThe pre-existing similar tag
new_tag_idUUIDFK → tags.idThe newly created tag
similarity_scoreFLOATCosine similarity at time of creation
article_idUUIDFK → articles.idThe article that triggered the new tag
statusVARCHARDefault 'pending''pending' or 'rejected'
resolved_atTIMESTAMPnullableSet on rejection
resolved_byUUIDnullableUser ID of resolver

Article Tags (junction)

FieldTypeConstraintsNotes
article_idUUIDFK → articles.id, CASCADE DELETEComposite PK
tag_idUUIDFK → tags.id, CASCADE DELETEComposite PK

TagsTranslation

FieldTypeConstraintsNotes
idUUIDPK
tag_idUUIDFK → tags.id
languageVARCHAR(10)e.g. "zh-TW"
nameTEXTNOT NULLTranslated tag name
created_atTIMESTAMP(tz)server_default=now()

Unique constraint: (tag_id, language)

TagGroupDefinitionsTranslation

FieldTypeConstraintsNotes
idUUIDPK
tag_group_definition_idUUIDFK → tag_group_definitions.id
languageVARCHAR(10)
display_nameVARCHAR(200)NOT NULL
descriptionTEXTnullable
created_atTIMESTAMP(tz)server_default=now()

Unique constraint: (tag_group_definition_id, language)

State Transitions

TagNormalizationSuggestion

        (created)


        ┌────────┐
        │ pending │
        └───┬────┘
        ┌───┴────┐
        ▼        ▼
   ┌─────────┐  ┌──────────┐
   │ approved│  │ rejected │
   │(deleted)│  │          │
   └─────────┘  └──────────┘
  • approved: Suggestion is deleted; article_tags re-pointed from new_tag to existing_tag; new_tag deleted
  • rejected: status set to "rejected", resolved_at and resolved_by recorded; both tags remain

TagMode (per Topic)

unsupervised ◄──► semi_supervised ◄──► supervised
  • Admin can switch between any mode at any time
  • Mode change does not retroactively alter existing tags
  • Mode affects only future LLM analysis prompts and group auto-creation