Django+MongoDBを使ってみた

作ったもの

自然言語処理100本ノックの69問目の問題で無駄に凝ってDjango+MongoDBでWebアプリケーションを作成した。

www.cl.ecei.tohoku.ac.jp

69.Webアプリケーションの作成
ユーザから入力された検索条件に合致するアーティストの情報を表示するWebアプリケーションを作成せよ.アーティスト名,アーティストの別名,タグ等で検索条件を指定し,アーティスト情報のリストをレーティングの高い順などで整列して表示せよ.

ソースはGitHubに置いています。

github.com

Django+MongoDBの記事が意外と少なくて苦労したので書きましたが、正直よくわかっていない所もあるので、間違っている箇所があったら指摘していただきたく。題意を満たす動作しか確認できていません。
※2019/02/18 不要な記述が多かったので大幅修正

使用例

  • トップ画面

f:id:ryu022304:20190217170411p:plain * アーティスト名「Queen」で検索

f:id:ryu022304:20190217170436p:plain * タグ「jpop」で検索&「レーティング(平均)」で降順にソート

f:id:ryu022304:20190217170504p:plain

内容

各種バージョン

OS : macOS High Sierra(10.13.4)
Anaconda : 4.5.11
Python : 3.6.0
pip : 19.0.2
MongoDB : 4.0.4
Django : 2.1.3
Djongo : 1.2.31

手順

基本的に下記サイトを参考にしています。分かりやすかったです。
細かい内容はこちらを見て頂ければ良いかと思います。
本ブログでは自分が書き換えた箇所のみを書いていこうかと。

qiita.com

パッケージ導入

必要なものは当然ですがDjangoと、DjangoでMongoDBを利用するために必要なDjongoをインストールする。

$ pip install django
$ pip install djongo

公式サイトに書いてあるが、Pythonは3.6以上、MongoDBは3.4以上が必要らしいので注意。 nesdis.github.io

また、DjangoでMongoDBを利用するためのパッケージにはdjango-mongodb-engineというものもあるが設定がうまくいかなかったので断念。

プロジェクト作成

$ django-admin startproject NLP_100knoks_69
$ tree NLP_100knoks_69/
NLP_100knoks_69/
├── NLP_100knoks_69
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
$ cd NLP_100knoks_69
$ python manage.py startapp webapp
  • settings.pyのINSTALLED_APPSに「webapp」を追記

Model作成

とりあえず以下のように作成。
※ここが一番要検討箇所

from djongo import models

# Create your models here.
class Artist(models.Model):
    #id = models.IntegerField()
    gid = models.CharField(max_length=100)
    name = models.CharField(max_length=30)
    sort_name = models.CharField(max_length=30)
    area = models.CharField(max_length=20)
    aliases = models.ListField(models.EmbeddedModelField('Aliase'))
    begin = models.EmbeddedModelField('Begin')
    end = models.EmbeddedModelField('End')
    tags = models.ListField(models.EmbeddedModelField('Tag'))
    rating = models.EmbeddedModelField('Rating')

    objects = models.DjongoManager()

class Aliase(models.Model):
    name = models.CharField(max_length=30)
    sort_name = models.CharField(max_length=30)

class Begin(models.Model):
    year = models.IntegerField()
    month = models.IntegerField()
    date = models.IntegerField()

class End(models.Model):
    year = models.IntegerField()
    month = models.IntegerField()
    date = models.IntegerField()

class Tag(models.Model):
    count = models.IntegerField()
    value = models.CharField(max_length=30)

class Rating(models.Model):
    count = models.IntegerField()
    value = models.CharField(max_length=100)
  • settings.pyにDjongoを使うこととデータベース名を指定
DATABASES = {
    'default': {
        'ENGINE': 'djongo',
        'NAME': 'test_database',
    }
}
  • データベースにモデルの反映
$ python manage.py makemigrations
$ python manage.py migrate
  • データをMongoDBに登録
$ python manage.py shell

下記シェルを実行して登録。

import gzip, json
from webapp.models import Artist
ipath = '/path/to/artist.json.gz'
with gzip.open(ipath+"artist.json.gz", "rt", "utf_8") as f:
    buf = []
    for i,line in enumerate(f):
        obj = json.loads(line)
        buf.append(obj)
        if i % 10000 == 0:
            Artist.objects.mongo_insert_many(buf)
            buf = []
    Artist.objects.mongo_insert_many(buf)

1つずつ登録すると時間がかかりすぎるので、10000件ごとに登録している。

基本的に元データと同じフィールド名を用いたが、idは普通に使おうとすると下記エラーになった。

ERRORS:
webapp.Artist: (models.E004) 'id' can only be used as a field name if the field also sets 'primary_key=True'.

軽く調べたが「id」というキーはデフォルトで利用されているから使えないとか。やむなくidはコメントアウト。IDを用いることはあまりないので問題ないと判断。

View作成

  • urls.py
from django.contrib import admin
from django.urls import path
import webapp.views as webapp_view

urlpatterns = [
    path('admin/', admin.site.urls),
    path('artist_list/', webapp_view.ArtistListView.as_view())
]
  • views.py
from django.shortcuts import render
from django.views.generic import TemplateView
from webapp.models import *

# Create your views here.
class ArtistListView(TemplateView):
    template_name = "artist_list.html"

    def search(self,item = '',content = '',limit = 100):
        if content == '':
            artists = Artist.objects.mongo_find()
        else:
            if item == 'name':
                artists = Artist.objects.mongo_find({'name':content})
            elif item == 'aliase':
                artists = Artist.objects.mongo_find({'aliases.name':content})
            elif item == 'tag':
                artists = Artist.objects.mongo_find({'tags.value':content})
            limit = artists.count()
        arts = []

        # 100件にしている。全体で921337件あるので表示に時間がかかりすぎる為
        for artist in artists[:limit]:
            art = artist

            # 別名の整形
            if 'aliases' in artist:
                aliase_name = []
                for aliase in artist['aliases']:
                    aliase_name.append(aliase['name'])
                art['aliases'] = ',\n'.join(aliase_name)

            # 活動開始日の整形
            if 'begin' in artist:
                begin_date = []
                if 'year' in artist['begin']:
                    begin_date.append(str(artist['begin']['year']))
                if 'month' in artist['begin']:
                    begin_date.append(str(artist['begin']['month']))
                if 'date' in artist['begin']:
                    begin_date.append(str(artist['begin']['date']))
                art['begin'] = '/'.join(begin_date)

            # 活動終了日の整形
            if 'end' in artist:
                end_date = []
                if 'year' in artist['end']:
                    end_date.append(str(artist['end']['year']))
                if 'month' in artist['end']:
                    end_date.append(str(artist['end']['month']))
                if 'date' in artist['end']:
                    end_date.append(str(artist['end']['date']))
                art['end'] = '/'.join(end_date)

            # タグの整形
            if 'tags' in artist:
                tag_contents = []
                for tag in artist['tags']:
                    tag_contents.append(tag['value']+':'+ str(tag['count']))
                art['tags'] = ',\n'.join(tag_contents)

            # レーティングの整形
            if 'rating' in artist:
                art['rating_num'] = str(artist['rating']['count'])
                art['rating_ave'] = str(artist['rating']['value'])

            arts.append(art)

        d = { 'objects' : arts }

        return render(self.request, self.template_name, d)

    def get(self, request, *args, **kwargs):
        if request.method == 'GET':
            if 'search' in request.GET:
                return self.search(request.GET['search_item'], request.GET['search'])
            else:
                return self.search()

HTML

参考サイトにほぼ準拠。ただBootStrapでSB Admin 2は最新版ではだいぶ内容が変わっていたので、下記から以前のバージョンを取得して使用した。全部載せると長くなりすぎるので省略。

Release v3.3.7+1 · BlackrockDigital/startbootstrap-sb-admin-2 · GitHub

最終的なディレクトリ構造

一部省略していますが、最終的に以下のような構造になりました。

$ tree .
.
├── NLP_100knoks_69
│   ├── __init__.py
│   ├── __pycache__ #以下のファイルを省略
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3
├── manage.py
├── static
│   ├── bootstrap #以下のファイルを省略
│   │   ├── dist
│   │   ├── js
│   │   └── vendor
│   └── webapp
│       └── css
│           └── structure.css
└── webapp
    ├── __init__.py
    ├── __pycache__ #以下のファイルを省略
    ├── admin.py
    ├── apps.py
    ├── migrations #以下のファイルを省略
    ├── models.py
    ├── templates
    │   ├── artist_list.html
    │   └── base.html
    ├── tests.py
    └── views.py

完了

参考サイトではこの後ログイン機能等の実装を行わせていますが、今回は不要なのでここで終了。
Djangoを起動し、localhost:8080/artist_listにアクセスすれば使用できる。

まとめ

せっかくだからとDjangoで作ってみたが、結構面白かった。
正直現状Djangoをあまり活用できている気がしないので、ちゃんと勉強したい。