Elasticsearchでセマンティックサーチを試す

Elasticsearchでknnクエリーを利用して、ベクトル検索を試すことができるの試してみる。まず、Elasticsearchを起動する。

git clone https://github.com/codelibs/docker-elasticsearch.git
cd docker-elasticsearch
docker compose -f compose.yaml up -d

という感じで、Dockerがあれば、http://localhost:9200でアクセスできるElasticsearchが起動できる。

ML機能はライセンスが必要なので、以下でトライアルを有効にする。

curl -XPOST -H "Content-Type:application/json" "http://localhost:9200/_license/start_trial?acknowledge=true"

ElasticsearchのML機能を利用して、テキストデータを渡されたら、ベクトルに変換してくれるようにingestを設定して、ingestでテキストからベクトルを推論するモデルを登録していく。

まずは、モデルを登録していくのが、モデルをインポートするためには、Pythonのelandを用いて、登録することになる。そのために、Pythonモジュールをインストールする。今回は、日本語を利用するので、以下のようなモジュールを入れておく。

pip install 'eland[pytorch]'
pip install fugashi
pip install unidic-lite

次はモデルのインポート。今回はtohoku/bert-base-japanese-v2を指定する。(今回は、簡単に試せるように、httpsなどのsecurityを無効な状態にしてあるので、ユーザー名などの指定が不要である)

eland_import_hub_model --url http://localhost:9200 --hub-model-id cl-tohoku/bert-base-japanese-v2 --task-type text_embedding

インポートできたら、モデルをデプロイする。

curl -XPOST -H "Content-Type:application/json" "http://localhost:9200/_ml/trained_models/cl-tohoku__bert-base-japanese-v2/deployment/_start"

ingestを登録して、テキストがベクトルに変換できるようにする。

curl -XPUT -H "Content-Type:application/json" "http://localhost:9200/_ingest/pipeline/text_embedding" -d '
{
  "description": "Text embedding pipeline",
  "processors": [
    {
      "inference": {
        "model_id": "cl-tohoku__bert-base-japanese-v2",
        "target_field": "content_vector",
        "field_map": {
          "content": "text_field"
        }
      }
    }
  ]
}
'

contentフィールドにテキスト情報を入れる。ベクトルは、content_vectorオブジェクトに入る。ベクトル自体はcontent_vector.predicted_valueに格納される。

ここまでくれば、下準備ができたので、次にデータを入れるインデックスを作る。

curl -XPUT -H "Content-Type:application/json" "http://localhost:9200/docs" -d '
{
  "settings": {
    "index": {
      "number_of_shards": 1,
      "number_of_replicas": 0
    }
  },
  "mappings": {
    "properties": {
      "content": {
        "type": "text",
        "analyzer": "standard"
      },
      "content_vector.model_id": {
        "type": "keyword"
      },
      "content_vector.predicted_value": {
        "type": "dense_vector",
        "dims": 768,
        "index": true,
        "similarity": "l2_norm"
      }
    }
  }
}
'

検証なので、contentにデータを入れるシンプルなインデックスです。このインデックスにデータを入れrます。テスト用のデータは、ChatGPTに10件生成してもらったので、それを入れます。

curl -XPOST -H "Content-Type: application/x-ndjson" "http://localhost:9200/_bulk?pipeline=text_embedding" -d '
{"index": {"_index": "docs"}}
{"content": "明日の天気は晴れの予報です。"}
{"index": {"_index": "docs"}}
{"content": "彼女が好きな色はピンクです。"}
{"index": {"_index": "docs"}}
{"content": "日本語を勉強しています。"}
{"index": {"_index": "docs"}}
{"content": "新しいレストランがオープンしました。"}
{"index": {"_index": "docs"}}
{"content": "今日は疲れたので早く寝ます。"}
{"index": {"_index": "docs"}}
{"content": "おいしいコーヒーを飲みました。"}
{"index": {"_index": "docs"}}
{"content": "新しいスマートフォンが発売されます。"}
{"index": {"_index": "docs"}}
{"content": "友達と映画を見に行きました。"}
{"index": {"_index": "docs"}}
{"content": "最近、本を読む習慣を始めました。"}
{"index": {"_index": "docs"}}
{"content": "子供たちが公園で遊んでいます。"}
'

シンプルなデータなので、面白みにかけますが、検索は以下のリクエストを投げると、結果が帰ってきます。

curl -XPOST -H "Content-Type: application/json" "http://localhost:9200/docs/_search" -d '
{
  "knn": {
    "field": "content_vector.predicted_value",
    "query_vector_builder": {
      "text_embedding": {
        "model_id": "cl-tohoku__bert-base-japanese-v2",
        "model_text": "彼女が好きな色は?"
      }
    },
    "k": 3,
    "num_candidates": 10
  },
  "_source": [
    "content"
  ]
}
'

「彼女が好きな色はピンクです。」が1件目で取得できると思いますが、2件目以降は類似度が高い順に返ってくるだけです。

という感じですが、詳しくはElasticsearchのドキュメントを参照するのが良いと思います。

Elasticsearch 8で初期パスワードを指定する

Elasticsearch 8を何も考えずに起動すると、初回起動時にelasticユーザーのパスワードが自動生成されます。起動する前にパスワードを設定しておきたい場合は、elasticsearch-keystoreコマンドで、bootstrap.passwordを設定しておけばそれが利用されるわけですが、インタラクティブに聞かれても面倒な場合は以下のような感じで渡してあげると設定できます。

$ echo ${es_password} | ./bin/elasticsearch-keystore add "bootstrap.password" -xf

eckctl

ElasticsearchをKubernetes上で便利に使えるものとして、Elastic Cloud on Kubernetes(ECK)があるけど、ローカル環境でも手軽にKubernetesクラスタを作って、そこでECKを動かせるようにしたいな、ということで、eckctlにまとめてみました。

手軽にKubernetesクラスタも作る必要があるので、eckctlではデフォルトでkindを利用することにしてます。なので、

$ ./eckctl create

とすると、ローカル環境にKubernetesクラスタができて、そこにECKを入れて、Elasticsearchクラスタができます。

サービス経由でアクセスするか、ポートフォワーディングで、

$ ./eckctl proxy

をして、別ターミナルで

$ ./eckctl curl https://localhost:9200/

みたいにするとElasticsearchのクラスタにアクセスできます。ECKのElasticsearchにはパスワードがかかっているので、eckctl経由にすることで、パスワードを付加してアクセスしています。

あとは、使い終わったら

$ ./eckctl delete

すれば、きれいに削除することができます。

使いながら、もう少し便利にしていこうかとは思いますが(ドキュメントを含めて…)、別にkindに依存しているわけではないので、そのうち、AWSやGCPにもデプロイできるようにしたいなとも思いますが、そこまで時間がないような気も…。とりあえず、FessのK8s対応が保留になっているので、eckctlベースに対応を進められないかなと考えています。

Docker Compose for Elasticsearch

ElasticsearchやKibanaをある程度はきちんとした状態で、手軽に試したいときがあるのだけど、そういうときにはdocker-fessを使っていた。でも、別にFessを使う必要はないし、プラグインとかも調整したいなということで、docker-elasticsearchにまとめてみた。

Elasticsearchだけを起動してみたいときには

$ docker-compose -f docker-compose.yml up

とすれば、localhost:9200でアクセスできるし、Kibanaも一緒に使いたいなー、というときには

$ docker-compose -f docker-compose.yml -f docker-compose.kibana.yml up

とすれば、localhost:5601でKibanaを使えるようになる。

プラグインとかをインストールしておきたいときには

$ ./bin/elasticsearch-plugin install analysis-kuromoji

とすれば、起動したElasticsearchで使えるはず。

今後も必要に応じて、微調整していく気はするけど、これで必要なときにコマンド1つで起動できるから便利になるかな…。

Workplace Searchの検索を試す

前回、インストールしたものの、クロール対象を用意しないとクロールできなかったので、今回はクロール対象の中で、簡単に準備できそうなDropboxをクロール対象にして試してみた。

DropboxのOAuthの設定が必要だが、手順自体はここにあるので、そこでApp KeyとApp Secretが生成できるので、それをWorkplace SearchのDropboxの接続情報に入れてConnectするとこんな感じで、取得したドキュメント件数が表示される。

今回は、Fessで使っているテスト用ファイルがfess-testdataにあるので、そこのファイルたちをDropboxに置いておいた。

とりあえず、今回は63ファイルがインデックスされたようなので、検索してみる。Go to Search Applicationで検索画面が表示される。

テスト用のファイルには「Lorem ipsum. (ロレム・イプサム) 吾輩は猫である。」という文字列があるので、試しに猫で検索してみると、こんな感じになる。

テキスト、HTML、MS Office、PDFあたりが検索できるようだ。ただ、Fessのテストデータにはいろんな種類のファイルが含まれているのですが、XMLファイルやzipに含まれるファイルなどはヒットしていなかった。あとは、仕方がない感はあるけど、Docuworks、AutoCAD,一太郎はヒットしていない。

検索結果をクリックすると、右側に情報が出てきた。それをさらにクリックすると対象のファイルに飛んでいく感じ。今回であれば、Dropboxのサイトに移動する。

絞り込みやソートについては、時間で絞り込み、関連度順と日付順のソートがある感じだった。

という感じで、今どきのUIだなという感じの印象でした。最近、Fessも今どきのUIが必要だなとは考えていたので、考えないとなと…。