Connectorを使ってWindowsにあるファイルをElasticsearchにインデックスする

# はじめに
生成AIの使い方の一つであるRAG(Retrieval Augmented Generation)について以下の記事を先日書きました。
https://qiita.com/takeo-furukubo/items/e5d43fa734e4338b895f
この利用方法では実はまずファイルにあるテキストデータを抽出してElasticsearchにインデックスする必要があります。
本記事では実際にWindowsにあるファイルからテキストを抽出してElasticsearchにインデックスを行います。

# Connector
**Connector**というのはユーザが検索したい元データ(PDF,Text File,Office系ドキュメントなど)からテキストを抽出して、Elasticsearchで全文検索できるようにするものです。
生成AIの使い方の一つであるRAG(Retrieval Augmented Generation)の初手になる部分です。

Elasticsearchにはテキストを入れますので、様々なファイルからテキストを抽出してから、属性情報を追加してインデックス化します。
![スクリーンショット 2023-12-11 13.33.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/9e91568c-6df2-4b11-0002-89ab9b9cfeef.png)
ここで**Connector**には2種類あります。
- Native Connector
    - Elastic側で管理していて、設定だけ入力するとそのまま利用可能
- Connector clients
    - ユーザ側で運用・管理するもの

SharepointやGoogle Driveなどクラウド上にあるデータであればNative Connectorが便利ですが、オンプレミスにあるデータの場合は外部からのアクセスは難しいので、Connector clientsを使います。

今回はオンプレミスのWindowsからConnector clientsを使ってテキスト抽出する方法の解説です。

# 環境
Windows 10 Pro / 21H2 / 19044.2130
Docker Desktop for windows: v24.0.6
Elasticsearch: 8.11.1 (Elastic Cloud)

データを集めるConnectorをコンテナ上で動かします。WindowsにDocker Desktopをインストールしておいてください。

# Elastic Cloud
（既にある場合は必要ありません）
こちらのブログをベースにElastic Cloudにクラスタを展開します。
https://qiita.com/tomo_s_el/items/3584d0b1fabb0bafa4fa

# Connector clients設定
ドキュメントはこちらです。
https://www.elastic.co/guide/en/enterprise-search/current/connectors-network-drive.html#connectors-network-drive-connector-client-reference

KibanaメニューからSearch -> Contentと選び、右上の**Createa new index**をクリック
![スクリーンショット 2023-12-11 17.04.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/0979974b-9b26-1506-6c04-4b912133987c.png)
次に**Use a connector**をクリックします。因みにデータの抽出ツールは今回ご紹介するConnectorも入れて3種類用意しています。他の2つはこちらです。
- Crawler
    - Web Siteからデータを引っ張ってくる
- API
    - APIでデータを持ってくる

![スクリーンショット 2023-12-11 17.07.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/9dcb4a57-3c42-e6f5-817f-17ffd27e9b90.png)
次の画面で**Network Drive**を選択して、右下の**Continue**をクリック
因みにNetwork DriveはSMB v2/v3のみです。
![スクリーンショット 2023-12-11 17.22.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/838c4c83-ca3f-58c0-ed24-8ec8ec0971cf.png)
次の画面ではインデックスの名前を入力します。**search-**が必ず頭に付きます。以下の例だと**search-windows-drive**という名前のインデックスが作成されます。入力したら右下の**Create Index**をクリック。
Language Analyzerは日本語を選んでも形態素解析は行われないので、後ほど設定します。
![スクリーンショット 2023-12-11 17.26.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/b0b9f5c7-681c-ca2a-6f91-57cbfcd0320a.png)

次の画面では右側にある**Convert connector**をクリックします。デフォルトではNative Connectorとして扱われるためです。
このような脅しが出てきますが、インデックス毎に変更できるので気にせず**Yes**を押しましょう。
![スクリーンショット 2023-12-11 17.33.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/a576f9c5-73d1-2722-5257-696ce6c8e4d4.png)

最後に設定を入れていきます。
まずは**Generate API Key**をクリックするとAPI Keyが表示されるので、コピーして何処かに保存しておきましょう。この画面から移ると表示されなくなるので、保存していなかった場合は再作成する必要があります。
次にWindowsの認証情報を入れます。
**Username**,**password**,**IP address**,**Path**を入力します。**Port**は必要に応じて変更してください。
最後に**Enable document level security**をチェックして**Save configuration**をクリックします。
![スクリーンショット 2023-12-11 18.01.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/d25e1a8a-6b5a-e944-e69c-f2dcf72dcf97.png)

次にWindows上で動くConnector clientsを設定していきます。**Deploy connector**の下にある`connectors:`から始まる部分をコピーしておきます。
ですが、その前に日本語対応の設定を行います。

# 日本語(形態素解析)設定
## インデックスclose
日本語の全文検索には特別な処理が必要です。詳細は下記リンクを参照して下さい。
https://www.elastic.co/jp/blog/how-to-implement-japanese-full-text-search-in-elasticsearch
settingとmappingを変更する必要がありますが、インデックスを開いた状態では変更できませんので、まず一旦インデックスをcloseします。
**Management**->**Dev Tools**と開いて、以下を入力して実行します。
`POST search-windows-drive/_close`
赤枠で囲った三角をクリックすると実行できます。右側に以下のように表示されると無事close出来ています。
![スクリーンショット 2023-12-14 10.15.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/8c8c55ff-0be0-2326-0ac8-2cc9dd133644.png)
## settings/mappings変更
まず形態素解析を指定しているsettingsを変更します。この設定を入れることで形態素解析として`kuromoji`を使う設定になります。
右側の画面にerrorが出ていないことを確認してください。
```json
PUT search-windows-drive/_settings
{
  "settings": {
    "index": {
      "analysis": {
        "char_filter": {
          "normalize": {
            "type": "icu_normalizer",
            "name": "nfkc",
            "mode": "compose"
          }
        },
        "tokenizer": {
          "ja_kuromoji_tokenizer": {
            "mode": "search",
            "type": "kuromoji_tokenizer"
          }
        },
        "analyzer": {
          "kuromoji_analyzer": {
            "tokenizer": "ja_kuromoji_tokenizer",
            "char_filter": ["normalize"],
            "filter": [
              "kuromoji_baseform",
              "kuromoji_part_of_speech",
              "cjk_width",
              "ja_stop",
              "kuromoji_stemmer",
              "lowercase"
            ]
          }
        }
      }
    }
  }
}
```
次にどのフィールドに対して適用するかをmappingで決めます。長いですが、以下を`Dev Tools`で実行します。
```json
PUT search-windows-drive/_mapping
{
    "dynamic": "true",
    "dynamic_templates": [
      {
        "data": {
          "match_mapping_type": "string",
          "mapping": {
            "analyzer": "iq_text_base",
            "fields": {
              "prefix": {
                "search_analyzer": "q_prefix",
                "analyzer": "i_prefix",
                "type": "text",
                "index_options": "docs"
              },
              "delimiter": {
                "analyzer": "iq_text_delimiter",
                "type": "text",
                "index_options": "freqs"
              },
              "joined": {
                "search_analyzer": "q_text_bigram",
                "analyzer": "i_text_bigram",
                "type": "text",
                "index_options": "freqs"
              },
              "enum": {
                "ignore_above": 2048,
                "type": "keyword"
              },
              "stem": {
                "analyzer": "iq_text_stem",
                "type": "text"
              }
            },
            "index_options": "freqs",
            "type": "text"
          }
        }
      }
    ],
    "properties": {
      "_allow_access_control": {
        "type": "text",
        "fields": {
          "delimiter": {
            "type": "text",
            "index_options": "freqs",
            "analyzer": "iq_text_delimiter"
          },
          "enum": {
            "type": "keyword",
            "ignore_above": 2048
          },
          "joined": {
            "type": "text",
            "index_options": "freqs",
            "analyzer": "i_text_bigram",
            "search_analyzer": "q_text_bigram"
          },
          "prefix": {
            "type": "text",
            "index_options": "docs",
            "analyzer": "i_prefix",
            "search_analyzer": "q_prefix"
          },
          "stem": {
            "type": "text",
            "analyzer": "iq_text_stem"
          }
        },
        "index_options": "freqs",
        "analyzer": "iq_text_base"
      },
      "_subextracted_as_of": {
        "type": "date"
      },
      "_subextracted_version": {
        "type": "keyword"
      },
      "_timestamp": {
        "type": "date"
      },
      "body": {
        "type": "text",
        "fields": {
          "delimiter": {
            "type": "text",
            "index_options": "freqs",
            "analyzer": "iq_text_delimiter"
          },
          "enum": {
            "type": "keyword",
            "ignore_above": 2048
          },
          "joined": {
            "type": "text",
            "index_options": "freqs",
            "analyzer": "i_text_bigram",
            "search_analyzer": "q_text_bigram"
          },
          "prefix": {
            "type": "text",
            "index_options": "docs",
            "analyzer": "i_prefix",
            "search_analyzer": "q_prefix"
          },
          "stem": {
            "type": "text",
            "analyzer": "iq_text_stem"
          },
          "japanese": {
            "type": "text",
            "analyzer": "kuromoji_analyzer"
          }
        },
        "index_options": "freqs",
        "analyzer": "iq_text_base"
      },
      "created_at": {
        "type": "date"
      },
      "id": {
        "type": "keyword"
      },
      "path": {
        "type": "text",
        "fields": {
          "delimiter": {
            "type": "text",
            "index_options": "freqs",
            "analyzer": "iq_text_delimiter"
          },
          "enum": {
            "type": "keyword",
            "ignore_above": 2048
          },
          "joined": {
            "type": "text",
            "index_options": "freqs",
            "analyzer": "i_text_bigram",
            "search_analyzer": "q_text_bigram"
          },
          "prefix": {
            "type": "text",
            "index_options": "docs",
            "analyzer": "i_prefix",
            "search_analyzer": "q_prefix"
          },
          "stem": {
            "type": "text",
            "analyzer": "iq_text_stem"
          }
        },
        "index_options": "freqs",
        "analyzer": "iq_text_base"
      },
      "size": {
        "type": "long"
      },
      "title": {
        "type": "text",
        "fields": {
          "delimiter": {
            "type": "text",
            "index_options": "freqs",
            "analyzer": "iq_text_delimiter"
          },
          "enum": {
            "type": "keyword",
            "ignore_above": 2048
          },
          "joined": {
            "type": "text",
            "index_options": "freqs",
            "analyzer": "i_text_bigram",
            "search_analyzer": "q_text_bigram"
          },
          "prefix": {
            "type": "text",
            "index_options": "docs",
            "analyzer": "i_prefix",
            "search_analyzer": "q_prefix"
          },
          "stem": {
            "type": "text",
            "analyzer": "iq_text_stem"
          },
          "japanese": {
            "type": "text",
            "analyzer": "kuromoji_analyzer"
          }
        },
        "index_options": "freqs",
        "analyzer": "iq_text_base"
      },
      "type": {
        "type": "text",
        "fields": {
          "delimiter": {
            "type": "text",
            "index_options": "freqs",
            "analyzer": "iq_text_delimiter"
          },
          "enum": {
            "type": "keyword",
            "ignore_above": 2048
          },
          "joined": {
            "type": "text",
            "index_options": "freqs",
            "analyzer": "i_text_bigram",
            "search_analyzer": "q_text_bigram"
          },
          "prefix": {
            "type": "text",
            "index_options": "docs",
            "analyzer": "i_prefix",
            "search_analyzer": "q_prefix"
          },
          "stem": {
            "type": "text",
            "analyzer": "iq_text_stem"
          }
        },
        "index_options": "freqs",
        "analyzer": "iq_text_base"
      }
    }
}
```
`title`と`body`に以下の部分が追加されています。これにより、`title.japanese`と`body.japanese`に対して検索をかけると形態素解析されたものになります。
```json
  "japanese": {
    "type": "text",
    "analyzer": "kuromoji_analyzer"
  }
```
設定が正しく入っているかどうかは以下を`Dev Tools`で実行すると確認できます。
```
GET search-windows-drive/_settings
GET search-windows-drive/_mapping
```
問題なければ以下を`Dev Tools`で実行してインデックスを再度開きます。
```
POST search-windows-drive/_open
```

# Connector clientを立ち上げる
## config fileの作成
Windowsマシン上でどこか適当な作業ディレクトリを作ります。
```
mkdir connectors-config
curl https://raw.githubusercontent.com/elastic/connectors/main/config.yml --output ./connectors-config/config.yml
```
と実行します。`config.yml`が保存されるので、notepadか何かで開きます。
先程保存した内容を`config.yml`に貼り付けます。
![スクリーンショット 2023-12-11 18.20.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/78bcbb51-bced-43b0-8334-39471f18bffe.png)

## Docker(Connector client)起動
Windows上で以下のコマンドを実行
`C:/Users/elastic/source/connectors/connectors-config`の部分は環境に合わせて変更してください。

:::note info
2024/08/02更新
imageは公式のリポジトリから確認してください
https://www.docker.elastic.co/r/enterprise-search/elastic-connectors
:::

```
docker network create elastic
docker run -v C:/Users/elastic/source/connectors/connectors-config:/config --network "elastic" --tty --rm docker.elastic.co/enterprise-search/elastic-connectors:<version> /app/bin/elastic-ingest -c /config/config.yml
```
エラーなどが出ていないことを確認してください。
## 同期
connector clientが待機状態になっているので、同期を開始します。
**Search**->**Content**から先程作ったインデックス(**search-windows-drive**)を選択し、**Configuration**タブをクリックして設定画面に戻ります。
![スクリーンショット 2023-12-14 13.26.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/4a525c05-ad61-16ac-2a3c-71f75ac1abcc.png)
一番下の**Sync**をクリックして**Full Content**を選んでしばらく待ちます。
Dockerを実行した画面に以下のような出力があればデータの同期ができています。
```
[FMWK][15:18:52][INFO] [Connector id: kGe9Y4wBBgiEiYrEOH-5, index name: search-windows-drive, Sync job id: w2fCY4wBBgiEiYrEKX_i] Sync progress -- created: 400 | updated: 0 | deleted: 0
[FMWK][15:18:57][INFO] [Connector id: kGe9Y4wBBgiEiYrEOH-5, index name: search-windows-drive, Sync job id: w2fCY4wBBgiEiYrEKX_i] Sync ended with status completed -- created: 409 | updated: 0 | deleted: 0 (took 17 seconds)
```
## 同期スケジュール
今回は手動で同期をしていますが、通常は一定間隔で自動的に行うと思います。
こちらの画面でその設定を行うことが出来ます。
![スクリーンショット 2023-12-15 14.49.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/eba56c2c-fdd4-c7a0-ed72-2f8dca0ba013.png)

## 同期ルール
フォルダを除外したり、拡張子で取り込むファイルの種類を決めたりすることができます。
ドキュメントはこちらです。
https://www.elastic.co/guide/en/enterprise-search/current/sync-rules.html
![スクリーンショット 2023-12-15 14.57.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/e2fb1379-a8e7-52e3-b7e8-dbedce3715a2.png)

## 制限
10MB以上のファイルはそのままではインデックス出来ません。
テキスト抽出を行うコンテナを別途立てる必要があります。
ドキュメントはこちらです。
https://www.elastic.co/guide/en/enterprise-search/current/connectors-content-extraction.html

## ドキュメントの確認
先程の画面で**Documents**タブをクリックするとインデックスされたドキュメントが表示されます。
![スクリーンショット 2023-12-14 14.06.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/8adccee2-cc5e-119b-5aa3-d9602e7e87a2.png)

## アクセス権限情報
ドキュメントの右上にあるところ（赤枠で囲ったところ）をクリックすると全てのフィールドが表示されます。
`_allow_access_control`というフィールドがそれです。WindowsのSIDが入っているのがわかるかと思います。
![スクリーンショット 2023-12-15 17.44.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/8ca1f93c-5e22-6e1b-77f8-143a46af8c9a.png)


### Document Level Security
Windowsサーバの場合、権限情報を別のインデックスに保存することで簡単にドキュメントレベルでアクセス制限をかけることが出来ます。
**Configuration**タブの**Sync**をクリックして、今度は**Access Control**をクリックします。
無事に同期が終わったら情報を確認してみます。
**Documents**タブに移動して、**Browse Documents**の横にあるPull Downをクリックして**Access control index**を選びます。
下図のように情報が入っていて、`query`が入っているのは実際に検索する際にこのqueryを使ってアクセス権限を確認するためです。このあたりはまた別のブログで紹介します。
Document Level Securityのドキュメントはこちらです。
https://www.elastic.co/guide/en/enterprise-search/current/dls-e2e-guide.html
![スクリーンショット 2023-12-14 14.29.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/46d452d8-576b-0c4d-99ea-18f2d69b285d.png)

# Discoverでデータを確認する
おなじみのDiscoverでもデータを見てみます。
**Stack Management**->**Index Management**から先ほど作成した**search-windows-drive**をクリックします。
![スクリーンショット 2023-12-15 9.30.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/c660f261-ee2b-e33a-e387-4e5174e32292.png)
次の画面で**Discover index**をクリックするとDiscoverのページに飛びます。
![スクリーンショット 2023-12-15 9.32.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/ccc36052-bb24-1a39-3713-6e73e18531a4.png)

Discoverのページで`body`か`title`をクリックしてみましょう。
![スクリーンショット 2023-12-15 9.55.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/e7135fe6-956b-e0b4-5ac7-3d8304618b67.png)
`kuromoji.analyzer`用に追加した`body.japanese`,`title.japanese`があるのが確認できます。

## 形態素解析
### ファイルの用意
ではごく簡単に形態素解析の効果を確認してみます。
まずは**東京都**とだけ書いたテキストファイルをWindowsの同期できる場所に置きます。そして**Search**->**Content**->**search-windows-drive**->**Configuration**で設定画面を開いて一番下にある**Sync**をクリックして**Full Content**を選んで同期待ちにしておきます。
コンテナが立ち上がったままであれば、自動的に同期されてDiscoverに表示されるはずです。
### 検索してみる
Discoverの検索ボックスに**東京**と入れてみると先程の文章が表示されますね。当然です。
では次に**京都**と入れてみてください。表示されてしまいましたね。
理由は東京都の後ろの「京都」に引っかかっているからです。
これは望んだ結果ではないはずです。

では次に`body.japanese:京都`と入力してみてください。表示されませんね。
`body.japanese`では「東京都」は「東京|都」と分割されているので検索ではヒットしません。

![スクリーンショット 2023-12-15 14.43.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603229/567db5fc-73d0-12e2-3d16-81bae504b40f.png)

ただ辞書ベースの分割のみに頼ると新しい言葉に対応できなくなります。
このあたりは様々な知見が必要となりますので、是非弊社コンサルティングサービスをご利用ください。

# まとめ
Connectorを使うと非常に簡単に色んな場所に散らばっている文書類をElasticsearchに取り込むことが出来ます。
ドキュメントの権限も設定することが出来るので、人事の書類を一般社員が見るなどの事故も起こりません。

全ての資料を一箇所に綺麗にまとめられているケースは少ないと思います。是非ElasticsearchのConnectorを使って社内文書の横断検索システムを構築して、従業員の生産性向上に役立ててみてください。