みるめも

Djangoで「1文字ずつ分解されない複数キーワード検索」をシンプルに実装する

Djangoで「1文字ずつ分解されない複数キーワード検索」をシンプルに実装する
みるみ
\ Follow Me! /
みるみ

ブロガー、エンジニア。

文章を書くのが好きです。

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!

関連しそうな記事