タグ: Solr

SolrをPHPで使う(検索編)

前回の記事でSolrをPHPから使う準備ができました。
今回はPHPで Solr の検索を実行する方法です。

検索結果のオブジェクトは以下のような構造になっています。

SolrObject Object
(
    [responseHeader] => SolrObject Object
        (
            [zkConnected] => 1
            [status] => 0
            [QTime] => 0
            [params] => SolrObject Object
                (
                    [q] => *:*
                    [indent] => on
                    [start] => 0
                    [rows] => 2
                    [version] => 2.2
                    [wt] => xml
                )

        )

    [response] => SolrObject Object
        (
            [numFound] => 52
            [start] => 0
            [docs] => Array
                (
                    [0] => SolrDocument Object
                        (
                            [_hashtable_index:SolrDocument:private] => 24804
                        )

                    [1] => SolrDocument Object
                        (
                            [_hashtable_index:SolrDocument:private] => 5340
                        )

                )

        )

)

docs という配列の要素である SolrDocument オブジェクトは SolrDocumentField オブジェクトを複数含んでいます。SolrDocument 内のすべての SolrDocumentField をたどるために current() や next() などのメソッドを利用します。

SolrObject ベースの検索結果は意外と扱いづらいので、JSON で扱えるようにしてみます。

SolrClient 作成時のオプションとして wt=json を追加しています。

getRawResponse() の応答は以下の通りです。

{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":0,
    "params":{
      "q":"*:*",
      "indent":"on",
      "start":"0",
      "rows":"2",
      "version":"2.2",
      "wt":"json"}},
  "response":{"numFound":52,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"0553573403",
        "cat":["book"],
        "name":"A Game of Thrones",
        "price":7.99,
        "price_c":"7.99,USD",
        "inStock":true,
        "author":"George R.R. Martin",
        "author_s":"George R.R. Martin",
        "series_t":"A Song of Ice and Fire",
        "sequence_i":1,
        "genre_s":"fantasy",
        "_version_":1730974266238697472,
        "price_c____l_ns":799},
      {
        "id":"0553579908",
        "cat":["book"],
        "name":"A Clash of Kings",
        "price":7.99,
        "price_c":"7.99,USD",
        "inStock":true,
        "author":"George R.R. Martin",
        "author_s":"George R.R. Martin",
        "series_t":"A Song of Ice and Fire",
        "sequence_i":2,
        "genre_s":"fantasy",
        "_version_":1730974266279591936,
        "price_c____l_ns":799}]
  }}

これを json_decode() することで、通常の PHP 配列として扱うことができます。

Array
(
    [responseHeader] => Array
        (
            [zkConnected] => 1
            [status] => 0
            [QTime] => 0
            [params] => Array
                (
                    [q] => *:*
                    [indent] => on
                    [start] => 0
                    [rows] => 2
                    [version] => 2.2
                    [wt] => json
                )

        )

    [response] => Array
        (
            [numFound] => 52
            [start] => 0
            [numFoundExact] => 1
            [docs] => Array
                (
                    [0] => Array
                        (
                            [id] => 0553573403
                            [cat] => Array
                                (
                                    [0] => book
                                )

                            [name] => A Game of Thrones
                            [price] => 7.99
                            [price_c] => 7.99,USD
                            [inStock] => 1
                            [author] => George R.R. Martin
                            [author_s] => George R.R. Martin
                            [series_t] => A Song of Ice and Fire
                            [sequence_i] => 1
                            [genre_s] => fantasy
                            [_version_] => 1730974266238697472
                            [price_c____l_ns] => 799
                        )

                    [1] => Array
                        (
                            [id] => 0553579908
                            [cat] => Array
                                (
                                    [0] => book
                                )

                            [name] => A Clash of Kings
                            [price] => 7.99
                            [price_c] => 7.99,USD
                            [inStock] => 1
                            [author] => George R.R. Martin
                            [author_s] => George R.R. Martin
                            [series_t] => A Song of Ice and Fire
                            [sequence_i] => 2
                            [genre_s] => fantasy
                            [_version_] => 1730974266279591936
                            [price_c____l_ns] => 799
                        )

                )

        )

)

SolrをPHPで使う(インストール編)

PHPではPECLで Solr Extension が用意されています。
https://www.php.net/manual/ja/book.solr.php

Solr Extension をインストールするためには、まず PECL を使えるようにします。(以下、Debian での手順です)

$ sudo apt install php-pear php-dev
$ sudo pecl channel-update pecl.php.net
$ ecl search solr

Matched packages, channel pecl.php.net:
=======================================
Package Stable/(Latest) Local
solr    2.5.1 (stable)        The Apache Solr PHP extension is an extremely fast,
                                  light-weight, feature-rich library that allows PHP applications to
                                  communicate easily and efficiently with Apache Solr server instances
                                  using an object-oriented API.

Solr Extension をコンパイルするのに必要なライブラリをインストールします。

$ sudo apt install libcurl4-openssl-dev libxml2-dev
$ mkdir -p /tmp/libcurl4/include
$ ln -s /usr/include/x86_64-linux-gnu/curl /tmp/libcurl4/include
$ mkdir -p /tmp/libxml2/include
$ ln -s /usr/include/libxml2/libxml /tmp/libxml2/include/libxml2

PECL が想定しているディレクトリ構成に合わせるためにシンボリックリンクを張っています。

ここまで準備ができたらインストール。

$ sudo pecl install solr
(略)
Build process completed successfully
Installing '/usr/lib/php/20190902/solr.so'
install ok: channel://pecl.php.net/solr-2.5.1
configuration option "php_ini" is not set to php.ini location
You should add "extension=solr.so" to php.ini

途中で libcurl と libxml2 の include パスを聞かれるので、それぞれ /tmp/libcurl /tmp/libxml2 を指定します。

コンパイルが正常に終了したら PHP から利用できるように設定します。

$ sudo sh -c "echo extension=solr.so > /etc/php/7.4/mods-available/solr.ini"
$ sudo ln -s /etc/php/7.4/mods-available/solr.ini /etc/php/7.4/cli/conf.d/30-solr.ini
$ sudo ln -s /etc/php/7.4/mods-available/solr.ini /etc/php/7.4/apache2/conf.d/30-solr.ini
$ sudo service apache2 restart

[Solr]Implicit RequestHander はどのように定義されているか

リファレンスによると、Implicit RequestHander は solrconfig.xml に設定を書かなくても使用できるリクエストハンドラである、と書かれています。
これらの Implicit RequestHander がどこでどのように定義されているのかを調べてみました。

Solr のソースでそれらしいものを探してみると solr/core/src/resources/ImplicitPlugins.json というファイルが見つかりました。

{
  "requestHandler": {
    "/update": {
      "useParams":"_UPDATE",
      "class": "solr.UpdateRequestHandler"
    },
    "/update/json": {
      "useParams":"_UPDATE_JSON",
      "class": "solr.UpdateRequestHandler",
      "invariants": {
        "update.contentType": "application/json"
      }
    },
    "/update/csv": {
      "useParams":"_UPDATE_CSV",
      "class": "solr.UpdateRequestHandler",
      "invariants": {
        "update.contentType": "application/csv"
      }
    },
    "/update/json/docs": {
      "useParams":"_UPDATE_JSON_DOCS",
      "class": "solr.UpdateRequestHandler",
      "invariants": {
        "update.contentType": "application/json",
        "json.command": "false"
      }
    },
(略)

これを読み込んでいるのが
solr/core/src/java/org/apache/solr/core/SolrCore.java
です。該当箇所は以下の通り。読み込んだ定義情報は getImplicitHandlers() で取得できます。

  private static final class ImplicitHolder {
    private ImplicitHolder() { }
    private static final List INSTANCE;
    static {
      @SuppressWarnings("unchecked")
      Map implicitPluginsInfo = (Map) Utils.fromJSONResource("ImplicitPlugins.json");
      @SuppressWarnings("unchecked")
      Map> requestHandlers = (Map>) implicitPluginsInfo.get(SolrRequestHandler.TYPE);
      List implicits = new ArrayList<>(requestHandlers.size());
      for (Map.Entry> entry : requestHandlers.entrySet()) {
        Map info = entry.getValue();
        info.put(CommonParams.NAME, entry.getKey());
        implicits.add(new PluginInfo(SolrRequestHandler.TYPE, info));
      }
      INSTANCE = Collections.unmodifiableList(implicits);
    }
  }
  public List getImplicitHandlers() {
    return ImplicitHolder.INSTANCE;
  }

RequestHanders クラスの初期化処理の中で、solrconfig.xml で定義されて RequestHander と implicit として定義されている RequestHander (SolrCore.getImplicitHanders()で取得)の両方が読み込まれて初期化されています。

  void initHandlersFromConfig(SolrConfig config) {
    List implicits = core.getImplicitHandlers();
    // use link map so we iterate in the same order
    Map infoMap= new LinkedHashMap<>();
    //deduping implicit and explicit requesthandlers
    for (PluginInfo info : implicits) infoMap.put(info.name,info);
    for (PluginInfo info : config.getPluginInfos(SolrRequestHandler.class.getName())) infoMap.put(info.name, info);
    ArrayList infos = new ArrayList<>(infoMap.values());
    List modifiedInfos = new ArrayList<>();
    for (PluginInfo info : infos) {
      modifiedInfos.add(applyInitParams(config, info));
    }
    handlers.init(Collections.emptyMap(),core, modifiedInfos);
    handlers.alias(handlers.getDefault(), "");
    if (log.isDebugEnabled()) {
      log.debug("Registered paths: {}", StrUtils.join(new ArrayList<>(handlers.keySet()), ','));
    }
    if (handlers.get("") == null && !handlers.alias("/select", "")) {
      if (handlers.get("") == null && !handlers.alias("standard", "")) {
        log.warn("no default request handler is registered (either '/select' or 'standard')");
      }
    }
  }

この時点で Implicit RequestHandler は solrconfig.xml で定義されているものと同等の扱いになっている訳です。

[Solr]solrconfig.xmlのinitParamsについて

solrconfig.xml には initParams というセクションがあります。これはリクエストハンドラのパラメータを指定するためのものです。リクエストハンドラのパラメータは基本的にそのハンドラの定義内で設定するものですが、initParams セクションには以下の役割があります。

  • いくつかのリクエストハンドラで共通するのパラメータのセットを1箇所で定義する
  • solrconfig.xml に定義の無い Implicit RequestHandler のパラメータを指定する

たとえば _default コンフィグセットの solrconfig.xml には以下の initParams 定義があります。

<initParams path="/update/**,/query,/select,/spell">
  <lst name="defaults">
    <str name="df">_text_</str>
  </lst>
</initParams>

path 属性で指定されているのは各リクエストハンドラのパスです。
アスタリスク2個のワイルドカードはそのパス以下のすべての階層を意味します。(アスタリスク1個なら1レベル下の階層まで)

initParams は name 属性を持つこともできます。たとえば以下の定義だとすると

<initParams path="/update/**,/query,/select,/spell" name="defaultParams">
  <lst name="defaults">
    <str name="df">_text_</str>
  </lst>
</initParams>

リクエストハンドラの定義において “defaultParams” という名前で参照できます。

<requestHandler name="/demo" class="DemoHandler" initParams="defaultParams"/>

各パラメータは defaults, appends, invariants のいずれかのオプションを指定できます。
それぞれのオプションの意味は以下の通りです。

  • defaults クエリ実行時に該当パラメータが指定されなかった場合のデフォルト値として使われる。
  • appends クエリ実行時に指定されたパラメータに追加して追加して使われる。
  • invariants クエリ実行時に必ず使われる。ユーザが指定したパラメータで上書きされることはない。

[Solr]コレクションのコロケーション

はじめに

Solr にはコレクションのコロケーションの機能があります。
この機能はコレクションを跨いだJOINのために必要なものです。

コレクション作成時の指定

コレクション作成APIでwithCollectionパラメータを指定できます。
たとえば以下のリクエストでコレクションc1と同じ場所にコレクションc2を作成します。

/admin/collections?action=CREATE&name=c2&numShards=1&replicationFactor=2&withCollection=c1

ここでいう「同じ場所に」というのは、c1のレプリカが存在するいずれかのSolrノードと同じノード上に、という意味です。

既存のコレクションのコロケーション

コレクション変更APIもwithCollectionパラメータをサポートします。
たとえば以下のようなリクエストです。

/admin/collections?action=MODIFYCOLLECTION&collection=c3&withCollection=c1

ただし、この変更は自動的にはクラスタに反映されません。
多数のレプリカを直ちに移動させ始めるとシステムが不安定になる恐れがあるからです。
このリクエスト実行後に手動でどのようにクラスタを変更させるべきかを知るために、Solr Admin UI の Suggestion ページを利用できます。

コロケーション指定されたコレクションの削除

コロケーションのリンク付けをされたコレクションを削除する場合、先にコレクション変更APIでwithCollection属性を解除しておく必要があります。

/admin/collections?action=MODIFYCOLLECTION&collection=c2&withCollection=

注意事項

  • 1つのコレクションに対してwithCollectionでコロケーション指定できるコレクションは1つだけです。ただしwithCollectionによるリンクを繋げることで任意の数のコレクションをコロケーションさせることができます。
  • withCollection 指定するコレクションが持つシャードは1つだけで、かつその名前は shard1 でなければなりません。