├── README.md ├── basic_search ├── Pipfile ├── Pipfile.lock ├── cities │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── citysearch_project │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── db.sqlite3 ├── manage.py └── templates │ ├── home.html │ └── search_results.html └── full_text_search ├── Dockerfile ├── Pipfile ├── Pipfile.lock ├── cities ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── citysearch_project ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── db.sqlite3 ├── docker-compose.yml ├── manage.py └── templates ├── home.html └── search_results.html /README.md: -------------------------------------------------------------------------------- 1 | # Search: From the Ground Up 2 | 3 | _Talk given at [DjangoCon US 2019](https://2018.djangocon.us/talk/finally-understand-authentication-in/)_ 4 | 5 | [Slides available here](https://tinyurl.com/djangocon2019-search) 6 | 7 | Search is a fundamental part of most websites but not built into Django itself. This talk explains how search works, how to configure it from scratch in a new project, and looks at built-in full text search options available via PostgreSQL. 8 | 9 | --- 10 | ## Basic Search Set Up 11 | Navigate to the `basic_search` directory and make sure [Pipenv](https://docs.pipenv.org/en/latest/) is installed. Then on the command line: 12 | 13 | ``` 14 | $ pipenv install 15 | $ pipenv shell 16 | (basic_search) $ python manage.py runserver 17 | ``` 18 | 19 | Navigate to [127.0.0.1:8000](127.0.0.1:8000). 20 | 21 | When done run `exit` to leave the virtual environment. 22 | 23 | ## Full Text Search Set Up 24 | Navigate to the `full_text_search` directory and make sure [Docker desktop](https://www.docker.com/products/docker-desktop) is installed and running. Then on the command line: 25 | 26 | ``` 27 | $ docker-compose up -d 28 | ``` 29 | Navigate to [127.0.0.1:8000](127.0.0.1:8000). When done run `docker-compose down` to stop the running Docker containers. 30 | 31 | --- 32 | Superuser admin for both is `djangocon` and `testpass123`. 33 | -------------------------------------------------------------------------------- /basic_search/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | django = "==2.2.5" 10 | 11 | [requires] 12 | python_version = "3.7" 13 | -------------------------------------------------------------------------------- /basic_search/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "356091b1a109ccf810b9c3672b6f020eb68426927a2dc8d99c02caa6b9a208e2" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "django": { 20 | "hashes": [ 21 | "sha256:148a4a2d1a85b23883b0a4e99ab7718f518a83675e4485e44dc0c1d36988c5fa", 22 | "sha256:deb70aa038e59b58593673b15e9a711d1e5ccd941b5973b30750d5d026abfd56" 23 | ], 24 | "index": "pypi", 25 | "version": "==2.2.5" 26 | }, 27 | "pytz": { 28 | "hashes": [ 29 | "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", 30 | "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" 31 | ], 32 | "version": "==2019.2" 33 | }, 34 | "sqlparse": { 35 | "hashes": [ 36 | "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", 37 | "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" 38 | ], 39 | "version": "==0.3.0" 40 | } 41 | }, 42 | "develop": {} 43 | } 44 | -------------------------------------------------------------------------------- /basic_search/cities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangocon2019-search/cf9af84b5831286cd60acb9ddb250c9062b508c6/basic_search/cities/__init__.py -------------------------------------------------------------------------------- /basic_search/cities/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import City 3 | 4 | class CityAdmin(admin.ModelAdmin): 5 | list_display = ('name', 'state') 6 | 7 | admin.site.register(City, CityAdmin) 8 | -------------------------------------------------------------------------------- /basic_search/cities/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CitiesConfig(AppConfig): 5 | name = 'cities' 6 | -------------------------------------------------------------------------------- /basic_search/cities/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | class SearchForm(forms.Form): 4 | q = forms.CharField(label='Search', max_length=50) 5 | -------------------------------------------------------------------------------- /basic_search/cities/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-19 17:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='City', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=100)), 19 | ('state', models.CharField(max_length=100)), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /basic_search/cities/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangocon2019-search/cf9af84b5831286cd60acb9ddb250c9062b508c6/basic_search/cities/migrations/__init__.py -------------------------------------------------------------------------------- /basic_search/cities/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class City(models.Model): 4 | name = models.CharField(max_length=100) 5 | state = models.CharField(max_length=100) 6 | 7 | class Meta: 8 | verbose_name_plural = 'cities' 9 | -------------------------------------------------------------------------------- /basic_search/cities/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /basic_search/cities/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import SearchResultsView, HomepageView 3 | 4 | urlpatterns = [ 5 | path('search/', SearchResultsView.as_view(), name='search_results'), 6 | path('', HomepageView.as_view(), name='home'), 7 | ] 8 | -------------------------------------------------------------------------------- /basic_search/cities/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q # new 2 | from django.views.generic import TemplateView, ListView, FormView 3 | from .models import City 4 | from .forms import SearchForm 5 | 6 | class HomepageView(FormView): # new 7 | template_name = 'home.html' 8 | form_class = SearchForm # new 9 | 10 | class SearchResultsView(ListView): 11 | model = City 12 | template_name = 'search_results.html' 13 | 14 | # def get_queryset(self): # new 15 | # query = self.request.GET.get('q') 16 | # object_list = City.objects.filter( 17 | # Q(name__icontains=query) | Q(state__icontains=query) 18 | # ) 19 | # return object_list 20 | def get_queryset(self): # new 21 | return City.objects.filter( 22 | Q(name__icontains='Boston') | Q(state__icontains='NY') 23 | ) 24 | -------------------------------------------------------------------------------- /basic_search/citysearch_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangocon2019-search/cf9af84b5831286cd60acb9ddb250c9062b508c6/basic_search/citysearch_project/__init__.py -------------------------------------------------------------------------------- /basic_search/citysearch_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for citysearch_project project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'w_p1iq!yyhqji-ubbfa$hk=)2!ll4g1=t5l4l!afc951zjewmw' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'cities.apps.CitiesConfig', # new 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'citysearch_project.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], # new 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'citysearch_project.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 120 | 121 | STATIC_URL = '/static/' 122 | -------------------------------------------------------------------------------- /basic_search/citysearch_project/urls.py: -------------------------------------------------------------------------------- 1 | """citysearch_project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include # new 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | path('', include('cities.urls')), # new 22 | ] 23 | -------------------------------------------------------------------------------- /basic_search/citysearch_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for citysearch_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'citysearch_project.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /basic_search/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangocon2019-search/cf9af84b5831286cd60acb9ddb250c9062b508c6/basic_search/db.sqlite3 -------------------------------------------------------------------------------- /basic_search/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'citysearch_project.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /basic_search/templates/home.html: -------------------------------------------------------------------------------- 1 |

Homepage

2 | 3 |
4 | {{ form }} 5 |
6 | -------------------------------------------------------------------------------- /basic_search/templates/search_results.html: -------------------------------------------------------------------------------- 1 |

Search Results

2 | 9 | -------------------------------------------------------------------------------- /full_text_search/Dockerfile: -------------------------------------------------------------------------------- 1 | # Pull base image 2 | FROM python:3.7-slim 3 | 4 | # Set environment varibles 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | # Set work directory 9 | WORKDIR /code 10 | 11 | # Install dependencies 12 | COPY Pipfile Pipfile.lock /code/ 13 | RUN pip install pipenv && pipenv install --system 14 | 15 | # Copy project 16 | COPY . /code/ 17 | -------------------------------------------------------------------------------- /full_text_search/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | django = "==2.2.5" 10 | psycopg2-binary = "==2.8.3" 11 | 12 | [requires] 13 | python_version = "3.7" 14 | -------------------------------------------------------------------------------- /full_text_search/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "b4d0536b3ac659b0959d724e0fd0423a52db5e1d1c78e3705322b3243454b36c" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "django": { 20 | "hashes": [ 21 | "sha256:148a4a2d1a85b23883b0a4e99ab7718f518a83675e4485e44dc0c1d36988c5fa", 22 | "sha256:deb70aa038e59b58593673b15e9a711d1e5ccd941b5973b30750d5d026abfd56" 23 | ], 24 | "index": "pypi", 25 | "version": "==2.2.5" 26 | }, 27 | "psycopg2-binary": { 28 | "hashes": [ 29 | "sha256:080c72714784989474f97be9ab0ddf7b2ad2984527e77f2909fcd04d4df53809", 30 | "sha256:110457be80b63ff4915febb06faa7be002b93a76e5ba19bf3f27636a2ef58598", 31 | "sha256:171352a03b22fc099f15103959b52ee77d9a27e028895d7e5fde127aa8e3bac5", 32 | "sha256:19d013e7b0817087517a4b3cab39c084d78898369e5c46258aab7be4f233d6a1", 33 | "sha256:249b6b21ae4eb0f7b8423b330aa80fab5f821b9ffc3f7561a5e2fd6bb142cf5d", 34 | "sha256:2ac0731d2d84b05c7bb39e85b7e123c3a0acd4cda631d8d542802c88deb9e87e", 35 | "sha256:2b6d561193f0dc3f50acfb22dd52ea8c8dfbc64bcafe3938b5f209cc17cb6f00", 36 | "sha256:2bd23e242e954214944481124755cbefe7c2cf563b1a54cd8d196d502f2578bf", 37 | "sha256:3e1239242ca60b3725e65ab2f13765fc199b03af9eaf1b5572f0e97bdcee5b43", 38 | "sha256:3eb70bb697abbe86b1d2b1316370c02ba320bfd1e9e35cf3b9566a855ea8e4e5", 39 | "sha256:51a2fc7e94b98bd1bb5d4570936f24fc2b0541b63eccadf8fdea266db8ad2f70", 40 | "sha256:52f1bdafdc764b7447e393ed39bb263eccb12bfda25a4ac06d82e3a9056251f6", 41 | "sha256:5b3581319a3951f1e866f4f6c5e42023db0fae0284273b82e97dfd32c51985cd", 42 | "sha256:63c1b66e3b2a3a336288e4bcec499e0dc310cd1dceaed1c46fa7419764c68877", 43 | "sha256:8123a99f24ecee469e5c1339427bcdb2a33920a18bb5c0d58b7c13f3b0298ba3", 44 | "sha256:85e699fcabe7f817c0f0a412d4e7c6627e00c412b418da7666ff353f38e30f67", 45 | "sha256:8dbff4557bbef963697583366400822387cccf794ccb001f1f2307ed21854c68", 46 | "sha256:908d21d08d6b81f1b7e056bbf40b2f77f8c499ab29e64ec5113052819ef1c89b", 47 | "sha256:af39d0237b17d0a5a5f638e9dffb34013ce2b1d41441fd30283e42b22d16858a", 48 | "sha256:af51bb9f055a3f4af0187149a8f60c9d516cf7d5565b3dac53358796a8fb2a5b", 49 | "sha256:b2ecac57eb49e461e86c092761e6b8e1fd9654dbaaddf71a076dcc869f7014e2", 50 | "sha256:cd37cc170678a4609becb26b53a2bc1edea65177be70c48dd7b39a1149cabd6e", 51 | "sha256:d17e3054b17e1a6cb8c1140f76310f6ede811e75b7a9d461922d2c72973f583e", 52 | "sha256:d305313c5a9695f40c46294d4315ed3a07c7d2b55e48a9010dad7db7a66c8b7f", 53 | "sha256:dd0ef0eb1f7dd18a3f4187226e226a7284bda6af5671937a221766e6ef1ee88f", 54 | "sha256:e1adff53b56db9905db48a972fb89370ad5736e0450b96f91bcf99cadd96cfd7", 55 | "sha256:f0d43828003c82dbc9269de87aa449e9896077a71954fbbb10a614c017e65737", 56 | "sha256:f78e8b487de4d92640105c1389e5b90be3496b1d75c90a666edd8737cc2dbab7" 57 | ], 58 | "index": "pypi", 59 | "version": "==2.8.3" 60 | }, 61 | "pytz": { 62 | "hashes": [ 63 | "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", 64 | "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" 65 | ], 66 | "version": "==2019.2" 67 | }, 68 | "sqlparse": { 69 | "hashes": [ 70 | "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", 71 | "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" 72 | ], 73 | "version": "==0.3.0" 74 | } 75 | }, 76 | "develop": {} 77 | } 78 | -------------------------------------------------------------------------------- /full_text_search/cities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangocon2019-search/cf9af84b5831286cd60acb9ddb250c9062b508c6/full_text_search/cities/__init__.py -------------------------------------------------------------------------------- /full_text_search/cities/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import City 3 | 4 | class CityAdmin(admin.ModelAdmin): 5 | list_display = ('name', 'state') 6 | 7 | admin.site.register(City, CityAdmin) 8 | -------------------------------------------------------------------------------- /full_text_search/cities/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CitiesConfig(AppConfig): 5 | name = 'cities' 6 | -------------------------------------------------------------------------------- /full_text_search/cities/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | class SearchForm(forms.Form): 4 | q = forms.CharField(label='Search', max_length=50) 5 | -------------------------------------------------------------------------------- /full_text_search/cities/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-21 17:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='City', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=100)), 19 | ('state', models.CharField(max_length=100)), 20 | ], 21 | options={ 22 | 'verbose_name_plural': 'cities', 23 | }, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /full_text_search/cities/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangocon2019-search/cf9af84b5831286cd60acb9ddb250c9062b508c6/full_text_search/cities/migrations/__init__.py -------------------------------------------------------------------------------- /full_text_search/cities/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class City(models.Model): 4 | name = models.CharField(max_length=100) 5 | state = models.CharField(max_length=100) 6 | 7 | class Meta: 8 | verbose_name_plural = 'cities' 9 | -------------------------------------------------------------------------------- /full_text_search/cities/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /full_text_search/cities/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import SearchResultsView, HomepageView 3 | 4 | urlpatterns = [ 5 | path('search/', SearchResultsView.as_view(), name='search_results'), 6 | path('', HomepageView.as_view(), name='home'), 7 | ] 8 | -------------------------------------------------------------------------------- /full_text_search/cities/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.postgres.search import ( 2 | SearchVector, SearchQuery, SearchRank 3 | ) 4 | from django.views.generic import FormView, ListView 5 | from .forms import SearchForm 6 | from .models import City 7 | 8 | class HomepageView(FormView): 9 | template_name = 'home.html' 10 | form_class = SearchForm 11 | 12 | class SearchResultsView(ListView): 13 | model = City 14 | template_name = 'search_results.html' 15 | 16 | ## SearchRank ## 17 | def get_queryset(self): 18 | query = self.request.GET.get('q') 19 | vector = SearchVector('state') 20 | search_query = SearchQuery(query) 21 | object_list = City.objects.annotate( 22 | rank=SearchRank(vector, search_query) 23 | ).order_by('-rank') 24 | return object_list 25 | 26 | ## SearchQuery ## 27 | # def get_queryset(self): 28 | # query = self.request.GET.get('q') 29 | # object_list = City.objects.annotate( 30 | # search=SearchVector('name', 'state'), 31 | # ).filter(search=SearchQuery(query)) 32 | # return object_list 33 | 34 | ## SearchVector ## 35 | # def get_queryset(self): 36 | # query = self.request.GET.get('q') 37 | # object_list = City.objects.annotate( 38 | # search=SearchVector('name', 'state'), 39 | # ).filter(search=query) 40 | # return object_list 41 | -------------------------------------------------------------------------------- /full_text_search/citysearch_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangocon2019-search/cf9af84b5831286cd60acb9ddb250c9062b508c6/full_text_search/citysearch_project/__init__.py -------------------------------------------------------------------------------- /full_text_search/citysearch_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for citysearch_project project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'y^*@m9(+prs8=!csba&qzbz*@3r%mzqc%8gzq43#3f-bqnx%@9' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'cities.apps.CitiesConfig', # new 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'citysearch_project.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], # new 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'citysearch_project.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.postgresql', 80 | 'NAME': 'postgres', 81 | 'USER': 'postgres', 82 | 'PASSWORD': 'postgres', 83 | 'HOST': 'db', 84 | 'PORT': 5432 85 | } 86 | } 87 | 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 104 | }, 105 | ] 106 | 107 | 108 | # Internationalization 109 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 110 | 111 | LANGUAGE_CODE = 'en-us' 112 | 113 | TIME_ZONE = 'UTC' 114 | 115 | USE_I18N = True 116 | 117 | USE_L10N = True 118 | 119 | USE_TZ = True 120 | 121 | 122 | # Static files (CSS, JavaScript, Images) 123 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 124 | 125 | STATIC_URL = '/static/' 126 | -------------------------------------------------------------------------------- /full_text_search/citysearch_project/urls.py: -------------------------------------------------------------------------------- 1 | """citysearch_project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | path('', include('cities.urls')), 22 | ] 23 | -------------------------------------------------------------------------------- /full_text_search/citysearch_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for citysearch_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'citysearch_project.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /full_text_search/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangocon2019-search/cf9af84b5831286cd60acb9ddb250c9062b508c6/full_text_search/db.sqlite3 -------------------------------------------------------------------------------- /full_text_search/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | web: 5 | build: . 6 | command: python /code/manage.py runserver 0.0.0.0:8000 7 | volumes: 8 | - .:/code 9 | ports: 10 | - 8000:8000 11 | depends_on: 12 | - db 13 | db: 14 | image: postgres:11 15 | volumes: 16 | - postgres_data:/var/lib/postgresql/data/ 17 | 18 | volumes: 19 | postgres_data: 20 | -------------------------------------------------------------------------------- /full_text_search/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'citysearch_project.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /full_text_search/templates/home.html: -------------------------------------------------------------------------------- 1 |

Homepage

2 | 3 |
4 | {{ form }} 5 |
6 | -------------------------------------------------------------------------------- /full_text_search/templates/search_results.html: -------------------------------------------------------------------------------- 1 |

Search Results

2 | 9 | --------------------------------------------------------------------------------