さいきんはDjangoでお問い合わせシステム的なものを作っています。
ブログサイトをベースにしているのですが、記事に加えて回答コメントも同時に検索したいです。
ということで今回は記事モデルとコメントモデルを同時にキーワード検索する実装についてのメモです。
端折っていますが、コアな部分はこんな感じのモデルです。
# models.py
class Post(models.Model):
"""投稿"""
title = models.CharField('タイトル', max_length=32)
text=models.TextField('本文')
class Comment(models.Model):
"""記事に紐づくコメント"""
name = models.CharField('名前', max_length=255, default='名無し')
text=models.TextField('本文')
target = models.ForeignKey(
Post, on_delete=models.CASCADE, verbose_name='対象投稿')
質問を表すPostとそちらを参照する形でコメントモデルを持たせています。
検索フォームは以下のような形です。
# forms.py
class PostSearchForm(forms.Form):
"""記事検索フォーム。"""
key_word = forms.CharField(
label='検索キーワード',
required=False,
widget=forms.TextInput(attrs={'autocomplete': 'off',
'placeholder': 'キーワード検索',
})
)
あとはviews.pyで検索が行われた時に絞り込みます。
# views.py
from django.db.models import Q
...
class PublicPostIndexView(generic.ListView):
"""公開記事の一覧を表示する。"""
model = Post
template_name = 'contact/index.html'
paginate_by = 10
def get_queryset(self):
queryset = super().get_queryset()
self.form = form = PostSearchForm(self.request.GET or None)
if form.is_valid():
key_word = form.cleaned_data.get('key_word')
if key_word:
# コメントモデルを取得
comments = Comment.objects.all()
# queryの絞り込み前に別名で定義して避難
posts = queryset
# キーワードが半角スペースで区切られていれば、その回数だけfilterするAND検索。
for word in key_word.split():
queryset = queryset.filter(
Q(title__icontains=word) | Q(text__icontains=word))
# コメントモデルを絞り込み
comments = comments.filter(text__icontains=word)
if comments:
# 絞り込まれたコメントモデルから、targetのpkつまりPostモデルのpkを取り出す
list_of_ids = set([comment.target.pk for comment in comments])
# 別名で避難していたpostsをidで絞り込んで足すイメージ
queryset |= posts.filter(id__in=list_of_ids)
return queryset
まずPostモデル自体はキーワードで絞り込みをしつつ、コメントモデルも絞り込みをします。
絞り込まれたコメントモデルから対象記事のidを抽出し、postモデルをquerysetとは別に絞り込みを行います。
最後に、querysetとコメントによって絞り込まれたpostモデルをmergeすれば、検索の実装完了です。
queryset |= some queryset
とするとqueryをmergeすることができます。
|=
は論理和の代入演算子で、上記の例ですと下記のように書くこともできます。
queryset = queryset | posts.filter(id__in=list_of_ids)
挙動を少し詳しくみてみましょう。
試しに投稿を二つしてみました。
queryset = super().get_queryset()
print(queryset)
for query in queryset:
print(query.id)
>>>
<QuerySet [<Post: テスト投稿>, <Post: テスト投稿2>]>
16
17
idが16と17の二つのクエリです。
フィルタリングして、mergeしてみましょう。
query_1 = queryset.filter(id=16)
query_2 = queryset.filter(id=17)
query_merge = query_1 | query_2
print(query_merge)
>>>
<QuerySet [<Post: テスト投稿>, <Post: テスト投稿2>]>
期待通りの結果となりました。
直感的には+でもいけそうですがエラーになります。
query_merge = query_1 + query_2
>>>
...
TypeError: unsupported operand type(s) for +: 'QuerySet' and 'QuerySet'
当然ですが重複するものを足しても、二つになることはありません。
query_1 = queryset.filter(id=16)
query_2 = queryset.filter(id=16)
query_merge = query_1 | query_2
print(query_merge)
>>>
<QuerySet [<Post: テスト投稿>]>
とりあえずは実装できました。