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が持っているlookupです。
参考→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節で分けています。
def get_queryset(self): queryset = Aritcle.objects.all() keyword = self.request.GET.get('search-form') if keyword: if " " in keyword or " " in keyword: keyword = keyword.split() for k in keyword: 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
)に置き換えるのを忘れずに。
ちなみにsplit()関数はもともと幅広い区切り文字に自動で対応してくれるので、スペースやタブ文字程度なら引数は空でもうまいことやってくれます。
で、これをまとめます。
def get_queryset(self): queryset = Aritcle.objects.all() keyword = self.request.GET.get('search-form') if keyword: keyword = keyword.split() for k in keyword: queryset = queryset.filter( Q(title__icontains=k) | Q(content__icontains=k) )
区切り文字が含まれていない文字列をsplit()しようがfor文は問題なく回るのでこれで大丈夫そう。
区切り文字を指定したい場合はsplit()の引数を利用すればOK!