Django 記事に紐づいたコメントもキーワード検索する

さいきんは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: テスト投稿>]>


とりあえずは実装できました。

TOPページ