Django で「1文字ずつ分解されない複数キーワード検索」をシンプルに実装する
Django で通常の GET リクエストによるフリーワード検索をしたいとき、意外にも簡素な実装が望めません。
外部のライブラリなどは使わない自然なフリーワード検索を考えてみます。
部分一致検索
まずは「部分一致検索」対応のために Qオブジェクト
+ queryset.filter
を利用します。
(本来ならこの時点で Q オブジェクトはまだ必要ありませんが、便宜上同じタイミングで説明しています)
from django.db.models import Q
from .models import Article
class IndexView(generic.ListView):
model = Article
def get_queryset(self):
queryset = Aritcle.objects.all()
keyword = self.request.GET.get("search-form")
if keyword:
queryset = queryset.filter(Q(title__icontains=keyword))
これで Article というモデルの title というフィールドに特定のキーワードが含まれるオブジェクトのみを扱えます。
__icontains
というプロパティは Q オブジェクトが持つプロパティではありません(そのような記載がなされているサイトがありました)。もともと queryset が持っているルックアップです。
参考:Django公式リファレンス
ちなみに i は case-insensitive の i なので、 __contains
とすれば大文字小文字を区別する厳密な絞り込みが可能です。
複数のフィールドにまたがる検索
上記を踏襲しつつ、title 以外のフィールドにも検索範囲を広げてみます。ここでは content としてみましょう。
def get_queryset(self):
queryset = Aritcle.objects.all()
keyword = self.request.GET.get("search-form")
if keyword:
queryset = queryset.filter(
Q(title__icontains=keyword) |
Q(content__icontains=keyword)
)
これは簡単です。
Q オブジェクトによって既に OR 検索が可能になっているので、filter メソッドの引数を |
(パイプ) で繋げていくだけです。
あとはこの状態で複数ワードで区切られたものごとに filter をその都度かけていくのが自然な書き方になりそうですね。
区切り文字でキーワードを分けて繰り返すだけ
というわけで、シンプルにキーワードを区切って区切られたワードごとに for 文で filter を適用させてみます。
区切り文字があるときは複数ワード検索できるような対応に変えてみましょう。
多くのユーザーが無意識に複数ワードを入力するときはほぼほぼ全角か半角のスペースで区切るでしょうから、下記のコードもそれに則っています。もちろん適宜好きな区切り文字を追加できます。
今は分かりやすくするため、一旦 if 節と else 節で分けています。
import re
def get_queryset(self):
queryset = Aritcle.objects.all()
keyword = self.request.GET.get("search-form")
if keyword:
if re.search("\s", keyword):
keywords = keyword.split()
for k in keywords:
queryset = queryset.filter(
Q(title__icontains=k) |
Q(content__icontains=k)
)
else:
queryset = queryset.filter(
Q(title__icontains=keyword) |
Q(content__icontains=keyword)
)
split() で分割されたキーワードひとつずつに対して filter() が効いています。Q オブジェクトの引数の値をリストの各要素( k
)に置き換えるのを忘れずに。
そして、これをまとめます。
def get_queryset(self):
queryset = Aritcle.objects.all()
keyword = self.request.GET.get("search-form")
if keyword:
keywords = keyword.split()
for k in keywords:
queryset = queryset.filter(
Q(title__icontains=k) |
Q(content__icontains=k)
)
区切り文字が含まれていない文字列を split()
しようが for 文的には問題ないのでこれで大丈夫そうです。区切り文字を指定したい場合は split()
の引数を利用しましょう。