ทำระบบ Search ให้กับ Django เว็บไซต์

   By: Withoutcoffee Icantbedev

   อัปเดตล่าสุด Feb. 12, 2023

ทำระบบ Search ให้กับ Django เว็บไซต์

Search เรียกได้ว่าเป็นฟีเจอร์ที่ต้องมีในแทบจะทุก ๆ เว็บไซต์หรือแอป ในบทความ Django Search นี้ แน่นอนว่าเราจะมาประยุกต์ใช้งาน Search เข้ากับ Django project ของเรากันครับ โดยจะใช้ Bootstrap form ในส่วน navbar สำหรับ UI ในครั้งนี้ 

Prerequisite

  • พื้นฐานความรู้ Django ในเบื้องต้นและต้องเคยทำ Django โปรเจคท์มาบ้างแล้ว

ตัวอย่าง Search  บน Web

หน้า search ที่เราคุ้นเคยกันดีอย่างเช่น Google Search ก็ถือว่าเป็น search ที่ทุก ๆ คนแทบจะคุ้นเคยกันดีที่สุด เช่นเราใช้มันเข้า Stackoverflow ในทุก ๆ วัน นี่ก็คือ search แต่ทาง Google ก็ค่อนข้างที่จะมีอัลกอริทึมที่ค่อนข้างซับซ้อน ซึ่งเราจะไม่ขอพูดถึงในบริบทนี้ จะเป็นการยกตัวอย่างการ search เท่านั้น


Google คือ web ที่ใช้สำหรับ search ที่เราคุ้นเคยกันดี


หน้า search ของ Django official website (Cr Photo: stackpython.medium.com)


จากที่ยกตัวอย่างมาในเบื้องต้นด้านบน จะเห็นได้ว่าเว็บส่วนใหญ่ก็มีฟีเจอร์สำหรับค้นหากันแทบทุกเว็บ จะง่ายหรือซับซ้อนก็ขึ้นอยู่กับ business domain ของเว็บไซต์นั้น ๆ 

Django Search workflow

  • User ทำการ search และกด submit button หรือ enter เพื่อค้นหาข้อมูลผ่านช่อง search form ในหน้าเว็บ
  • Django server ทำการรับ request และเช็คว่ามีข้อความอะไรที่ user ได้ search เข้ามาผ่านฟังก์ชันใน views
  • views ทำการเขียนรับค่าและประมวลผล และทำการเช็คข้อมูลในตารางในฐานข้อมูลว่ามีชื่อบทความ (title) นี้อยู่หรือไม่ โดยในบทความนี้จะทำการเสิร์ชโดยฟิลเตอร์ผ่าน title
  • ทำการการส่งข้อมูล (search result) ที่ได้ออกมาที่หน้าเว็บเพื่อแสดงผล 


Django Project directories and files paths

...
blog/
    templates/
        blog/
            home.html
            search.html  # This
            ...
    __init__.py        
    admin.py
    models.py
    views.py
    urls.py
...


blog/models.py

# blog/models.py
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=100)
    body = models.TextField()
    
    def __str__(self):
        return self.title



จากนั้นก็ทำการ makemigrations และ migrate ตามปกติ โดยแน่นอนว่าบทความนี้เราจะไม่ได้สอนทำโปรเจคท์ตั้งแต่เริ่มต้นเพราะว่าทุกคนต้องมีพื้นฐานตรงนั้นมาก่อนนั่นเอง แต่ถ้าอยากทบทวนหรือเอาไว้ใช้อ้างอิง ก็สามารถเริ่มต้นสร้างโปรเจคท์ได้ที่บทความด้านล่างนี้ครับ


ในหน้า Django Admin และสมมติว่าตอนนี้ในตาราง   Post  ของเรามีข้อมูลดังนี้



โพสต์ในหน้า Django Admin

ทดสอบ Search filter และ Q objects ผ่าน Django Shell

เข้าหน้า Shell เพื่อทดสอบ search

$ python manage.py shell


จะเข้าสู่หน้า Django Shell และทำการอิมพอร์ตสองส่วนคือตาราง  Post  ใน  models.py  ของเราและก็ตัว  Q   objects ซึ่งเป็น complex lookup ที่จะเข้ามาช่วยให้การ implement การ search ง่ายและสะดวกมากยิ่งขึ้น เข้ามาใช้งานใน Shell อ่านเพิ่มเติมสำหรับ Q objects

(InteractiveConsole)
>>> from blog.models import Post
>>> from django.db.models import Q
>>> post = Post.objects.filter(Q(title__icontains="django"))
>>> post
<QuerySet [<Post: Django 1>, <Post: Django 2>]>
>>> post2 = Post.objects.filter(Q(title__icontains="numpy"))
>>> post2
<QuerySet []>
>>> post3 = Post.objects.filter(Q(title__contains="django"))
>>> post3
<QuerySet []>
>>> post4 = Post.objects.filter(Q(title__icontains="python"))
>>> post4
<QuerySet [<Post: Basic Python>]>
>>> exit()

เมื่อทดสอบเรียบร้อยและสามารถแสดงได้ใน Shell แล้ว ก็แสดงว่าเราสามารถดึงข้อมูลโดยใช้ Q objects ในการช่วย Search filter ได้ โดยเราเปลี่ยนจากการ hardcode เข้าไปเช่นพิมพ์ django, python, numpy หรือคำอะไรก็ตามที่เราต้องการค้นหา ซึ่งวางไว้ด้านหลัง icontains โดยเปลี่ยนเป็นการเก็บเป็นตัวแปรและส่งเข้ามาแทนการ hardcode ซึ่งตัวแปรเราก็จะไปเขียนเพื่อรอรับค่าที่ส่งเข้ามาจากฝั่ง client ผ่าน GET เมธอด ซึ่งจะพูดถึงในส่วนถัดไป

Note

  • icontains --> Case - Insensitive: ตัวพิมพ์เล็กพิมพ์ใหญ่ไม่มีผล ค้นหาเจอเหมือนกันหมด
  • contains --> Case Sensitive: ตัวพิมพ์เล็กพิมพ์ใหญ่มีผล ต้องพิมพ์ให้ตรงตามตัวเท่านั้น


ในโปรเจคท์นี้จะมีการสร้างเพียงแค่ 2 ฟังก์ชัน คือ  home  และ  search  จากนั้นทำการ render หน้า HTML ออกไปแสดงผลตามปกติ

blog/views.py

# blog/views.py
from django.shortcuts import render
from .models import Post

def home(request):
    return render(request, 'blog/home.html')

def search(request):
    return render(request, 'blog/search.html')


blog/urls.py

# blog/urls.py
from django.urls import path
from .views import home, search

urlpatterns = [
    path('', home, name="home"),
    path('search', search, name="search")
]


mysite/urls.py

# mysite/urls.py
from django.contrib import admin
from django.urls import path, include 

urlpatterns = [
    path('', include('blog.urls')),  # New
    path('admin/', admin.site.urls),  
]


Bootstrap Navbar and Search form

บทความนี้จะใช้ Search form ของ Bootstrap ที่มีมาให้ใน Navbar เรียบร้อย โดยสิ่งที่ต้องมีคือ Bootstrap CSS CDN  และ Bootstrap Navbar ให้ทำการก็อปปี้และนำมาวางในหน้า  base.html  ซึ่งเป็น parent file ที่เราจะใช้สืบทอดเท็มเพลต (Template Inheritance)  โดยจากตัว default Navbar ที่มากับ Bootstrap ให้เพิ่มแอตทริบิวต์   action={% url 'search' %}   

        <form class="form-inline my-2 my-lg-0" action="{% url 'search' %}">

และในแท็ก   ให้เพิ่มแอตทริบิวต์ที่มีชื่อว่า   name="q"   เพื่อกำหนด query parameter  เข้ามา

 <input class="form-control mr-sm-2" type="search" ... name="q">


จะได้

        <form class="form-inline my-2 my-lg-0" action="{% url 'search' %}">
            <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" name="q">
            <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
        </form>


base.html (final)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    {% block title %} {% endblock %}
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
          integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
    <a class="navbar-brand" href="{% url 'home' %}">STACKPYTHON</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav mr-auto">
            <li class="nav-item active">
                <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="#">Link</a>
            <li class="nav-item dropdown">
                <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown"
                   aria-haspopup="true" aria-expanded="false">
                    Dropdown
                </a>
                <div class="dropdown-menu" aria-labelledby="navbarDropdown">
                    <a class="dropdown-item" href="#">Action</a>
                    <a class="dropdown-item" href="#">Another action</a>
                    <div class="dropdown-divider"></div>
                    <a class="dropdown-item" href="#">Something else here</a>
                </div>
            </li>
            <li class="nav-item">
                <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
            </li>
        </ul>
        <form class="form-inline my-2 my-lg-0" action="{% url 'search' %}">
            <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" name="q">
            <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
        </form>
    </div>
</nav>

{% block content %}
{% endblock %}

</body>
</html>


ทำการสืบทอดจาก   base.html  เข้าไปใน home.html และ  search.html  สำหรับหน้า home นั้นก็แสดงผลเพียงแค่คำสั่ง   <h1>Hello, this is a homepage</h1>  เพียงเท่านั้น ไม่ได้มีการดึงข้อมูลอะไรมาแสดงผล เพราะบทความนี้จะทดสอบทำการ search เท่านั้น

home.html

{% extends 'blog/base.html' %}
{% block title %}Home{% endblock %}

{% block content %}
    <h1>Hello, this is a homepage</h1>
{% endblock %}


สำหรับหน้า  search.html  ก็แสดงผลเพียงคำสั่ง  <h1>Hello, Django Search</h1>  และมีการ for loop ข้อมูลออกมาแสดงผลไว้รอ (แต่ตอนนี้ยังไม่ได้ส่งค่าหรือรีเทิร์นออกเป็น context ออกมา) โดยใช้ django template tag  {% for p in post %}  และแสดงผลโดยใช้ Value tag  {{ p.title }}   โดยให้แสดงออกมาในรูปแบบของ HTML list ในแท็ก  <li></li>  

search.html

{% extends 'blog/base.html' %}
{% block title %}Search{% endblock %}

{% block content %}
    <div class="container">
        <h1>Hello, Django Search</h1>
        {% for p in post %}
            <li>{{ p.title }}</li>
        {% endfor %}
    </div>
{% endblock %}


base.html (Search form)

ให้ไปที่ส่วนของ Search ใน Navbar ซึ่งสังเกตง่าย ๆ จะอยู่ในแท็ก  <form>   และให้ทำการเพิ่มแอตทริบิวต์คือ action เข้าไป เพื่อที่จะให้ยิงไปที่ url endpoint ที่ต้องการเมื่อมีการกด submit ตัว Search form ซึ่งแน่นอนว่าในที่นี้จะให้ยิงไปที่  /search   ซึ่งในที่นี้จะไม่ hardcode เข้าไปแต่จะใช้ ref name ที่ได้เขียนแทน URL ของ search โดยสามารถเรียกใช้งานได้โดย  action="{% url 'search' %}"  dfdและในส่วนของ input ที่จะส่งเข้าไปต้องทำการเพิ่มแอตทริบิวต์ที่มีชื่อว่า   name  เพื่อที่จะส่งเข้าไปใน server เมื่อมีการ search เกิดขึ้น โดยจะส่งเป็น query string หรือ parameter เข้าไปที่มีชื่อว่า 

<!-- base.html -->
...
<form class="form-inline my-2 my-lg-0" action="{% url 'search' %}">
    <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" name="q">
    <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
</form>
...


โดยไม่ว่าเราจะพิมพ์อะไรเข้าไปเช่น python ซึ่งก็จะได้   q=python   ซึ่งตัว q นี้แหละจะเป็นตัวแทนของคำว่า python ที่เราค้นหาและจะถูกส่งไปในฟังก์ชัน search ที่เราเขียนใน  views.py  ซึ่งแน่นอนว่าเราก็ต้องทำการเขียนลอจิกหรือฟังก์ชันก์เพื่อที่จะรับค่านี้นั้นเองครับ แล้วนำไปประมวลผล ซึ่งการประมวลผลในบริบทนี้คือจะทำการค้นหาคำว่า python ใน database ของเรานั่นเอง ซึ่งแน่นอนว่าจะไปค้นหาในตาราง Post  ซึ่งสมมติว่าเราพิมพ์คำว่า django ในช่อง search ฟอร์ม จะได้ URL ดังนี้

http://127.0.0.1:8000/search?q=django


blog/views.py

# blog/views.py
...
def search(request):
    search_post = request.GET.get('q')
    if search_post:
        print(search_post)
        post = Post.objects.filter(Q(title__icontains=search_post))
    else:
        print("Empty")
        return redirect("/")

    return render(request, 'blog/search.html', {'post': post})



การทำงานของโค้ดด้านบน 

  • ทำการกำหนดฟังก์ชัน  search  
  • กำหนดตัวแปรที่มีชื่อว่า  search_post  เพื่อรับค่าที่เข้ามาจาก Search form ที่ client ได้ส่งเข้ามาเมื่อมีการกด submit หรือ enter ซึ่งแน่นอนว่าได้ส่งเข้ามาและถูกแทนค่าไว้ใน   q  โดยถูกส่งเข้ามาผ่านเมธอด  GET  ซึ่งแน่นอนว่าอะไรก็ตามที่เป็น GET จะไม่มีการ effect ให้เกิดการเปลี่ยนแปลงใน Database ของเรา ซึ่งในบริบทนี้เราทำการ Search ก็ชัวร์อยู่แล้วว่าแค่ทำการดึงข้อมูลมาแสดงผลเท่านั้นครับ (ถ้าเป็นการบันทึกข้อมูลใน form หรือมีการทำฟังก์ชัน login มีการใช้ Username และ Password ให้ใช้ POST)
  • ทำการกำหนดเงื่อนไข โดยใช้เงื่อนไข if-else เพื่อทดสอบค่าจริงหรือเท็จ ถ้าค่าที่ถูกส่งเข้าไปใน   search_post  เป็นจริง ซึ่งเป็นจริงในที่นี้คือ user มีการพิมพ์อะไรในช่อง search ก็จะให้ทำการปริ้นซ์แสดงผลข้อความที่มีการ search เข้ามา เช่น ถ้า user มีการค้นหาคำว่า django ตัว terminal ของเราก็จะปริ้นซ์แสดงผลคำว่า "django" เพื่อเป็นการเช็คว่า คำที่ user ค้นหานั้นเข้ามาในฟังก์ชันนี้และเงื่อนไขนี้แล้วนะ 
  • กำหนดตัวแปร  post  เพื่อเก็บค่าจาก Queryset ที่ได้ดึงข้อมูลผ่าน method   filter()   โดยในเมธอดนี้ก็จะส่ง   เข้าไปเพื่อทำการค้นหาคำที่มีการ search เข้ามา ซึ่งแน่นอนว่าเราจะทำการค้นหาผ่าน title ฟีลด์ และกำหนดเป็นแบบ case - insensitive คือไม่ว่าจะพิมพ์ตัวพิมพ์ใหญ่หรือพิมพ์เล็ก เช่น "Python" หรือ "python" ก็จะยังเจอบทความที่มี title ชื่อ Python เสมอ
  • สำหรับเงื่อนไข  else  นั้นจะให้ทำการพิมพ์แสดงผลคำว่า  print("Empty")   คือไม่มีการพิมพ์อะไรใน search แต่กด enter หรือ submit button โดยที่ยังไม่ได้พิมพ์อะไร จากนั้นก็ให้ทำการรีเทิร์นไปที่หน้า homepage ตามปกติ ซึ่ง URL ของหน้า home ก็คือ  "/"  

ทดสอบ Search 

เมื่อทำการพิมพ์เพื่อ search โดยใช้คำค้นหาที่ต้องการ ทดสอบโดยพิมพ์คำว่า "django" เพื่อค้นหาบทความเกี่ยวกับ Django




Console

System check identified no issues (0 silenced).
March 21, 2021 - 23:48:27
Django version 3.1.7, using settings 'myWeb2.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
...
...
python # This
...
...
[21/Mar/2021 23:48:34] "GET /search?search=python HTTP/1.1" 200 2762


Empty # If not search anything
[21/Mar/2021 23:52:34] "GET /search?search= HTTP/1.1" 302 0
[21/Mar/2021 23:52:34] "GET / HTTP/1.1" 200 5635


ในหน้าเว็บก็จะแสดงผลรายชื่อบทความเกี่ยวกับ django ในตาราง Post ของ Database โดยแสดงเป็นแบบลิสต์ตามที่ได้เขียนใน HTML <li> tag




Congrats !! ถึงตอนนี้ทุกคนก็สามารถที่จะประยุกต์ใช้งาน search ใน Django project ของเราได้กันแล้วครับ โดยจากรายชื่อบทความ Django 2, Django 1 ด้านบนเราก็สามารถเขียนโค้ดเพิ่มเติมในการลิ้งค์ไปแสดงผลหน้ารายละเอียด (post-details) ของโพสต์นั้น ๆ ได้เลยครับ


Reference

[ djangoprojects.com ] - Complex lookups with Q objects


เปิดโลกการเขียนโปรแกรมและ Software Development ด้วย online courses ที่จะพาคุณอัพสกิลและพัฒนาสู่การเป็นมืออาชีพ เรียนออนไลน์ เรียนจากที่ไหนก็ได้ พร้อมซัพพอร์ตหลังเรียน

คอร์สเรียนเขียนโปรแกรม