Hele maika'i

趣味とか技術とかざっくばらんに書いてます。タイトルはハワイの言葉で「幸せな一歩」

SolrでSlackの投稿検索をする

どうもコッピーです.

初ブログで言ったとおり,今回の目標はSolrを立てて,Slackの投稿データを全文検索できるようにすることです.

基本的にはこちらの本を読みつつやっています.どうでもいいですが,この本の著者の半分がメルカリで働いているらしいですね.

前提条件

  • Solrは公式のDockerイメージを使用してDocker環境に構築する
  • 今回,SolrにSlackの投稿データを投げる部分はRubyで書きました

以下,簡単に理由を記載しています.

まず,あまりローカル環境を汚したくなかった(Java系は色々インストールとかめんどくさい)ので,上記の書籍ではローカル環境でやっているところをDockerで動かしています.

また,Rubyを選んだのは自分が書き慣れていたからです.それだけです.とはいえあまりAPI周りをRubyで書いたことはなかったので地味に時間を取られましたが...

環境

  • OS: macOS Mojave 10.14.4
  • Docker: 18.09.2
  • docker-compose: 1.23.2
  • Ruby: 2.5.5p157
  • Solr: 8.1.1

各種環境は以上のようになっています.最新環境との違いはあまり無いはずなので,以下環境構築手順は最新環境の構築を前提として記述しています.

各種環境構築手順

以下,上記環境構築手順になります.

  • Docker

macの場合,Docker公式から落としてくる方法と,brewで入れる方法の2種類の方法で入れることが出来ます. おそらくどちらで入れても問題ないと思います.ここでは,簡単なbrewを使用する方法で入れたいと思います.

$ brew cask install docker

はい,これだけです.簡単ですね.ちなみにこれでdocker-composeについても自動で入るはずなので,以下コマンドでバージョン確認をしてみてください.このように出力されていれば問題ないと思います.

$ docker version
Client: Docker Engine - Community
 Version:           18.09.2
 API version:       1.39
 Go version:        go1.10.8
 Git commit:        6247962
 Built:             Sun Feb 10 04:12:39 2019
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          18.09.2
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.6
  Git commit:       6247962
  Built:            Sun Feb 10 04:13:06 2019
  OS/Arch:          linux/amd64
  Experimental:     false
  
$ docker-compose -v
docker-compose version 1.23.2, build 1110ad01

RubyについてはデフォルトでMacに入っていますが,デフォルトのRubyはクソなので今回はrbenvを使用して入れていきます.ここで,あくまで僕自身がRubyで書きたかったからRubyを使用しただけなので,各自得意な言語,使いたい言語があればそちらでやっていただければ問題ありません.その場合はここは飛ばしていただいて構いません.

rbenv自体はgitbrewで入れることになります.ここではbrewを使用して入れていきます.また,rbenvは裏でruby-buildを使用するため,ここで一緒に入れてしまいます.

$ brew install rbenv ruby-build

rbenvが入ったら,パスの追加をします.自分はzshを使っているため,zshrcに下記のようなコードを追加します.bashorfishの方はそれぞれにあった場所に追加してください.

export PATH=/usr/local/bin/bin:$PATH
export PATH=$HOME/.rbenv/bin:$PATH
eval "$(rbenv init - zsh)"

これが追加できたらsource ~/.zshrcで読み込んでください. これでrbenvrubyをインストールする準備が整いました.早速入れていきたいと思います.今回は2.6.0を入れたいと思います.

$ rbenv install --list
$ rbenv install 2.6.0
$ rbenv global 2.6.0
$ rbenv rehash

これでインストールが完了したはずなので,最後にrubyのバージョンを確認してあげましょう.

$ ruby -v
ruby 2.5.5p157 (2019-03-15 revision 67260) [x86_64-darwin18]

自分の環境では2.5.5なのでこんな感じですが,何かしらバージョンがこんな感じで出力されていれば問題ありません.

長くなりましたが,アンド結構雑ではありますが,環境構築については以上です.Solrについてはこのあと構築していきます.

Solrのコア用ディレクトリを作成

まず,コアと呼ばれる,Solrの検索集合を用意します.これはRDBでいうスキーマのようなもので,コアごとにスキーマ定義やクエリの設定が出来ます.Solrは1つのインスタンスの中に複数のコアを持つことができ,それぞれにインデックスを保持出来ます.

例えば,複数のサービスで同一プラットフォームの検索システムを使用する場合にサービスごとに検索したいフィールドが異なる・ランキングの算出方法を変えたいということがあると思います.

このとき,それぞれのサービスごとにコアを用意することで,同じ検索システムを使用していても異なる設定を適用することが出来るというわけです.

では,コア用のディレクトリを作成していきます.

$ mkdir data
$ sudo chown 8983:8983 data

Solrコンテナの構築

まず,docker-compose.yamlを用意します. 中身はひとまずこのように書いてください.

version: '2'
services:
  solr:
    image: solr:latest
    container_name: 'solr_test'
    ports:
     - "8983:8983"
    volumes:
      - ./data:/var/solr/data
    entrypoint:
      - docker-entrypoint.sh
      - solr-precreate
      - slackCore
    restart: always

これはほとんどSolr公式Docker Imageから持ってきています.

今回はdocker-compose.yamlの中身については触れることはしませんが,簡単に言うとSolrの最新バージョンを使用し,コンテナ起動時にslackCoreというコアを/var/solr/data配下に作成しています.

/var/solr/dataはSolrがコアを作成するディレクトリで,いつのバージョンかまでは/opt/solr/server/solr/mycoresに作成されていたようです.

あとはDockerを起動するだけです.

$ docker-compose up -d

これが無事成功したら,ブラウザでlocalhost:8983にアクセスするとSolrのホームを起動することができます.

このような画面が出れば成功です. まだcoreにはデータは何も入っていないので,検索などは出来ません.

スキーマ定義

では,まずはスキーマ定義をしていきます.

スキーマ定義はSolrにどのようなデータを入れるか,どの項目が検索できるか,表示する項目はどれかなどを定義するもので,Solrはスキーマ定義がなければ原則データを入れることも検索することも出来ません.

原則というのは,Solrにはスキーマレスモードが存在し,スキーマ定義をしなくてもインデクシングされたデータからスキーマを推測する機能があるためです.

しかし,今回はスキーマレスモードについては取り扱わないため,スキーマ定義をしていくことになります.

スキーマを定義する上で,今回はSlackAPIからデータを引いてくるため,その中のどのフィールドを使いたいかを考える必要があります.

自分は今回,

  • client_msg_id : 投稿ID(一意)
  • type : イベントの種類(投稿,リアクションなど)
  • ts : イベントの発生時間
  • user : イベントを行ったユーザ
  • text : 投稿本文

を定義しました.全部定義するのはさすがにダルいので,少しだけピックアップした形になります.詳細についてはSlackAPIの仕様を見た上で,個々人の使用したいフィールドをピックアップしてみてください.

スキーマ定義の流れ

使用するフィールドを決めたので,いよいよスキーマ定義をしていきます.まずは簡単にスキーマ定義の流れを把握して,それから定義していきましょう.

スキーマ定義に必要なのは フィールドフィールドタイプ の2つです.フィールドは先程決定したclient_msg_idtextのようなもので,検索対象のデータのタイトルや本文などを格納する,Solrへの入れ物となるものです.それぞれのフィールドに対してどのような型(数値,文字列,日付など)であるかを定義したものがフィールドタイプです.

フィールドタイプについては基本的にデフォルトで用意されているもので十分であるため,今回定義することはしません.

スキーマ定義には大きく2つの方法があり,一つ目はschema.xmlを直接編集する方法です.もう一つはSchema APIを使用して定義する方法で,今回は後者のSchema APIを使用して定義する方法を採用しています.

フィールドの定義

では,フィールドの定義をしていきましょう.先程書いたようにAPIを使用して定義していくため,JSONファイルに記述していきます.

定義の方法は次のようにします.

{
    "追加or更新or削除を指定するキー": {
        "name": "フィールドの名前",
        "type": "フィールドタイプの名前",
        "オプション": "オプションの値"
    }
}

フィールドの追加,更新,削除の場合で記述が異なります.ここで指定できるキーは下記の通りです.

キー名 説明
add-field 追加時
replace-field 更新時
delete-field 削除時

基本的には登録時にはadd-fieldしか使いませんが,ユニークキーの設定のために一部replace-fieldも使用します.これについては後ほど説明します.

次に設定項目についてです.書き方からおそらく分かると思いますが,nametypeは必須項目です.オプションも含め,設定項目について簡単にまとめました.

項目名 説明
name(必須) フィールドの名前 半角英数字
type(必須) フィールドタイプの名前 フィールドで使用する型
indexed(オプション) true/false 検索可能なフィールドであればtrue.デフォルトはtrue
stored(オプション) true/false 検索結果として表示したい場合にはtrue.デフォルトはtrue
multiValued true/false 複数の値を登録する場合にはtrue.デフォルトはfalse

これ以外にも設定は可能ですが,今回は使用する範囲のみでまとめさせていただいています.もっと詳しく知りたい方は調べてみてください.

今回は下記のように(schemaDefinition.jsonとして)設定しています.

{
    "replace-field": {
        "name": "id",
        "type": "string",
        "indexed": "true",
        "stored": "true",
        "multiValued": "false"
    },
    "add-field": {
        "name": "type",
        "type": "string",
        "indexed": "false",
        "stored": "true",
        "multiValued": "false"
    },
    "add-field": {
        "name": "ts",
        "type": "string",
        "indexed": "false",
        "stored": "true",
        "multiValued": "false"
    },
    "add-field": {
        "name": "user",
        "type": "string",
        "indexed": "false",
        "stored": "true",
        "multiValued": "false"
    },
    "add-field": {
        "name": "text",
        "type": "text_ja",
        "indexed": "true",
        "stored": "true",
        "multiValued": "false"
    }
}

idについては既存フィールドであるため,replace-fieldを使用しています.この項目はユニークキーとなるため,検索・表示ともにtrueとし,multiValuedfalseとしています.必ず設定しなければいけないということはありませんが,ユニークキーを設定することで,データを取り込んだときに同一データを抜いて差分のみの取り込みが可能となるため今回は設定しています.

また,今回基本的に検索可能なフィールドは投稿本文のみとしています.検索可能なフィールド(日本語データが保存される場合)ではフィールドタイプにtext_jaを設定します.このフィールドタイプは裏でいい感じに形態素解析をしてくれます.この辺についてはSolr AdminでAnalysisを使ってみると分かりやすいと思います.

jsonファイルの準備も出来たので,APIを使用してフィールド定義を適用します.

$ curl -X POST -H 'Content-type:application/json' --data-binary @schemaDefinition.json http://localhost:8983/solr/slackCore/schema
{
  "responseHeader":{
    "status":0,
    "QTime":1319}}

上のようなレスポンスが返ってこれば成功です.これでフィールドの登録が出来ました.次はいよいよSlackの投稿データを流し込んでいきます.

SlackAPIのトークン取得

Slackの投稿データを流し込もうにも,SlackAPIを使用できるようにしなければいけません.

まずは

https://api.slack.com/apps

ここでSlackアプリを作成します.アプリを作成したら

Add features and functionality -> Permissions -> Select Permission Scopesからchannels:historyを選択します. これで保存していただければOAuth Access Tokenからトークンを取得出来ます.

また,ここでついでに投稿を取得したいチャンネルのIDについても取得しておきましょう.こちらは特にAPIを叩く必要もなく,Web上でSlackを開いたときにURLの最後の/以下の文字列がチャンネルIDです.

これで投稿データを流し込む準備が出来ました.あとはデータを流し込むだけですね!

Slack投稿データの流し込み

Slack投稿データについては先程取得したトークンを使用すれば簡単に取ってこれます.今回はRubyでプログラムを作成したのでプログラムの中で

  1. Slack投稿データの取得
  2. Json生成
  3. SolrへのAPI経由でのデータ流し込み

までやっています.ひとまず下にコードを載せておきますので,こちらを参考にしてください

@Update 2019.08.27 一部プログラムにミスがあったので修正

require "slack"
require "json"
require "net/http"
require "uri"
require "dotenv"

# .envファイルから環境変数を読み込む
Dotenv.load

Slack.configure do |config|
  config.token = ENV["TOKEN"]
end

# これで指定したチャンネルの投稿を取得できる
messages = Slack.channels_history(channel: ENV["CHANNEL_ID"])['messages']

# 必要なフィールドのみで新しい配列を生成
messageArray = messages.map do |message|
  messageHash = {}
  if message["client_msg_id"] != nil
    messageHash["id"] = message["client_msg_id"]
    messageHash["type"] = message["type"]
    messageHash["ts"] = message["ts"]
    messageHash["user"] = message["user"]
    messageHash["text"] = message["text"]
  else
    next
  end

  messageHash
end

# ここ以降でSolrへのデータ注入
uri = URI.parse("http://localhost:8983/solr/slackCore/update?commit=true")
http = Net::HTTP.new(uri.host, uri.port)
req = Net::HTTP::Post.new(uri.request_uri)
req["Content-Type"] = "text/json; charset=utf-8"
req.body = messageArray.compact.to_json

res = http.request(req)

puts res.code, res.msg, res.body

コード自体は難しいものでもないのですが,Solrに流し込む上で注意しなければならない点があるため,そこを説明していきたいと思います.

SolrにAPI経由でデータを流し込むとき,流し込むデータ全体を大カッコで囲う必要があります.そこで,プログラムの中で取得した投稿データのうち必要なフィールドのみをハッシュとして整形した上で配列に格納しています.

分かりやすいように例をあげておくと,Solrに流すときにはデータは下のような形になっています.

[
    {
        "id": "1234",
        "type": "message",
        "ts": "123456.789",
        "user": "1234",
        "text": "テスト"
    },
    {}...
]

また,Slack上ではclient_msg_idとなっているIDをidとして保存しています.これはフィールド定義のときにユニークキーとして使用するためにidという名前で定義したためです.

あとはこのコードを実行するだけです.コードをgetSlackMessage.rbという名前で保存して,実行してみましょう.下のように200 OKが返ってこれば成功です.

$ ruby getSlackMessage.rb
200
OK
{
  "responseHeader":{
    "status":0,
    "QTime":376}}

データの検索

さて,これでやることは全て終わりました.Solr Adminの確認をしてみましょう. Solr Adminで今回作成したコアを選択し,Queryを選んでください.ここで,何も変更を加えずにExecute Queryを押してみてください.

どうですか?投稿データの検索が出来たでしょうか? 何も変更していなければ全てのフィールドに対して全てのワードで検索をするという動作をしているため,登録したドキュメント全ての検索が出来ているはずです.

qとなっている部分をtext:検索したいワードとして再度Execute Queryを押すと検索したいワードでの検索が出来ます.

まとめ

以上,少し長くなってしまいましたが,Slackの投稿データをSolrを使って検索することが出来ました.

自分自身初めてSolrを触ってここまで出来るとかなり面白く,やりごたえもありました.

これを機に少しでも検索に興味を持ってもらえれば嬉しいです. 次回はこのデータを使ったちょっとした応用をしてみたいと思います.

今回使用したRubyのプログラムは下記GitHubに上げておきます. https://github.com/Kryota/slackSearch

参考文献