PostgreSQL: Über die Relationale Datenbank Hinaus
PostgreSQL bietet erweiterte Funktionen, die es perfekt für moderne Anwendungen machen: semi-strukturierte Daten mit JSONB und integrierte Textsuche.
1. JSONB in Django
Modell mit JSONField
from django.db import models
from django.contrib.postgres.fields import ArrayField
class Product(models.Model):
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
# JSONB für flexible Daten
metadata = models.JSONField(default=dict)
specifications = models.JSONField(default=dict)
# String-Array
tags = ArrayField(
models.CharField(max_length=50),
default=list,
blank=True
)
class Meta:
indexes = [
# GIN-Index für effiziente JSONB-Abfragen
models.Index(
name='idx_product_metadata_gin',
fields=['metadata'],
opclasses=['jsonb_path_ops']
),
]
JSONB-Abfragen
from django.db.models import F
from django.contrib.postgres.fields.jsonb import KeyTextTransform
# Zugriff auf bestimmte Schlüssel
products = Product.objects.filter(
metadata__brand='Apple'
)
# Verschachtelte Schlüssel
products = Product.objects.filter(
specifications__display__size__gte=6.0
)
# Schlüsselexistenz prüfen
products = Product.objects.filter(
metadata__has_key='warranty'
)
# Enthält Wert
products = Product.objects.filter(
metadata__contains={'featured': True}
)
# Enthalten in
products = Product.objects.filter(
metadata__contained_by={'brand': 'Apple', 'category': 'phone'}
)
# Annotation mit JSON-Wert
from django.db.models.functions import Cast
from django.db.models import FloatField
products = Product.objects.annotate(
rating=Cast(
KeyTextTransform('rating', 'metadata'),
FloatField()
)
).filter(rating__gte=4.5)
Atomare JSONB-Updates
from django.db.models import F
from django.db.models.functions import JSONObject
from django.contrib.postgres.fields.jsonb import KeyTransform
# Bestimmten Schlüssel aktualisieren (Django 4.2+)
Product.objects.filter(pk=1).update(
metadata=F('metadata') | {'updated_at': '2024-01-15'}
)
# Schlüssel entfernen
from django.contrib.postgres.expressions import ArraySubquery
Product.objects.filter(pk=1).update(
metadata=models.Func(
F('metadata'),
models.Value('deprecated_key'),
function='jsonb_delete_path'
)
)
# Numerischen Wert in JSON inkrementieren
from django.db.models.expressions import RawSQL
Product.objects.filter(pk=1).update(
metadata=RawSQL(
"jsonb_set(metadata, '{views}', (COALESCE(metadata->>'views', '0')::int + 1)::text::jsonb)",
[]
)
)
2. Volltextsuche
Modell-Setup
from django.contrib.postgres.search import (
SearchVector, SearchQuery, SearchRank,
SearchVectorField, TrigramSimilarity
)
from django.contrib.postgres.indexes import GinIndex
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
excerpt = models.TextField(blank=True)
# Feld für indizierte Suche
search_vector = SearchVectorField(null=True)
class Meta:
indexes = [
GinIndex(fields=['search_vector']),
]
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# Search-Vektor aktualisieren
Article.objects.filter(pk=self.pk).update(
search_vector=(
SearchVector('title', weight='A', config='german') +
SearchVector('excerpt', weight='B', config='german') +
SearchVector('content', weight='C', config='german')
)
)
Volltext-Abfragen
# Einfache Suche
from django.contrib.postgres.search import SearchVector, SearchQuery
articles = Article.objects.annotate(
search=SearchVector('title', 'content', config='german')
).filter(
search=SearchQuery('django python', config='german')
)
# Mit Ranking
from django.contrib.postgres.search import SearchRank
query = SearchQuery('machine learning', config='german')
articles = Article.objects.annotate(
rank=SearchRank(F('search_vector'), query)
).filter(
search_vector=query
).order_by('-rank')
# Headline (Treffer hervorheben)
from django.contrib.postgres.search import SearchHeadline
articles = Article.objects.annotate(
headline=SearchHeadline(
'content',
query,
start_sel='',
stop_sel='',
config='german'
)
)
# Erweiterte Abfragen
# UND
query = SearchQuery('django') & SearchQuery('api')
# ODER
query = SearchQuery('django') | SearchQuery('flask')
# NICHT
query = SearchQuery('python') & ~SearchQuery('java')
# Phrasensuche
query = SearchQuery('machine learning', search_type='phrase')
Trigram-Suche (Fuzzy)
# Erweiterung installieren
# CREATE EXTENSION pg_trgm;
from django.contrib.postgres.search import TrigramSimilarity, TrigramDistance
# Ähnliche Artikel finden (typo-tolerant)
articles = Article.objects.annotate(
similarity=TrigramSimilarity('title', 'djnago') # absichtlicher Typo
).filter(
similarity__gt=0.3
).order_by('-similarity')
# Trigram-Distanz
articles = Article.objects.annotate(
distance=TrigramDistance('title', 'machine lerning')
).filter(
distance__lt=0.7
).order_by('distance')
# Kombination von Volltext und Trigram
from django.db.models import Q, Value
from django.db.models.functions import Greatest
search_term = 'machin lernin' # Mit Fehlern
articles = Article.objects.annotate(
similarity=Greatest(
TrigramSimilarity('title', search_term),
TrigramSimilarity('content', search_term)
),
rank=SearchRank(
F('search_vector'),
SearchQuery(search_term, config='german')
)
).filter(
Q(similarity__gt=0.2) | Q(rank__gt=0.1)
).order_by('-similarity', '-rank')
3. Optimierte Indizes
class Article(models.Model):
# ... fields ...
class Meta:
indexes = [
# GIN für Volltext
GinIndex(
fields=['search_vector'],
name='article_search_gin'
),
# GIN für JSONB
GinIndex(
fields=['metadata'],
name='article_metadata_gin',
opclasses=['jsonb_path_ops']
),
# Partieller Index
models.Index(
fields=['published_at'],
name='article_published_idx',
condition=models.Q(status='published')
),
# Trigram-Index
GinIndex(
fields=['title'],
name='article_title_trgm',
opclasses=['gin_trgm_ops']
),
]
4. Management-Command für Search-Rebuild
# management/commands/rebuild_search.py
from django.core.management.base import BaseCommand
from django.contrib.postgres.search import SearchVector
from blog.models import Article
class Command(BaseCommand):
help = 'Search-Vektoren für alle Artikel neu erstellen'
def handle(self, *args, **options):
self.stdout.write('Erstelle Search-Vektoren neu...')
Article.objects.update(
search_vector=(
SearchVector('title', weight='A', config='german') +
SearchVector('excerpt', weight='B', config='german') +
SearchVector('content', weight='C', config='german')
)
)
count = Article.objects.count()
self.stdout.write(
self.style.SUCCESS(f'{count} Artikel aktualisiert')
)
Best Practices
- Verwenden Sie JSONB statt JSON für effiziente Abfragen
- Erstellen Sie GIN-Indizes für häufig abgefragte JSONB-Felder
- Verwenden Sie SearchVectorField für häufige Suchen
- Kombinieren Sie Volltext und Trigram für Fuzzy-Suchen
- Aktualisieren Sie Search-Vektoren mit Triggern oder Signals
Fazit
PostgreSQL mit JSONB und Volltextsuche macht Django zu einer leistungsstarken Plattform für Anwendungen mit erweiterten Such- und Datenflexibilitätsanforderungen.