PHPを使ってOpenSearchServiceで日本語全文検索する（Official PHP Client for OpenSearchを用いて）

この記事は朝日新聞社 [Advent Calendar 2022](https://qiita.com/advent-calendar/2022/asahi)の17日目の記事です🌞
朝日新聞のエンジニアがバラエティに富んだテック記事をお届けしております！
本日は開発部・中阪が担当です。
***

PHPとOpenSearchの公式PHPクライアントであるOfficial PHP Client for OpenSearchを使って、AWSが提供しているフルマネージドのAmazon OpenSearch Serviceにアクセスし、データを取得するための方法を**初心者でも分かるように**解説します。

# 環境
・Amazon OpenSearch Service
・EC2 (Amazon Linux 2)
・PHP 8.0 (インストール済み)
・Apache/2.4.54 （インストール済み）
・Official PHP Client for OpenSearch https://github.com/opensearch-project/opensearch-php

PHPとOfficial PHP Client for OpenSearchをインストールしたEC2からOpenSearchに登録した新聞記事データの見出しを取得しに行くことを目指します。

# Composerのインストール
ComposerはPHPのパッケージ依存管理ツールです。Official PHP Client for OpenSearchを使用するためには、まずComposerをインストールする必要があります。
```
# php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
# php composer-setup.php
# ./composer.phar -v

   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
```
上記のようにComposerのロゴが表示されればインストール成功です。
composerコマンドをどこからでも使えるようにするためにcomposer.pharを/usr/local/bin配下に移動させます。
```
# mv composer.phar /usr/local/bin/composer
```

# Official PHP Client for OpenSearchのインストール
次のコマンドで一発でインストールできます。
```
composer require opensearch-project/opensearch-php
```

# 使い方
ページの上部でComposerをインストールした際にできた/vendor/autoload.phpを指定し、
次にOpenSearchのエンドポイントとプロトコル、ポート番号を記述します。
ここでschemeとportを記述しないと後々「**No alive nodes found in your cluster**」とエラーが出ますので気を付けましょう。

次の$clientの箇所でOpenSearchとのコネクションを確立しています。OpenSearchへのアクセスにBasic認証を使用している場合、
ここで突破するためのユーザー名とパスワードを記述しておいてください。


```php:serach.php
require __DIR__ . '/vendor/autoload.php';

$opensearchHost = [
    'host' => '***********.ap-northeast-1.es.amazonaws.com',
    'scheme' => 'https',
    'port' => '443'
];


$client = (new \OpenSearch\ClientBuilder())
    ->setHosts([$opensearchHost])
    ->setBasicAuthentication('username', 'password') //BASIC認証のユーザー名とパスワードを記載 
    ->setSSLVerification(false)
    ->build();
```

ここから、OpneSearchへのクエリを記述していきます。今回はmidashiというインデックスに対して、simple_query_stringを用いてデータ取得を行っています。勿論、matchクエリを用いていただいても全く問題ないです。
フィールドにはkuromojiを用いた形態素解析用のMIDASHI_kuromojiとN-gram用のMIDASHI_ngの2つを用意し、そのOR検索を行うことで2通りの手法で全文検索を実現しています。
ただし、N-gramについては形態素解析の補完の意味合いが強いのでboostを用いて傾斜をかけています。

```php:search.php
$indexName = 'midashi';

$params = [
        'index' => $indexName,
        'body' => [
            'size' => 50,
            'query' => [
                "bool"=>[
                    'should'=>[
                        ['simple_query_string' => [
                            'query' => $_POST["keyword"],
                            "fields" => ["MIDASHI_kuromoji"],
                            "default_operator" => "and",
                            "boost"=>2
                        ]],
                        ['simple_query_string' => [
                            'query' =>  $_POST["keyword"],
                            "fields" => ["MIDASHI_ng"],
                            "default_operator" => "and",
                            "boost"=>1
                        ]],
                    ]
                ]
            ]
        ]
    ];
    
```
今、記述したクエリを以下のように投げることでデータを取得できます。
```php:search.php
$result =  $client->search($params);
```

最後に簡単ではありますが、取得したデータを表形式で並べてみます。

```php:search.php
<table border="1">
    <tr>
      <th>見出し</th>
      <th>日付</th>
      <th>発行社</th>
      <th>刊種</th>
    </tr>

    <?php
    for($i=0; $i<count($result["hits"]["hits"]); $i++){
        echo "<tr>";
        echo "<td>".$result["hits"]["hits"][$i]["_source"]["MIDASHI_kuromoji"]."</td>";
        echo "<td>".$result["hits"]["hits"][$i]["_source"]["DATE"]."</td>";
        echo "<td>".$result["hits"]["hits"][$i]["_source"]["SHA"]."</td>";
        echo "<td>".$result["hits"]["hits"][$i]["_source"]["KIND"]."</td>";
        echo "</tr>";

    }
    ?>


  </table>

```
キーワード「新幹線」で検索してみました。
![qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273624/991a3dc6-b57e-5481-09a6-7fc8135f97e0.png)


以下余談。Kuromoji用とN-gram用のフィールドを定義するには、インデックス作成の段階で次のようなクエリを投げ、マッピング時に解析方法を指定すればOKです。analyzerとしてja_morphological_analyzerとngram_analyzerの2つを定義していることがポイントです。
```
PUT midashi/
{
  "settings": {
    "analysis": {
        "analyzer": {
            "ja_morphological_analyzer": {
              "type": "custom",
              "char_filter":[
                    "icu_normalizer"
              ],
              "tokenizer": "kuromoji_tokenizer",
              "filter": [
                "kuromoji_baseform",
                "kuromoji_part_of_speech",
                "ja_stop",
                "kuromoji_number",
                "kuromoji_stemmer"
              ]
            },
            "ngram_analyzer": {
                "tokenizer": "ngram_compounder"
            }
        },
        "tokenizer": {
            "ngram_compounder": {
                "type": "ngram",
                "min_gram": 2,
                "max_gram": 3,
                "token_chars": [
                    "letter",
                    "digit",
                    "punctuation",
                    "symbol",
                    "whitespace"
                ]
            }
        }
    }
  }
}
```
```
PUT midashi/_mapping/
{
    "dynamic":"strict",
	  "properties": {
	    ・
	    ・
	    ・
	    "MIDASHI_kuromoji": {"type": "text", "analyzer": "ja_morphological_analyzer"},
	    "MIDASHI_ng": {"type": "text", "analyzer": "ngram_analyzer"},
	    ・
	    ・
	    ・
	}
}
```