Apache Solrを使った、Webクローリングと全文検索をどうやってやろうかなぁ〜と思いまして。
参考)
Comparison of existing open-source tools forWeb crawling and indexing of free Music
http://ja.scribd.com/doc/123153248/Comparison-of-existing-open-source-tools-for-Web-crawling-and-indexing-of-free-Music#scribd
FessやNutchのようなものを使ってもよいのですが、今回はある程度カスタマイズなどを前提にして、自分でプログラムを書くタイプのものがいいなぁと思いまして。
で、今回選んだのがこちらのcrawler4j。
crawler4j
https://github.com/yasserg/crawler4j
Javaでクローラーを作る(crawler4j)
http://qiita.com/MT-01/items/78691f946ba928387153
Javaでクローラを実装する
http://qiita.com/petitviolet/items/e3b850fece36a176bf97
全文検索エンジンを作ろうと思って、まずはクローラーを作ってみた
http://white-azalea.hatenablog.jp/entry/2015/05/24/165125
いくつか日本語情報もあるので、これらも参考に。
今回は、crawler4jを使ってこのサイト(はてなの自分のページ)の2015年分をクローリングして、Solrにインデキシングするところまでやってみます。
準備
Maven依存関係。
まずはcrawler4j。
<dependency> <groupId>edu.uci.ics</groupId> <artifactId>crawler4j</artifactId> <version>4.1</version> </dependency>
その他、SolrにアクセスするためにSolrJ,HTMLの中身を解析するためにjsoup、あとはロギングでslf4j-simpleをとりあえず入れました。
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.12</version> </dependency> <dependency> <groupId>org.apache.solr</groupId> <artifactId>solr-solrj</artifactId> <version>5.3.0</version> </dependency> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.8.3</version> </dependency>
プログラムを書く
crawler4jの使い方は、GitHubのQuickstartを見るのがよさそうです。
Quickstart
https://github.com/yasserg/crawler4j#quickstart
これで、たいていのことはできる気がします。
今回は、Solrにインデックスを作ることを踏まえてプログラムを書きます。
まずは、クローラーそのものの実装。
src/main/java/org/littlewings/mycrawler/MyLittleCrawler.java
package org.littlewings.mycrawler; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import edu.uci.ics.crawler4j.crawler.Page; import edu.uci.ics.crawler4j.crawler.WebCrawler; import edu.uci.ics.crawler4j.parser.HtmlParseData; import edu.uci.ics.crawler4j.url.WebURL; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyLittleCrawler extends WebCrawler { private final static Pattern FILTERS = Pattern.compile(".*(\\.(css|js|gif|jpg" + "|png|mp3|mp3|zip|gz))$"); private Logger logger = LoggerFactory.getLogger(getClass()); @Override public boolean shouldVisit(Page referringPage, WebURL url) { String hrefUrl = url.getURL(); String href = hrefUrl.toLowerCase(); return !FILTERS.matcher(href).matches() && hrefUrl.matches("http://d\\.hatena\\.ne\\.jp/Kazuhira/2015\\d\\d.+"); } @Override public void visit(Page page) { String url = page.getWebURL().getURL(); logger.info("URL: {}", url); if (page.getParseData() instanceof HtmlParseData) { HtmlParseData htmlParseData = (HtmlParseData) page.getParseData(); String html = htmlParseData.getHtml(); Set<WebURL> links = htmlParseData.getOutgoingUrls(); logger.info("Number of outgoing links: {}", links.size()); Document jsoupDoc = Jsoup.parse(html); String title = jsoupDoc.title(); Element elm = jsoupDoc.select("div.body div.section").get(0); String text = elm.text(); logger.info("add Solr Document."); Map<String, Object> document = new LinkedHashMap<>(); document.put("url", url); document.put("title", title); document.put("contents", text); Solr.getInstance().addDocument(document); } } }
画像やCSSなどは省いて、うちのブログの2015年のもののみを対象に。
@Override public boolean shouldVisit(Page referringPage, WebURL url) { String hrefUrl = url.getURL(); String href = hrefUrl.toLowerCase(); return !FILTERS.matcher(href).matches() && hrefUrl.matches("http://d\\.hatena\\.ne\\.jp/Kazuhira/2015\\d\\d.+"); }
visitメソッドの中では、HTMLの中からタイトルとセレクタで指定した範囲を抜きだし、Solrのインデックスに登録します。
HtmlParseData htmlParseData = (HtmlParseData) page.getParseData(); String html = htmlParseData.getHtml(); Set<WebURL> links = htmlParseData.getOutgoingUrls(); logger.info("Number of outgoing links: {}", links.size()); Document jsoupDoc = Jsoup.parse(html); String title = jsoupDoc.title(); Element elm = jsoupDoc.select("div.body div.section").get(0); String text = elm.text(); logger.info("add Solr Document."); Map<String, Object> document = new LinkedHashMap<>(); document.put("url", url); document.put("title", title); document.put("contents", text); Solr.getInstance().addDocument(document);
起動元となる、Mainクラスはこちら。
src/main/java/org/littlewings/mycrawler/Launcher.java
package org.littlewings.mycrawler; import edu.uci.ics.crawler4j.crawler.CrawlConfig; import edu.uci.ics.crawler4j.crawler.CrawlController; import edu.uci.ics.crawler4j.fetcher.PageFetcher; import edu.uci.ics.crawler4j.robotstxt.RobotstxtConfig; import edu.uci.ics.crawler4j.robotstxt.RobotstxtServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Launcher { public static void main(String... args) throws Exception { Logger logger = LoggerFactory.getLogger(Launcher.class); String crawlStorageFolder = "data/crawl/strage"; int numberOfCrawlers = 7; CrawlConfig config = new CrawlConfig(); config.setCrawlStorageFolder(crawlStorageFolder); config.setPolitenessDelay(1 * 1000); // 1秒ごとにリクエスト PageFetcher pageFetcher = new PageFetcher(config); RobotstxtConfig robotstxtConfig = new RobotstxtConfig(); RobotstxtServer robotstxtServer = new RobotstxtServer(robotstxtConfig, pageFetcher); CrawlController controller = new CrawlController(config, pageFetcher, robotstxtServer); logger.info("add Seed URL under... {}", "http://d.hatena.ne.jp/Kazuhira/"); controller.addSeed("http://d.hatena.ne.jp/Kazuhira/"); logger.info("Start crawler."); controller.start(MyLittleCrawler.class, numberOfCrawlers); logger.info("commit!!"); Solr.getInstance().commit(); Solr.getInstance().close(); } }
クローラーはスレッド7つで起動するようにして、1秒のスリープが入るように設定しています。
また、最後にSolrにコミットして、シャットダウンします。
Solrって書かれたクラスの内容は、こちら。ただのSolrJの簡易ラッパーです。
src/main/java/org/littlewings/mycrawler/Solr.java
package org.littlewings.mycrawler; import java.io.IOException; import java.util.Map; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.HttpSolrClient; import org.apache.solr.common.SolrInputDocument; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Solr { private static final Solr INSTANCE = new Solr(); private SolrClient client; private Logger logger = LoggerFactory.getLogger(getClass()); private Solr() { PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(cm) .build(); client = new HttpSolrClient("http://localhost:8983/solr/mycore", httpClient); } public static Solr getInstance() { return INSTANCE; } public void addDocument(Map<String, Object> document) { try { SolrInputDocument solrDocument = new SolrInputDocument(); document.entrySet().forEach(entry -> solrDocument.addField(entry.getKey(), entry.getValue())); client.add(solrDocument); } catch (IOException | SolrServerException e) { logger.info("add Document error.", e); } } public void commit() { try { client.commit(); } catch (IOException | SolrServerException e) { logger.info("commit error.", e); } } public void close() { try { client.close(); } catch (IOException e) { logger.info("close error.", e); } } }
SolrJの使い方は、こちらを参考に。
Using SolrJ
https://cwiki.apache.org/confluence/display/solr/Using+SolrJ
Solr側の設定
Solr側のスキーマ定義は、このように。
<field name="url" type="string" indexed="true" stored="true" required="true" multiValued="false" /> <field name="title" type="text_ja" indexed="true" stored="true" required="true" multiValued="false" /> <field name="contents" type="text_ja" indexed="true" stored="true" required="true" multiValued="false" /> <uniqueKey>url</uniqueKey>
「text_ja」は、SolrのデフォルトのKuromojiの設定です。
ユニークキーは、URLということで。
動作確認
それでは、クローラーを動かしてみます。
[main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Deleted contents of: data/crawl/strage/frontier ( as you have configured resumable crawling to false ) [main] INFO org.littlewings.mycrawler.Launcher - add Seed URL under... http://d.hatena.ne.jp/Kazuhira/ [main] INFO org.littlewings.mycrawler.Launcher - Start crawler. [main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Crawler 1 started [main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Crawler 2 started [main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Crawler 3 started [main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Crawler 4 started [main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Crawler 5 started [main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Crawler 6 started [main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Crawler 7 started [Crawler 1] INFO org.littlewings.mycrawler.MyLittleCrawler - URL: http://d.hatena.ne.jp/Kazuhira/ [Crawler 1] INFO org.littlewings.mycrawler.MyLittleCrawler - Number of outgoing links: 251 [Crawler 1] INFO org.littlewings.mycrawler.MyLittleCrawler - add Solr Document.
こんな感じでクローラーが起動して、
[Crawler 2] INFO org.littlewings.mycrawler.MyLittleCrawler - URL: http://d.hatena.ne.jp/Kazuhira/20150906 [Crawler 2] INFO org.littlewings.mycrawler.MyLittleCrawler - Number of outgoing links: 159 [Crawler 2] INFO org.littlewings.mycrawler.MyLittleCrawler - add Solr Document. [Crawler 2] INFO org.littlewings.mycrawler.MyLittleCrawler - URL: http://d.hatena.ne.jp/Kazuhira/20150905 [Crawler 2] INFO org.littlewings.mycrawler.MyLittleCrawler - Number of outgoing links: 163 [Crawler 2] INFO org.littlewings.mycrawler.MyLittleCrawler - add Solr Document. [Crawler 2] INFO org.littlewings.mycrawler.MyLittleCrawler - URL: http://d.hatena.ne.jp/Kazuhira/20150513/1431530070 [Crawler 2] INFO org.littlewings.mycrawler.MyLittleCrawler - Number of outgoing links: 167 [Crawler 2] INFO org.littlewings.mycrawler.MyLittleCrawler - add Solr Document. [Crawler 5] INFO org.littlewings.mycrawler.MyLittleCrawler - URL: http://d.hatena.ne.jp/Kazuhira/20150517/1431859643 [Crawler 5] INFO org.littlewings.mycrawler.MyLittleCrawler - Number of outgoing links: 171 [Crawler 5] INFO org.littlewings.mycrawler.MyLittleCrawler - add Solr Document. [Crawler 2] INFO org.littlewings.mycrawler.MyLittleCrawler - URL: http://d.hatena.ne.jp/Kazuhira/20150912/1442047726 [Crawler 2] INFO org.littlewings.mycrawler.MyLittleCrawler - Number of outgoing links: 174 [Crawler 2] INFO org.littlewings.mycrawler.MyLittleCrawler - add Solr Document.
クローリングが進んでSolrにドキュメントを追加していきます。
こんなのが出力されたら、終了。
[Thread-1] INFO edu.uci.ics.crawler4j.crawler.CrawlController - It looks like no thread is working, waiting for 10 seconds to make sure... [Thread-1] INFO edu.uci.ics.crawler4j.crawler.CrawlController - No thread is working and no more URLs are in queue waiting for another 10 seconds to make sure... [Thread-1] INFO edu.uci.ics.crawler4j.crawler.CrawlController - All of the crawlers are stopped. Finishing the process... [Thread-1] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Waiting for 10 seconds before final clean up... [main] INFO org.littlewings.mycrawler.Launcher - commit!!
では、検索してみます。
「Lucene」
curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d '{ "query": "title:Lucene contents:Lucene" }' { "responseHeader":{ "status":0, "QTime":5, "params":{ "indent":"true", "json":"{ \"query\": \"title:Lucene contents:Lucene\" }", "wt":"json"}}, "response":{"numFound":70,"start":0,"docs":[ { "url":"http://d.hatena.ne.jp/Kazuhira/20150716/1437059068", "title":"Lucene Kuromojiのトークナイズを、Graphvizを使ってビジュアル化する - CLOVER", "contents":"Lucene Kuromojiのトークナイズを、Graphvizを使ってビジュアル化する Lucene ちょっと前に見ていたこちらのエントリ。 〜省略〜
「Hibernate Search」
$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d '{ "query": "(title:Hibernate contents:Hibernate) AND (title:Search contents:Search)" }' { "responseHeader":{ "status":0, "QTime":3, "params":{ "indent":"true", "json":"{ \"query\": \"(title:Hibernate contents:Hibernate) AND (title:Search contents:Search)\" }", "wt":"json"}}, "response":{"numFound":32,"start":0,"docs":[ { "url":"http://d.hatena.ne.jp/Kazuhira/20150205/1423152546", "title":"Hibernate Searchと数値フィールド - CLOVER", "contents":"Hibernate Searchと数値フィールド Lucene, HibernateSearch Luceneでドキュメントに数値を保存する時、数値向けのフィールドとしてIntFieldやLongField、DoubleFieldなどを使うことができます。ですが、これらはTermとしてはちょっと別物になるので、普通のクエリでは検索できずNumericRangeQueryを使うことになります。 Luceneを試していた時は、最初この差を知らなくて検索できなかったり、たまにコロッと忘れてハマったりするのですが…Hibernate Search越しだとどうなるのかがちょっと気になって試してみました。 結果からいうと、Query DSLでもあまり意識することなく使えました。 〜省略〜
OKそうですね。
とりあえず、簡単なWebクローリングと検索ができましたよっと。