Luceneのコードを書いていて、いつも同じようなコードの変形パターンみたいな書き方をしていましたが、基本パターン的なものを書いていなかったのでいつもあちこちを確認しながら書いている感じでした…。
なので、ちょっとまとめておきます。完全に個人的メモです。
まずは依存関係の定義。
build.sbt name := "lucene-basic-template" version := "0.0.1-SNAPSHOT" scalaVersion := "2.10.3" organization := "org.littlewings" scalacOptions ++= Seq("-deprecation") { val luceneVersion = "4.6.0" libraryDependencies ++= Seq( "org.apache.lucene" % "lucene-queryparser" % luceneVersion, "org.apache.lucene" % "lucene-analyzers-kuromoji" % luceneVersion ) }
初めて、sbtでvalを使ってみました。
続いて、コードのサンプル。やることは
- Documentの登録
- 対話形式でのQuery受付と、結果表示
です。
src/main/scala/org/littlewings/lucene/basic/LuceneBasic.scala
package org.littlewings.lucene.basic import scala.collection.JavaConverters._ import scala.util.{Failure, Try} import org.apache.lucene.analysis.Analyzer import org.apache.lucene.analysis.ja.JapaneseAnalyzer import org.apache.lucene.document.{Document, Field, StringField, TextField} import org.apache.lucene.index.{DirectoryReader, IndexWriter, IndexWriterConfig} import org.apache.lucene.queryparser.classic.QueryParser import org.apache.lucene.search.{IndexSearcher, Query, Sort, SortField} import org.apache.lucene.search.{ScoreDoc, TopDocs, TopFieldCollector, TotalHitCountCollector, TopScoreDocCollector} import org.apache.lucene.store.{Directory, RAMDirectory} import org.apache.lucene.util.Version object LuceneBasic { def main(args: Array[String]): Unit = { val version = Version.LUCENE_CURRENT val analyzer = createAnalyzer(version) for (directory <- new RAMDirectory) { registryDocuments(directory, version, analyzer) queryWhile(directory, version, analyzer) } } private def createAnalyzer(version: Version): Analyzer = new JapaneseAnalyzer(version) private def registryDocuments(directory: Directory, version: Version, analyzer: Analyzer): Unit = for (writer <- new IndexWriter(directory, new IndexWriterConfig(version, analyzer))) { Array( createDocument { Map("isbn" -> "978-4774127804", "title" -> "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築", "price" -> 3360, "summary" -> "Luceneは全文検索システムを構築するためのJavaのライブラリです。") }, createDocument { Map("isbn" -> "978-4774161631", "title" -> "[改訂新版] Apache Solr入門 オープンソース全文検索エンジン", "price" -> 3780, "summary" -> "最新版Apaceh Solr Ver.4.5.1に対応するため大幅な書き直しと原稿の追加を行い、現在の開発環境に合わせて完全にアップデートしました。Apache Solrは多様なプログラミング言語に対応した全文検索エンジンです。") }, createDocument { Map("isbn" -> "978-4797352009", "title" -> "集合知イン・アクション", "price" -> 3990, "summary" -> "レコメンデーションエンジンをつくるには?ブログやSNSのテキスト分析、ユーザー嗜好の予測モデル、レコメンデーションエンジン……Web 2.0の鍵「集合知」をJavaで実装しよう!") } ).foreach(writer.addDocument) writer.commit() } private def createDocument(entry: Map[String, Any]): Document = { val document = new Document document.add(new StringField("isbn", entry("isbn").toString, Field.Store.YES)) document.add(new TextField("title", entry("title").toString, Field.Store.YES)) document.add(new StringField("price", entry("price").toString, Field.Store.YES)) document.add(new TextField("summary", entry("summary").toString, Field.Store.YES)) document } private def queryWhile(directory: Directory, version: Version, analyzer: Analyzer): Unit = for (reader <- DirectoryReader.open(directory)) { val searcher = new IndexSearcher(reader) val queryParser = new QueryParser(version, "title", analyzer) val limit = 1000 def parseQuery(queryString: String): Try[Query] = Try(queryParser.parse(queryString)).recoverWith { case e => println(s"Invalid Query[$queryString], Reason: $e") Failure(e) } def search(query: Query): (Int, Array[ScoreDoc]) = { println(s" Input Query => [$query]") val totalHitCountCollector = new TotalHitCountCollector searcher.search(query, totalHitCountCollector) val totalHits = totalHitCountCollector.getTotalHits val docCollector = TopFieldCollector.create(Sort.RELEVANCE, limit, true, // fillFields false, // trackDocScores false, // traxMaxScore false) // docScoreInOrder searcher.search(query, docCollector) val topDocs = docCollector.topDocs val hits = topDocs.scoreDocs (totalHits, hits) } Iterator .continually(readLine("Query> ")) .takeWhile(_ != "exit") .withFilter(line => line != null && !line.isEmpty) .map(parseQuery) .withFilter(_.isSuccess) .map(query => search(query.get)) .foreach { case (totalHits, hits) => if (totalHits > 0) { println(s" ${totalHits}件ヒットしました") hits.foreach { h => val hitDoc = searcher.doc(h.doc) println(s" ScoreDoc, id[${h.score}:${h.doc}]: Doc => " + hitDoc .getFields .asScala .map(_.stringValue) .mkString("|")) } } else { println("ヒット件数は0です") } println() } } implicit class AutoCloseableWrapper[A <: AutoCloseable](val underlying: A) extends AnyVal { def foreach(fun: A => Unit): Unit = try { fun(underlying) } finally { underlying.close() } } }
これを基本パターンに使えば、今までの(自分の)苦労はきっと減るはず!そんな、なんでもないエントリです。
今回のコードは、こちらにアップしています。
https://github.com/kazuhira-r/lucene-examples/tree/master/lucene-basic-template
合わせて、こちらのエントリも参考に。
LuceneのAnalyzer、KuromojiのModeごとの挙動を確認する - CLOVER
LuceneのClassic QueryParserを使ってみる - CLOVER
Luceneでソート - CLOVER
http://d.hatena.ne.jp/Kazuhira/20130714/1373821387
LuceneのSearcherManagerを使う - CLOVER
LuceneのAnalyzer/Tokenizerを、フィールドごとに切り替える - CLOVER