10
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

Organization

PHP から ElasticSearch で日本語検索をするためのインデックス作成と設定

この記事は ElasticSearch で日本語検索をするためのインデックス作成と設定です。

環境

  • Elasticsearch はローカル開発環境については Laradock
  • Dev環境と本番環境については AWS Elasticsearch Service を使います
  • Elasticsearch のバージョンは 6.7とし、これは2019/8/2時点での AWS Elasticsearch Service の最新版です
  • elasticsearch-php のバージョンは 7.1
  • PHP のバージョンは 7.2 (これも現時点での ElasticBeanstalk の対応最新版です)

プラグインなど環境整備

local開発環境の Laradock においては DockerFile に入れるプラグインを書いておきます

$ cat elasticsearch/Dockerfile
// ここのバージョンを書き換えてます
FROM docker.elastic.co/elasticsearch/elasticsearch:6.7.0

// 以下のプラグインを2つ入れます。日本語解析のためのものです。
RUN elasticsearch-plugin install analysis-kuromoji
RUN elasticsearch-plugin install analysis-icu

// 以下変更なし
EXPOSE 9200 9300

kibana も elasticsearch も Laradock に含まれているので引数指定すれば入ります。

$ docker-compose up -d nginx mysql workspace elasticsearch kibana

AWS ElasticSearch Service にはプラグインなどだいたい入ってそう

なのでコンソールでぽちぽちしてつながれば特にやることなし。ここについてはまた別の一つの記事になる感じのところなんでここでは書かない。

elasticsearch-php について

インストール

PHP から Elasticsearch に繋げるためのはこれを使います。
ref: https://github.com/elastic/elasticsearch-php

インストール方法などはREADMEがわかりやすいので略。

使い方資料

使い方のドキュメントが、TOPのREADMEだけではなくて docsが別途あって ここにもたくさん資料あります。
また、Elasticsearch の方の本体のドキュメントにも見やすく書かれているので、そっちを見てみるのも良いかと思います。
https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/quickstart.html

Index作成とSetting

Index作成とSettingをセットで行います。


        // typeをtextにしたいリスト。analyzerを指定するときに同時にtypeが必須だったのでここにtextのものだけリストアップしている。
        $types = [
            'hoge' => 'text',
            'fuga' => 'text',
        ];
        $mappingProperties = [];
        foreach ($types as $key => $type) {
            $mappingProperties[$key] = [
                'search_analyzer' => 'my_ja_analyzer',
                'analyzer' => 'my_ja_analyzer',
                'type' => $type
            ];
        }

        // indexの作られていない初回のみ作成とsettingを投げ込む
        // settingは初回だけなので別の管理にする案もあったが、Index内容と密接に関係にしているのでここに一緒に書いておきたい
        $indexCheckResponse = $this->getClient()->indices()->get([
            'index' => 'your_index_name',
            'client' => ['ignore' => 404]
        ]);
        if (isset($indexCheckResponse['status']) && $indexCheckResponse['status'] == '404') {
            $this->getClient()->indices()->create([
                'index' => 'your_index_name',
                'body' => [
                    'settings' => [
                        "analysis" => [
                            "analyzer" => [
                                "my_ja_analyzer" => [
                                    "type" => "custom",
                                    "tokenizer" => "kuromoji_tokenizer",
                                    "char_filter" => [
                                        "icu_normalizer",
                                        "kuromoji_iteration_mark"
                                    ],
                                    "filter" => [
                                        "kuromoji_baseform",
                                        "kuromoji_part_of_speech",
                                        "ja_stop",
                                        "kuromoji_stemmer"
                                    ]
                                ]
                            ]
                        ]
                    ],
                    'mappings' => [
                        // この type 指定はelasticsearch 7系からは削除されています。あんまり意味のないパラメータのように見えるので適当な値を入れています。
                        '_doc' => [
                            'properties' =>
                                $mappingProperties
                        ]
                    ]
                ]
            ]);
        }

import のコード

$documentBatch はDBから引っこ抜いてきたデータの集合です。
例えば以下のような構造をしています。


[
  'id' = 1,
  'fields' => [
     'hoge' => 'ここにテキスト1',
     'fuga' => 'ここにテキスト2'
  ]
]

これを bulk import のエンドポイントを使って一気に入れます。

        foreach ($documentBatch as $document) {
            $params['body'][] = [
                'index' => [
                    '_index' => 'your_index_name',
                    // この type 指定はelasticsearch 7系からは削除されています。あんまり意味のないパラメータのように見えるので適当な値を入れています。
                    '_type' => '_doc',
                    '_id' => $document['id']
                ]
            ];

            $params['body'][] = $document['fields'];
        }
        $this->getClient()->bulk($params);

単語検索のコード

    public function search(string $words)
    {
        // ここについてはコード後述
        $queryString = $this->getWordQueryString($words);

        $searchResults = $this->getClient()->search([
            'index' => 'your_index_name',
            'from' => 0,
            // 設定可能上限値なので実質無制限扱い
            'size' => 10000,
            'body' => [
                'query' => [
                    'query_string' => [
                        // prefixで検索するためにワイルドカードを許可
                        'analyze_wildcard' => true,
                        // この設定は tokenize された後の文字断片の間の関係についてにも使われているっぽい(推測)
                        // tokenizer により意図しない分割をされた単語においては"OR"検索をされると無関係な場所までマッチしてしまうことになるので、デフォルトはANDにしておいたほうが良い。例えば(架空の会社名) "日伸" は "日"と"伸"になるが、これがORだと意図しない"日"へのマッチが紛れ込んでしまう
                        // ここで文字列として作成する queryString の中においては AND と OR は明示的に設定されるので影響しない
                        'default_operator' => "AND",
                        'query' => $queryString
                    ]
                ],
            ]
        ]);

        $idList = [];
        foreach ($searchResults['hits']['hits'] as $result) {
            $idList[] = $result['_id'];
        }

        return $idList;
    }

    private function getWordQueryString(string $words): string
    {
        // 全角スペースを半角スペースに変換してexplodeでの分割がうまくいくようにしている
        // 使うためには composer で ext-mbstring をインストールしておいてください
        $spaceConvertedWords = mb_convert_kana($words, 's');
        $splittedWords = explode(' ', $spaceConvertedWords);

        $queries = array_map(function ($splittedWords) {
            $trimmedWord = $this->trimEscapedChar($splittedWords);
            return "${trimmedWord}*";
        }, $splittedWords);
        $queryString = implode(' AND ', $queries);

        return $queryString;
    }

    private function trimEscapedChar($word)
    {
        return str_replace(['\\', "'"], '', $word);
    }

参考にしたQiita

kuromoji解析周りは大体こちらにお世話になってます
https://qiita.com/shin_hayata/items/41c07923dbf58f13eec4

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
10
Help us understand the problem. What are the problem?