Skip to content

Database Architecture

MySQL and Qdrant database architecture for WordPress projects and vector-based AI search.

Overview

WDG uses two database systems:

  • MySQL: WordPress data storage
  • Qdrant: Vector embeddings for AI search

MySQL Architecture

Single MySQL Instance

All projects share one MySQL container with isolated databases:

MySQL Container wdg-mysql (host port 6306 → container 3306)
├── mysql (system database)
├── information_schema
├── performance_schema
├── wp_my_site (project database)
├── wp_client_website (project database)
└── wp_demo (project database)

Database Naming Convention

  • Project databases: wp_{project-name}
  • Underscores replace hyphens: my-sitewp_my_site
  • Lowercase only

Configuration

yaml
# docker-compose.yml
mysql:
  image: mysql:8.0
  container_name: wdg-mysql
  command: --default-authentication-plugin=mysql_native_password --require_secure_transport=OFF
  ports:
    - "${MYSQL_PORT}:3306"   # default host 6306
  environment:
    MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    MYSQL_DATABASE: ${WP_DB_NAME}
    MYSQL_USER: ${WP_DB_USER}
    MYSQL_PASSWORD: ${WP_DB_PASSWORD}
  volumes:
    - ./data/mysql:/var/lib/mysql   # host bind mount, not a named volume

The mysql service mounts only its data directory (./data/mysql); it does not mount a custom my.cnf. Tuning is supplied via the command: flags above (--require_secure_transport=OFF allows non-TLS client connections, which the local WordPress containers use).

A my.cnf does exist at services/wordpress/my.cnf, but it is baked into the WordPress image as the MySQL client configuration — it is not mounted into the wdg-mysql server container. There is no services/mysql/ directory.

WordPress Database Schema

Standard Tables

Each project database includes:

sql
-- Core tables
wp_posts
wp_postmeta
wp_users
wp_usermeta
wp_terms
wp_term_taxonomy
wp_term_relationships
wp_comments
wp_commentmeta
wp_options

-- Multisite (if enabled)
wp_blogs
wp_blog_versions
wp_site
wp_sitemeta

Project-Specific Tables

Custom tables created by themes/plugins:

sql
-- Example custom tables
wp_wikit_blocks
wp_wikit_patterns
wp_custom_post_types
wp_user_preferences

Indexing Strategy

sql
-- Performance indexes
CREATE INDEX idx_post_type ON wp_posts(post_type, post_status);
CREATE INDEX idx_post_author ON wp_posts(post_author);
CREATE INDEX idx_post_date ON wp_posts(post_date);
CREATE INDEX idx_meta_key ON wp_postmeta(meta_key(191));

Qdrant Vector Database

Collection Architecture

Qdrant Container wdg-qdrant (host ports 6333 REST / 6334 gRPC)
├── wdg_framework        (Wikit code)
├── platform             (CLI, MCP server, indexer, dashboard, docs)
├── project_my_site
└── project_client_website

Vector counts and sizes vary by codebase. Collection names map one-to-one to scopes: wdg_framework, platform, and project_{name} (hyphens in the project name become underscores, e.g. my-siteproject_my_site).

Vector Schema

The payload is a flat dict (no nested metadata object). Each point's id is md5("{file_path}:{line_number}:{name}"). Core keys:

json
{
  "id": "md5(file_path:line_number:name)",
  "vector": [0.1, 0.2, 0.3, "..."],
  "payload": {
    "file_path": "wp-content/themes/custom/functions.php",
    "line_number": 45,
    "name": "register_custom_post_types",
    "component_type": "function",
    "language": "php",
    "content": "function register_custom_post_types() {...}",
    "project": "my-site"
  }
}

The indexer also stores a few descriptive keys (file_extension, is_wikit, repository) and component-specific optional fields (e.g. calls, extends, implements, ACF and hook metadata). There is no line_start/line_end, commit_hash, branch, or indexed_at — only the single line_number above.

Collection Configuration

Collections are created on demand by the indexer with a single fixed config: 384-dimensional vectors and Cosine distance (size follows the loaded model). The indexer does not set custom optimizer or HNSW parameters — Qdrant defaults apply.

python
# How the indexer creates a collection (indexer/indexer.py)
client.create_collection(
    collection_name="project_my_site",
    vectors_config=VectorParams(
        size=EMBEDDING_DIM,      # 384 for all-MiniLM-L6-v2
        distance=Distance.COSINE
    )
)

Database Operations

MySQL Operations

Create Project Database

bash
# Via CLI
wdg db create my-site

# Manual
docker exec wdg-mysql mysql -uroot -p$MYSQL_ROOT_PASSWORD -e \
    "CREATE DATABASE wp_my_site CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

Backup Database

bash
# Export
wdg db export my-site backup.sql

# Manual
docker exec wdg-mysql mysqldump \
    -uroot -p$MYSQL_ROOT_PASSWORD \
    wp_my_site > backup.sql

Import Database

bash
# Import
wdg db import my-site backup.sql

# Manual
docker exec -i wdg-mysql mysql \
    -uroot -p$MYSQL_ROOT_PASSWORD \
    wp_my_site < backup.sql

Search & Replace

bash
# Update domain
wdg db search-replace my-site "old-domain.com" "new-domain.com"

# Via WP-CLI
wdg wp my-site search-replace \
    "http://localhost" \
    "https://production.com" \
    --all-tables

Qdrant Operations

Query Vectors

python
# Semantic search
results = client.search(
    collection_name="project_my_site",
    query_vector=query_embedding,
    limit=10,
    with_payload=True
)

Filter by Metadata

python
# Search only PHP functions
results = client.search(
    collection_name="project_my_site",
    query_vector=query_embedding,
    query_filter={
        "must": [
            {"key": "language", "match": {"value": "php"}},
            {"key": "component_type", "match": {"value": "function"}}
        ]
    },
    limit=10
)

Performance Optimization

MySQL Tuning

bash
# Check status
docker exec wdg-mysql mysql -uroot -p -e "SHOW STATUS LIKE 'Threads_connected';"

# Optimize tables
wdg wp my-site db optimize

# Analyze tables
docker exec wdg-mysql mysqlcheck -uroot -p wp_my_site --analyze

Qdrant Optimization

bash
# Optimize collection
curl -X POST http://localhost:6333/collections/project_my_site/optimizers

# Check collection info
curl http://localhost:6333/collections/project_my_site

Backup Strategies

Automated Backups

bash
#!/bin/bash
# backup-databases.sh

DATE=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR="backups/$DATE"

mkdir -p $BACKUP_DIR

# Backup all MySQL databases
for db in $(docker exec wdg-mysql mysql -uroot -p$MYSQL_ROOT_PASSWORD -e "SHOW DATABASES" | grep ^wp_); do
    echo "Backing up $db..."
    docker exec wdg-mysql mysqldump -uroot -p$MYSQL_ROOT_PASSWORD $db > "$BACKUP_DIR/$db.sql"
done

# Backup Qdrant collections
curl -X POST http://localhost:6333/collections/snapshot -d '{}' > "$BACKUP_DIR/qdrant.snapshot"

# Compress
tar -czf "backups/full-backup-$DATE.tar.gz" $BACKUP_DIR
rm -rf $BACKUP_DIR

echo "Backup complete: backups/full-backup-$DATE.tar.gz"

Monitoring

MySQL Monitoring

bash
# Connection count
docker exec wdg-mysql mysql -uroot -p -e "SHOW STATUS LIKE 'Threads_connected';"

# Query performance
docker exec wdg-mysql mysql -uroot -p -e "SHOW FULL PROCESSLIST;"

# Database sizes
docker exec wdg-mysql mysql -uroot -p -e "
SELECT
  table_schema AS 'Database',
  ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)'
FROM information_schema.tables
WHERE table_schema LIKE 'wp_%'
GROUP BY table_schema;
"

Qdrant Monitoring

bash
# Collection stats
curl http://localhost:6333/collections/project_my_site

# Cluster status
curl http://localhost:6333/cluster

# Metrics
curl http://localhost:6333/metrics

Troubleshooting

MySQL Issues

bash
# Connection refused
docker logs wdg-mysql
docker restart wdg-mysql

# Corrupted table
wdg wp my-site db repair

# Slow queries
docker exec wdg-mysql mysql -uroot -p -e "SELECT * FROM mysql.slow_log ORDER BY query_time DESC LIMIT 10;"

Qdrant Issues

bash
# Collection not found
curl http://localhost:6333/collections

# Recreate collection
wdg collections delete project_my_site
wdg index my-site

# Check logs
docker logs wdg-qdrant

See Also: