仕事で検索プラットフォームの Apache Solr を扱うことになったのですが、今までブラックボックスにしてきてしまっていたので、この機会に勉強することにしました。
http://lucene.apache.org/solr/
今回は、Mac上にSolr (7.5.0) の環境を構築し、け日記のエントリを検索できるようにします。チュートリアルと↓の本を参考にしています。
前提条件
Java 8.0以降のランタイムがインストールされている必要があります。
Solrのダウンロードと起動
Solrのダウンロードと起動を行っていきます。Dockerを使った方法もありますが、今回はバイナリをダウンロードしてきて直接起動するようにします。
バイナリをダウンロード・展開します。
$ pwd /tmp/solr $ wget http://ftp.jaist.ac.jp/pub/apache/lucene/solr/7.5.0/solr-7.5.0.zip $ unzip solr-7.5.0.zip $ cd solr-7.5.0
次に、Solrサーバの起動です。同梱されているbin/solr
コマンドで立ち上げます。
- SolrCloudで立ち上げてます
- 動的なロードバランシングやZooKeeperによるクラスタ管理を行います
- 2ノードを立ち上げます
- ポートは8983 (pid=12771) と7574 (pid=12872) を割り当てます
$ ./bin/solr start -e cloud Welcome to the SolrCloud example! ... This interactive session will help you launch a SolrCloud cluster on your local workstation. To begin, how many Solr nodes would you like to run in your local cluster? (specify 1-4 nodes) [2]: 2 Ok, let's start up 2 Solr nodes for your example SolrCloud cluster. Please enter the port for node1 [8983]: 8983 Please enter the port for node2 [7574]: 7574 Creating Solr home directory /tmp/solr/solr-7.5.0/example/cloud/node1/solr Cloning /tmp/solr/solr-7.5.0/example/cloud/node1 into /tmp/solr/solr-7.5.0/example/cloud/node2 Starting up Solr on port 8983 using command: "/tmp/solr/solr-7.5.0/bin/solr" start -cloud -p 8983 -s "/tmp/solr/solr-7.5.0/example/cloud/node1/solr" ... Started Solr server on port 8983 (pid=12771). Happy searching! Starting up Solr on port 7574 using command: "/tmp/solr/solr-7.5.0/bin/solr" start -cloud -p 7574 -s "/tmp/solr/solr-7.5.0/example/cloud/node2/solr" -z localhost:9983 ... Started Solr server on port 7574 (pid=12872). Happy searching! INFO - 2018-11-24 12:46:41.796; org.apache.solr.common.cloud.ConnectionManager; zkClient has connected INFO - 2018-11-24 12:46:41.816; org.apache.solr.common.cloud.ZkStateReader; Updated live nodes from ZooKeeper... (0) -> (2) INFO - 2018-11-24 12:46:41.843; org.apache.solr.client.solrj.impl.ZkClientClusterStateProvider; Cluster at localhost:9983 ready ...続く...
次に、け日記用のコレクションを作ります。
- コレクションは、インデックスを作って検索するドキュメント集合の単位です
- 後ほど検索クエリが出てきますが、パスにここで設定したコレクション名
kenikki
が付きます
- 後ほど検索クエリが出てきますが、パスにここで設定したコレクション名
- シャード数は2で設定
- シャードはコレクションの論理的なパーティションで、ドキュメントのインデックス作成時にどのシャードに属するのかが決められます
- クエリは各シャードにルーティングしてそれぞれで検索されます
- レプリカ数も2で設定
- レプリカはシャードごとに設定し、各シャードは1つ以上のレプリカを持ちます
- レプリカの1つはleaderに指名され、leaderはインデックスの更新を他のreplicaに伝搬します
Now let's create a new collection for indexing documents in your 2-node cluster. Please provide a name for your new collection: [gettingstarted] kenikki How many shards would you like to split kenikki into? [2] 2 How many replicas per shard would you like to create? [2] 2 Please choose a configuration for the kenikki collection, available options are: _default or sample_techproducts_configs [_default] _default Created collection 'kenikki' with 2 shard(s), 2 replica(s) with config-set 'kenikki' Enabling auto soft-commits with maxTime 3 secs using the Config API POSTing request to Config API: http://localhost:8983/solr/kenikki/config {"set-property":{"updateHandler.autoSoftCommit.maxTime":"3000"}} Successfully set-property updateHandler.autoSoftCommit.maxTime to 3000 SolrCloud example running, please visit: http://localhost:8983/solr
これでSolrサーバが起動します。最後に表示されたhttp://localhost:8983/solr
にブラウザからアクセスすると、ダッシュボードが表示されます。
管理画面からCloudタブを開くと、作成したコレクション"kenikki"に2本のシャードとそれぞれに2つのレプリカが紐付いていることがわかります。
なおコレクションを消すときは以下のコマンドです。
$ bin/solr delete -c kenikki
またサーバを停止するコマンドは以下です。
$ bin/solr stop -all
け日記のエントリページのダウンロード
け日記から過去の全エントリをHTMLファイルでダウンロードしておきます。このHTMLファイルをSolrで検索できるようにします。
import requests import re import urllib.request import time list_urls = [ 'https://ohke.hateblo.jp/archive/2015', 'https://ohke.hateblo.jp/archive/2016', 'https://ohke.hateblo.jp/archive/2017?page=1', 'https://ohke.hateblo.jp/archive/2017?page=2', 'https://ohke.hateblo.jp/archive/2018?page=1', 'https://ohke.hateblo.jp/archive/2018?page=2' ] downloaded = set() # ダウンロード済みのURL for list_url in list_urls: # リストページからエントリのURLを抽出 (ダウンロード済みのURLは除外) list_html = requests.get(list_url).text entry_urls = set(re.findall(r'https://ohke.hateblo.jp/entry/[0-9]{4}/[0-9]{2}/[0-9]{2}/[0-9]{6}', list_html)) - downloaded # エントリページ (HTMLファイル) をダウンロードして保存 for entry_url in entry_urls: urllib.request.urlretrieve(entry_url, '/tmp/solr/documents/'+ entry_url[30:].replace('/', '-') + '.html') downloaded.add(entry_url) time.sleep(5) # アクセスレート制限
ダウンロードされたファイルは/tmp/solr/documents/
配下に保存してます。
$ ls /tmp/solr/documents 2015-06-18-231834.html 2015-12-07-000504.html ... 2018-11-17-230000.html
ドキュメントのインデックス
上でダウンロードしたエントリのファイルを、ドキュメントに追加します。
solrのパッケージに含まれるbin/post
を実行することでインデックスされます。HTMLファイル以外にもxmlやjson、pdfやwordにも対応しているようです。
$ pwd /tmp/solr/solr-7.5.0 $ ./bin/post -c kenikki /tmp/solr/documents/* java -classpath /tmp/solr/solr-7.5.0/dist/solr-core-7.5.0.jar -Dauto=yes -Dc=kenikki -Ddata=files org.apache.solr.util.SimplePostTool /tmp/solr/documents/2015-06-18-231834.html /tmp/solr/documents/2015-12-07-000504.html ... /tmp/solr/documents/2018-11-17-230000.html SimplePostTool version 5.0.0 Posting files to [base] url http://localhost:8983/solr/kenikki/update... Entering auto mode. File endings considered are xml,json,jsonl,csv,pdf,doc,docx,ppt,pptx,xls,xlsx,odt,odp,ods,ott,otp,ots,rtf,htm,html,txt,log POSTing file 2015-06-18-231834.html (text/html) to [base]/extract POSTing file 2015-12-07-000504.html (text/html) to [base]/extract ... POSTing file 2018-11-17-230000.html (text/html) to [base]/extract 113 files indexed. COMMITting Solr index changes to http://localhost:8983/solr/kenikki/update... Time spent: 0:00:11.996
これで検索の準備が整いました。
検索
インデックスされたドキュメントの検索するためには、Solrのエンドポイントを直接叩く方法と、管理画面から行う方法 (ここではhttp://localhost:8983/solr/#/kenikki/query
から) があります。
まずはクエリに何も指定せずに検索してみます。URLはhttp://localhost:8983/solr/kenikki/select?q=*:*
となります。
- 検索時間は15ミリ秒 (
responseHeader.QTime
) - ヒット件数が113件 (
response.numFound
) response.docs
に検索結果が入ってます
{ "responseHeader":{ "zkConnected":true, "status":0, "QTime":15, "params":{ "q":"*:*", "_":"1543032835024"}}, "response":{"numFound":113,"start":0,"maxScore":1.0,"docs":[ { "id":"/tmp/solr/documents/2015-06-18-231834.html", "og_image":["https://cdn.image.st-hatena.com/image/scale/0b8f3ad86af86640c8733aa2f27dd3f0b2c09090/backend=imager;enlarge=0;height=1000;version=1;width=1200/https%3A%2F%2Fcdn.user.blog.st-hatena.com%2Fdefault_entry_og_image%2F99455833%2F1533459624629378"], "twitter_app_url_iphone":["hatenablog:///open?uri=https%3A%2F%2Fohke.hateblo.jp%2Fentry%2F2015%2F06%2F18%2F231834"], "article_published_time":[1434637114], "twitter_card":["summary"], "stream_content_type":["text/html"], "og_site_name":["け日記"], "description":["はじめまして。 このブログを始めた経緯と目的を忘れないようにするために、初投稿としてメモしておきます。 経緯 私自身はいわゆるシステムインテグレータ(SIer)に勤めており、主にシステム開発、部内の開発標準・開発環境の整備を担当しています。 古き良きSIerのエンジニアのご多分に漏れず、ありがちなもやもやを抱えています。 レガシーな技術・・・Java 1.4もCOBOLもバリバリの現役です 社内に閉じた謎ツール・・・DAOを生成するエクセルファイル コミュニケーション能力、マネジメント能力の方が大事です・・・携帯電話、メーラー、エクセルが必須ツール このあたりが原因となって、エンジニアとしての…"], "title":["このブログの趣旨 - け日記"], "twitter_app_id_iphone":[583299321], "og_description":["はじめまして。 このブログを始めた経緯と目的を忘れないようにするために、初投稿としてメモしておきます。 経緯 私自身はいわゆるシステムインテグレータ(SIer)に勤めており、主にシステム開発、部内の開発標準・開発環境の整備を担当しています。 古き良きSIerのエンジニアのご多分に漏れず、ありがちなもやもやを抱えています。 レガシーな技術・・・Java 1.4もCOBOLもバリバリの現役です 社内に閉じた謎ツール・・・DAOを生成するエクセルファイル コミュニケーション能力、マネジメント能力の方が大事です・・・携帯電話、メーラー、エクセルが必須ツール このあたりが原因となって、エンジニアとしての…"], "dc_title":["このブログの趣旨 - け日記"], "content_encoding":["UTF-8"], "content_type":["text/html; charset=UTF-8"], "stream_size":[37889], "x_parsed_by":["org.apache.tika.parser.DefaultParser", "org.apache.tika.parser.html.HtmlParser"], "og_type":["article"], "twitter_title":["このブログの趣旨 - け日記"], "google_site_verification":["j4_wDEBc7hC57lE-AA2X0V_vBF_DIzL1kf9mX-igpZk"], "og_title":["このブログの趣旨 - け日記"], "resourcename":["/tmp/solr/documents/2015-06-18-231834.html"], "article_tag":["雑記"], "x_ua_compatible":["IE=7; IE=9; IE=10; IE=11"], "twitter_description":["はじめまして。 このブログを始めた経緯と目的を忘れないようにするために、初投稿としてメモしておきます。 経緯 私自身はいわゆるシステムインテグレータ(SIer)に勤めており、主にシステム開発、部内の開発標準・開発環境の整備を担当しています。 古き良きSIerのエンジニアのご多分に漏れず、ありがちなもやもやを抱えています…"], "og_url":["https://ohke.hateblo.jp/entry/2015/06/18/231834"], "twitter_app_name_iphone":["はてなブログアプリ"], "_version_":1617986237913628672}, ...
次に、"numpy"を含むエントリ1件を公開日時で降順ソートして検索します。クエリのURLはhttp://localhost:8983/solr/kenikki/select?q=numpy&rows=1&sort=article_published_time%20desc
となります。
{ "responseHeader":{ "zkConnected":true, "status":0, "QTime":10, "params":{ "q":"numpy", "sort":"article_published_time desc", "rows":"1", "_":"1543034009297"}}, "response":{"numFound":28,"start":0,"docs":[ { "id":"/tmp/solr/documents/2018-10-20-235500.html", "og_image":["https://cdn.user.blog.st-hatena.com/default_entry_og_image/99455833/1533459624629378"], "twitter_app_url_iphone":["hatenablog:///open?uri=https%3A%2F%2Fohke.hateblo.jp%2Fentry%2F2018%2F10%2F20%2F235500"], "article_published_time":[1540047300], "twitter_card":["summary_large_image"], "stream_content_type":["text/html"], "og_site_name":["け日記"], "description":["単語ベクトル化モデルの一つであるGloVeを試してみます。 GloVe GloVeは単語のベクトル表現を得る手法の一つで、Word2Vecの後発となります。論文はこちらです。 nlp.stanford.edu Word2Vec (skip-gram with negative sampling: SGNS) では各単語から周辺単語を予測するというタスクをニューラルネットワークで解くことによって単語ベクトルを得ますが、GloVeではコーパス全体から得られる単語間の共起行列を持ち込んだ最適化関数 (重み付き最小二乗法) で学習します。 単語iと単語jの共起行列が は、次元削減された単語ベクトル (…"], "title":["GloVeで単語ベクトルを得る - け日記"], ...
まとめ
Solrことはじめということで、今回はMacbookにSolrサーバーを立ち上げ、け日記を全文検索できるようにしました。
参考文献
![[改訂第3版]Apache Solr入門――オープンソース全文検索エンジン (Software Design plus) [改訂第3版]Apache Solr入門――オープンソース全文検索エンジン (Software Design plus)](https://images-fe.ssl-images-amazon.com/images/I/51Es%2BgviRlL._SL160_.jpg)
[改訂第3版]Apache Solr入門――オープンソース全文検索エンジン (Software Design plus)
- 作者: 打田智子,大須賀稔,大杉直也,西潟一生,西本順平,平賀一昭,株式会社ロンウイット,株式会社リクルートテクノロジーズ
- 出版社/メーカー: 技術評論社
- 発売日: 2017/04/27
- メディア: 大型本
- この商品を含むブログを見る