Skip to content

Instantly share code, notes, and snippets.

@chaddupuis
Created December 21, 2022 19:18
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save chaddupuis/12acb3ad934d15667f7fd7239c6768b7 to your computer and use it in GitHub Desktop.
Django Full Text Search using Postgres SearchQuery with Faceted and Paginated Results
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
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