Created
December 21, 2022 19:18
-
-
Save chaddupuis/12acb3ad934d15667f7fd7239c6768b7 to your computer and use it in GitHub Desktop.
Django Full Text Search using Postgres SearchQuery with Faceted and Paginated Results
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django.db import models | |
from django.utils.text import slugify, Truncator | |
from django.db.models.functions import Lower | |
from django.db.models.signals import pre_save, post_save, post_delete, m2m_changed | |
from django.contrib.postgres.search import SearchVectorField, SearchVector | |
from django.contrib.postgres.indexes import GinIndex | |
from django.db.models import Value | |
import re | |
from natsort import natsorted | |
from datetime import date, datetime | |
from django.core.mail import send_mail | |
from django.utils.html import strip_tags | |
from your.models import models | |
# This is a partial snippet but the primary working parts are shown here. | |
# The example model uses SearchQuery with an GinIndex to the model and | |
# at every save will update the search field. | |
### Example Model to use with Search | |
class BlogPage(models.Model): | |
title = models.CharField(max_length=250) | |
slug = models.SlugField(max_length=250, null=True, blank=True, unique=True) | |
author = models.ForeignKey(User,on_delete=models.CASCADE,related_name='blog_posts') | |
body = RichTextBleachUploadingField(blank=True, null=True, config_name='memberfull') | |
publish = models.DateTimeField(default=timezone.now) | |
created = models.DateTimeField(auto_now_add=True) | |
updated = models.DateTimeField(auto_now=True) | |
search_vector = SearchVectorField(null=True) | |
class Meta: | |
ordering = ('-publish',) | |
verbose_name_plural = "blog posts" | |
indexes = [GinIndex(fields=['search_vector'])] | |
def __str__(self): | |
return self.title | |
## | |
#### Updates Search Vector at Every Save | |
## | |
def save(self, *args, **kwargs): | |
'''Create or update search vector at every save.''' | |
if not self.body == '': | |
self.search_vector = ( | |
SearchVector(Value(self.title)) | |
+ SearchVector(Value(self.body)) | |
) | |
else: | |
self.search_vector = ( | |
SearchVector(Value(self.title)) | |
) | |
if not self.slug: | |
'''Newly created so set slug''' | |
ftime = dateformat.format(self.publish, 'Y-m-d') | |
btitle = self.title+'-'+ftime | |
self.slug = slugify(btitle) | |
if BlogPage.objects.filter(slug=self.slug): | |
'''Check for duplicates.''' | |
curslug = slugify(self.title) | |
bcount = BlogPage.objects.filter(slug__startswith=curslug).count() | |
nonduptitle = self.title+'-'+str(bcount+1)+'-'+ftime | |
self.slug = slugify(nonduptitle) | |
super(BlogPage, self).save(*args, **kwargs) | |
self.__hx_body = self.body | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django.shortcuts import get_object_or_404, render, redirect | |
from django.http import Http404, HttpResponse, HttpResponseRedirect | |
from django.contrib import messages | |
from django.core.paginator import Paginator | |
from django.conf import settings | |
from django.core.mail import send_mail, BadHeaderError | |
from django.contrib.auth.models import Group | |
from django.utils.html import mark_safe | |
from django.utils.text import Truncator | |
from django.utils.html import conditional_escape | |
from django.views import View | |
import re,string | |
from urllib.parse import urlparse | |
from django.contrib.postgres.search import SearchQuery | |
from your.models import yourmodels | |
''' | |
path('search/search-results/blog-posts/', search_views.main_search_results_view, name='main_search_results_blog_posts'), | |
path('search/search-results/forum-posts/', search_views.main_search_results_view, name='main_search_results_forum_posts'), | |
''' | |
def main_search_results_view(request): | |
if not 'qs' in request.GET: | |
messages.success(request, 'Please enter a search phrase to use our search tool.') | |
search_query = request.GET.get('qs') | |
'''Remove everything except spaces and alphanumeric''' | |
search_query = re.sub(r'[^a-zA-Z0-9 ]', '', search_query) | |
'''Remove multiple spaces, etc. which was erroring in the case of word double space otherword.''' | |
search_query = " ".join(search_query.split()) | |
or_query = search_query.replace(" ","|") | |
my_or_query = SearchQuery(or_query, search_type='raw') | |
my_phrase_query = SearchQuery(search_query, search_type='phrase') | |
pagination_int = 10 | |
initial_results_int = 2 | |
'''For all views just get the counts first.''' | |
try: | |
'''ForumPost result counts''' | |
forum_posts_or = False | |
forum_posts_count = ForumPost.objects.filter(search_vector=my_phrase_query).count() | |
if forum_posts_count == 0: | |
'''Use the OR query''' | |
forum_posts_or = True | |
forum_posts_count = ForumPost.objects.filter(search_vector=my_or_query).count() | |
except: | |
forum_posts_count = 0 | |
try: | |
'''BlogPost result counts''' | |
blog_posts_or = False | |
blog_posts_count = BlogPage.objects.filter(search_vector=my_phrase_query).count() | |
if blog_posts_count == 0: | |
'''Use the OR query''' | |
blog_posts_or = True | |
blog_posts_count = BlogPage.objects.filter(search_vector=my_or_query).count() | |
except: | |
blog_posts_count = 0 | |
'''Now either get partial results for initial page display or check path for full results.''' | |
is_generic = False | |
generic_path = f'/search/search-results/' | |
if request.path == generic_path: | |
is_generic = True | |
forum_posts_path = f'/search/search-results/forum-posts/' | |
blog_posts_path = f'/search/search-results/blog-posts/' | |
'''Generic variables.''' | |
total_count = forum_posts_count + blog_posts_count | |
hero = "Search" | |
sub = f'for "{search_query}"' | |
title1 = f'{hero} {sub}' | |
if request.path == generic_path: | |
'''Give partial results and full counts in buttons.''' | |
if forum_posts_or == True: | |
'''Use the or search.''' | |
forum_posts = ForumPost.objects.filter(search_vector=my_or_query).order_by('-created')[:initial_results_int] | |
else: | |
'''Use the plain phrase search.''' | |
forum_posts = ForumPost.objects.filter(search_vector=my_phrase_query).order_by('-created')[:initial_results_int] | |
if blog_posts_or == True: | |
'''Use the or search.''' | |
blog_posts = BlogPage.objects.filter(search_vector=my_or_query).order_by('-created')[:initial_results_int] | |
else: | |
'''Use the plain phrase search.''' | |
blog_posts = BlogPage.objects.filter(search_vector=my_phrase_query).order_by('-created')[:initial_results_int] | |
my_template = 'search-results-initial.html' | |
return render(request, my_template, { | |
'search_query':search_query, | |
'is_generic':is_generic, | |
'forum_posts':forum_posts, | |
'forum_posts_count':forum_posts_count, | |
'blog_posts':blog_posts, | |
'blog_posts_count':blog_posts_count, | |
'hero':hero, | |
'sub':sub}) | |
elif request.path == forum_posts_path: | |
if forum_posts_or == True: | |
'''Use the or search.''' | |
forum_posts = ForumPost.objects.filter(search_vector=my_or_query).order_by('-created') | |
else: | |
'''Use the plain phrase search.''' | |
forum_posts = ForumPost.objects.filter(search_vector=my_phrase_query).order_by('-created') | |
post_paginator = Paginator(forum_posts, pagination_int) | |
page_number = request.GET.get('page') | |
post_obj = post_paginator.get_page(page_number) | |
my_template = 'search-results-forum-posts.html' | |
return render(request, my_template, { | |
'search_query':search_query, | |
'is_generic':is_generic, | |
'post_obj':post_obj, | |
'forum_posts_count':forum_posts_count, | |
'blog_posts_count':blog_posts_count, | |
'hero':hero, | |
'sub':sub}) | |
elif request.path == blog_posts_path: | |
if blog_posts_or == True: | |
'''Use the or search.''' | |
blog_posts = BlogPage.objects.filter(search_vector=my_or_query).order_by('-created') | |
else: | |
'''Use the plain phrase search.''' | |
blog_posts = BlogPage.objects.filter(search_vector=my_phrase_query).order_by('-created') | |
post_paginator = Paginator(blog_posts, pagination_int) | |
page_number = request.GET.get('page') | |
post_obj = post_paginator.get_page(page_number) | |
my_template = 'search-results-blog-posts.html' | |
return render(request, my_template, { | |
'search_query':search_query, | |
'is_generic':is_generic, | |
'post_obj':post_obj, | |
'forum_posts_count':forum_posts_count, | |
'blog_posts_count':blog_posts_count, | |
'hero':hero, | |
'sub':sub}) | |
else: | |
'''something went wrong, just redirect to front''' | |
messages.success(request, 'Something went wrong with your search. Please try again.') | |
return redirect('/') | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment