nazoです。
最近は全文検索エンジンの選択肢も増えましたが、弊社ではElasticsearchを使っています。 Symfony(2.x)でElasticsearchを使う上で、FOSElasticaBundleという便利なBundleがあるので、紹介したいと思います。
概要
そもそもPHPにはElasticaという、Elasticsearchを使う上で便利なライブラリがあるのですが、これをSymfonyに合わせてBundle化したものがFOSElasticaBundleです。
特長
- EntityとElasticsearchをほぼ完全に連動させることができます。Entityが保存されたら同時にElasticsearchに保存、ということができるようになります。Elasticsearch上にデータを保存することを意識する必要がなくなります。
- ElasticsearchとEntityのマッピングをYAMLで簡単に定義することができるので、多少のElasticsearchの知識があれば、簡単にデータをElasticsearch上にマッピングすることができます。もちろんtokenizerやanalyzerの設定もできます。
- Doctrine以外のデータソースにも対応しています(あまり使うことはないと思いますが)。
インストール
composer require friendsofsymfony/elastica-bundle
して、 app/AppKernel.php
に new FOS\ElasticaBundle\FOSElasticaBundle(),
を足します。(他のBundleと同じ)
設定
ざっくりと以下のようなEntityを用意します。
<?php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Post * * @ORM\Table(name="post") * @ORM\Entity(repositoryClass="AppBundle\Repository\PostRepository") */ class Post { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string * * @ORM\Column(name="title", type="string", length=255) */ private $title; /** * @var string * * @ORM\Column(name="body", type="text") */ private $body; (以下略)
app/config/config.yml に、以下の設定を追加します。
fos_elastica: clients: default: url: "http://localhost:9200" indexes: symfony: settings: index: analysis: tokenizer: kuromoji: type: kuromoji_tokenizer analyzer: kuromoji_analizer: type: custom tokenizer: kuromoji filter: - kuromoji_baseform - lowercase types: post: mappings: id: type: integer index: not_analyzed title: type: string index: analyzed analyzer: kuromoji_analizer body: type: string index: analyzed analyzer: kuromoji_analizer persistence: driver: orm model: AppBundle\Entity\Post listener: ~ provider: ~ finder: ~ elastica_to_model_transformer: ignore_missing: true
データの投入
適当にSQLでデータを足します。
INSERT INTO post(title, body) values('こんにちは', '本日は晴天なり'), ('UUUM', 'YouTuberの会社です');
SQLから入れた場合は当然 Elasticsearch にはデータが入りません。
ここで fos:elastica:populate
コマンドを実行すると、「インデックスの作成」と「データの投入」を同時に行ってくれます。初期設定や開発中に便利です。
% app/console fos:elastica:populate 100/100 [============================] 100% Populating symfony/postRefreshing symfony Refreshing symfony
Elasticsearch側にデータが入ったか確認してみましょう。
% curl http://localhost:9200/symfony/_search\?pretty\=true { "took" : 2, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "failed" : 0 }, "hits" : { "total" : 2, "max_score" : 1.0, "hits" : [ { "_index" : "symfony", "_type" : "post", "_id" : "1", "_score" : 1.0, "_source":{"id":1,"title":"こんにちは","body":"本日は晴天なり"} }, { "_index" : "symfony", "_type" : "post", "_id" : "2", "_score" : 1.0, "_source":{"id":2,"title":"UUUM","body":"YouTuberの会社です"} } ] } }
問題なさそうです。
画面から投入してみる
データを入れる画面を適当に作りデータを入れます。
- Form/PostType.php ->
app/console doctrine:generate:form AppBundle:Post
で作る - Resources/views/Default/add.html.twig
<html> <body> <h1>Add Post</h1> {{ form_start(form) }} {{ form_widget(form) }} <input type="submit" value="add" /> {{ form_end(form) }} </body> </html>
- Controller/DefaultController.php
use AppBundle\Form\PostType; use AppBundle\Entity\Post; ... /** * @Route("/add", name="add") * @Template */ public function addAction(Request $request) { $form = $this->createForm(PostType::class, new Post()); if ($request->isMethod('post')) { $form->handleRequest($request); if ($form->isValid()) { $post = $form->getData(); $this->getDoctrine()->getManager()->persist($post); $this->getDoctrine()->getManager()->flush(); return $this->redirectToRoute('index'); } } return ['form' => $form->createView()]; }
画面が出来上がるので、画面からデータを入れてみましょう。
Elasticsearch側から見てみます。
% curl http://localhost:9200/symfony/_search\?pretty\=true { "took" : 1, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "failed" : 0 }, "hits" : { "total" : 3, "max_score" : 1.0, "hits" : [ { "_index" : "symfony", "_type" : "post", "_id" : "1", "_score" : 1.0, "_source":{"id":1,"title":"こんにちは","body":"本日は晴天なり"} }, { "_index" : "symfony", "_type" : "post", "_id" : "2", "_score" : 1.0, "_source":{"id":2,"title":"UUUM","body":"YouTuberの会社です"} }, { "_index" : "symfony", "_type" : "post", "_id" : "3", "_score" : 1.0, "_source":{"id":3,"title":"UUUMネットワーク 開始から1年でチャンネル月間総再生回数が2億回を突破!","body":"UUUM株式会社(本社:東京都港区、代表取締役:鎌田 和樹)は、当社が提供するYouTuberを支援するサポートサービス「UUUMネットワーク」において、2015年12月のネットワーク所属チャンネルの月間総再生回数が2億回を突破したことをお知らせ致します。"} } ] } }
このように、Entityの操作だけでElasticsearchにデータが入っていることが確認できました。
検索
検索は普通にElasticsearchのクエリをPHPのarrayで入力するのが基本になります。
- Controller/DefaultController.php
/** * @Route("/", name="index") * @Template */ public function indexAction(Request $request) { $keyword = $request->query->get('keyword', ''); $query = [ 'sort' => [ 'id' => [ 'order' => 'desc', ], ], 'query' => [ 'bool' => [ 'must' => [ [ 'query_string' => [ 'fields' => ['title', 'body'], 'query' => $keyword, ], ], ] ] ] ]; $posts = $this->get('fos_elastica.finder.symfony.post')->find($query); return ['posts' => $posts, 'keyword' => $keyword]; }
- Resources/views/Default/index.html.twig
<html> <body> <h1>Posts</h1> <form action="{{ path('index') }}" method="get"> <input type="text" name="keyword" value="{{ keyword }}" /> <input type="submit" value="search" /> </form> {% for post in posts %} <h2>{{ post.title }}</h2> <p>{{ post.body | nl2br }}</p> {% endfor %} <p><a href="{{ path('add') }}">Add Post</a></p> </body> </html>
このように、Elasticsearchのクエリを理解していれば、簡単にクエリを作ることができると思います。
ページネーション
KnpPaginatorBundleと組み合わせたページネーションにも対応しています。
- Controller/DefaultController.php
/** * @Route("/", name="index") * @Template */ public function indexAction(Request $request) { $keyword = $request->query->get('keyword', ''); $query = [ 'sort' => [ 'id' => [ 'order' => 'desc', ], ], 'query' => [ 'bool' => [ 'must' => [ [ 'query_string' => [ 'fields' => ['title', 'body'], 'query' => $keyword, ], ], ] ] ] ]; $paginator = $this->get('knp_paginator'); $pagination = $paginator->paginate( $this->get('fos_elastica.finder.symfony.post')->createPaginatorAdapter($query), $request->query->getInt('page', 1), 5 ); return ['pagination' => $pagination, 'keyword' => $keyword]; }
- Resources/views/Default/index.html.twig
<html> <body> <h1>Posts</h1> <form action="{{ path('index') }}" method="get"> <input type="text" name="keyword" value="{{ keyword }}" /> <input type="submit" value="search" /> </form> <p>{{ knp_pagination_render(pagination) }}</p> {% for post in pagination %} <h2>{{ post.title }}</h2> <p>{{ post.body | nl2br }}</p> {% endfor %} <p><a href="{{ path('add') }}">Add Post</a></p> </body> </html>
簡単ですね。
まとめ
AWSでも最近Elasticsearch Serviceが始まり、Elasticsearchを使う場面は増えてきたと思われますが、このようなBundleを便利に活用することによって、気軽に検索を実装することができます。
検索が重要なところでは、検索APIとのつなぎ込みをあまり気にすることなく、また、データベース側のインデックスや高速化を考えることからも開放されるので、Symfonyを使っている場合は検討してみてください。
UUUMでは、大量のYouTuberのデータを分析するエンジニアも募集しています。詳しくは以下をご覧下さい。