Solr vs elasticsearch 類似文書検索(MoreLikeThis)


こんにちは、馬場です。
Solrとelasticsearchを比較するシリーズ、最終回は類似文書検索機能について比較します。

Solrとelasticsearch、両方のベースとなっているLuceneにはMoreLikeThisという類似文書検索の機能が実装されています。両者とも当然LuceneのMoreLikeThisを利用して類似文書検索機能を提供していますが、API の形式などはかなり異なります。

Solrの場合

Solrでは、類似文書検索のプログラムは以下のようになります。

import org.apache.solr.client.solrj.impl.HttpSolrServer
import org.apache.solr.client.solrj.SolrQuery
import org.apache.solr.common.util.NamedList
import org.apache.solr.common.SolrDocument

object MoreLikeThis {
  val url = "http://localhost:8983/solr"
  val count = 6

  /**
   * id で指定されたドキュメントと似たドキュメントをcountの数だけ取得する
   * @param id
   * @return 似た記事のIDのList
   */
  def getRelatedDocs(id: Int, count: Int = count): List[String] = {

    val server = new HttpSolrServer(url)

    val query = new SolrQuery("id:" + id)
    query.set("mlt", true)
    query.set("mlt.fl", "title,body")
    query.set("mlt.midf", 1)
    query.set("mlt.mintf", 1)
    query.set("mlt.count", count)
    query.set("fl", "id,score,title,body")

    val response = server.query(query)
    val doc = response.getResults.get(0)

    response.getResponse().get("moreLikeThis").asInstanceOf[NamedList[Object]]
      .getVal(0).asInstanceOf[java.util.List[SolrDocument]]
      .toList.map(_.get("id").toString))

  }

}

elasticsearchの場合

elasticsearchでは類似文書検索のプログラムは以下のようになります。

import scala.collection.JavaConversions._
import org.elasticsearch.client.transport.TransportClient
import org.elasticsearch.common.transport.InetSocketTransportAddress
import org.elasticsearch.common.settings.ImmutableSettings

object MoreLikeThis {
  val count = 6

    /**
   * id で指定されたドキュメントと似たドキュメントをcountの数だけ取得する
   * @param id
   * @return 似た記事のIDのList
   */
  def getRelatedDocs(id: String, count: Int = count): List[String] = {
    val settings = ImmutableSettings.settingsBuilder()
      .put("client.transport.sniff", false).build()
    val client = new TransportClient(settings)
      .addTransportAddress(new InetSocketTransportAddress("localhost", 9300))

    val response=client.prepareMoreLikeThis("test", "docs", id.toInt)
      .setField("title", "body")
      .setSearchTypes("docs").execute().actionGet()

    response.getHits.getHits.take(count).map (_.id().toString).toList

  }

}

Solrと同じようにmidf/mintf の設定を1にすることはできるのですが、その場合結果が返ってきません。

// この設定だと検索結果は0件になる
val response=client.prepareMoreLikeThis("test", "docs", id.toInt)
      .setMinDocFreq(1)
      .setMinTermFreq(1)
      .setField("title", "body")
      .setSearchTypes("docs").execute().actionGet()

類似文書検索は、文書のIDを指定する方法だけではなく、文書のタイトルや本文そのものを指定する方法もあります。

import scala.collection.JavaConversions._
import org.elasticsearch.client.transport.TransportClient
import org.elasticsearch.common.transport.InetSocketTransportAddress
import org.elasticsearch.common.settings.ImmutableSettings
import org.elasticsearch.index.query.{FilterBuilders,  QueryBuilders}


object MoreLikeThis {
  val count = 6

  def getRelatedDocs(id: String, title:String, body:String,  count: Int = count): List[String] = {
    val settings = ImmutableSettings.settingsBuilder()
      .put("client.transport.sniff", false).build()
    val client = new TransportClient(settings)
      .addTransportAddress(new InetSocketTransportAddress("localhost", 9300))

    val query = QueryBuilders.moreLikeThisQuery("title", "body")
      .likeText(title + "\n" + body)
    val response = client.prepareSearch("test").setQuery(query)
      .setFilter(FilterBuilders.notFilter(FilterBuilders.idsFilter("docs").addIds(id))) //検索結果からidのドキュメントを除外する
      .setFrom(0).setSize(count).setExplain(true).execute().actionGet

    response.getHits.getHits.take(count).map (_.id().toString).toList

  }

}

下記のプログラムの結果の方が、Solrの検索結果と似ています(が、完全に一致しません...)。内部的に何か違うのかもしれません。

まとめ

検索API は、Solr が Map を構築して渡すのに対して、elasticsearchはメソッドをつなげて行く形なので書いていて心地よいです。ただ、ここでもelasticsearch のドキュメントのIDを指定して類似文書検索をする方法はマニュアルには書いてなく、APIドキュメントから「発見」したものですので、elasticsearchはまだまだ発展中だと感じました。

3回にわたって、Solr と elasticsearchの比較を行いました。実際にどちらを採用するか決定する場合、運用やパフォーマンスも要因となると思いますが、この記事がすこしでもみなさんの参考になればと思います。


This entry was posted in その他. Bookmark the permalink.