LIFULL Creators Blog

「株式会社LIFULL(ライフル)」の社員によるブログです。

solr で独自基準ソート(search component plugin 前編)

古川です。

前回 から少し時間がたってしまいましたが、独自ソートを実現の続きで、search component plugin を使って実現する方法を、二回に分けて紹介します。solr が持っている検索の機能をすべて満たした実装は難しいので、

  • 他のコンポーネントとの組み合わせ
  • グルーピング処理
  • 分散検索

はあきらめて、とにかく検索にヒットしたドキュメントを望みの順番で返すことができるsearch component を作成することを目的とします。

今回はまず、solr の search component plugin の作り方を紹介します。前回と違い、とりあえずこうすれば動いたという感じなので、間違えている点あれば、ご指摘いただければ幸いです。

Search Component の作成

solr におけるデフォルトの検索コンポーネントは、SearchComponent クラスを継承して実装されています。

solr/core/src/java/org/apache/solr/handler/component/SearchComponent.java

SearchComponent.java のコードを見ると、以下4つの関数を実装すればよいことが分かります。

  public abstract void prepare(ResponseBuilder rb) throws IOException;
  public abstract void process(ResponseBuilder rb) throws IOException;
  public abstract String getDescription();
  public abstract String getSource();

次に、Solr の検索コンポーネントの本体 QueryComponent.java をベースに、分散と、グルーピングに関連する部分を除いたクラス MyQueryComponent.java を作成します。

solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
package jp.co.homes.searchcomponent;

import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;

import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.*;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.ResultContext;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.schema.FieldType;
import org.apache.solr.search.QParser;
import org.apache.solr.search.QParserPlugin;
import org.apache.solr.search.QueryParsing;
import org.apache.solr.search.ReturnFields;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.search.SolrReturnFields;
import org.apache.solr.search.SyntaxError;

import org.apache.solr.handler.component.SearchComponent;
import org.apache.solr.handler.component.ResponseBuilder;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

public class MyQueryComponent extends SearchComponent {
  public static final String COMPONENT_NAME = "my_query";
  
  @Override
  public void prepare(ResponseBuilder rb) throws IOException {
    SolrQueryRequest req = rb.req;
    SolrParams params = req.getParams();
    if (!params.getBool(COMPONENT_NAME, true)) {
      return;
    }
    SolrQueryResponse rsp = rb.rsp;

    ReturnFields returnFields = new SolrReturnFields( req );
    rsp.setReturnFields( returnFields );
    int flags = 0;
    if (returnFields.wantsScore()) {
      flags |= SolrIndexSearcher.GET_SCORES;
    }
    rb.setFieldFlags( flags );

    String defType = params.get(QueryParsing.DEFTYPE, QParserPlugin.DEFAULT_QTYPE);

    String queryString = rb.getQueryString();
    if (queryString == null) {
      queryString = params.get( CommonParams.Q );
      rb.setQueryString(queryString);
    }

    try {
      QParser parser = QParser.getParser(rb.getQueryString(), defType, req);
      Query q = parser.getQuery();
      if (q == null) {
        q = new BooleanQuery();        
      }
      rb.setQuery( q );
      rb.setSortSpec( parser.getSort(true) );
      rb.setQparser(parser);
      rb.setScoreDoc(parser.getPaging());
      
      String[] fqs = req.getParams().getParams(CommonParams.FQ);
      if (fqs!=null && fqs.length!=0) {
        List<Query> filters = rb.getFilters();
        filters = filters == null ? new ArrayList<Query>(fqs.length) : new ArrayList<Query>(filters);
        for (String fq : fqs) {
          if (fq != null && fq.trim().length()!=0) {
            QParser fqp = QParser.getParser(fq, null, req);
            filters.add(fqp.getQuery());
          }
        }

        if (!filters.isEmpty()) {
          rb.setFilters( filters );
        }
      }
    } catch (SyntaxError e) {
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
    }
  }

  @Override
  public void process(ResponseBuilder rb) throws IOException {
    SolrQueryRequest req = rb.req;
    SolrQueryResponse rsp = rb.rsp;
    SolrParams params = req.getParams();
    if (!params.getBool(COMPONENT_NAME, true)) {
      return;
    }
    SolrIndexSearcher searcher = req.getSearcher();

    if (rb.getQueryCommand().getOffset() < 0) {
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "'start' parameter cannot be negative");
    }

    long timeAllowed = (long)params.getInt( CommonParams.TIME_ALLOWED, -1 );

    SolrIndexSearcher.QueryCommand cmd = rb.getQueryCommand();
    cmd.setTimeAllowed(timeAllowed);
    SolrIndexSearcher.QueryResult result = new SolrIndexSearcher.QueryResult();

    searcher.search(result,cmd);
    rb.setResult( result );

    ResultContext ctx = new ResultContext();
    ctx.docs = rb.getResults().docList;
    ctx.query = rb.getQuery();
    rsp.add("response", ctx);
    rsp.getToLog().add("hits", rb.getResults().docList.matches());
  }

  @Override
  public String getDescription() {
    return "my query component";
  }
  
  @Override
  public String getSource() {
    return "$URL: dummy $";
  }
}

コンパイル

以下のjarファイルにpathを通して、上記ファイルをコンパイルして、homes-serch-component.jar を作成します(jar ファイル名は何でも構いません)

solr-4.6.1/example/solr-webapp/webapp/WEB-INF/lucene-core-4.6.1.jar
solr-4.6.1/example/solr-webapp/webapp/WEB-INF/solr-core-4.6.1.jar
solr-4.6.1/example/solr-webapp/webapp/WEB-INF/solr-solrj-4.6.1.jar

設定

前回同様、作成したjarファイルをcollection1フォルダのlib以下にコピーします。

cp homes-function-search-component.jar solr-4.6.1/example/solr/collection1/lib

solrconfig.xml の、hilighting コンポーネント設定の後に、今回作成した MyQueryComponent にアクセスするための設定を追加します。これで、http://localhost:8983/solr/collection1/my_select にアクセスするとMyQueryComponentにクエリが渡されるようになります。

  <searchComponent class="solr.HighlightComponent" name="highlight">
    <highlighting>
      <!-- 省略 実際にはhighligの設定がある-->
    </highlighting>
  </searchComponent>

  <!-- 追加分 -->
  <searchComponent name="MyQueryComponent" class="jp.co.homes.searchcomponent.MyQueryComponent" />
  <requestHandler name="/my_select" class="solr.SearchHandler">
     <arr name="components">
       <str>MyQueryComponent</str>
     </arr>
  </requestHandler>

動作確認

solr を起動します。solr には前回投入したインデックスが既に存在していると仮定します。

cd ./solr-4.6.1/example
java -jar start.jar &

通常のsolr検索selectと、新しく作成したmy_selectの検索結果が同じであることを確認します。

  • select
http://localhost:8983/solr/collection1/select?echoParams=none&q=*:*&fl=x,y,myfunc(x,y,1,2,3,4,5,6)&sort=x asc,y desc
<result name="response" numFound="1000006" start="0">
  <doc>
    <str name="id">id828205</str>
    <float name="x">0.0</float>
    <float name="y">999.0</float>
  </doc>
  <doc>
    <str name="id">id91438</str>
    <float name="x">0.0</float>
    <float name="y">998.0</float>
  </doc>
  <doc>
    <str name="id">id628349</str>
    <float name="x">0.0</float>
    <float name="y">998.0</float>
  </doc>
</result>
  • my_select
http://localhost:8983/solr/collection1/my_select?echoParams=none&q=*:*&fl=x,y,myfunc(x,y,1,2,3,4,5,6)&sort=x asc,y desc
<result name="response" numFound="1000006" start="0">
  <doc>
    <str name="id">id828205</str>
    <float name="x">0.0</float>
    <float name="y">999.0</float>
  </doc>
  <doc>
    <str name="id">id91438</str>
    <float name="x">0.0</float>
    <float name="y">998.0</float>
  </doc>
  <doc>
    <str name="id">id628349</str>
    <float name="x">0.0</float>
    <float name="y">998.0</float>
  </doc>
</result>

グルーピングや、分散検索以外は同じはずなので、当たり前ですが同じ結果が取得できています。

次に、グルーピングを行うクエリを実行してみます。

  • select
http://localhost:8983/solr/collection1/select?group=true&group.field=x&echoParams=none&q=*:*&fl=x,y,myfunc(x,y,1,2,3,4,5,6)&rows=2
<lst name="grouped">
  <lst name="x">
    <int name="matches">1000006</int>
    <arr name="groups">
      <lst>
        <float name="groupValue">1.0</float>
        <result name="doclist" numFound="997" start="0">
          <doc>
            <float name="x">1.0</float>
            <float name="y">4.0</float>
            <float name="myfunc(x,y,1,2,3,4,5,6)">87.0</float>
         </doc>
       </result>
     </lst>
     <lst>
       <float name="groupValue">2.0</float>
       <result name="doclist" numFound="975" start="0">
         <doc>
           <float name="x">2.0</float>
           <float name="y">5.0</float>
           <float name="myfunc(x,y,1,2,3,4,5,6)">138.0</float>
         </doc>
       </result>
     </lst>
   </arr>
  </lst>
</lst>
  • my_select
http://localhost:8983/solr/collection1/my_select?group=true&group.field=x&echoParams=none&q=*:*&fl=x,y,myfunc(x,y,1,2,3,4,5,6)&rows=2
<result name="response" numFound="1000006" start="0">
  <doc>
    <float name="x">1.0</float>
    <float name="y">4.0</float>
    <float name="myfunc(x,y,1,2,3,4,5,6)">87.0</float>
  </doc>
  <doc>
    <float name="x">2.0</float>
    <float name="y">5.0</float>
    <float name="myfunc(x,y,1,2,3,4,5,6)">138.0</float>
  </doc>
</result>

グルーピング関連の部分を削除したので、my_select の方は、groupを指定しても、通常の検索が行われていることが分かります。

まとめ

グルーピングや分散処理を除けば、SearchComponent は意外とシンプルな構造でした。次回は、MySearchComponent.java に以前紹介した 自作のCollectorクラスを使う方法を組み込んで、独自ソートを実現します。