Slik reddet jeg et europeisk selskap svindlet av et byrå. Migrering fra PHP 5.6 og jQuery til React, Django, PostgreSQL, Redis og Rust. Kode, målinger og erfaringer.
NB

Slik reddet jeg et selskap svindlet av et byrå: gjenoppbygging til moderne teknologistak

5.00 /5 - (31 votes )
Sist verifisert: 1. mai 2026
18min lesetid
Casestudie

Han ringte meg på en fredagskveld. Stemmen var rolig, men jeg hørte trettheten i den, trettheten til en mann som i ukevis har kjempet mot et problem han ikke kan kontrollere. Han driver et tjenesteselskap som betjener kunder i fire europeiske land. Han betalte et byrå for en profesjonell nettplattform. Han fikk noe som så ut som en profesjonell plattform. Under overflaten lå katastrofen.

På grunn av en taushetserklæring kan jeg ikke avsløre selskapets navn eller bransje. Jeg kan imidlertid fortelle nøyaktig hva jeg fant, hva jeg gjorde, og hvorfor jeg valgte akkurat disse teknologiene. Denne historien er en advarsel til alle som setter ut plattformbygging til et eksternt byrå.


#Hva jeg fant etter revisjonen av byråets plattform

Det første steget er alltid revisjon. Jeg dømmer ikke, jeg kritiserer ikke. Jeg samler fakta. Etter tre dagers analyse hadde jeg et fullstendig bilde.

Spaghetti-kode på PHP 5.6: byrået brukte ikke noe rammeverk. Hele plattformen var monolitisk prosedyrekode i PHP 5.6 (støtten opphørte i 2018) med SQL-spørringer limt direkte inn i HTML-maler. Ingen ORM, ingen abstraksjonslag, ingen ruter. Filer på 3 000 linjer som blandet forretningslogikk med presentasjon.

// Funnet kode -- SQL-spørring direkte i mal (anonymisert)
<?php
$result = mysql_query("SELECT * FROM services 
    WHERE category = '" . $_GET['cat'] . "' 
    ORDER BY id DESC");
// SQL injection -- ingen validering av inndata
while ($row = mysql_fetch_assoc($result)) {
    echo "<div class='service'>";
    echo "<h2>" . $row['title'] . "</h2>"; // XSS -- ingen escaping
    echo "<p>" . $row['description'] . "</p>";
    echo "</div>";
}
?>

MySQL 5.5 uten indekser: database med 47 tabeller, ingen hadde indekser utover primærnøkler. En spørring som listet tjenester med filtre utførte full table scan på 200 000 oppføringer, med gjennomsnittlig svartid på 4,7 sekunder.

jQuery 1.x + Bootstrap 3: frontend fra 2014. Tolv jQuery-filer lastet på hver side, inkludert tre forskjellige versjoner av biblioteket. Ingen minifisering, ingen bundler, ingen tree-shaking. Total skriptstørrelse: 2,8 MB.

FTP som “utrulling”: ingen Git-repository, ingen CI/CD, ingen staging-miljø. Byrået lastet opp filer direkte via FTP til produksjonsserveren. Ingen versjonskontroll. Ingen tester.

Null sikkerhet: brukerpassord lagret i MD5 uten salt. Sesjoner i filer på delt server. SQL injection på 23 steder. XSS i skjemaer. Ingen CSRF-tokens. Ingen HTTPS på innloggingspanelet.

#Funnet målinger

MetrikkVerdiVurdering
PageSpeed (mobil)18Kritisk
LCP12,4sKritisk
INP1100msKritisk
CLS0,52Kritisk
TTFB4,7sKritisk
Sidestørrelse11,2 MBOverdreven
API-svartid4,7s (gjennomsnitt)Kritisk
Organisk trafikkFall 72% år/årKritisk
Oppdagede sårbarheter23 SQL injection, 14 XSSKritisk

Det verste var at kunden ikke visste om noen av disse problemene. Byrået hadde i ett år sendt ham rapporter om “optimaliseringer” som ikke hadde rot i virkeligheten.


#Hvorfor jeg valgte dette teknologistakket

Beslutningen om målarkitektur er det viktigste øyeblikket i prosjektet. Kunden hadde berettiget bekymring: det forrige byrået hadde lovet en “moderne løsning” og levert kode fra 2014. Jeg måtte velge teknologier som løste konkrete problemer, ikke de som tilfeldigvis er på moten.

Jeg analyserte kravene og tilpasset verktøyene til oppgavene:

Python + Django (backend-API): kunden trengte en solid backend med administrasjonspanel, autentisering, datavalidering og REST-API. Django leverer alt dette ut av esken. Django REST Framework er et modent, stabilt økosystem med utmerket dokumentasjon. Kunden betjener 4 europeiske markeder, og Django har innebygd internasjonalisering.

PostgreSQL (database): migrering fra MySQL 5.5 til PostgreSQL er ikke et innfall. PostgreSQL tilbyr bedre indekser (GIN, GiST for fulltekstsøk), bedre datatyper (JSONB, arrays), moden tabellpartisjonering og pålitelige ACID-transaksjoner. For 200 000 oppføringer med flerspråklig fulltekstsøk er det et naturlig valg.

Redis (cache og køer): API-svartiden på 4,7 sekunder måtte ned under 100 millisekunder. Redis cacher spørringsresultater, lagrer brukersesjoner og håndterer asynkrone oppgavekøer (Celery). Ett verktøy, tre kritiske funksjoner.

React + TypeScript (interaktiv frontend): klientdashboard, søkemotor med filtre, flertrinns skjemaer. Alt dette krever reaktivt brukergrensesnitt. React med TypeScript gir typede komponenter, utmerkede utviklerverktøy og et enormt bibliotekøkosystem.

Rust (ytelsesmikrotjeneste): indeksering av søk for 200 000 oppføringer i 4 språkversjoner, behandling av CSV/Excel-filer fra klienter, datatransformasjoner. Disse oppgavene krevde rå ytelse. Rust behandler søkeindeksen på 1,8 sekunder i stedet for 47 sekunder i den gamle PHP-implementasjonen. Dette er ikke en prosentvis forskjell, det er en størrelsesorden.

Astro (markedsføringsside): hjemmeside, blogg, tilbud, tjenestesider. Dette er statisk innhold som ikke trenger JavaScript. Astro genererer ren HTML uten runtime-kostnader. Interaktive elementer (søkemotor, kontaktskjema) kjører som isolerte React-øyer. Som dedikert Astro-utvikler var dette rammeverket det naturlige valget for den offentlige nettsiden.

Målarkitektur:

┌─────────────────────────────────────────────────────┐
│                  Cloudflare CDN                       │
├──────────────┬───────────────┬────────────────────────┤
│  Astro SSG   │  React SPA    │  Django REST API       │
│  (marketing) │  (dashboard)  │  (backend)             │
│  HTML/CSS    │  TypeScript   │  Python 3.12           │
├──────────────┴───────────────┴────────────────────────┤
│                                                       │
│  ┌─────────────┐  ┌──────────┐  ┌──────────────────┐ │
│  │ PostgreSQL   │  │  Redis   │  │  Rust service    │ │
│  │ (primary DB) │  │  (cache) │  │  (search index,  │ │
│  │              │  │  (queue) │  │   data processing│ │
│  └─────────────┘  └──────────┘  └──────────────────┘ │
│                                                       │
│  ┌─────────────────────────────────────────────────┐ │
│  │  Python AI pipeline (content processing, NLP)    │ │
│  └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘

#Backend-API i Django REST Framework

Hjertet i den nye plattformen er Django med Django REST Framework. Jeg bygde et API som håndterer flerspråklig tjenestekatalog, system for kundehenvendelser, JWT-autentisering og administrasjonspanel.

# services/models.py -- tjenestemodell med flerspråklighet
from django.db import models
from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import SearchVectorField

class Service(models.Model):
    slug = models.SlugField(max_length=200, unique=True)
    category = models.ForeignKey(
        'Category', on_delete=models.PROTECT, related_name='services'
    )
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-created_at']

class ServiceTranslation(models.Model):
    service = models.ForeignKey(
        Service, on_delete=models.CASCADE, related_name='translations'
    )
    language = models.CharField(max_length=5, choices=[
        ('pl', 'Polski'), ('en', 'English'),
        ('de', 'Deutsch'), ('fr', 'Français'),
    ])
    title = models.CharField(max_length=200)
    description = models.TextField()
    meta_title = models.CharField(max_length=70)
    meta_description = models.CharField(max_length=160)
    search_vector = SearchVectorField(null=True)

    class Meta:
        unique_together = ['service', 'language']
        indexes = [
            GinIndex(fields=['search_vector']),
            models.Index(fields=['language', 'service']),
        ]


# services/serializers.py -- serialisator med validering
from rest_framework import serializers

class ServiceSerializer(serializers.ModelSerializer):
    translations = ServiceTranslationSerializer(many=True, read_only=True)
    category_name = serializers.CharField(
        source='category.name', read_only=True
    )

    class Meta:
        model = Service
        fields = [
            'id', 'slug', 'category_name',
            'is_active', 'translations', 'created_at',
        ]


# services/views.py -- API-visninger med Redis-cache
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend

class ServiceViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Service.objects.filter(
        is_active=True
    ).select_related(
        'category'
    ).prefetch_related(
        'translations'
    )
    serializer_class = ServiceSerializer
    filter_backends = [DjangoFilterBackend, filters.SearchFilter]
    filterset_fields = ['category__slug']
    search_fields = ['translations__title', 'translations__description']

    @method_decorator(cache_page(60 * 15))  # Cache 15 minut
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

Konfigurasjon av Redis som cache-backend og Celery-kømegler:

# settings.py -- konfiguracja Redis
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/0',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
            'SERIALIZER': 'django_redis.serializers.json.JSONSerializer',
            'CONNECTION_POOL_KWARGS': {'max_connections': 50},
        },
        'KEY_PREFIX': 'platform',
        'TIMEOUT': 900,  # 15 minut domyślnie
    }
}

# Sesje w Redis (szybsze niż baza danych)
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'

# Celery z Redis jako broker
CELERY_BROKER_URL = 'redis://127.0.0.1:6379/1'
CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/2'
CELERY_TASK_SERIALIZER = 'json'

Effekt etter distribusjon av Django + PostgreSQL + Redis: gjennomsnittlig API-svartid falt fra 4,7 sekunder til 45 millisekunder. Spørringer fra Redis-cache håndteres på 3-5 millisekunder.


#Rust-mikrotjeneste for søkeindeksering

Den mest interessante tekniske utfordringen var søkemotoren. Kunden har en katalog med 200 000 oppføringer i 4 språkversjoner. Den gamle PHP-implementasjonen utførte LIKE '%term%' på MySQL uten indekser: 47 sekunder per spørring. Ubrukelig.

PostgreSQL med GIN-indekser og tsvector løste problemet for standardspørringer. Men kunden trengte også:

  • søk med toleranse for skrivefeil (fuzzy matching),
  • filtrering på flere attributter samtidig med øyeblikkelige resultater,
  • gjenoppbygging av indeks etter dataimport fra CSV/Excel-filer.

For disse oppgavene bygde jeg en mikrotjeneste i Rust med biblioteket Tantivy (tilsvarende Apache Lucene for Rust):

// search-service/src/indexer.rs -- indeksowanie wyszukiwania w Rust
use tantivy::{
    schema::*, doc, Index, IndexWriter,
    tokenizer::NgramTokenizer,
};
use serde::Deserialize;
use std::time::Instant;

#[derive(Deserialize)]
pub struct ServiceRecord {
    pub id: i64,
    pub slug: String,
    pub title: String,
    pub description: String,
    pub category: String,
    pub language: String,
    pub attributes: Vec<String>,
}

pub struct SearchIndexer {
    index: Index,
    schema: Schema,
}

impl SearchIndexer {
    pub fn new(index_path: &str) -> Result<Self, Box<dyn std::error::Error>> {
        let mut schema_builder = Schema::builder();

        schema_builder.add_i64_field("id", STORED | INDEXED);
        schema_builder.add_text_field("slug", STORED);
        schema_builder.add_text_field("title", TEXT | STORED);
        schema_builder.add_text_field("description", TEXT | STORED);
        schema_builder.add_text_field("category", STRING | STORED);
        schema_builder.add_text_field("language", STRING | STORED);
        schema_builder.add_text_field("attributes", TEXT | STORED);
        // N-gram field for fuzzy/partial matching
        schema_builder.add_text_field("title_ngram", TEXT);

        let schema = schema_builder.build();
        let index = Index::create_in_dir(index_path, schema.clone())?;

        // Register n-gram tokenizer for typo tolerance
        let ngram_tokenizer = NgramTokenizer::new(2, 4, false)
            .expect("Failed to create ngram tokenizer");
        index
            .tokenizers()
            .register("ngram", ngram_tokenizer);

        Ok(Self { index, schema })
    }

    pub fn build_index(
        &self,
        records: Vec<ServiceRecord>,
    ) -> Result<usize, Box<dyn std::error::Error>> {
        let start = Instant::now();
        let mut writer: IndexWriter = self.index.writer(128_000_000)?; // 128MB buffer

        let title_field = self.schema.get_field("title").unwrap();
        let description_field = self.schema.get_field("description").unwrap();
        let title_ngram_field = self.schema.get_field("title_ngram").unwrap();

        let count = records.len();
        for record in records {
            writer.add_document(doc!(
                self.schema.get_field("id").unwrap() => record.id,
                self.schema.get_field("slug").unwrap() => record.slug,
                title_field => record.title.clone(),
                description_field => record.description,
                self.schema.get_field("category").unwrap() => record.category,
                self.schema.get_field("language").unwrap() => record.language,
                self.schema.get_field("attributes").unwrap() =>
                    record.attributes.join(" "),
                title_ngram_field => record.title,
            ))?;
        }

        writer.commit()?;
        let duration = start.elapsed();
        println!(
            "Indexed {} records in {:.2}s",
            count,
            duration.as_secs_f64()
        );

        Ok(count)
    }
}

HTTP-API i Rust med Actix-web-rammeverket:

// search-service/src/main.rs -- API wyszukiwania
use actix_web::{web, App, HttpServer, HttpResponse};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct SearchQuery {
    q: String,
    lang: Option<String>,
    category: Option<String>,
    limit: Option<usize>,
}

#[derive(Serialize)]
struct SearchResult {
    id: i64,
    slug: String,
    title: String,
    excerpt: String,
    category: String,
    score: f32,
}

async fn search(
    query: web::Query<SearchQuery>,
    indexer: web::Data<SearchIndexer>,
) -> HttpResponse {
    let limit = query.limit.unwrap_or(20);
    let lang = query.lang.as_deref().unwrap_or("pl");

    let results = indexer.search(
        &query.q, lang, query.category.as_deref(), limit
    );

    HttpResponse::Ok().json(results)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let indexer = SearchIndexer::new("./search_index")
        .expect("Failed to create indexer");
    let indexer_data = web::Data::new(indexer);

    HttpServer::new(move || {
        App::new()
            .app_data(indexer_data.clone())
            .route("/search", web::get().to(search))
            .route("/health", web::get().to(|| async {
                HttpResponse::Ok().body("ok")
            }))
    })
    .bind("127.0.0.1:8081")?
    .run()
    .await
}

Benchmark-resultater for Rust-mikrotjenesten:

OperasjonGammel PHPNy RustForbedring
Bygg indeks (200k oppføringer)47s1,8s26x raskere
Enkelt søk4,7s2ms2350x raskere
Søk med filtre8,3s5ms1660x raskere
Fuzzy matching (skrivefeil)Ingen8msNy funksjon
Minnebruk512 MB84 MB6x mindre

Rust var ikke et valg fordi det er på moten. Det var et valg fordi for denne konkrete oppgaven, behandling av 200 000 oppføringer med n-gram-indeksering og fuzzy matching, gir det ytelse som ikke er oppnåelig for tolkere.


#React-frontend med interaktivt dashboard

Klientdashboardet krevde reaktivt brukergrensesnitt: tabeller med sortering, flernivåfiltre, flertrinns skjemaer, diagrammer med sanntidsdata. React med TypeScript er det naturlige valget.

// src/components/ServiceSearch.tsx -- wyszukiwarka z filtrami
import { useState, useCallback, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useDebounce } from '@/hooks/useDebounce';

interface SearchResult {
  id: number;
  slug: string;
  title: string;
  excerpt: string;
  category: string;
  score: number;
}

interface SearchFilters {
  category: string | null;
  language: string;
}

export function ServiceSearch({ locale }: { locale: string }) {
  const [query, setQuery] = useState('');
  const [filters, setFilters] = useState<SearchFilters>({
    category: null,
    language: locale,
  });

  const debouncedQuery = useDebounce(query, 300);

  const { data: results, isLoading } = useQuery<SearchResult[]>({
    queryKey: ['search', debouncedQuery, filters],
    queryFn: async () => {
      const params = new URLSearchParams({
        q: debouncedQuery,
        lang: filters.language,
        ...(filters.category && { category: filters.category }),
      });
      const res = await fetch(`/api/search?${params}`);
      return res.json();
    },
    enabled: debouncedQuery.length >= 2,
    staleTime: 5 * 60 * 1000, // 5 minut cache
  });

  const handleCategoryChange = useCallback((category: string | null) => {
    setFilters(prev => ({ ...prev, category }));
  }, []);

  return (
    <div className="search-container">
      <div className="relative">
        <input
          type="search"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder={locale === 'pl' ? 'Szukaj usług...' : 'Search services...'}
          className="w-full px-4 py-3 rounded-lg border border-gray-200
                     dark:border-gray-700 bg-white dark:bg-gray-800
                     focus:ring-2 focus:ring-emerald-500 focus:outline-none"
        />
        {isLoading && (
          <div className="absolute right-3 top-1/2 -translate-y-1/2">
            <span className="animate-spin h-5 w-5 border-2
                           border-emerald-500 border-t-transparent rounded-full
                           inline-block" />
          </div>
        )}
      </div>

      {results && results.length > 0 && (
        <div className="mt-4 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
          {results.map((result) => (
            <a
              key={result.id}
              href={`/${locale}/${result.slug}/`}
              className="block p-4 rounded-lg border border-gray-100
                         dark:border-gray-700 hover:border-emerald-500
                         transition-colors"
            >
              <span className="text-xs font-medium text-emerald-600
                              dark:text-emerald-400">
                {result.category}
              </span>
              <h3 className="mt-1 font-semibold text-gray-900
                            dark:text-white">
                {result.title}
              </h3>
              <p className="mt-2 text-sm text-gray-600 dark:text-gray-400
                           line-clamp-2">
                {result.excerpt}
              </p>
            </a>
          ))}
        </div>
      )}
    </div>
  );
}

React-komponenten er innebygd i Astro-siden som en interaktiv øy:

---
// src/pages/[lang]/services.astro -- strona usług
import { ServiceSearch } from '../../components/ServiceSearch';
import Layout from '../../layouts/Layout.astro';
---

<Layout title="Usługi">
  <section class="services-hero">
    <h1>Nasze usługi</h1>
  </section>

  <!-- Wyspa React -- ładuje się przy scrollu, nie blokuje reszty strony -->
  <ServiceSearch client:visible locale={Astro.params.lang} />
</Layout>

Takket være Islands-arkitekturen laster markedsføringssiden 0 KB JavaScript som standard. Søkemotorkomponenten (38 KB gzipped) lastes først når brukeren scroller til seksjonen.


#AI-pipeline for innholdsbehandling

Migrering av 1 200 innholdssider fra skittent HTML til ren Markdown krevde automatisering. Mange sider hadde ufullstendig SEO-metadata, manglende beskrivelser og ikke-optimale overskrifter. I stedet for å rette dem manuelt bygde jeg en pipeline i Python som brukte en tilpasset språkmodell.

Pipelinen arbeidet i tre faser:

# ai_content_pipeline.py -- przetwarzanie treści z AI
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor

import markdownify

@dataclass
class ContentAnalysis:
    entities: list[dict] = field(default_factory=list)
    meta_description: str = ''
    heading_issues: list[str] = field(default_factory=list)
    word_count: int = 0
    is_thin: bool = False
    suggested_internal_links: list[str] = field(default_factory=list)

def clean_legacy_html(raw_html: str) -> str:
    """Faza 1: Czyszczenie brudnego HTML z legacy platformy."""
    # Usuwanie inline styles, komentarzy warunkowych IE, pustych tagów
    cleaned = re.sub(r'style="[^"]*"', '', raw_html)
    cleaned = re.sub(r'<!--\[if.*?\]>.*?<!\[endif\]-->', '', cleaned,
                     flags=re.DOTALL)
    cleaned = re.sub(r'<(div|span|p)[^>]*>\s*</\1>', '', cleaned)
    cleaned = re.sub(r'&nbsp;', ' ', cleaned)
    return cleaned

def convert_to_markdown(html: str) -> str:
    """Faza 2: Konwersja HTML na Markdown."""
    return markdownify.markdownify(
        html,
        heading_style="ATX",
        strip=['script', 'style', 'iframe', 'object', 'embed'],
        convert=['h1', 'h2', 'h3', 'h4', 'p', 'a', 'img',
                 'ul', 'ol', 'li', 'strong', 'em', 'table'],
    )

def analyze_with_ai(content: str, model_client) -> ContentAnalysis:
    """Faza 3: Analiza treści za pomocą niestandardowego modelu AI."""
    prompt = f"""Przeanalizuj poniższą treść strony internetowej.
Zwróć JSON z polami:
- entities: lista obiektów {{name, type, relevance_score}}
- meta_description: optymalny opis meta (max 155 znaków, po polsku)
- heading_issues: lista problemów ze strukturą nagłówków
- word_count: liczba słów
- suggested_internal_links: sugerowane frazy do linkowania wewnętrznego

Treść:
{content[:4000]}"""

    response = model_client.generate(prompt, max_tokens=1024)
    data = json.loads(response.text)

    return ContentAnalysis(
        entities=data.get('entities', []),
        meta_description=data.get('meta_description', ''),
        heading_issues=data.get('heading_issues', []),
        word_count=data.get('word_count', 0),
        is_thin=data.get('word_count', 0) < 300,
        suggested_internal_links=data.get('suggested_internal_links', []),
    )

def process_batch(
    content_dir: Path, model_client, max_workers: int = 4
) -> dict:
    """Przetwarzanie wsadowe z wielowątkowością."""
    stats = {'processed': 0, 'thin': 0, 'entities': 0}

    def process_file(md_file: Path) -> ContentAnalysis:
        content = md_file.read_text(encoding='utf-8')
        return analyze_with_ai(content, model_client)

    files = list(content_dir.glob('*.md'))
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        results = executor.map(process_file, files)
        for analysis in results:
            stats['processed'] += 1
            if analysis.is_thin:
                stats['thin'] += 1
            stats['entities'] += len(analysis.entities)

    return stats

Pipelinen behandlet 1 247 sider på 4 timer på egen infrastruktur, ikke i skyen, ikke via ekstern API. Den fant 3 400 unike entiteter, genererte manglende metabeskrivelser for 680 sider og identifiserte 89 sider med tynt innhold som krevde utbygging.

AI-modellen kjørte på en dedikert server med GPU. Jeg brukte ikke AWS, Azure eller noen skyforvaltet løsning. Full kontroll over klientdata, null overføring av innhold til eksterne API-er.


#Datamigrering fra MySQL til PostgreSQL

Migrering av databasen fra MySQL 5.5 til PostgreSQL krevde ikke bare overføring av data, men en grunnleggende ombygging av skjemaet. Det gamle skjemaet hadde ingen relasjoner, indekser eller integritetsbegrensninger.

-- Migracja schematu: od chaosu do porządku

-- Stary MySQL (brak relacji, brak indeksów)
-- CREATE TABLE services (id INT AUTO_INCREMENT, title VARCHAR(255), ...);
-- CREATE TABLE categories (id INT AUTO_INCREMENT, name VARCHAR(100), ...);
-- Brak FOREIGN KEY, brak INDEX poza PK

-- Nowy PostgreSQL z prawidłową strukturą
CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    slug VARCHAR(200) UNIQUE NOT NULL,
    parent_id INTEGER REFERENCES categories(id),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE services (
    id SERIAL PRIMARY KEY,
    slug VARCHAR(200) UNIQUE NOT NULL,
    category_id INTEGER NOT NULL REFERENCES categories(id),
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE service_translations (
    id SERIAL PRIMARY KEY,
    service_id INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE,
    language VARCHAR(5) NOT NULL,
    title VARCHAR(200) NOT NULL,
    description TEXT NOT NULL,
    meta_title VARCHAR(70) NOT NULL,
    meta_description VARCHAR(160) NOT NULL,
    search_vector TSVECTOR,
    UNIQUE(service_id, language)
);

-- Indeksy dla wydajnego wyszukiwania
CREATE INDEX idx_translations_search ON service_translations
    USING GIN(search_vector);
CREATE INDEX idx_translations_lang ON service_translations(language);
CREATE INDEX idx_services_category ON services(category_id)
    WHERE is_active = true;

-- Trigger automatycznej aktualizacji search_vector
CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
    NEW.search_vector :=
        setweight(to_tsvector('simple', COALESCE(NEW.title, '')), 'A') ||
        setweight(to_tsvector('simple', COALESCE(NEW.description, '')), 'B');
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_search_vector
    BEFORE INSERT OR UPDATE ON service_translations
    FOR EACH ROW EXECUTE FUNCTION update_search_vector();

#SEO-migrering: omdirigeringer og strukturerte data

Det mest kritiske elementet i migreringen er å bevare plasseringene i Google. Til tross for at den gamle plattformen mistet trafikk, hadde den fortsatt hundrevis av indekserte URL-er og dusinvis av backlenker.

Jeg kartla hver gammel URL til den nye og implementerte 301-omdirigeringer på Cloudflare Workers-nivå:

// redirects.ts -- mapa przekierowań ze starej platformy
const redirectMap: Record<string, string> = {
  '/uslugi.php?id=1': '/pl/uslugi/konsulting/',
  '/services.php?id=1': '/en/services/consulting/',
  '/index.php?page=about': '/pl/o-nas/',
  '/kontakt.php': '/pl/kontakt/',
  '/leistungen.php': '/de/dienstleistungen/',
  // ... 963 przekierowania wygenerowane automatycznie
};

export function handleRedirects(url: URL): Response | null {
  // Sprawdź dokładne dopasowanie
  const exactMatch = redirectMap[url.pathname + url.search];
  if (exactMatch) {
    return new Response(null, {
      status: 301,
      headers: { Location: exactMatch },
    });
  }

  // Sprawdź dopasowanie bez query string
  const pathMatch = redirectMap[url.pathname];
  if (pathMatch) {
    return new Response(null, {
      status: 301,
      headers: { Location: pathMatch },
    });
  }

  return null;
}

Jeg implementerte fullstendige Schema.org-strukturerte data med hreflang for hver språkversjon. Den gamle plattformen hadde ingen strukturerte data, og Google forsto ikke hva siden handlet om.


#Resultater etter 4 måneder

MetrikkFørEtterEndring
PageSpeed (mobil)1899+450%
LCP12,4s0,3s-98%
INP1100ms22ms-98%
CLS0,520,01-98%
TTFB4,7s0,03s-99%
Sidestørrelse11,2 MB0,28 MB-97%
API-svartid4700ms45ms-99%
Søk (200k oppføringer)47s2ms23500x
Organisk trafikkGrunnlinje+340%Vekst
Kundehenvendelser~2/uke~14/uke+600%
Sikkerhetssårbarheter370Eliminert

Fullstendig teknologistak for prosjektet:

  • Backend-API: Python 3.12, Django 5, Django REST Framework, Celery
  • Database: PostgreSQL 16 med GIN-indekser og fulltekstsøk
  • Cache og køer: Redis 7 (cache, sesjoner, Celery-megler)
  • Søkemikrotjeneste: Rust, Actix-web, Tantivy
  • Interaktiv frontend: React 19, TypeScript, TanStack Query, Tailwind CSS
  • Markedsføringsside: Astro 5 med React-øyer
  • AI-pipeline: Python, tilpasset språkmodell på dedikert GPU
  • Distribusjon: Cloudflare Pages + Workers, GitHub Actions CI/CD
  • Overvåking: Sentry, Prometheus, Grafana

#Hva dette prosjektet lærte meg

Ikke døm, diagnostiser. Kunden kom såret av en dårlig byråopplevelse. Det siste han trengte var enda en “ekspert” som fortalte ham hvor ille det var. I stedet presenterte jeg fakta i form av en rapport, forklarte risikoen og foreslo en handlingsplan med tydelig tidsplan.

Velg verktøy for oppgaven, ikke oppgaver for verktøyet. Django til API, React til interaktivt brukergrensesnitt, Rust til databehandling, Astro til statiske sider. Hver teknologi løser et konkret problem. Et monolittisk rammeverk for alt er en oppskrift på kompromisser.

AI er et verktøy, ikke magi. AI-pipelinen behandlet 1 247 sider på 4 timer, arbeid som manuelt ville tatt uker. Men hvert resultat krevde menneskelig verifisering. AI genererte forslag, mennesket tok beslutninger. En tilpasset modell på egen server gir full kontroll over klientdata.

Rust rettferdiggjør seg i konkrete tilfeller. Jeg skrev ikke hele plattformen i Rust. Jeg skrev en mikrotjeneste i Rust som behandler 200 000 oppføringer, og der er ytelsesforskjellen en størrelsesorden. Resten av systemet fungerer utmerket i Python og TypeScript.

SEO-migrering er ikke valgfritt. Det er obligatorisk. Uten URL-mapping og 301-omdirigeringer ville kunden ha mistet restene av organisk trafikk. Takket være korrekt migrering økte trafikken med 340% på 4 måneder.


#Trenger du redning etter en dårlig byråopplevelse?

Hvis plattformen din ble bygget på utdaterte teknologier, er treg, usikker eller rett og slett ikke fungerer som den skal, kontakt WPPoland. Jeg gjennomfører en gratis innledende revisjon og presenterer en utbedringsplan med tydelig tidsplan og arbeidsomfang.

Hvert redningsprosjekt begynner med en samtale. Denne kunden ringte på en fredagskveld. Mandag morgen hadde han en rapport. Åtte uker senere hadde han en plattform han er stolt av.

Neste steg

Gjor artikkelen om til faktisk implementering

Denne blokken styrker intern lenking og sender leseren videre til de mest relevante tjenestene og innholdet.

Vil du fa dette implementert pa nettstedet ditt?

Hvis synlighet i Google og AI-systemer betyr noe, kan jeg bygge innholdsarkitektur, FAQ, schema og intern lenking for SEO, GEO og AEO.

Relevant klynge

Utforsk andre WordPress-tjenester og kunnskapsbase

Styrk virksomheten din med profesjonell teknisk støtte innen kjerneområdene i WordPress-økosystemet.

Hvor lang tid tar en plattformmigrering fra et utdatert stak til moderne arkitektur?
Tidsrammen avhenger av prosjektets omfang og fastsettes individuelt etter revisjon. En enkel plattform tar 4-6 uker. Et komplekst system med mange integrasjoner, flerspråklighet og interaktive komponenter tar 8-14 uker.
Hvorfor Rust fremfor Node.js eller Go for mikrotjenester?
Rust tilbyr ytelse sammenlignbar med C/C++ med garantier for minnesikkerhet. For oppgaver som krever behandling av store datasett, søkeindeksering og filtransformasjoner gir Rust en størrelsesorden bedre ytelse enn Node.js.
Påvirker migrering til et nytt stak plasseringene i Google?
En korrekt gjennomført migrering med URL-mapping og 301-omdirigeringer beskytter eksisterende plasseringer. Forbedring av Core Web Vitals etter migrering fører vanligvis til 30-50 prosent økning i organisk trafikk innen 3 måneder.
Hva gjør man når et byrå har svindlet og etterlatt plattformen uten dokumentasjon?
Det første steget er en innledende revisjon: gjennomgang av kode, lisenser, sikkerhet og ytelse. Deretter datagjenoppretting og innholdshenting fra den eksisterende installasjonen. Deretter planlegging av migrering til ny plattform med fullstendig dokumentasjon.

Trenger du FAQ tilpasset bransje og marked? Vi lager en versjon som støtter dine forretningsmål.

Ta kontakt

Relaterte artikler

Astro 5 eller Next.js 15 - hvilket rammeverk velge i 2026? Sammenligning av ytelse, arkitektur og brukstilfeller for Headless WordPress.
wordpress

Astro 5 vs Next.js 15: Komplett teknisk sammenligning 2026

Astro 5 eller Next.js 15 - hvilket rammeverk velge i 2026? Sammenligning av ytelse, arkitektur og brukstilfeller for Headless WordPress.

En detaljert casestudie som viser hvordan WPPoland optimaliserte en treg WooCommerce-mobelbutikk fra PageSpeed 40 til 98, kuttet lastetider fra 8 sekunder til under 1 sekund og doblet konverteringsraten.
performance

Fra 40 til 98 PageSpeed: Hvordan Vi Transformerte en WooCommerce-Butikk

En detaljert casestudie som viser hvordan WPPoland optimaliserte en treg WooCommerce-mobelbutikk fra PageSpeed 40 til 98, kuttet lastetider fra 8 sekunder til under 1 sekund og doblet konverteringsraten.

Hvordan WordPress Abilities API gjør det mulig for AI-agenter å oppdage og bruke WordPress-funksjonalitet programmatisk. Bygg intelligente arbeidsflyter med MCP-servere, ChatGPT-plugins og Claude-verktøy.
wordpress

WordPress AI Workflows: Abilities API-revolusjonen i WordPress 7.x

Hvordan WordPress Abilities API gjør det mulig for AI-agenter å oppdage og bruke WordPress-funksjonalitet programmatisk. Bygg intelligente arbeidsflyter med MCP-servere, ChatGPT-plugins og Claude-verktøy.