お客様との対話要約を全文検索出来る環境を構築する

thumbnail

サントリーウエルネス DX推進部 エンジニアリングGの武田です。
フルスタックエンジニアとしてコンタクトセンターで使うシステムが抱える課題に日々取り組んでいます。

弊社ではお客様とお話した内容を対話要約として保存しています。
お客様の声を詳しく分析するための方法の一つとして、対話要約を全文検索出来るようにすることを検討しています。

この記事では「テキストデータをElasticsearchに投入して全文検索出来るようにする方法」を紹介したいと思います。

この記事から分かること

テキストデータをElasticseachに投入してKibanaで可視化する方法

キーワード

全文検索というと転置インデックス、N-Gram、シノニム、ユーザー辞書、文字列正規化、頻出語句除去など様々な技術要素がありますが今回は触れません。

キーワード概要
Elasticsearch全文検索エンジンです。大量の文書に対して高速で全文検索することが出来るようになります。
KibanaElasticserach用のデータ可視化ツールです。
Dockerコンテナ型の仮想環境プラットフォームです。設定ファイルから同一のアプリケーション実行環境を立ち上げることが出来るようになります。
形態素解析文章を意味を持つ最小の単位に分割・判別することです。例:「セサミンには抗酸化作用があります」->「セサミン(名刺)」「に(格助詞)」「は(係助詞)」「抗(接続詞)」「酸化(名刺)」「作用(名刺)」「が(格助詞)」「あり(動詞)」「ます(助動詞)」
タグクラウド件数、出現頻度、重要度を加味してタグの一覧を可視化したものです。
全文検索複数の文書の先頭から末尾までテキスト全てを対象に特定の文字列を検索することです。

構成

将来的には以下のようにデータをElasticsearchに連携し、全文検索出来る環境を作っていきたいと考えています。

※弊社ではコールセンターにて従事していただいているオペレーターをエージェントと言います。

前段階として今回はDockerでElastic Stack(Elasticsearch、Kibana、Logstash、Beats)を構築しました。
CSVを所定のディレクトリに置くとBeatsがファイルを検知してデータをLogstashに送り、Logstashでデータを処理してElasticsearchに投入し、KibanaでElasticsearchのデータを可視化します。

MW概要
Beatsデータシッパーです。今回はCSVをLogstashに転送します。
Logstashデータ処理パイプラインです。今回はFilebeatから受信したデータをElasticsearchに流します。
Elasticsearch全文検索エンジンです。テキストを形態素解析し全文検索出来るようにします。
KibanaElasticserach用のデータ可視化ツールです。Elasticsearchに投入したデータをフリーワード検索したり、タグクラウドを作成したりすることが出来ます。

テキストデータ準備

今回は対話要約のテキストデータ(CSV形式)を使います。
ヘッダーは "日時", "対話内容", "ID" です。

$ head -n 3 conversation_history.csv
"2023/08/01 09:00","お話内容その1","00001"
"2023/08/01 10:00","お話内容その2","00002"
"2023/08/01 11:00","お話内容その3","00003"

Elastic Stack 構築

Elasticsearch、Kibana、Logstash、BeatsをDockerで構築します。

コンテナ作成起動

$ docker-compose up

構成

$ tree . -L 3
.
├── docker-compose.yml
├── elasticsearch
│   ├── Dockerfile
│   ├── config
│   │   └── stopwords.txt
│   ├── data
│   └── mappings.json
├── filebeat
│   ├── conf
│   │   └── filebeat.yml
│   └── log
│       └── conversation_history_20230803.csv
└── logstash
    └── pipeline
        └── logstash.conf

docker-compose.yml

Elasticsearchにプラグインを入れたいため別にDockerfileを用意しました。
データ投入を高速で処理するためにElasticsearch、Logstashのメモリを固定で設定していますが、こちらはマシンスペックに応じて適宜変更してください。

version: "3"

services:
  elasticsearch:
    build: elasticsearch
    environment:
      - discovery.type=single-node
      - cluster.name=docker-cluster
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms2048m -Xmx2048m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    ports:
      - 9200:9200
    volumes:
      - ./elasticsearch/data:/usr/share/elasticsearch/data
      - ./elasticsearch/config/stopwords.txt:/usr/share/elasticsearch/config/stopwords.txt
  kibana:
    image: docker.elastic.co/kibana/kibana:7.17.0
    ports:
      - 5601:5601
  logstash:
    image: docker.elastic.co/logstash/logstash:7.17.0
    ports:
      - 5044:5044
    environment:
      - "LS_JAVA_OPTS=-Xms2048m -Xmx2048m"
    volumes:
      - ./logstash/pipeline:/usr/share/logstash/pipeline
  filebeat:
    image: docker.elastic.co/beats/filebeat:7.17.0
    volumes:
      - ./filebeat/conf/filebeat.yml:/usr/share/filebeat/filebeat.yml
      - ./filebeat/log:/usr/share/filebeat/log
      - /var/run/docker.sock:/var/run/docker.sock
    user: root

elasticsearch/Dockerfile

形態素解析のプラグインを入れます。

FROM docker.elastic.co/elasticsearch/elasticsearch:7.17.0
RUN elasticsearch-plugin install analysis-kuromoji
RUN elasticsearch-plugin install analysis-icu

elasticsearch/stopwords.txt

除去する頻出語句を定義します。 stopwords.txtの詳細を知りたい方はLuceneのstopwords.txtを参考にしてください。

filebeat/conf/filebeat.yml

filebeat/log配下のCSVをLogstashに流すように設定しています。

filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /usr/share/filebeat/log/*conversation_history*.csv

output.logstash:
  hosts: ["logstash:5044"]

logstash/pipeline/logstash.conf

FilebeatのCSVを受けてElasticserachに流す設定をしています。

logstash.conf

input {
# input from Filebeat
  beats {
    port => 5044
  }
}

filter {
  csv {
    columns => ["date", "conversation", "id"]
  }
  date {
    match => ["Date", "YYYY-MM-dd HH:mm"]
    timezone => "UTC"
  }
}

output {
  elasticsearch { 
    hosts    => [ 'elasticsearch' ]
    index => "conversation_history" 
  }
}

Elasticserachデータ投入

スキーマ定義(elasticsearch/mapping.json)

対話内容に形態素解析、N-Gramをかけます。

mapping.json

{
    "settings": {
        "analysis": {
            "char_filter": {
                "normalize": {
                    "type": "icu_normalizer",
                    "name": "nfkc",
                    "mode": "compose"
                }
            },
            "tokenizer": {
                "ja_kuromoji_normal_tokenizer": {
                    "mode": "normal",
                    "type": "kuromoji_tokenizer",
                    "discard_compound_token": true
                },
                "ja_kuromoji_search_tokenizer": {
                    "mode": "search",
                    "type": "kuromoji_tokenizer",
                    "discard_compound_token": true
                },
                "ja_ngram_tokenizer": {
                    "type": "ngram",
                    "min_gram": 2,
                    "max_gram": 3,
                    "token_chars": [
                        "letter",
                        "digit"
                    ]
                }
            },
            "filter": {
                "ja_index_synonym": {
                    "type": "synonym",
                    "lenient": false,
                    "synonyms": []
                },
                "ja_search_synonym": {
                    "type": "synonym_graph",
                    "lenient": false,
                    "synonyms": []
                },
                "ja_custom_stop": {
                    "type": "stop",
                    "stopwords_path": "stopwords.txt"
                },
                "kuromoji_custom_part_of_speech": {
                    "type": "kuromoji_part_of_speech",
                    "stoptags": [
                        "接続詞",
                        "助詞",
                        "助詞-格助詞",
                        "助詞-格助詞-一般",
                        "助詞-格助詞-引用",
                        "助詞-格助詞-連語",
                        "助詞-接続助詞",
                        "助詞-係助詞",
                        "助詞-間投助詞",
                        "助詞-並立助詞",
                        "助詞-終助詞",
                        "助詞-副助詞/並立助詞/終助詞",
                        "助詞-連体化",
                        "助詞-副詞化",
                        "助詞-特殊",
                        "助動詞",
                        "記号",
                        "記号-一般",
                        "記号-読点",
                        "記号-句点",
                        "記号-空白",
                        "記号-括弧開",
                        "記号-括弧閉",
                        "その他-間投",
                        "フィラー",
                        "非言語音"
                    ]
                }
            },
            "analyzer": {
                "ja_kuromoji_index_analyzer": {
                    "type": "custom",
                    "char_filter": [
                        "normalize"
                    ],
                    "tokenizer": "ja_kuromoji_normal_tokenizer",
                    "filter": [
                        "kuromoji_baseform",
                        "kuromoji_custom_part_of_speech",
                        "ja_index_synonym",
                        "cjk_width",
                        "ja_custom_stop",
                        "kuromoji_stemmer",
                        "lowercase"
                    ]
                },
                "ja_kuromoji_search_analyzer": {
                    "type": "custom",
                    "char_filter": [
                        "normalize"
                    ],
                    "tokenizer": "ja_kuromoji_normal_tokenizer",
                    "filter": [
                        "kuromoji_baseform",
                        "kuromoji_custom_part_of_speech",
                        "ja_search_synonym",
                        "cjk_width",
                        "ja_custom_stop",
                        "kuromoji_stemmer",
                        "lowercase"
                    ]
                },
                "ja_ngram_index_analyzer": {
                    "type": "custom",
                    "char_filter": [
                        "normalize"
                    ],
                    "tokenizer": "ja_ngram_tokenizer",
                    "filter": [
                        "cjk_width",
                        "kuromoji_stemmer",
                        "lowercase"
                    ]
                },
                "ja_ngram_search_analyzer": {
                    "type": "custom",
                    "char_filter": [
                        "normalize"
                    ],
                    "tokenizer": "ja_ngram_tokenizer",
                    "filter": [
                        "cjk_width",
                        "kuromoji_stemmer",
                        "lowercase"
                    ]
                }
            }
        }
    },
    "mappings": {
        "properties": {
            "date": {
                "type": "date",
                "format": "yyyy/MM/dd HH:mm"
            },
            "conversation": {
                "type": "text",
                "fielddata": true,
                "search_analyzer": "ja_kuromoji_search_analyzer",
                "analyzer": "ja_kuromoji_index_analyzer",
                "fields": {
                    "ngram": {
                        "type": "text",
                        "search_analyzer": "ja_ngram_search_analyzer",
                        "analyzer": "ja_ngram_index_analyzer"
                    }
                }
            },
            "id": {
                "type": "keyword"
            }
        }
    }
}

スキーマ作成

conversation_historyというインデックス名でスキーマを作成します。

$ cd elasticsearch
$ curl -XPUT "http://localhost:9200/conversation_history?pretty" -H "Content-Type: application/json" -d @mappings.json

データ投入

filebeat/logにCSVを置くとBeatsがファイルを検知しLogstashに流しElasticsearchにデータが投入されます。
(Elasticsearchにデータを投入することをインデキシングといいます。)
データが入っていることを確認します。

$ curl -sS -XGET "http://localhost:9200/conversation_history/_count?pretty"
{
  "count" : 1000,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  }
}

Kibana設定

http://localhost:5601 にアクセスするとKibanaのホーム画面が表示されます。

Elasticsearchインデックス確認

Managemant/Stack Management > Data/Index Managementより確認できます。

Kibanaインデックスパターン作成

Managemant/Stack Management > Kibana Index Patterns > Create index patternの順に進み下記設定で [Create index pattern] をクリックします。

keyvalue
Nameconversation_history*
Timestamp fielddate

Kibanaインデックスパターンが作成されます。

Kibanaデータ可視化

全文検索

Analytics Discoverをクリックします。
投入したデータが表示されます。

例えばテキストボックスに「conversation : “セサ”」と入力しEnterを押下すると「セサ」が含まれるレコードがヒットします。
(画像にモザイク処理をかけていてわかりにくいですが、検索にヒットしたキーワードに黄色でマーカーが引かれます。弊社ではセサミンをセサと略して表現することが多いです。)

タグクラウド

Kibanaではタグクラウドを作成することが出来るので試作しました。

Analytics Visualize Library > [Create new visualization] をクリックします。

Aggregation basedをクリックします。

Tag cloudをクリックします。

インデックスパターンを選択します。

Buckets > Add > Add bucket / Tagsをクリックします。

下記設定し [Update] をクリックするとタグクラウドが表示されます。

keyvalue
AggregationTerms
Fieldsconversation
Size100
Exclude.\[0-9]{1,4}

テキストボックスに「conversation : セサ」と入力しEnterを押下するとセサに関するタグクラウドが描画されます。

まとめ

この記事ではテキストデータをElasticsearchに投入して全文検索出来るようにする方法を紹介しました。
今回はCSVファイルを取り込みましたが、それ以外のフォーマットでもデータを連携することが出来るので是非お試しください。
(こちら内部で共有したところ柔軟に検索出来るようになって良さそうと好評でした。デモを作成してみて良かったです。)