[Django]サイト内検索を実装した話
2018/05/23 23:29
  • いきなりですが、サイト内検索を実装しました。(ぱちぱち

    サイト内検索を作ろうと思ったきっかけ


    作ろうと思った経緯は色々有るんですが、一番大きな理由は「自分が過去に書いた記事を
    効率よく探すため」です。笑

    なんじゃそら?!っていう感じですが、「あのテーマで記事書いたなー」っていうのは
    断片的に覚えているのだけど、それをいつ書いたのかを全く覚えていないことが多いので
    探し当てるのが結構しんどかったんですよね。

    もう一つの理由は、 Google Analytics に「サイト内検索」という項目がいつの間にか出来ていたことです。
    せっかくだから自分のサイトにサイト内検索機能を実装して、自分以外の人にも使ってもらえるのも悪くないかな、と。

    そんな軽い気持ちで実装を決意しました。


    仕様

    ざっとではありますが、仕様を考えてみます。


    1. 複数キーワードで AND 検索ができる

    2. よくあるサブクエリ的な検索はしない(このサイトに書いてあるようなオプション指定)

    3. 検索対象は Articles, Apps 内のみとする

    4. 検索結果ページは /?q=検索ワード とする


    1., 2. について、最低限 AND 検索ができれば上出来かな、と。WordPress の検索フォームもシンプルな AND 検索ができる仕様のようだし。

    3. について、ほんとは photosInstagram も対象にしたかったのですが、
    格納されているテーブルが別になるし、ちょっと実装がめんどくさそうだったので対象外にしました。笑

    4. のようにしたのは、よくある検索サイトが GET でフォームを送信しているし、何より Google Analytics と連携するのにも
    何も考えずに設定できそうだったからです。

    (参考)
    Google Analytics の検索クエリ設定画面


    検索フォームの実装

    続いて、実装について。

    複数のキーワードで検索することを想定すると、 GET で受け取ったパラメータを、半角スペースをデリミタとしてリストにしてあげるとクエリを発行するときに便利そうなので、以下のような関数をまずは作りました。

    lib/functions.py
    (このファイルに自作の関数を置くようにしています)

    from typing import List
    
    
    # 検索フォーム
    class Search:
        @staticmethod
        def parse_search_params(words: str) -> List[str]:
            search_words = words.replace(' ', ' ').split()
            return search_words
    

    全角スペースも入り得ることを想定して、全角スペースを半角に変換した上で split() しています。
    次に、 views.py の実装です。
    views.py
    from django.views.generic import TemplateView
    from .models import Blog
    from django.db.models import Q
    from .lib.functions import Search
    import operator
    from functools import reduce
    
    
    class IndexView(TemplateView):
        template_name = 'index.jade'
    
        def get_context_data(self, **kwargs):
            context = super(IndexView, self).get_context_data(**kwargs)
    
            if 'q' in self.request.GET:
                # クエリパラメータがあるので検索用の処理をする
    
                query_params = self.request.GET.get('q')
                context['query_params'] = query_params
    
                # 検索結果用のテンプレートをセットする
                self.template_name = 'search.jade'
                # 検索キーワードのリストを取得
                q = Search.parse_search_params(query_params)
    
                # QuerySet を利用してクエリを発行する
                if q:
                    query = reduce(operator.and_, (
                        Q(content__contains=w) | Q(subject__contains=w) for w in q))
                    search_result = Blog.objects.filter(
                        created_at__lte=now
                    ).filter(query).order_by('-updated_at')
    
                    context['search_result'] = search_result
            else:
                # 通常のトップページを表示する処理
    
            return context
    

    いろいろ省略した上でざっくりと書いていますが、だいたい上記のような感じで、
    qGET に存在すれば検索結果用のフォームを表示し、なければ通常のトップページを表示する処理を行います。
    また、 QuerySet については実際はもう少し複雑なものを使っています。
                    query = reduce(operator.and_, (
                        Q(content__contains=w) | Q(subject__contains=w) for w in q))
                    search_result = Blog.objects.filter(
                        created_at__lte=now
                    ).filter(query).order_by('-updated_at')
    

    ↑ちなみにこれ、結構複雑なクエリを書いているように見えますが、実際に SQL に展開すると以下のようなクエリになります。


    SELECT
        `blog`.`*`
    FROM
        `blog`
    WHERE
        (
            `blog`.`created_at` <= '2018-05-23 23:09:34.460151'
        AND (
                `blog`.`content` LIKE BINARY '%wordpress%'
            OR  `blog`.`subject` LIKE BINARY '%wordpress%'
            )
        AND (
                `blog`.`content` LIKE BINARY '%コントリビューター%'
            OR  `blog`.`subject` LIKE BINARY '%コントリビューター%'
            )
        )
    ORDER BY
        `blog`.`updated_at` DESC
    

    ちなみに次のサイトを参考にさせていただきました。
    Making queries
    Django で OR を使ったクエリを実行する方法
    DjangoのORMのすごいところ

    最後に、画面のことを考えてみます。

    検索フォーム自体はサイドバーに置きたいので、検索フォームのみが記述されたテンプレートを作成しておくと便利そう。
    検索結果を表示する画面も必要なので、こちらも別途用意する。


    サイドバー用テンプレート

    form(method='GET', action='/')
        .input-group
            input.form-control(type='text', name='q', placeholder='記事内をAND検索できます', value='#{query_params}')
            span.input-group-btn
                button.btn.btn-default.glyphicon.glyphicon-search.top0(type='submit')
    

    検索結果用テンプレート

    block content
        .col-sm-9
            h2.font-cursive
                span.glyphicon.glyphicon-search
                span.mr-2 Search Results
            include search_form
            .row.mr-top-10
                .col-sm-12
                    if search_result
                        .list-group
                            for article in search_result
                                if article.is_app_page
                                    a.list-group-item(href='/apps/post/#{article.id}')
                                else
                                    a.list-group-item(href='/article/post/#{article.id}')
                                        dl
                                            dt.list-group-item-heading #{article.subject}
                                            dd.list-group-item-text #{article.description}
                                            dd
                                                small #{article.updated_at|date:"Y/m/d H:i"}
    

    毎度毎度 jade のテンプレートで申し訳なさでいっぱいなんですが、我慢してください。笑
    長くなりましたが、以上で検索フォームの実装が完了しました。


    実際に検索したときのキャプチャも貼っておきます。

    検索フォーム1


    検索フォーム2


    人気ブログランキングへ ブログランキング・にほんブログ村へ
    ↑応援よろしくお願いします!m(_ _)m

  • 2018/05/23 23:29
  • Python
  • DjangoPythonサイト内検索検索フォームAND検索QuerySetクエリセットSQLGoogle Analytics検索クエリ
  • 新しい記事へ
    [shell] フォルダ内にある大量の ZIP ファイルをまとめて解凍する

    古い記事へ
    [WordPress] Core コントリビューターことはじめ

profile picture

自己紹介的な何か

@wkmettyでついったーやってます。時々。 6年間勤めたゲーム会社を2018年2月に退職しフリーランスのプログラマに。 WordPress Core, WP-CLI コントリビューター。 お仕事募集中です。