HumHubの検索にApacheSolrを使わせてみる（前編）

#はじめに
HumHubには標準で検索機能がついてはいるものの、検索はコンテンツ本文、タグ、トピックに対する単語の列記でのAND検索、可能なフィルタは情報ソースのタイプ（種類：スペース名かユーザー名かコンテンツか）とスペースの絞り込みの対応である。このままでも使えなくはないのだが、将来、ウェブサーバーの冗長化に対応できるようにすることをモクロミ、全文検索システムサーバーを別に立てて利用することを検討する。

#調査
HumHubでは、そのプロフェッショナルバージョン（有料）で、HumHub謹製Solrプラグインを追加利用できる。お金とHumHubを信じる心があるときはそれを使えばよいだろう。

いずれも持ち合わせない金銭的にも心も貧しい私のような人は、Solrに接続してクエリを投げて回答を得られる機能を持つプラグインを、自前で作ればよいのだ。その方向で調査を続行した。そう、私は心が貧しい・・・。

####HumHubの検索機能
HumHubはDBにMysqlを使っているので、Mysqlの全文検索機能を使っているのかと思ったが、違った。
（参考）
https://tech.bita.jp/article/4
https://techblog.gmo-ap.jp/2020/01/06/mysql-innodb-fulltext-search-tuto/
https://recruit.gmo.jp/engineer/jisedai/blog/mysql_fulltextsearch/

HumHubでは、全文検索用に自前でZendLuceneエンジンのためのインデックスファイルを作成する機能をもっていて、そのインデックスを使った検索を行う。なんと、収録したPDFやWord，Excelなんかのファイルの文章もインデックスに入れてしまうこともできるらしい。
https://docs.humhub.org/docs/admin/search/

HumHubでSolrを使うときも、Luceneを使えばいいのかも。


####プラグインの材料
プラグインの材料として以下を見つけてきた。

https://github.com/solariumphp/solarium

2021年11月現在においても開発が継続されている。注意点は対応環境（[Requirements](https://github.com/solariumphp/solarium#requirements)）。Ver.6.x以降はPHP Ver.7.3以上が必要だ。Ver.1.9のHumHubにはちょうどよい(ハズ）。

#開発環境構築
まずは、solariumのComposer.jsonを眺めながら、HumHubとのマッチングを調査する。
・・・はい、ダメ。HumHubの使っているCodeceptionのバージョンは、solariumよりも旧いバージョンのphpuinitを要求するため、Composerがインストールに失敗する。そこで、solariumのcomposer.json のrequire-devセクションから、phpunitに対する要求を削除してインストールしてみた。テストをしなければいいのだ、たぶん・・・。
HumHubがいつまでも旧いパッケージに依存しているのでよくないのかもしれないが、それがオープンソースというものだ。万人受けするように作られる最新パッケージに対応してセキュリティも万全オッケーなオープンソースなどありえない、と悟ったふりをして遠くを見つめる目をしよう・・・心のなかでHumHubへの悪態を掃いて捨てるほど唱えながら。

#開発開始
####モジュールの設計
まずはHumHubのモジュールをいつもの雛形にそって作成する。今回はサーチエンジンのプラグインモジュールになるので、engineディレクトリを作ってその中にモジュールを記述しよう。例えばディレクトリの構成は次のようなカンジだ。

    customsearch
      |
      |-engine
      |   |
      |   |-SolrSearch.php
      |
      |-config.php
      |-Events.php
      |-module.json
      |-Module.php

さて、config.phpとEvents.phpの中身だが・・・フックするEventはない。そのため、記述内容は必須事項以上のものがなく、最低限の形で記述した。

####サーチエンジンの指定
HumHubでは、Solrサーチエンジンは、HumHub/protected/config/common.php のcomponentsセクションに'search'アレイを記述して指定する。
https://www.humhub.com/en/marketplace/solr/

デフォルトでは、この'search'アレイ自体存在していないか、'search'アレイの中の'class'キーのみ、'humhub\modules\search\engine\ZendLuceneSearch'と値が記述して存在していている（ハズ）。そこで、それを参考にして、Searchエンジンをカスタムモジュールのものに仕向けることにした。

    'components' => 
    array (
      ....
      'search' => 
        array (
          'class' => 'humhub\modules\customsearch\engine\SolrSearch',
          'host' => 'solr',
          'port' => 8983,
          'path' => '/',
          'core' => 'humhubcore',
        ),
      ....
    ),
 
host,port,path,coreは、solariumに渡すオプション設定値だ（設定値の利用については後述）。

####サーチエンジンの設定
HumHubのデフォルトのサーチエンジン /humhub/protected/modules/search/engine/ZendLuceneSearch.php を解読しながらカスタムサーチエンジンを作っていこうと思う。まず、このファイルに記載されている ZendLuceneSearch class は、 Search class （/humhub/protected/modules/search/engine/Search.php）の拡張（extends）だ。Search class　は、component class の extends による abstract class として宣言されている。いくつかの定数と、find, add, update, delete, flush の 4つの abstract public function と rebuild, optimize, getMetaInfoArray, getDocumentType, setDefaultFindOptions, getAdditionalAttributes の ６つの protected function をもっている。 


####サーチエンジンの実装
abstract function には次の解説がついていた。

    /**
     * Retrieves results from search
     *
     * Available options:
     *      page
     *      pageSize
     *
     *      sortField           Mixed String/Array
     *      model               Mixed String/Array
     *      type                Mixed String/Array
     *      checkPermissions    boolean (TRUE/false)
     *      limitSpaces         Array (Limit Content to given Spaces(
     *      filters             Array (Additional filter Field=>Value)
     *
     * @param type $query
     * @param array $options
     * @return SearchResultSet
     */
    abstract public function find($query, Array $options);

    /**
     * Stores an object in search index.
     *
     * @param Searchable $object
     */
    abstract public function add(Searchable $object);

    /**
     * Updates an object in search index.
     *
     * @param Searchable $object
     */
    abstract public function update(Searchable $object);

    /**
     * Deletes an object from search.
     *
     * @param Searchable $object
     */
    abstract public function delete(Searchable $object);

    /**
     * Deletes all objects from search index.
     */
    abstract public function flush();


abstract public fungion は、これから作るモジュールで自前で実装する必要があるわけで、その必要な働きが記載してあることはありがたい。例えば、flush() には、 solr のコアに登録されているインデックスを全部消すコマンドを書けばいいわけだ。solr の扱いについては別の機会に述べるとして今回は省略することとするので一部の読者には申し訳ないが、いきなりsolariumでの処理を考えるとこんなふうになるだろう。

https://solarium.readthedocs.io/en/stable/getting-started/#deleting-documents


    public function flush()
    {
        $adapter = new \Solarium\Core\Client\Adapter\Http(); // or any other adapter implementing AdapterInterface
        $eventDispatcher = new \Psr\EventDispatcher\EventDispatcherInterface();
        $config = array(
            'endpoint' => array(
                'localhost' => array(
                  // protected/config/common.php で記載した値を引用するように作成するクラス変数                    
                    'host' => $this->host,  
                    'port' => $this->port,
                    'path' => $this->path,
                    'core' => $this->core,
                )
            )
        );
         
        // create a client instance
        $client = new Solarium\Client($adapter, $eventDispatcher, $config);

        // get an update query instance
        $update = $client->createUpdate();

        // add the delete query and a commit command to the update query
        $update->addDeleteQuery('*:*');
        $update->addCommit();

        // this executes the query and returns the result
        $result = $client->update($update);
    }


・・・よめる、よめるぞ私にも！ふあははは・・・と、そこへ冷静なツッコミが。

そりゃ、こいつは簡単だよ、なりそこないの天上人よ。

HumHub側から何の入力もなく、solrに対して何か情報を渡すわけでもなく、solrの特定のコアのインデックスを削除すればいいのだから。

・・・とはいえ、これで一つ、solariumの使い方もわかった。\Solarium\Clientインスタンスを作って、そのquery instanceを使ってあれこれ操作するようだ。よーし・・・残りの、delete, update, add, find もなんとか実装していくぞ！

（後編へ続く）