อัปเดตล่าสุด Feb. 12, 2023
Search เรียกได้ว่าเป็นฟีเจอร์ที่ต้องมีในแทบจะทุก ๆ เว็บไซต์หรือแอป ในบทความ Django Search นี้ แน่นอนว่าเราจะมาประยุกต์ใช้งาน Search เข้ากับ Django project ของเรากันครับ โดยจะใช้ Bootstrap form ในส่วน navbar สำหรับ UI ในครั้งนี้
หน้า search ที่เราคุ้นเคยกันดีอย่างเช่น Google Search ก็ถือว่าเป็น search ที่ทุก ๆ คนแทบจะคุ้นเคยกันดีที่สุด เช่นเราใช้มันเข้า Stackoverflow ในทุก ๆ วัน นี่ก็คือ search แต่ทาง Google ก็ค่อนข้างที่จะมีอัลกอริทึมที่ค่อนข้างซับซ้อน ซึ่งเราจะไม่ขอพูดถึงในบริบทนี้ จะเป็นการยกตัวอย่างการ search เท่านั้น
Google คือ web ที่ใช้สำหรับ search ที่เราคุ้นเคยกันดี
หน้า search ของ Django official website (Cr Photo: stackpython.medium.com)
จากที่ยกตัวอย่างมาในเบื้องต้นด้านบน จะเห็นได้ว่าเว็บส่วนใหญ่ก็มีฟีเจอร์สำหรับค้นหากันแทบทุกเว็บ จะง่ายหรือซับซ้อนก็ขึ้นอยู่กับ business domain ของเว็บไซต์นั้น ๆ
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
เข้าหน้า 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
ในโปรเจคท์นี้จะมีการสร้างเพียงแค่ 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),
]
บทความนี้จะใช้ 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 เข้าไปที่มีชื่อว่า q
<!-- 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 โดยใช้คำค้นหาที่ต้องการ ทดสอบโดยพิมพ์คำว่า "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 ที่จะพาคุณอัพสกิลและพัฒนาสู่การเป็นมืออาชีพ เรียนออนไลน์ เรียนจากที่ไหนก็ได้ พร้อมซัพพอร์ตหลังเรียน
เรียนเขียนโปรแกรม