記事
· 2020年9月15日 14m read

Cachéでのカスタムインデックスタイプの作成

Cachéデータベースのオブジェクトおよびリレーショナルデータモデルは、標準、ビットマップ ビットスライスの3種類のインデックスをサポートします。 これら3つのネイティブタイプに加えて、開発者は独自のカスタムタイプのインデックスを宣言し、バージョン2013.1以降の任意のクラスで使用できます。 たとえば、iFindテキストインデックスは、そのメカニズムを使用しています。

カスタムインデックスタイプは、挿入、更新、削除を実行するための%Library.FunctionalIndexインターフェースのメソッドを実装するクラスです。 新しいインデックスを宣言するときに、そのようなクラスをインデックスタイプとして指定できます。

例:

Property A As %String;
Property B As %String;
Index someind On (A,B) As CustomPackage.CustomIndex;

 CustomPackage.CustomIndex クラスは、カスタムインデックスを実装するまさにそのクラスです。

たとえば、ハッカソン中に私たちのチーム(Andrey Rechitsky  Aleksander Pogrebnikov、そして)が開発した空間データのクワッドツリーベースのインデックスの小さなプロトタイプを分析してみましょう。 (ハッカソンは、InterSystems Russia Innovation School Training(インターシステムズ・ロシア・イノベーション・スクール・トレーニング)で毎年開催されます。ハッカソンの主な閃きとなったTimur Safin氏に感謝いたします。)

この記事では、クワッドツリーとそれらの操作方法については説明しません。 代わりに、既存のクワッドツリーアルゴリズム実装のための%Library.FunctionalIndexインターフェースを実装する新しいクラスを作成する方法を検討してみましょう。 私たちのチームでは、このタスクはAndreyに割り当てられました。 Andeyは、次の2つの方法で SpatialIndex.Indexerクラスを作成しました。

  • Insert(x, y, id)
  • Delete(x, y, id)

 SpatialIndex.Indexerの新しいインスタンスを作成するとき、インデックスデータを格納するためのグローバルノード名を定義する必要がありました。

InsertIndex UpdateIndex DeleteIndex PurgeIndex メソッドを使って SpatialIndex.Indexのクラスを作成するだけで済みました。 最初の3つのメソッドは、変更する文字列のIdを受け入れ、インデックス付きの値は、対応するクラス内のインデックス宣言で定義されているのとまったく同じ順序です。 この例では、入力引数はpArg(1)A and pArg(2)Bです。

 
Spoiler

 

 IndexLocation  は、インデックス値が保存されているグローバル内のノードの名前を返す補足メソッドです。

ここで、 SpatialIndex.Indexタイプのインデックスが使われているテストクラスを分析しましょう。

Class SpatialIndex.Test Extends %Persistent
{

Property Name As %String(MAXLEN = 300);

Property Latitude As %String;

Property Longitude As %String; Index coord On (Latitude, Longitude) As SpatialIndex.Index;

}

 SpatialIndex.Testクラスがコンパイルされている場合、システムは、SpatialIndex.Indexの各インデックスのINTコードで次のメソッドを生成します。

zcoordInsertIndex(pID,pArg...) public {
set indexer = ##class(SpatialIndex.Indexer).%New($Name(^SpatialIndex.TestI("coord")))
do indexer.Insert(pArg(1),pArg(2),pID) }
zcoordPurgeIndex() public {
kill ^SpatialIndex.TestI("coord") }
zcoordSegmentInsert(pIndexBuffer,pID,pArg...) public {
do ..coordInsertIndex(pID, pArg...) }
zcoordUpdateIndex(pID,pArg...) public {
set indexer = ##class(SpatialIndex.Indexer).%New($Name(^SpatialIndex.TestI("coord")))
do indexer.Delete(pArg(3),pArg(4),pID)
do indexer.Insert(pArg(1),pArg(2),pID) }

 %SaveData  %DeleteData  %SQLInsert  %SQLUpdate および%SQLDelete メソッドはインデックス内のメソッドを呼び出します。 たとえば、次のコードは%SaveDataメソッドの一部です。

if insert {
     ...
     do ..coordInsertIndex(id,i%Latitude,i%Longitude,"")
      ...
 } else {
      ...
     do ..coordUpdateIndex(id,i%Latitude,i%Longitude,zzc27v3,zzc27v2,"")
      ...
 }

実際の例は常に理論よりも優れているため、次のリポジトリからファイルをダウンロードできます: https://github.com/intersystems-ru/spatialindex/tree/no-web-interface  。 これは、Web UIのないブランチへのリンクです。 このコードを使用するには、以下を行います。

  1. クラスをインポートする
  2. RuCut.zipを解凍する
  3. 次の呼び出しを使用してデータをインポートする:
do $system.OBJ.LoadDir("c:\temp\spatialindex","ck")
do ##class(SpatialIndex.Test).load("c:\temp\rucut.txt")

rucut.txtファイルには、ロシアの10万の都市や町に関するデータとその名前と座標が含まれています。 Loadメソッドは各ファイル文字列を読み取り、それをSpatialIndex.Testクラスの個別のインスタンスとして保存します。 Loadメソッドが実行されると、グローバル^ SpatialIndex.TestI( 「coord」)には、緯度と経度の座標を持つクワッドツリーが含まれます。

では、クエリを実行しましょう!

インデックスの構築は、一番おもしろい部分ではありません。 さまざまなクエリでインデックスを使用します。 Cachéには、非標準インデックスの標準構文があります。

SELECT *
FROM SpatialIndex.Test
WHERE %ID %FIND search_index(coord, 'window', 'minx=56,miny=56,maxx=57,maxy=57')

%ID %FIND search_index  は構文の固定部分です。 次に、インデックス名coordがあります。引用符は必要ありません。 他のすべてのパラメーター( 'window'、 'minx = 56、miny = 56、maxx = 57、maxy = 57')は  Findメソッドに渡されます。これもインデックスタイプのクラスで定義する必要があります(この例では、 SpatialIndex.Indexです  )。

ClassMethod Find(queryType As %Binary, queryParams As %String) As %Library.Binary [ CodeMode = generator, ServerOnly = 1, SqlProc ]
{
    if %mode'="method" {
        set IndexGlobal = ..IndexLocation(%class,%property)
        set IndexGlobalQ = $$$QUOTE(IndexGlobal)
        $$$GENERATE($C(9)_"set result = ##class(SpatialIndex.SQLResult).%New()")
        $$$GENERATE($C(9)_"do result.PrepareFind($Name("_IndexGlobal_"), queryType, queryParams)")
        $$$GENERATE($C(9)_"quit result")
    }
}

このコードサンプルでは、パラメーターは2つしかありません。  queryType queryParams ですが、必要な数のパラメータを自由に追加できます。

  SpatialIndex.Indexが使われているクラスをコンパイルする場合、 Find メソッドは、 z <IndexName> Findと呼ばれる補足メソッドを生成しますが 、これはSQLクエリの実行に使用されます。

zcoordFind(queryType,queryParams) public s:'$isobject($g(%sqlcontext)) %sqlcontext=##class(%Library.ProcedureContext).%New()
set result = ##class(SpatialIndex.SQLResult).%New()
do result.PrepareFind($Name(^SpatialIndex.TestI("coord")), queryType, queryParams)
quit result }

 Findメソッドは、%SQL.AbstractFindインターフェースを実装するクラスのインスタンスを返す必要があります。 このインターフェイスのメソッド、 NextChunk および PreviousChunk は、それぞれ64,000ビットのチャンクでビット文字列を返します。 特定のIDのレコードが選択基準を満たし、対応するビット(chunk_number * 64000 + position_number_within_chunk)が1に設定されます。

 
Spoiler
Class SpatialIndex.SQLResult Extends %SQL.AbstractFind
{

Property ResultBits [ MultiDimensional, Private ];

Method %OnNew() As %Status [ Private, ServerOnly = 1 ]
{
    kill i%ResultBits
    kill qHandle
    quit $$$OK
}

Method PrepareFind(indexGlobal As %String, queryType As %String, queryParams As %Binary) As %Status
{
    if queryType = "window" {
        for = 1:1:4 {
            set item = $Piece(queryParams, ",", i)
            set IndexGlobal = ..IndexLocation(%class,%property)
            $$$GENERATE($C(9)_"kill " _ IndexGlobal) set param = $Piece(item, "=", 1)
            set value = $Piece(item, "=" ,2)
            set arg(param) = value
        }         set qHandle("indexGlobal") = indexGlobal
        do ##class(SpatialIndex.QueryExecutor).InternalFindWindow(.qHandle,arg("minx"),arg("miny"),arg("maxx"),arg("maxy"))         set id = ""         for  {
            set id = $O(qHandle("data", id),1,idd)
            quit:id=""
            set tChunk = (idd\64000)+1, tPos=(idd#64000)+1
            set $BIT(i%ResultBits(tChunk),tPos) = 1
        }
    }     quit $$$OK }

Method ContainsItem(pItem As %String) As %Boolean
{
    set tChunk = (pItem\64000)+1, tPos=(pItem#64000)+1
    quit $bit($get(i%ResultBits(tChunk)),tPos)
}

Method GetChunk(pChunk As %Integer) As %Binary
{
    quit $get(i%ResultBits(pChunk))
}

Method NextChunk(ByRef pChunk As %Integer = "") As %Binary
{
    set pChunk = $order(i%ResultBits(pChunk),1,tBits)
    quit:pChunk="" ""
    quit tBits
}

Method PreviousChunk(ByRef pChunk As %Integer = "") As %Binary
{
    set pChunk = $order(i%ResultBits(pChunk),-1,tBits)
    quit:pChunk="" ""
    quit tBits
}
}

 上記のコードサンプルに示すように、 SpatialIndex.QueryExecutorクラスの InternalFindWindow  メソッドは、指定された長方形内にあるポイントを検索します。 次に、一致する行のIDがFORループのビットセットに書き込まれます。

私たちのハッカソンプロジェクトでは、Andreyは楕円の検索機能も実装しました。

SELECT *
FROM SpatialIndex.Test
WHERE %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2')
and name %StartsWith 'Z'

%FINDに関する補足情報

 %FIND 述語には追加パラメーター SIZEがあり、これは、SQLエンジンが一致する行の数を推定するのに役立ちます。 SQLエンジンは、このパラメーターに基づいて%FIND述語で指定されたインデックスを使用するかどうかを決定します。

たとえば、次のインデックスをSpatialIndex.Testクラスに追加してみましょう。

Index ByName on Name;

次に、クラスを再コンパイルして、このインデックスを作成します。

write ##class(SpatialIndex.Test).%BuildIndices($LB("ByName"))

最後に、TuneTableを実行します。

do $system.SQL.TuneTable("SpatialIndex.Test", 1)

クエリプランは次のとおりです。

SELECT *
FROM SpatialIndex.Test
WHERE name %startswith 'za'
and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((10))

https://lh5.googleusercontent.com/qJYDnZ9OmUQ9ZzRvwB3ZdnOYx0FXLobAh5JxMuHGIvO1rWGC7z1S9VZBy1WKKXzqX8q7gDFBSDokG_egrFuU88T9N5HvemUBtIVw1wc_i6hQ1oZ774XZV2UQYDt4j4v9wG3_utOD

coordインデックスは数行を返す可能性が高いため、SQLエンジンは Nameプロパティのインデックスを使用しません。

次のクエリには別のプランがあります。

SELECT *FROM SpatialIndex.Test
WHERE name %startswith 'za'
and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((1000))

https://lh3.googleusercontent.com/eS64e9v_Suc2GWeaUwM_O2dg5WX4iPGsJeWx2UfASqnCceBBLg-TmpHl0C0BFlSKBkNEXVMFdugRjq-JtJHZFoPI5QMbHmdT6zHS9j-oAJdIgZ1uDr1hlNSWPTLy6ljuKSKBQqb3

SQLエンジンは、両方のインデックスを使用してこのクエリを実行します。

そして最後の例として、coordインデックスはおそらく約10万行を返すため、ほとんど使用できないので、 Nameフィールドのインデックスのみを使用するリクエストを作成しましょう。

SELECT *
FROM SpatialIndex.Test
WHERE name %startswith 'za'
and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((100000))

https://lh4.googleusercontent.com/xw0oNiI7_qQmSioTApOOVydSf3pgFFOXChiMQodDh9UavcSQfdz7w_6su65rs4-m_06100eyr2oHBa1Th0J0TV9BOx7DCO0XNIzIyOD0w64HZOzpwXyHEUlW26U9yzuB_F4VptQs

この記事を読んでくださった方、または少なくとも最後までスクロールしてくださったすべての方に感謝いたします。

参考:

ディスカッション (0)2
続けるにはログインするか新規登録を行ってください