Tag System
Status: Implemented Depends On: Page System
Overview
Section titled “Overview”The Tag System provides categorization and filtering for workspace pages. Tags are named labels with optional colors and
grouping, applied to pages via a many-to-many join table (page_tags). Tags are integrated into FTS5 full-text search
as a third column alongside title and content, with BM25 weight boosting (10:1:5 ratio).
The system supports tag groups for organizing related tags, tag merging for consolidation, and sidebar AND/OR filtering
for multi-tag page discovery. Tags also serve as the backend for the tags property in the property system — frontend
writes to the “tags” property slug are routed to tag use cases via Tauri command routing.
Diagram
Section titled “Diagram”Architecture
Section titled “Architecture”Framework (Tauri) └── Tauri commands apps/desktop/src-tauri/src/commands/ create_tag, update_tag, delete_tag, list_tags, search_tags, get_page_tags, set_page_tags, add_tag_to_page, remove_tag_from_page, merge_tags, filter_pages_by_tags, create_tag_group, list_tag_groups, update_tag_group, delete_tag_group
Application ├── Tag use cases crates/application/src/tag/ │ CreateTagUseCase, UpdateTagUseCase, DeleteTagUseCase, │ ListTagsUseCase, SearchTagsUseCase, MergeTagsUseCase, │ GetPageTagsUseCase, SetPageTagsUseCase, AddTagToPageUseCase, │ RemoveTagFromPageUseCase, FilterPagesByTagsUseCase, │ ManageGroupsUseCase └── TagRepository (trait) crates/application/src/tag/services.rs 17 methods, no default impls on mutations
Domain ├── Tag crates/domain/src/tag.rs │ Name validation, slug derivation, color validation ├── TagGroup Tag grouping entity └── TagSummary Lightweight tag for page display
Infrastructure └── SqliteTagRepository crates/infrastructure/sqlite/src/workspace/tag_repository.rs tags, tag_groups, page_tags tables; FTS5 triggersDependencies flow inward: Framework -> Infrastructure -> Application -> Domain.
Key Design Decisions
Section titled “Key Design Decisions”1. FTS5 Three-Column Index with BM25 Weighting
Section titled “1. FTS5 Three-Column Index with BM25 Weighting”The FTS5 virtual table indexes three columns with different BM25 weights:
CREATE VIRTUAL TABLE IF NOT EXISTS pages_fts USING fts5( title, -- weight 10.0 (title matches rank highest) content, -- weight 1.0 (body text baseline) tags, -- weight 5.0 (tag matches rank between title and body) content='', contentless_delete=1);Tag names are concatenated as a space-separated string in the tags column. This means a search for “adventure” matches
pages tagged “Adventure” with 5x the weight of a body-text match, but half the weight of a title match.
2. Trigger-Based FTS Synchronization
Section titled “2. Trigger-Based FTS Synchronization”Four SQLite triggers keep the FTS index in sync with tag changes:
| Trigger | Event | Effect |
|---|---|---|
pages_fts_insert | INSERT ON pages | Index new page with its tags |
pages_fts_update | UPDATE ON pages | Re-index page (title/content/tags) |
page_tags_fts_insert | INSERT ON page_tags | Re-index page when tag added |
page_tags_fts_delete | DELETE ON page_tags | Re-index page when tag removed |
tags_fts_name_update | UPDATE OF name ON tags | Re-index all pages with renamed tag |
The tag change triggers (page_tags_fts_insert, page_tags_fts_delete) delete the old FTS entry and re-insert with
current tag names, ensuring the tag column stays consistent without application-level coordination.
3. Property Bridge (slug == “tags” Routing)
Section titled “3. Property Bridge (slug == “tags” Routing)”The property system defines a tags property (UUID 00000000-0000-0000-0000-000000000013, type multi_select). When
the frontend writes to the “tags” property, the Tauri command layer detects slug == "tags" and routes the write to tag
use cases instead of the generic property store. This bridges two conceptual models — properties (key-value metadata)
and tags (first-class entities with groups, merge, and filtering) — through a single UI surface.
4. Tag Merge Behavior
Section titled “4. Tag Merge Behavior”MergeTagsUseCase transfers all page references from a source tag to a target tag, then deletes the source. The merge
handles deduplication: if a page already has both the source and target tags, the source association is simply removed
(no duplicate target). The operation returns the count of affected pages.
MergeTagsUseCase::execute(source_id, target_id) 1. Validate source != target 2. TagRepository::merge_tags(source_id, target_id) a. Transfer page_tags rows from source to target (INSERT OR IGNORE for dedup) b. Delete source tag 3. Return affected page count5. AND/OR Sidebar Filtering
Section titled “5. AND/OR Sidebar Filtering”FilterPagesByTagsUseCase accepts a list of tag IDs and a match_all flag:
- AND mode (
match_all = true): Returns pages that have ALL specified tags. Implemented viaGROUP BY page_id HAVING COUNT(DISTINCT tag_id) = N. - OR mode (
match_all = false): Returns pages that have ANY of the specified tags. SimpleINclause on thepage_tagsjoin.
The frontend TagFilter component presents this as a toggle in the sidebar, allowing users to narrow or broaden their
page list.
6. Tag Groups
Section titled “6. Tag Groups”Tags can be organized into groups via TagGroup entities. A group has a name, slug, optional color, and sort order. The
relationship is optional (group_id is nullable on tags). Deleting a group sets group_id = NULL on member tags (via
ON DELETE SET NULL in the schema). Groups are purely organizational — they don’t affect search ranking or filtering
behavior.
7. No Default Implementations on Mutations
Section titled “7. No Default Implementations on Mutations”The TagRepository trait has no default implementations on any mutation method. This is an explicit design decision to
prevent the “silent no-op” bug where a missing SqliteTagRepository override compiles but silently drops data. Read
methods may have defaults; mutations must be explicitly implemented.
Storage Schema
Section titled “Storage Schema”tags table
Section titled “tags table”CREATE TABLE tags ( id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL UNIQUE, slug TEXT NOT NULL UNIQUE, color TEXT, group_id TEXT REFERENCES tag_groups(id) ON DELETE SET NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);CREATE INDEX idx_tags_group_id ON tags(group_id);tag_groups table
Section titled “tag_groups table”CREATE TABLE tag_groups ( id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL UNIQUE, slug TEXT NOT NULL UNIQUE, color TEXT, sort_order INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);page_tags table
Section titled “page_tags table”CREATE TABLE page_tags ( page_id TEXT NOT NULL REFERENCES pages(id) ON DELETE CASCADE, tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE, assigned_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')), PRIMARY KEY (page_id, tag_id));CREATE INDEX idx_page_tags_tag_id ON page_tags(tag_id);ON DELETE CASCADE on both foreign keys ensures that deleting a page removes its tag associations, and deleting a tag
removes all its page associations.
Domain Rules
Section titled “Domain Rules”| Rule | Enforcement |
|---|---|
| Tag name non-empty, max 100 chars (Unicode scalar values) | Tag::validate_name() in domain |
| Tag name no leading/trailing whitespace | Tag::validate_name() in domain |
Color must be valid 6-digit hex (e.g., #ff5733) | Tag::validate_color() in domain |
Slug derived from name via slugify() | Tag::new() constructor |
| Cannot merge a tag into itself | MergeTagsUseCase validation |
| Tag group name same rules as tag name | Shared validate_label_name() |
Permissions: TagsRead for queries, TagsWrite for mutations | PermissionGuard::require() in each use case |
Key Code Paths
Section titled “Key Code Paths”| Scenario | Entry Point |
|---|---|
| Create tag | CreateTagUseCase::execute() in crates/application/src/tag/create_tag.rs |
| Merge tags | MergeTagsUseCase::execute() in crates/application/src/tag/merge_tags.rs |
| Set page tags atomically | SetPageTagsUseCase::execute() in crates/application/src/tag/set_page_tags.rs |
| Filter pages by tags | FilterPagesByTagsUseCase::execute() in crates/application/src/tag/filter_pages_by_tags.rs |
| Search tags by prefix | SearchTagsUseCase::execute() in crates/application/src/tag/search_tags.rs |
| FTS5 tag triggers | crates/infrastructure/sqlite/src/migrations/mod.rs (lines 488-521) |
| Frontend tag bar | TagBar.tsx in apps/desktop/src-react/components/ |
| Frontend tag filter | TagFilter.tsx in apps/desktop/src-react/components/ |
Related
Section titled “Related”- Page System (
page-system): Tags are applied to pages via thepage_tagsjoin table - Property System (
property-system): Tags bridge viaslug == "tags"routing
Enables page categorization and filtering. See also: Layout System, Wiki-Link System.
Was this page helpful?
Thanks for your feedback!