PythonDjango

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

公開日:2021-11-28 更新日:2023-06-10

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

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

Twitter Share