Skip to content

Commit c9de696

Browse files
committed
Add part 2
1 parent 8adb94e commit c9de696

File tree

580 files changed

+111329
-81
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

580 files changed

+111329
-81
lines changed

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -1 +1,15 @@
11
# django-postgres-elasticsearch
2+
3+
If you're starting with **Part 2** or later, don't forget to run the following commands first:
4+
5+
```bash
6+
$ docker-compose exec server python manage.py createsuperuser
7+
```
8+
9+
```bash
10+
$ docker-compose exec server \
11+
python manage.py loaddata \
12+
catalog/fixtures/wines.json \
13+
--app catalog \
14+
--format json
15+
```

part2/server/catalog/admin.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
from django.contrib import admin
22

3-
from .models import Wine
3+
from .models import Wine, WineSearchWord
44

55

66
@admin.register(Wine)
77
class WineAdmin(admin.ModelAdmin):
8-
fields = ('id', 'country', 'description', 'points', 'price', 'variety', 'winery',)
8+
fields = (
9+
'id', 'country', 'description', 'points', 'price', 'variety', 'winery',
10+
'search_vector',
11+
)
912
list_display = ('id', 'country', 'points', 'price', 'variety', 'winery',)
1013
list_filter = ('country', 'variety', 'winery',)
1114
ordering = ('variety',)
1215
readonly_fields = ('id',)
16+
17+
18+
@admin.register(WineSearchWord)
19+
class WineSearchWordAdmin(admin.ModelAdmin):
20+
fields = ('word',)
21+
list_display = ('word',)
22+
ordering = ('word',)

part2/server/catalog/apps.py

+3
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@
33

44
class CatalogConfig(AppConfig):
55
name = 'catalog'
6+
7+
def ready(self):
8+
from . import signals

part2/server/catalog/filters.py

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1-
from django.db.models import Q
2-
31
from django_filters.rest_framework import CharFilter, FilterSet
42

5-
from .models import Wine
3+
from .models import Wine, WineSearchWord
64

75

86
class WineFilterSet(FilterSet):
97
query = CharFilter(method='filter_query')
108

119
def filter_query(self, queryset, name, value):
12-
search_query = Q(
13-
Q(variety__contains=value) |
14-
Q(winery__contains=value) |
15-
Q(description__contains=value)
16-
)
17-
return queryset.filter(search_query)
10+
return queryset.search(value)
1811

1912
class Meta:
2013
model = Wine
2114
fields = ('query', 'country', 'points',)
15+
16+
17+
class WineSearchWordFilterSet(FilterSet):
18+
query = CharFilter(method='filter_query')
19+
20+
def filter_query(self, queryset, name, value):
21+
return queryset.search(value)
22+
23+
class Meta:
24+
model = WineSearchWord
25+
fields = ('query',)

part2/server/catalog/fixtures/test_wines.json

+12
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@
2323
"winery": "Barnard Griffin"
2424
}
2525
},
26+
{
27+
"model": "catalog.Wine",
28+
"fields": {
29+
"country": "France",
30+
"description": "A creamy wine with full Chardonnay flavors.",
31+
"id": "000bbdff-30fc-4897-81c1-7947e11e6d1a",
32+
"points": 89,
33+
"price": 33.00,
34+
"variety": "Champagne Blend",
35+
"winery": "Phillipe Gonet"
36+
}
37+
},
2638
{
2739
"model": "catalog.Wine",
2840
"fields": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 3.1.7 on 2021-03-23 01:48
2+
3+
import django.contrib.postgres.indexes
4+
import django.contrib.postgres.search
5+
from django.db import migrations
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('catalog', '0001_initial'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='wine',
17+
name='search_vector',
18+
field=django.contrib.postgres.search.SearchVectorField(blank=True, null=True),
19+
),
20+
migrations.AddIndex(
21+
model_name='wine',
22+
index=django.contrib.postgres.indexes.GinIndex(fields=['search_vector'], name='search_vector_index'),
23+
),
24+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.contrib.postgres.search import SearchVector
2+
from django.db import migrations
3+
4+
5+
def update_search_vector(apps, schema_editor):
6+
Wine = apps.get_model('catalog', 'Wine')
7+
Wine.objects.all().update(search_vector=(
8+
SearchVector('variety', weight='A') +
9+
SearchVector('winery', weight='A') +
10+
SearchVector('description', weight='B')
11+
))
12+
13+
14+
class Migration(migrations.Migration):
15+
16+
dependencies = [
17+
('catalog', '0002_search_vector'),
18+
]
19+
20+
operations = [
21+
migrations.RunPython(update_search_vector, elidable=True),
22+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 3.1.7 on 2021-03-23 02:03
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('catalog', '0003_update_search_vector'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='WineSearchWord',
15+
fields=[
16+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17+
('word', models.CharField(max_length=255, unique=True)),
18+
],
19+
),
20+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from django.contrib.postgres.operations import TrigramExtension
2+
from django.db import connection, migrations
3+
4+
5+
def update_wine_search_word(apps, schema_editor):
6+
sql = """
7+
INSERT INTO catalog_winesearchword (word)
8+
SELECT word FROM ts_stat('
9+
SELECT to_tsvector(''simple'', winery) ||
10+
to_tsvector(''simple'', coalesce(description, ''''))
11+
FROM catalog_wine
12+
');
13+
"""
14+
with connection.cursor() as cursor:
15+
cursor.execute(sql)
16+
17+
18+
class Migration(migrations.Migration):
19+
20+
dependencies = [
21+
('catalog', '0004_winesearchword'),
22+
]
23+
24+
operations = [
25+
TrigramExtension(),
26+
migrations.RunSQL(sql="""
27+
CREATE INDEX IF NOT EXISTS wine_search_word_trigram_index
28+
ON catalog_winesearchword
29+
USING gin (word gin_trgm_ops);
30+
""", elidable=True),
31+
migrations.RunPython(update_wine_search_word, elidable=True),
32+
]

part2/server/catalog/models.py

+50-6
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,64 @@
11
import uuid
22

3+
from django.contrib.postgres.indexes import GinIndex
4+
from django.contrib.postgres.search import (
5+
SearchQuery, SearchRank, SearchVectorField, TrigramSimilarity
6+
)
37
from django.db import models
8+
from django.db.models import F, Q
9+
10+
11+
class WineQuerySet(models.query.QuerySet):
12+
def search(self, query):
13+
search_query = Q(
14+
Q(search_vector=SearchQuery(query))
15+
)
16+
return self.annotate(
17+
variety_headline=SearchHeadline(F('variety'), SearchQuery(query)),
18+
winery_headline=SearchHeadline(F('winery'), SearchQuery(query)),
19+
description_headline=SearchHeadline(F('description'), SearchQuery(query)),
20+
search_rank=SearchRank(F('search_vector'), SearchQuery(query))
21+
).filter(search_query).order_by('-search_rank', 'id')
422

523

624
class Wine(models.Model):
7-
id = models.UUIDField(
8-
primary_key=True, default=uuid.uuid4, editable=False
9-
)
25+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
1026
country = models.CharField(max_length=255)
1127
description = models.TextField(null=True, blank=True)
1228
points = models.IntegerField()
13-
price = models.DecimalField(
14-
decimal_places=2, max_digits=10, null=True, blank=True
15-
)
29+
price = models.DecimalField(decimal_places=2, max_digits=10, null=True, blank=True)
1630
variety = models.CharField(max_length=255)
1731
winery = models.CharField(max_length=255)
32+
search_vector = SearchVectorField(null=True, blank=True)
33+
34+
objects = WineQuerySet.as_manager()
35+
36+
class Meta:
37+
indexes = [
38+
GinIndex(fields=['search_vector'], name='search_vector_index')
39+
]
1840

1941
def __str__(self):
2042
return f'{self.id}'
43+
44+
45+
class SearchHeadline(models.Func):
46+
function = 'ts_headline'
47+
output_field = models.TextField()
48+
template = '%(function)s(%(expressions)s, \'StartSel = <mark>, StopSel = </mark>, HighlightAll=TRUE\')'
49+
50+
51+
class WineSearchWordQuerySet(models.query.QuerySet):
52+
def search(self, query):
53+
return self.annotate(
54+
similarity=TrigramSimilarity('word', query)
55+
).filter(similarity__gte=0.3).order_by('-similarity')
56+
57+
58+
class WineSearchWord(models.Model):
59+
word = models.CharField(max_length=255, unique=True)
60+
61+
objects = WineSearchWordQuerySet.as_manager()
62+
63+
def __str__(self):
64+
return self.word

part2/server/catalog/serializers.py

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
11
from rest_framework import serializers
22

3-
from .models import Wine
3+
from .models import Wine, WineSearchWord
44

55

66
class WineSerializer(serializers.ModelSerializer):
7+
variety = serializers.SerializerMethodField()
8+
winery = serializers.SerializerMethodField()
9+
description = serializers.SerializerMethodField()
10+
11+
def get_variety(self, obj):
12+
if hasattr(obj, 'variety_headline'):
13+
return getattr(obj, 'variety_headline')
14+
return getattr(obj, 'variety')
15+
16+
def get_winery(self, obj):
17+
if hasattr(obj, 'winery_headline'):
18+
return getattr(obj, 'winery_headline')
19+
return getattr(obj, 'winery')
20+
21+
def get_description(self, obj):
22+
if hasattr(obj, 'description_headline'):
23+
return getattr(obj, 'description_headline')
24+
return getattr(obj, 'description')
25+
726
class Meta:
827
model = Wine
928
fields = ('id', 'country', 'description', 'points', 'price', 'variety', 'winery',)
29+
30+
31+
class WineSearchWordSerializer(serializers.ModelSerializer):
32+
class Meta:
33+
model = WineSearchWord
34+
fields = ('word',)

part2/server/catalog/signals.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from django.contrib.postgres.search import SearchVector
2+
from django.db import connection
3+
from django.db.models.signals import post_save
4+
from django.dispatch import receiver
5+
6+
from .models import Wine
7+
8+
9+
@receiver(post_save, sender=Wine, dispatch_uid='on_wine_save')
10+
def on_wine_save(sender, instance, *args, **kwargs):
11+
sender.objects.filter(pk=instance.id).update(search_vector=(
12+
SearchVector('variety', weight='A') +
13+
SearchVector('winery', weight='A') +
14+
SearchVector('description', weight='B')
15+
))
16+
17+
with connection.cursor() as cursor:
18+
cursor.execute("""
19+
INSERT INTO catalog_winesearchword (word)
20+
SELECT word FROM ts_stat('
21+
SELECT to_tsvector(''simple'', winery) ||
22+
to_tsvector(''simple'', coalesce(description, ''''))
23+
FROM catalog_wine
24+
WHERE id = '%s'
25+
')
26+
ON CONFLICT (word) DO NOTHING;
27+
""", [str(instance.id),])

0 commit comments

Comments
 (0)