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

mull

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 メソッドの引数を| (パイプ) で繋げていくだけです。

あとはこの状態で複数ワードで区切られたものごとにf ilter をその都度かけていくのが自然な書き方になりそうですね。

区切り文字でキーワードを分けて繰り返すだけ

というわけで、シンプルにキーワードを区切って区切られたワードごとに 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!

Comments

タイトルとURLをコピーしました