CSV形式のオープンデータを Bluemix の Cloudant へロードするメモ の続編で、読み込んだ郵便番号データに対して、Cloudantの日本語検索が、どの程度動作するか確認したメモです。
Cloudantの日本語対応
Cloundat には、各言語特有のアナライザー(1)が提供され、その中に japanese も含まれているので、日本語検索も期待しても良いと思わる。 この日本語対応アナライザーには、kuromoji (2), cjk (4) が利用できる。 しかし、Cloudantのドキュメントは英語がほとんどで、日本語処理への対応が心配なので、検証することにした。
インデックス無しでの検索
文字列一致で検索
インデックスを設定していない福岡県の郵便番号データベースの中から、町域で、港町と魚町に一致する市町村を探してみる。
query = {
'selector': {
'$or': [
{'cyoiki': '港町'},
{'cyoiki': '魚町'},
]
},
"fields": ["cyoiki", "jusho_CD", "todoufuken", "sicyoson"]
};
cdb.find(query,function(err, result) {
console.log("err = ", err);
console.log("size = ", result.docs.length);
console.log("result = ", result);
});
実行結果では、港町と魚町が表示された。 インデックスを設定しなくても、日本語文字列の一致を取得できる。
err = null
size = 6
result = { warning: 'no matching index found, create an index to optimize query time',
docs:
[ { cyoiki: '港町',
jusho_CD: '800031500',
todoufuken: '福岡県',
sicyoson: '京都郡苅田町' },
{ cyoiki: '港町',
jusho_CD: '801085200',
todoufuken: '福岡県',
sicyoson: '北九州市門司区' },
{ cyoiki: '港町',
jusho_CD: '801850300',
todoufuken: '福岡県',
sicyoson: '北九州市門司区' },
{ cyoiki: '魚町',
jusho_CD: '802000600',
todoufuken: '福岡県',
sicyoson: '北九州市小倉北区' },
{ cyoiki: '魚町',
jusho_CD: '825001400',
todoufuken: '福岡県',
sicyoson: '田川市' },
{ cyoiki: '港町',
jusho_CD: '836002200',
todoufuken: '福岡県',
sicyoson: '大牟田市' } ] }
確認のために、grep コマンドで、答え合わせをしておくが、結果は同じであった。
imac:test maho$ grep -e ',魚町' -e ',港町' 40fukuok.csv.utf-8
801085200,40,40101,401010100,801-0852,0,0,福岡県,フクオカケン,北九州市門司区,キタキュウシュウシモジク,港町,ミナトマチ,,,,,,,,,
801850300,40,40101,401010100,801-8503,1,0,福岡県,フクオカケン,北九州市門司区,キタキュウシュウシモジク,港町,ミナトマチ,,,,,,株式会社 門司港ホテル,カブシキカイシヤ モジコウホテル,港町9番11号,
802000600,40,40106,401060015,802-0006,0,0,福岡県,フクオカケン,北九州市小倉北区,キタキュウシュウシコクラキタク,魚町,ウオマチ,,,,,,,,,
836002200,40,40202,402020144,836-0022,0,0,福岡県,フクオカケン,大牟田市,オオムタシ,港町,ミナトマチ,,,,,,,,,
825001400,40,40206,402060006,825-0014,0,0,福岡県,フクオカケン,田川市,タガワシ,魚町,ウオマチ,,,,,,,,,
800031500,40,40621,406210036,800-0315,0,0,福岡県,フクオカケン,京都郡苅田町,ミヤコグンカンダマチ,港町,ミナトマチ,,,,,,,,,
文字列先頭一致で検索(失敗例)
次の様に先頭一文字だけで検索すると、一致したものだけが、返される
query = {
'selector': {
'cyoiki': '港'
},
"fields": ["cyoiki", "jusho_CD", "todoufuken", "sicyoson"]
};
cdb.find(query,function(err, result) {
result = { warning: 'no matching index found, create an index to optimize query time',
docs:
[ { cyoiki: '港',
jusho_CD: '810007500',
todoufuken: '福岡県',
sicyoson: '福岡市中央区' },
{ cyoiki: '港',
jusho_CD: '810864800',
todoufuken: '福岡県',
sicyoson: '福岡市中央区' } ] }
grepの結果と上記実行結果は同じであった。
$ grep -e ',港,' 40fukuok.csv.utf-8
810007500,40,40133,401330047,810-0075,0,0,福岡県,フクオカケン,福岡市中央区,フクオカシチュウオウク,港,ミナト,,,,,,,,,
810864800,40,40133,401330047,810-8648,1,0,福岡県,フクオカケン,福岡市中央区,フクオカシチュウオウク,港,ミナト,,,,,,株式会社 トクスイコーポレーション,カブシキガイシヤ トクスイコ−ポレ−シヨン,港2丁目2−21,
文字列先頭一致で検索(成功例)
$regex で正規表現が利用できるので、試してみると、先頭に"港"がつくデータがリストされた。
query = {
"selector": {
"cyoiki": {
"$regex": "^港"
}
},
"fields": ["cyoiki", "jusho_CD", "todoufuken", "sicyoson"]
};
cdb.find(query,function(err, result) {
result = { warning: 'no matching index found, create an index to optimize query time',
docs:
[ { cyoiki: '港町',
jusho_CD: '800031500',
todoufuken: '福岡県',
sicyoson: '京都郡苅田町' },
{ cyoiki: '港町',
jusho_CD: '801085200',
todoufuken: '福岡県',
sicyoson: '北九州市門司区' },
{ cyoiki: '港町',
jusho_CD: '801850300',
todoufuken: '福岡県',
sicyoson: '北九州市門司区' },
{ cyoiki: '港',
jusho_CD: '810007500',
todoufuken: '福岡県',
sicyoson: '福岡市中央区' },
{ cyoiki: '港',
jusho_CD: '810864800',
todoufuken: '福岡県',
sicyoson: '福岡市中央区' },
{ cyoiki: '港町',
jusho_CD: '836002200',
todoufuken: '福岡県',
sicyoson: '大牟田市' } ] }
インデックス ワーニング対策
以下のワーニングを解消するためには、インデックスを設定する必要がある。
result = { warning: 'no matching index found, create an index to optimize query time',
docs:
[ { cyoiki: '港町',
JSON形式のインデックスを設定した場合
次の様な、JSON形式のインデックスを生成した場合、
// JSON インデックス
var ddoc_name = "index1";
var key = "_design/" + ddoc_name;
var index = {
type: "json",
name: "index-1",
ddoc: ddoc_name,
index: {
fields: ["jusho_CD:string","cyoiki:string"]
}
}
下記のクエリーで得られる結果で、確認します。
query = {
"selector": {
"cyoiki": "港町"
},
"fields": ["cyoiki", "jusho_CD", "todoufuken", "sicyoson"],
"sort": [{"jusho_CD:string": "desc"}]
};
cdb.find(query,function(err, result) {
次の様に、warningは出るが、意図した結果が得られる。
size = 4
result = { warning: 'no matching index found, create an index to optimize query time',
docs:
[ { cyoiki: '港町',
jusho_CD: '836002200',
todoufuken: '福岡県',
sicyoson: '大牟田市' },
{ cyoiki: '港町',
jusho_CD: '801850300',
todoufuken: '福岡県',
sicyoson: '北九州市門司区' },
{ cyoiki: '港町',
jusho_CD: '801085200',
todoufuken: '福岡県',
sicyoson: '北九州市門司区' },
{ cyoiki: '港町',
jusho_CD: '800031500',
todoufuken: '福岡県',
sicyoson: '京都郡苅田町' } ] }
ここで、インデックス作成時に、項目の順番を入れ替えた場合、言い換えると、クエリーの "sort" の項目の順番が一致していない場合は、エラーになる。 Index と Query の項目の順番を注意しておく必要がある。
fields: ["jusho_CD:string","cyoiki:string"]
fields: ["cyoiki:string","jusho_CD:string"]
JSON形式のインデックスでは、warningが解消しないことが判った。
TEXT形式のインデックスを利用した場合
次が、TEXT形式のインデックス作成のJSONデータです。 違いは、type の項目が text になること、fields の書式が異なる点です。内部的な違いは、Apache CouchDB のソースを参照してください。
var ddoc_name = "index1";
var key = "_design/" + ddoc_name;
var index = {
"type": "text",
"name": "index-1",
"ddoc": ddoc_name,
"index": {
"fields": [
{ "name": "cyoiki", "type": "string" },
{ "name": "jusho_CD", "type": "string" }
],
}
}
このインデックスを設定して、前述と同じ条件で検索を実施した結果が、以下です。 JSON形式で表示されていた Warning は解消して、新たに bookmark が表示されています。このbookmarkは、クエリーの結果のページ送りするために、利用することができます。(3)
result = { docs:
[ { cyoiki: '港町',
jusho_CD: '836002200',
todoufuken: '福岡県',
sicyoson: '大牟田市' },
{ cyoiki: '港町',
jusho_CD: '801850300',
todoufuken: '福岡県',
sicyoson: '北九州市門司区' },
{ cyoiki: '港町',
jusho_CD: '801085200',
todoufuken: '福岡県',
sicyoson: '北九州市門司区' },
{ cyoiki: '港町',
jusho_CD: '800031500',
todoufuken: '福岡県',
sicyoson: '京都郡苅田町' } ],
bookmark: 'g1AAAAFJeJzLYWBgYMlgTmFQTUlKzi9KdUhJMjTQS8rVTU7WTUnM0TUw1kvOyS9NScwr0ctLLckBKmdKUgCSSfr____PAvNzgQSnhYGBgbGhqYFB4sssNPOMCJkXADIvHt08QwtTA2OgeaJg81Tg5pkRMq4AZFw9hnEGFqZGQOO4SXVeHguQZJgApIBGzkc209jMwMAIZGZHVhYAzIVcSw' }
インデックスの作成と明示的なインデックスの指定
インデックスは、JSON形式とTEXT形式で作ることができ、クラウドのコンソール画面だけでなく、次の様なnodeのスクリプトでも作成することができます。
var ddoc_name = "index-json";
var key = "_design/" + ddoc_name;
var index = {
type: "json",
name: "index-json",
ddoc: ddoc_name,
index: {
fields: ["jusho_CD:string","cyoiki:string"]
}
}
cdb.index(index, function(err, response) {
if (err) throw err;
console.log('Index creation result: %s', response.result);
callback(err, response);
});
var ddoc_name = "index-text";
var key = "_design/" + ddoc_name;
var index = {
"type": "text",
"name": "index-text",
"ddoc": ddoc_name,
"index": {
"fields": [
{ "name": "cyoiki", "type": "string" },
{ "name": "jusho_CD", "type": "string" }
],
}
}
cdb.index(index, function(err, response) {
if (err) throw err;
console.log('Index creation result: %s', response.result);
callback(err, response);
});
Cloudantは、必要に応じて、DDOC (Design Document) として書き込むことで、インデックスを設定していくことが出ます。しかし、この2系統のインデックスの使い分けがわからないですね。 名前からすると、JSONがTEXTに置き換わるのではないかと思われますが、特に書かれていないので、本家のApache CouchDBのプロジェクトのドキュメントやコードを探してみたいといけないですが、warning以外に違いがあったので、メモがわりに書いておきます。
次の様に、JSONインデックスとTEXTインデックスを作成しておき、クエリーで明示的にインデックスをDDOC名で指定する機能が働くか試してみます。
JSONインデックスの場合
次の様なQueryを書いて、"use_index" で、index-jsonを指定すると、エラーが発生して実行できませんでした。
query = {
"selector": {
"cyoiki": "港町"
},
"fields": ["cyoiki", "jusho_CD", "todoufuken", "sicyoson"],
"use_index": "_design/index-json",
"sort": [{"jusho_CD:string": "desc"}]
};
cdb.find(query,function(err, result) {
console.log("err = ", err);
console.log("size = ", result.docs.length);
console.log("result = ", result);
});
上記のクエリーで発生したエラーメッセージの抜粋です。
name: 'Error',
error: 'no_usable_index',
reason: 'There is no index available for this selector.',
scope: 'couch',
statusCode: 400,
TEXTインデックスの場合
これに対して、_design/index-text を指定した場合は、次の様に期待通りの動作になります。 どうもJSONインデックスは、発展途上なのか、特別な目的のために用意された様です。
err = null
size = 4
result = { docs:
[ { cyoiki: '港町',
jusho_CD: '836002200',
todoufuken: '福岡県',
sicyoson: '大牟田市' },
{ cyoiki: '港町',
jusho_CD: '801850300',
todoufuken: '福岡県',
sicyoson: '北九州市門司区' },
{ cyoiki: '港町',
jusho_CD: '801085200',
todoufuken: '福岡県',
sicyoson: '北九州市門司区' },
{ cyoiki: '港町',
jusho_CD: '800031500',
todoufuken: '福岡県',
sicyoson: '京都郡苅田町' } ],
bookmark: 'g1AAAAFIeJzLYWBgYMlgTmFQSUlKzi9KdUhJstRLytVNTtZNSczRNTDWS87JL01JzCvRy0styQGqZkpSAJJJ-v___88C83OBBKeFgYGBsaGpgUHiyyyQcapw4wwNCZkXADIvHt08QwtTA2OgeaJZqM4zI2RcAci4egzjDCxMjYDGcZPqvDwWIMkwAUgBjZyPbKaxmYGBEcjMjqwsAJTIXCA' }
日本語の全文検索を試してみる
まずは、インデックスの設定です。 TEXTインデックスで、フィールド名 cyoikiとjusho_CDをインデックスにセットします。これで、全文検索は、この二つのフィールドに対して適用されます。
var ddoc_name = "index-text2";
var key = "_design/" + ddoc_name;
var index = {
"type": "text",
"name": "index-fulltext",
"ddoc": ddoc_name,
"index": {
"fields": [
{ "name": "jusho_CD", "type": "string" },
{ "name": "cyoiki", "type": "string" }
],
}
}
次のクエリーとして、「門司」で全文検索してみたいと思います。
query = {
"selector": {
"$text": '門司'
},
"fields": ["cyoiki", "sicyoson"],
"use_index": "_design/index-text2",
"sort": [{"jusho_CD:string": "asc"}]
};
結果は、「門司」以外のものが沢山出てきています。良くみると、「門」と「司」が含まれる文字列が全てヒットしていることが、解ります。 困りましたね。 やりたいと事は、門司を含む町域をリストしたいのに、これでは使えませんね。
err = null
size = 42
result = { docs:
[ { cyoiki: '新門司北', sicyoson: '北九州市門司区' },
{ cyoiki: '新門司', sicyoson: '北九州市門司区' },
{ cyoiki: '旧門司', sicyoson: '北九州市門司区' },
{ cyoiki: '門司', sicyoson: '北九州市門司区' },
{ cyoiki: '庄司町', sicyoson: '北九州市門司区' },
{ cyoiki: '東門司', sicyoson: '北九州市門司区' },
{ cyoiki: '山門町', sicyoson: '北九州市小倉北区' },
{ cyoiki: '大門', sicyoson: '北九州市小倉北区' },
{ cyoiki: '正門町', sicyoson: '遠賀郡芦屋町' },
{ cyoiki: '古門', sicyoson: '鞍手郡鞍手町' },
{ cyoiki: '黒門', sicyoson: '福岡市中央区' },
{ cyoiki: '大手門', sicyoson: '福岡市中央区' },
{ cyoiki: '大手門', sicyoson: '福岡市中央区' },
{ cyoiki: '大手門', sicyoson: '福岡市中央区' },
{ cyoiki: '大手門', sicyoson: '福岡市中央区' },
{ cyoiki: '黒門', sicyoson: '福岡市中央区' },
{ cyoiki: '大手門', sicyoson: '福岡市中央区' },
{ cyoiki: '大手門', sicyoson: '福岡市中央区' },
{ cyoiki: '大手門', sicyoson: '福岡市中央区' },
{ cyoiki: '老司', sicyoson: '福岡市南区' },
{ cyoiki: '宮司', sicyoson: '福津市' },
{ cyoiki: '宮司元町', sicyoson: '福津市' },
{ cyoiki: '宮司浜', sicyoson: '福津市' },
{ cyoiki: '宮司ヶ丘', sicyoson: '福津市' },
{ cyoiki: '島門', sicyoson: '遠賀郡遠賀町' },
{ cyoiki: '古門戸町', sicyoson: '福岡市博多区' },
{ cyoiki: '古門戸町', sicyoson: '福岡市博多区' },
{ cyoiki: '古門戸町', sicyoson: '福岡市博多区' },
{ cyoiki: '下山門団地', sicyoson: '福岡市西区' },
{ cyoiki: '下山門', sicyoson: '福岡市西区' },
{ cyoiki: '上山門', sicyoson: '福岡市西区' },
{ cyoiki: '大門', sicyoson: '糸島市' },
{ cyoiki: '庄司', sicyoson: '飯塚市' },
{ cyoiki: '大門', sicyoson: '飯塚市' },
{ cyoiki: '門樋町', sicyoson: '行橋市' },
{ cyoiki: '門樋町', sicyoson: '行橋市' },
{ cyoiki: '三毛門', sicyoson: '豊前市' },
{ cyoiki: '長門石町', sicyoson: '久留米市' },
{ cyoiki: '長門石', sicyoson: '久留米市' },
{ cyoiki: '六ツ門町', sicyoson: '久留米市' },
{ cyoiki: '北野町赤司', sicyoson: '久留米市' },
{ cyoiki: '瀬高町山門', sicyoson: 'みやま市' } ],
bookmark: 'g1AAAAUXeJyd0z9OwzAUBnALWGChN4ABxoTnOP-6QCUmprIzgJ1UQlaaIFTECBdAcAM6sMMBkOAGcAM4AAJuEN6rESLuEDmLo0jWT8_fZxeMsd7xYs42cpVVp6NBrkJfjb0s83JZeCD8rKjOcllO_HI0KXD3gmRqta5rPfsZ47KciggAQgD5qcna_LN40IKpHq5qzfICAM7JO9fN2fpt3DpxWxbH0Qv6yHkWl7RxQNyOzUEaAXHb2i05NSBuaIeH2QmO3IFzePvkHc6dNo15it6ea3hHxFV2F9gDxMhNLS5u406Iu7Cn46EQeGT54RheuYQru8QPitd2H5wCzFwDNOaNMW9tM0jQHDqGaMipIe-tSw0RjXnleAsN-WDIpwaZ_l6du05hPhvypdmPEPRe5GO3MF-N-fbfBEwzoc5XOoX5bsgvq58Y6wG52ynMb0POvcNZ5ZXWP51UcoE' }
アナライザー指定
テキストの中の単語をどの様に認識するかを設定するのが、アナライザー(1)です。これを設定して、動作を確認します。最初は、次のコードの通り、japanese (kuromoji)です。
var ddoc_name = "index-text2";
var key = "_design/" + ddoc_name;
var index = {
"type": "text",
"name": "index-fulltext",
"ddoc": ddoc_name,
"index": {
"fields": [
{ "name": "jusho_CD", "type": "string" },
{ "name": "cyoiki", "type": "string" }
],
"default_field": {
'enabled': true,
'analyzer': 'japanese' //<<--- ココ
}
}
}
実行結果は、期待ハズレですね。 kuromojiは、Javaで書かれているオープンソースの日本語形態素解析エンジンなのですが、地名を品詞に分解するのは、逆に難しい様ですね。
err = null
size = 2
result = { docs:
[ { cyoiki: '旧門司', sicyoson: '北九州市門司区' },
{ cyoiki: '門司', sicyoson: '北九州市門司区' } ],
次に keyword を試してみます。 まったくトークン化しない、形態素解析しない指定です。
"default_field": {
'enabled': true,
'analyzer': 'keyword' // <<--- ココ
}
実行結果は、以下の通りです。 「門司」にぴったり一致したものだけが、ヒットしていますね。これも使えません。
err = null
size = 1
result = { docs: [ { cyoiki: '門司', sicyoson: '北九州市門司区' } ],
次は、cjk (Chinese, Japanese, Korean)を試してみます。 cjkアナライザー(4)は、基本は bi-gram のトークナイザーですが、どうでしょうか? うまくいくでしょうか
"default_field": {
'enabled': true,
'analyzer': 'cjk'
}
結果は次の通りです。 もっとも期待する通りの結果が出た様に思います。 地名を処理するなら、cjkが良さそうですね。
err = null
size = 5
result = { docs:
[ { cyoiki: '新門司北', sicyoson: '北九州市門司区' },
{ cyoiki: '新門司', sicyoson: '北九州市門司区' },
{ cyoiki: '旧門司', sicyoson: '北九州市門司区' },
{ cyoiki: '門司', sicyoson: '北九州市門司区' },
{ cyoiki: '東門司', sicyoson: '北九州市門司区' } ],
日本語のソート
検索結果の日本語文字列のソートについて、確認してみます。最初にソートなしの結果を確認します。
query = {
"selector": {
'$or': [
{'cyoiki': '港町'},
{'cyoiki': '魚町'},
]
},
"fields": ["cyoiki", "sicyoson"],
"use_index": "_design/index-text2"
};
次の様に、港町、魚町が入り乱れる感じで結果が返ってきています。
err = null
size = 6
result = { docs:
[ { cyoiki: '港町', sicyoson: '北九州市門司区' },
{ cyoiki: '魚町', sicyoson: '田川市' },
{ cyoiki: '港町', sicyoson: '北九州市門司区' },
{ cyoiki: '港町', sicyoson: '大牟田市' },
{ cyoiki: '港町', sicyoson: '京都郡苅田町' },
{ cyoiki: '魚町', sicyoson: '北九州市小倉北区' } ],
それでは、次のクエリーで町域にソート条件を設定してみます。
query = {
"selector": {
'$or': [
{'cyoiki': '港町'},
{'cyoiki': '魚町'},
]
},
"fields": ["cyoiki", "sicyoson"],
"use_index": "_design/index-text2",
"sort": [{"cyoiki:string": "asc"}]
};
結果は、期待どおり、魚町と港町が順番になっています。
err = null
size = 6
result = { docs:
[ { cyoiki: '港町', sicyoson: '北九州市門司区' },
{ cyoiki: '港町', sicyoson: '北九州市門司区' },
{ cyoiki: '港町', sicyoson: '大牟田市' },
{ cyoiki: '港町', sicyoson: '京都郡苅田町' },
{ cyoiki: '魚町', sicyoson: '北九州市小倉北区' },
{ cyoiki: '魚町', sicyoson: '田川市' } ],
まとめ
Cloundant の日本語処理の対応について確認した結果、十分使えると言えるが、設定を正しく理解しないと、判断を間違えるので注意が必要ですね。
- JSONインデックスとTEXTインデックスがあるが、確実に動作するのは、TEXTインデックス
- 日本語処理では、TEXTインデックスのアナライザーの設定が重要
- 日本語アナライザーは、kuromoji と cjk の2つが選択できる。地名などは、cjk が期待値を返してくれた。
参考資料
(1) Cloudant NoSQL DB API Reference Search Analyzers https://console.bluemix.net/docs/services/Cloudant/api/search.html#analyzers
(2) Lucene 4.2.1 analyzers-kuromoji API http://lucene.apache.org/core/4_2_1/analyzers-kuromoji/overview-summary.html
(3) Cloudant NoSQL DB API Reference Request body https://console.bluemix.net/docs/services/Cloudant/api/cloudant_query.html#finding-documents-by-using-an-index
(4) Package org.apache.lucene.analysis.cjk https://lucene.apache.org/core/4_0_0/analyzers-common/org/apache/lucene/analysis/cjk/package-summary.html
(5) Luene/SolrのCJKAnalyzerをカスタマイズして遊んでみる http://www.mwsoft.jp/programming/lucene/lucene_custom_cjk.html
(6) Cloudant NoSQL DB Query https://console.bluemix.net/docs/services/Cloudant/api/cloudant_query.html#query
(7) Cloudant NoSQL DB Design Documents https://console.bluemix.net/docs/services/Cloudant/api/design_documents.html#design-documents