couchDBで厳密な文字列検索をする

N文字単位で分割した値をキーにしたViewを複数用意する。
データ容量とView作成時間との兼ね合いがあるがかなり高速に動作する。
本来ならこんなことはするものではない。
厳密でなくてもいいならLuceneを使うといい。
http://www.atmarkit.co.jp/ait/articles/1003/18/news098.html






データ構造



詳細は割愛するがcouchDBはjson形式で値を持つ。
今回は例として以下のようデータを想定。
{
  "_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1",
  "_rev": "1-8de9025cc518231f1f37a57f823af5db",
  "docKind": 3,
  "name": "ボールペン"
}
{
  "_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2",
  "_rev": "1-8de9025cc518231f1f37a57f823af5db",
  "docKind": 3,
  "name": "シャープペンシル"
}
{
  "_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx3",
  "_rev": "1-8de9025cc518231f1f37a57f823af5db",
  "docKind": 3,
  "name": "バスケットボール"
}


文字列検索用のViewを構築するためのMap Functions



以下のように文字列検索用のViewを作成する。


function (doc) {
  if( doc.docKind === 3 ) {
      // 検索対象 今回は商品名(name)
    var columnName = "name"
    var columnData = doc[columnName]
    // 1文字ずつ分割
    var chars = columnData.split("")
    for( var i=0; i<chars.length; i++ ) {
      // 商品名を単語単位をキーにしてemit
      emit(chars[i], null);
    }
    
    // 検索対象の文字列長(商品名のサイズ)
    var nameLength = columnData.length
    // N文字ずつ分割
    var N = 2;
    var key = ""
    for( i=0; i<nameLength - N + 1; i++ ) {
      key = columnData.slice(i, i+N);
      // 商品名を2文字ずつ分割した値をキーにしてemit
      emit(key, null);
    }
    
    N = 3;
    for( i=0; i<nameLength - N + 1; i++ ) {
      key = columnData.slice(i, i+N);
      emit(key, null);
    }
    
    N = 4;
    for( i=0; i<nameLength - N + 1; i++ ) {
      key = columnData.slice(i, i+N);
      emit(key, null);
    }
    
    N = 5;
    for( i=0; i<nameLength - N + 1; i++ ) {
      key = columnData.slice(i, i+N);
      emit(key, null);
    }
    
  }
}

わかりやすくするため若干冗長にしている。まとめてもいい。
docKindは今回は関係がないが擬似的にテーブルとして扱うために付与している。
Nを増やすごとに高速になるがViewのサイズとView構築時間が増える。
Viewサイズが増えすぎるのでvalueはnullにすること。



作成したViewから文字列検索をする



以下のようなViewができる(抜粋)
{"id":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1","key":"ボ","value":null},
{"id":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1","key":"ー","value":null},
{"id":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2","key":"ー","value":null},
{"id":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx3","key":"ー","value":null},
{"id":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2","key":"シ","value":null},
{"id":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2","key":"シ","value":null},
{"id":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1","key":"ペン","value":null},
{"id":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2","key":"ペン","value":null},
{"id":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1","key":"ボール","value":null},
{"id":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx3","key":"ボール","value":null},

keyの値を検索条件に指定してViewを検索できる。
idは元データが同じなら同じ値になる。(xxx1がボールペン、xxx2がシャープペン、xxx3がバスケットボール)
例えば"ボール"で検索した場合、一番下の2つのデータ(ボールペン、バスケットボール)を取得することができる。
データの重複が発生するのでidを見て絞り込む必要がある。
Nが1のとき文字数分だけデータが増えるので削ってもいい(1文字の検索ができなくなる)

上記のデータだけ取得しても意味がないのでinclude_docオプションを付与して元のデータも一緒に取得する。

https://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options


couchDBURL/_design/item-view/_view/search-name?include_docs=true にたいしてGETリクエスト

{"total_rows":xxx,"offset":0,"rows":[
    {"id":"0147d249bec79d6802f87ce6761d3f78","key":"-・","value":null,"doc":{
        "_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1",
        "_rev": "1-8de9025cc518231f1f37a57f823af5db",
        "docKind": 3,
        "name": "ボールペン"
    }},
    {"id":"0147d249bec79d6802f87ce6761d4931","key":"-・","value":null,"doc":{
        "_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx3",
        "_rev": "1-8de9025cc518231f1f37a57f823af5db",
        "docKind": 3,
        "name": "バスケットボール"
    }}
]}

上記は"ボール"で検索した例(見やすく整形している)
JSON形式で取得できるのでパースして利用する。
今回の例だと5文字までしか検索できないのでそれ以上のキーワードは取得後に絞り込む必要がある。



参考



http://couchdb-jp.github.io/couchdb-guide/editions/1/ja/index.html
http://couchdb-jp.github.io/couchdb-guide/editions/1/ja/views.html


どこ探しても見当たらないし割とレア情報ではなかろうか(上記参考の前方一致検索が一番近い)
これが複数条件とかになってくるとカオスになっていく。
最初にも記述したが本来こういう用途に使うものではない。
願わくばやむを得ない事情により不適切な用途に使って不幸になる人が減りますように。

2018年3月28日水曜日