From 2f142017a60c21764991131f95fcc7c5b4f8b6db Mon Sep 17 00:00:00 2001 From: Harshil Goel Date: Wed, 20 Mar 2024 17:07:03 +0530 Subject: [PATCH] added first draft --- go.mod | 8 +- go.sum | 16 +- protos/pb.proto | 6 +- protos/pb/pb.pb.go | 809 +++++++++++++++---------------- schema/parse.go | 275 +++++++++-- schema/parse_test.go | 5 +- schema/schema.go | 58 ++- schema/state.go | 35 +- tok/constraints/constraints.go | 49 ++ tok/hnsw/heap.go | 63 +++ tok/hnsw/helper.go | 684 ++++++++++++++++++++++++++ tok/hnsw/persistent_factory.go | 161 ++++++ tok/hnsw/persistent_hnsw.go | 448 +++++++++++++++++ tok/hnsw/persistent_hnsw_test.go | 614 +++++++++++++++++++++++ tok/hnsw/search_layer.go | 120 +++++ tok/hnsw/test_helper.go | 281 +++++++++++ tok/index/helper.go | 79 +++ tok/index/index.go | 167 +++++++ tok/index/search_path.go | 41 ++ tok/index/types.go | 30 ++ tok/index_factory.go | 120 +++++ tok/options/README.md | 71 +++ tok/options/allowed_options.go | 121 +++++ tok/options/option_parser.go | 68 +++ tok/options/options.go | 92 ++++ tok/options/options_test.go | 67 +++ tok/tok.go | 64 +++ 27 files changed, 4069 insertions(+), 483 deletions(-) create mode 100644 tok/constraints/constraints.go create mode 100644 tok/hnsw/heap.go create mode 100644 tok/hnsw/helper.go create mode 100644 tok/hnsw/persistent_factory.go create mode 100644 tok/hnsw/persistent_hnsw.go create mode 100644 tok/hnsw/persistent_hnsw_test.go create mode 100644 tok/hnsw/search_layer.go create mode 100644 tok/hnsw/test_helper.go create mode 100644 tok/index/helper.go create mode 100644 tok/index/index.go create mode 100644 tok/index/search_path.go create mode 100644 tok/index/types.go create mode 100644 tok/index_factory.go create mode 100644 tok/options/README.md create mode 100644 tok/options/allowed_options.go create mode 100644 tok/options/option_parser.go create mode 100644 tok/options/options.go create mode 100644 tok/options/options_test.go diff --git a/go.mod b/go.mod index 9ae89b68f48..3da8a99fca6 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/IBM/sarama v1.41.0 github.com/Masterminds/semver/v3 v3.1.0 github.com/blevesearch/bleve/v2 v2.3.10 + github.com/chewxy/math32 v1.10.1 github.com/dgraph-io/badger/v4 v4.2.0 github.com/dgraph-io/dgo/v230 v230.0.2-0.20240314155021-7b8d289e37f3 github.com/dgraph-io/gqlgen v0.13.2 @@ -58,14 +59,14 @@ require ( go.opencensus.io v0.24.0 go.uber.org/zap v1.16.0 golang.org/x/crypto v0.21.0 + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 golang.org/x/net v0.22.0 golang.org/x/sync v0.6.0 golang.org/x/sys v0.18.0 golang.org/x/term v0.18.0 golang.org/x/text v0.14.0 - golang.org/x/tools v0.18.0 + golang.org/x/tools v0.19.0 google.golang.org/grpc v1.62.1 - google.golang.org/protobuf v1.33.0 gopkg.in/square/go-jose.v2 v2.3.1 gopkg.in/yaml.v2 v2.4.0 ) @@ -141,12 +142,13 @@ require ( github.com/xdg/stringprep v1.0.3 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/mod v0.15.0 // indirect + golang.org/x/mod v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/api v0.122.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 // indirect google.golang.org/grpc/examples v0.0.0-20230821201920-d51b3f41716d // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/DataDog/dd-trace-go.v1 v1.22.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 2471094f420..9d5cfb73420 100644 --- a/go.sum +++ b/go.sum @@ -118,6 +118,8 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chewxy/math32 v1.10.1 h1:LFpeY0SLJXeaiej/eIp2L40VYfscTvKh/FSEZ68uMkU= +github.com/chewxy/math32 v1.10.1/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -153,6 +155,9 @@ github.com/dgraph-io/dgo/v230 v230.0.2-0.20240314155021-7b8d289e37f3 h1:SmlpAiFh github.com/dgraph-io/dgo/v230 v230.0.2-0.20240314155021-7b8d289e37f3/go.mod h1:N63npbH9PPVitTNUW4xlT66LNsMhqjuz57ZvgUmgATI= github.com/dgraph-io/gqlgen v0.13.2 h1:TNhndk+eHKj5qE7BenKKSYdSIdOGhLqxR1rCiMso9KM= github.com/dgraph-io/gqlgen v0.13.2/go.mod h1:iCOrOv9lngN7KAo+jMgvUPVDlYHdf7qDwsTkQby2Sis= +github.com/dgraph-io/gqlparser/v2 v2.1.1/go.mod h1:MYS4jppjyx8b9tuUtjV7jU1UFZK6P9fvO8TsIsQtRKU= +github.com/dgraph-io/gqlparser/v2 v2.2.1 h1:15msK9XEHOSrRqQO48UU+2ZTf1R1U8+tfL9H5D5/eQQ= +github.com/dgraph-io/gqlparser/v2 v2.2.1/go.mod h1:MYS4jppjyx8b9tuUtjV7jU1UFZK6P9fvO8TsIsQtRKU= github.com/dgraph-io/graphql-transport-ws v0.0.0-20210511143556-2cef522f1f15 h1:X2NRsgAtVUAp2nmTPCq+x+wTcRRrj74CEpy7E0Unsl4= github.com/dgraph-io/graphql-transport-ws v0.0.0-20210511143556-2cef522f1f15/go.mod h1:7z3c/5w0sMYYZF5bHsrh8IH4fKwG5O5Y70cPH1ZLLRQ= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= @@ -752,8 +757,9 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -780,8 +786,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -999,8 +1005,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= -golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/protos/pb.proto b/protos/pb.proto index 2822f0124d6..8f0fbe5e977 100644 --- a/protos/pb.proto +++ b/protos/pb.proto @@ -93,7 +93,7 @@ message Result { repeated FacetsList facet_matrix = 5; repeated LangList lang_matrix = 6; bool list = 7; - map vector_metrics = 8; + map extra_metrics = 8; } message Order { @@ -520,10 +520,10 @@ message SchemaUpdate { reserved 7; reserved "explicit"; - repeated VectorIndexSpec index_specs = 15; + repeated VectorSpec vector_specs = 15; } -message VectorIndexSpec { +message VectorSpec { // This names the kind of Vector Index, e.g., // hnsw, lsh, hypertree, ... string name = 1; diff --git a/protos/pb/pb.pb.go b/protos/pb/pb.pb.go index ff00e68c9df..2a653d6a868 100644 --- a/protos/pb/pb.pb.go +++ b/protos/pb/pb.pb.go @@ -744,7 +744,7 @@ type Result struct { FacetMatrix []*FacetsList `protobuf:"bytes,5,rep,name=facet_matrix,json=facetMatrix,proto3" json:"facet_matrix,omitempty"` LangMatrix []*LangList `protobuf:"bytes,6,rep,name=lang_matrix,json=langMatrix,proto3" json:"lang_matrix,omitempty"` List bool `protobuf:"varint,7,opt,name=list,proto3" json:"list,omitempty"` - VectorMetrics map[string]uint64 `protobuf:"bytes,8,rep,name=vector_metrics,json=vectorMetrics,proto3" json:"vector_metrics,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` + ExtraMetrics map[string]uint64 `protobuf:"bytes,8,rep,name=extra_metrics,json=extraMetrics,proto3" json:"extra_metrics,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` } func (m *Result) Reset() { *m = Result{} } @@ -829,9 +829,9 @@ func (m *Result) GetList() bool { return false } -func (m *Result) GetVectorMetrics() map[string]uint64 { +func (m *Result) GetExtraMetrics() map[string]uint64 { if m != nil { - return m.VectorMetrics + return m.ExtraMetrics } return nil } @@ -3600,9 +3600,9 @@ type SchemaUpdate struct { NonNullableList bool `protobuf:"varint,11,opt,name=non_nullable_list,json=nonNullableList,proto3" json:"non_nullable_list,omitempty"` // If value_type is OBJECT, then this represents an object type with a // custom name. This field stores said name. - ObjectTypeName string `protobuf:"bytes,12,opt,name=object_type_name,json=objectTypeName,proto3" json:"object_type_name,omitempty"` - NoConflict bool `protobuf:"varint,13,opt,name=no_conflict,json=noConflict,proto3" json:"no_conflict,omitempty"` - IndexSpecs []*VectorIndexSpec `protobuf:"bytes,15,rep,name=index_specs,json=indexSpecs,proto3" json:"index_specs,omitempty"` + ObjectTypeName string `protobuf:"bytes,12,opt,name=object_type_name,json=objectTypeName,proto3" json:"object_type_name,omitempty"` + NoConflict bool `protobuf:"varint,13,opt,name=no_conflict,json=noConflict,proto3" json:"no_conflict,omitempty"` + VectorSpecs []*VectorSpec `protobuf:"bytes,15,rep,name=vector_specs,json=vectorSpecs,proto3" json:"vector_specs,omitempty"` } func (m *SchemaUpdate) Reset() { *m = SchemaUpdate{} } @@ -3729,14 +3729,14 @@ func (m *SchemaUpdate) GetNoConflict() bool { return false } -func (m *SchemaUpdate) GetIndexSpecs() []*VectorIndexSpec { +func (m *SchemaUpdate) GetVectorSpecs() []*VectorSpec { if m != nil { - return m.IndexSpecs + return m.VectorSpecs } return nil } -type VectorIndexSpec struct { +type VectorSpec struct { // This names the kind of Vector Index, e.g., // // hnsw, lsh, hypertree, ... @@ -3744,18 +3744,18 @@ type VectorIndexSpec struct { Options []*OptionPair `protobuf:"bytes,2,rep,name=options,proto3" json:"options,omitempty"` } -func (m *VectorIndexSpec) Reset() { *m = VectorIndexSpec{} } -func (m *VectorIndexSpec) String() string { return proto.CompactTextString(m) } -func (*VectorIndexSpec) ProtoMessage() {} -func (*VectorIndexSpec) Descriptor() ([]byte, []int) { +func (m *VectorSpec) Reset() { *m = VectorSpec{} } +func (m *VectorSpec) String() string { return proto.CompactTextString(m) } +func (*VectorSpec) ProtoMessage() {} +func (*VectorSpec) Descriptor() ([]byte, []int) { return fileDescriptor_f80abaa17e25ccc8, []int{42} } -func (m *VectorIndexSpec) XXX_Unmarshal(b []byte) error { +func (m *VectorSpec) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } -func (m *VectorIndexSpec) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { +func (m *VectorSpec) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { - return xxx_messageInfo_VectorIndexSpec.Marshal(b, m, deterministic) + return xxx_messageInfo_VectorSpec.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) @@ -3765,26 +3765,26 @@ func (m *VectorIndexSpec) XXX_Marshal(b []byte, deterministic bool) ([]byte, err return b[:n], nil } } -func (m *VectorIndexSpec) XXX_Merge(src proto.Message) { - xxx_messageInfo_VectorIndexSpec.Merge(m, src) +func (m *VectorSpec) XXX_Merge(src proto.Message) { + xxx_messageInfo_VectorSpec.Merge(m, src) } -func (m *VectorIndexSpec) XXX_Size() int { +func (m *VectorSpec) XXX_Size() int { return m.Size() } -func (m *VectorIndexSpec) XXX_DiscardUnknown() { - xxx_messageInfo_VectorIndexSpec.DiscardUnknown(m) +func (m *VectorSpec) XXX_DiscardUnknown() { + xxx_messageInfo_VectorSpec.DiscardUnknown(m) } -var xxx_messageInfo_VectorIndexSpec proto.InternalMessageInfo +var xxx_messageInfo_VectorSpec proto.InternalMessageInfo -func (m *VectorIndexSpec) GetName() string { +func (m *VectorSpec) GetName() string { if m != nil { return m.Name } return "" } -func (m *VectorIndexSpec) GetOptions() []*OptionPair { +func (m *VectorSpec) GetOptions() []*OptionPair { if m != nil { return m.Options } @@ -5740,7 +5740,7 @@ func init() { proto.RegisterType((*ValueList)(nil), "pb.ValueList") proto.RegisterType((*LangList)(nil), "pb.LangList") proto.RegisterType((*Result)(nil), "pb.Result") - proto.RegisterMapType((map[string]uint64)(nil), "pb.Result.VectorMetricsEntry") + proto.RegisterMapType((map[string]uint64)(nil), "pb.Result.ExtraMetricsEntry") proto.RegisterType((*Order)(nil), "pb.Order") proto.RegisterType((*SortMessage)(nil), "pb.SortMessage") proto.RegisterType((*SortResult)(nil), "pb.SortResult") @@ -5782,7 +5782,7 @@ func init() { proto.RegisterType((*SchemaNode)(nil), "pb.SchemaNode") proto.RegisterType((*SchemaResult)(nil), "pb.SchemaResult") proto.RegisterType((*SchemaUpdate)(nil), "pb.SchemaUpdate") - proto.RegisterType((*VectorIndexSpec)(nil), "pb.VectorIndexSpec") + proto.RegisterType((*VectorSpec)(nil), "pb.VectorSpec") proto.RegisterType((*OptionPair)(nil), "pb.OptionPair") proto.RegisterType((*TypeUpdate)(nil), "pb.TypeUpdate") proto.RegisterType((*MapHeader)(nil), "pb.MapHeader") @@ -5823,361 +5823,360 @@ func init() { func init() { proto.RegisterFile("pb.proto", fileDescriptor_f80abaa17e25ccc8) } var fileDescriptor_f80abaa17e25ccc8 = []byte{ - // 5650 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x3b, 0x4b, 0x6f, 0x1c, 0x67, - 0x72, 0x9c, 0xf7, 0x74, 0x0d, 0x67, 0x38, 0xfc, 0x24, 0xcb, 0xe3, 0x91, 0x25, 0x6a, 0xdb, 0x96, - 0x4d, 0x4b, 0x16, 0x25, 0xd1, 0xde, 0xcd, 0xda, 0x8b, 0x05, 0x96, 0x14, 0x49, 0x99, 0x16, 0x45, - 0x72, 0x7b, 0x46, 0xda, 0x07, 0x90, 0x0c, 0x9a, 0xdd, 0x1f, 0xc9, 0x5e, 0xf6, 0x74, 0xf7, 0x76, - 0xf7, 0x70, 0x49, 0xdf, 0xf6, 0xb4, 0x97, 0x1c, 0x16, 0xc9, 0x31, 0x40, 0x10, 0xe4, 0x9a, 0x9c, - 0x82, 0x00, 0x09, 0x02, 0xe4, 0x10, 0x20, 0x08, 0x16, 0x39, 0xed, 0x31, 0xc8, 0x26, 0x42, 0xe0, - 0xcd, 0x49, 0x87, 0x00, 0xc9, 0x1f, 0x48, 0x50, 0x55, 0x5f, 0xbf, 0x86, 0xa3, 0x87, 0x1d, 0xe4, - 0x92, 0xd3, 0x7c, 0x55, 0xdf, 0xbb, 0xbe, 0x7a, 0x57, 0x0f, 0x34, 0x83, 0x83, 0x95, 0x20, 0xf4, - 0x63, 0x5f, 0x94, 0x83, 0x83, 0xbe, 0x66, 0x06, 0x0e, 0x83, 0xfd, 0x5b, 0x47, 0x4e, 0x7c, 0x3c, - 0x39, 0x58, 0xb1, 0xfc, 0xf1, 0x5d, 0xfb, 0x28, 0x34, 0x83, 0xe3, 0x3b, 0x8e, 0x7f, 0xf7, 0xc0, - 0xb4, 0x8f, 0x64, 0x78, 0xf7, 0xf4, 0xe3, 0xbb, 0xc1, 0xc1, 0xdd, 0x64, 0x6a, 0xff, 0x4e, 0x6e, - 0xec, 0x91, 0x7f, 0xe4, 0xdf, 0x25, 0xf4, 0xc1, 0xe4, 0x90, 0x20, 0x02, 0xa8, 0xc5, 0xc3, 0xf5, - 0x3e, 0x54, 0x77, 0x9c, 0x28, 0x16, 0x02, 0xaa, 0x13, 0xc7, 0x8e, 0x7a, 0xa5, 0x1b, 0x95, 0xe5, - 0xba, 0x41, 0x6d, 0xfd, 0x31, 0x68, 0x43, 0x33, 0x3a, 0x79, 0x6a, 0xba, 0x13, 0x29, 0xba, 0x50, - 0x39, 0x35, 0xdd, 0x5e, 0xe9, 0x46, 0x69, 0x79, 0xde, 0xc0, 0xa6, 0x58, 0x81, 0xe6, 0xa9, 0xe9, - 0x8e, 0xe2, 0xf3, 0x40, 0xf6, 0xca, 0x37, 0x4a, 0xcb, 0x9d, 0xd5, 0x4b, 0x2b, 0xc1, 0xc1, 0xca, - 0xbe, 0x1f, 0xc5, 0x8e, 0x77, 0xb4, 0xf2, 0xd4, 0x74, 0x87, 0xe7, 0x81, 0x34, 0x1a, 0xa7, 0xdc, - 0xd0, 0xf7, 0xa0, 0x35, 0x08, 0xad, 0xad, 0x89, 0x67, 0xc5, 0x8e, 0xef, 0xe1, 0x8e, 0x9e, 0x39, - 0x96, 0xb4, 0xa2, 0x66, 0x50, 0x1b, 0x71, 0x66, 0x78, 0x14, 0xf5, 0x2a, 0x37, 0x2a, 0x88, 0xc3, - 0xb6, 0xe8, 0x41, 0xc3, 0x89, 0x1e, 0xf8, 0x13, 0x2f, 0xee, 0x55, 0x6f, 0x94, 0x96, 0x9b, 0x46, - 0x02, 0xea, 0x7f, 0x55, 0x81, 0xda, 0xf7, 0x27, 0x32, 0x3c, 0xa7, 0x79, 0x71, 0x1c, 0x26, 0x6b, - 0x61, 0x5b, 0x5c, 0x86, 0x9a, 0x6b, 0x7a, 0x47, 0x51, 0xaf, 0x4c, 0x8b, 0x31, 0x20, 0xae, 0x82, - 0x66, 0x1e, 0xc6, 0x32, 0x1c, 0x4d, 0x1c, 0xbb, 0x57, 0xb9, 0x51, 0x5a, 0xae, 0x1b, 0x4d, 0x42, - 0x3c, 0x71, 0x6c, 0xf1, 0x16, 0x34, 0x6d, 0x7f, 0x64, 0xe5, 0xf7, 0xb2, 0x7d, 0xda, 0x4b, 0xbc, - 0x03, 0xcd, 0x89, 0x63, 0x8f, 0x5c, 0x27, 0x8a, 0x7b, 0xb5, 0x1b, 0xa5, 0xe5, 0xd6, 0x6a, 0x13, - 0x2f, 0x8b, 0xb4, 0x33, 0x1a, 0x13, 0xc7, 0x26, 0x22, 0xde, 0x82, 0x66, 0x14, 0x5a, 0xa3, 0xc3, - 0x89, 0x67, 0xf5, 0xea, 0x34, 0x68, 0x01, 0x07, 0xe5, 0x6e, 0x6d, 0x34, 0x22, 0x06, 0xf0, 0x5a, - 0xa1, 0x3c, 0x95, 0x61, 0x24, 0x7b, 0x0d, 0xde, 0x4a, 0x81, 0xe2, 0x1e, 0xb4, 0x0e, 0x4d, 0x4b, - 0xc6, 0xa3, 0xc0, 0x0c, 0xcd, 0x71, 0xaf, 0x99, 0x2d, 0xb4, 0x85, 0xe8, 0x7d, 0xc4, 0x46, 0x06, - 0x1c, 0xa6, 0x80, 0xf8, 0x08, 0xda, 0x04, 0x45, 0xa3, 0x43, 0xc7, 0x8d, 0x65, 0xd8, 0xd3, 0x68, - 0x4e, 0x87, 0xe6, 0x10, 0x66, 0x18, 0x4a, 0x69, 0xcc, 0xf3, 0x20, 0xc6, 0x88, 0x6b, 0x00, 0xf2, - 0x2c, 0x30, 0x3d, 0x7b, 0x64, 0xba, 0x6e, 0x0f, 0xe8, 0x0c, 0x1a, 0x63, 0xd6, 0x5c, 0x57, 0xbc, - 0x89, 0xe7, 0x33, 0xed, 0x51, 0x1c, 0xf5, 0xda, 0x37, 0x4a, 0xcb, 0x55, 0xa3, 0x8e, 0xe0, 0x30, - 0x42, 0xba, 0x5a, 0xa6, 0x75, 0x2c, 0x7b, 0x9d, 0x1b, 0xa5, 0xe5, 0x9a, 0xc1, 0x00, 0x62, 0x0f, - 0x9d, 0x30, 0x8a, 0x7b, 0x0b, 0x8c, 0x25, 0x40, 0x5c, 0x81, 0xba, 0x7f, 0x78, 0x18, 0xc9, 0xb8, - 0xd7, 0x25, 0xb4, 0x82, 0xf4, 0x55, 0xd0, 0x88, 0xab, 0x88, 0x6a, 0x37, 0xa1, 0x7e, 0x8a, 0x00, - 0x33, 0x5f, 0x6b, 0xb5, 0x8d, 0xc7, 0x4e, 0x19, 0xcf, 0x50, 0x9d, 0xfa, 0x75, 0x68, 0xee, 0x98, - 0xde, 0x51, 0xc2, 0xad, 0xf8, 0x9c, 0x34, 0x41, 0x33, 0xa8, 0xad, 0xff, 0x49, 0x05, 0xea, 0x86, - 0x8c, 0x26, 0x6e, 0x2c, 0xde, 0x07, 0xc0, 0xc7, 0x1a, 0x9b, 0x71, 0xe8, 0x9c, 0xa9, 0x55, 0xb3, - 0xe7, 0xd2, 0x26, 0x8e, 0xfd, 0x98, 0xba, 0xc4, 0x3d, 0x98, 0xa7, 0xd5, 0x93, 0xa1, 0xe5, 0xec, - 0x00, 0xe9, 0xf9, 0x8c, 0x16, 0x0d, 0x51, 0x33, 0xae, 0x40, 0x9d, 0xf8, 0x83, 0x79, 0xb4, 0x6d, - 0x28, 0x48, 0xdc, 0x84, 0x8e, 0xe3, 0xc5, 0xf8, 0x7e, 0x56, 0x3c, 0xb2, 0x65, 0x94, 0x30, 0x50, - 0x3b, 0xc5, 0x6e, 0xc8, 0x28, 0x16, 0xf7, 0x81, 0x1f, 0x21, 0xd9, 0xb0, 0x46, 0x1b, 0x76, 0xd2, - 0xc7, 0x8d, 0x78, 0x47, 0x1a, 0xa3, 0x76, 0xbc, 0x03, 0x2d, 0xbc, 0x5f, 0x32, 0xa3, 0x4e, 0x33, - 0xe6, 0xe9, 0x36, 0x8a, 0x1c, 0x06, 0xe0, 0x00, 0x35, 0x1c, 0x49, 0x83, 0x4c, 0xca, 0x4c, 0x45, - 0x6d, 0xb1, 0x01, 0x9d, 0x53, 0x69, 0xc5, 0x7e, 0x38, 0x1a, 0xcb, 0x38, 0x74, 0xac, 0xa8, 0xd7, - 0xa4, 0x55, 0xae, 0xe1, 0x2a, 0x4c, 0xb3, 0x95, 0xa7, 0x34, 0xe0, 0x31, 0xf7, 0x6f, 0x7a, 0x71, - 0x78, 0x6e, 0xb4, 0x4f, 0xf3, 0xb8, 0xfe, 0xf7, 0x40, 0x5c, 0x1c, 0x84, 0x7a, 0xe1, 0x44, 0x9e, - 0x2b, 0xc9, 0xc3, 0x26, 0xb2, 0x02, 0x51, 0x8c, 0x94, 0x42, 0xd5, 0x60, 0xe0, 0xd3, 0xf2, 0xb7, - 0x4b, 0xfa, 0x26, 0xd4, 0xf6, 0x42, 0x5b, 0x86, 0x33, 0xe5, 0x55, 0x40, 0xd5, 0x96, 0x91, 0x45, - 0xb3, 0x9a, 0x06, 0xb5, 0x33, 0x19, 0xae, 0xe4, 0x64, 0x58, 0xff, 0xe3, 0x12, 0xb4, 0x06, 0x7e, - 0x18, 0x3f, 0x96, 0x51, 0x64, 0x1e, 0x49, 0xb1, 0x04, 0x35, 0x1f, 0x97, 0x55, 0x2f, 0xad, 0xe1, - 0xad, 0x68, 0x1f, 0x83, 0xf1, 0x53, 0xfc, 0x50, 0x7e, 0x31, 0x3f, 0x20, 0x6f, 0x93, 0xf4, 0x57, - 0x14, 0x6f, 0x93, 0xec, 0x67, 0x5c, 0x5c, 0xcd, 0x73, 0xf1, 0x0b, 0x45, 0x44, 0xff, 0x26, 0x00, - 0x9e, 0xef, 0x2b, 0x72, 0xa3, 0xfe, 0x8b, 0x12, 0xb4, 0x0c, 0xf3, 0x30, 0x7e, 0xe0, 0x7b, 0xb1, - 0x3c, 0x8b, 0x45, 0x07, 0xca, 0x8e, 0x4d, 0x34, 0xaa, 0x1b, 0x65, 0xc7, 0xc6, 0xd3, 0x1d, 0x85, - 0xfe, 0x24, 0x20, 0x12, 0xb5, 0x0d, 0x06, 0x88, 0x96, 0xb6, 0x1d, 0xd2, 0x91, 0x91, 0x96, 0xb6, - 0x1d, 0x8a, 0x25, 0x68, 0x45, 0x9e, 0x19, 0x44, 0xc7, 0x7e, 0x8c, 0xa7, 0xab, 0xd2, 0xe9, 0x20, - 0x41, 0x0d, 0x23, 0x14, 0x7e, 0x27, 0x1a, 0xb9, 0xd2, 0x0c, 0x3d, 0x19, 0x92, 0x42, 0x6b, 0x1a, - 0x9a, 0x13, 0xed, 0x30, 0x42, 0xff, 0x45, 0x05, 0xea, 0x8f, 0xe5, 0xf8, 0x40, 0x86, 0x17, 0x0e, - 0x71, 0x0f, 0x9a, 0xb4, 0xef, 0xc8, 0xb1, 0xf9, 0x1c, 0xeb, 0x6f, 0x3c, 0x7f, 0xb6, 0xb4, 0x48, - 0xb8, 0x6d, 0xfb, 0x43, 0x7f, 0xec, 0xc4, 0x72, 0x1c, 0xc4, 0xe7, 0x46, 0x43, 0xa1, 0x66, 0x1e, - 0xf0, 0x0a, 0xd4, 0x5d, 0x69, 0xe2, 0x9b, 0xb1, 0x98, 0x28, 0x48, 0xdc, 0x81, 0x86, 0x39, 0x1e, - 0xd9, 0xd2, 0xb4, 0xf9, 0x50, 0xeb, 0x97, 0x9f, 0x3f, 0x5b, 0xea, 0x9a, 0xe3, 0x0d, 0x69, 0xe6, - 0xd7, 0xae, 0x33, 0x46, 0x7c, 0x82, 0xb2, 0x11, 0xc5, 0xa3, 0x49, 0x60, 0x9b, 0xb1, 0x24, 0x9d, - 0x5b, 0x5d, 0xef, 0x3d, 0x7f, 0xb6, 0x74, 0x19, 0xd1, 0x4f, 0x08, 0x9b, 0x9b, 0x06, 0x19, 0x16, - 0xf5, 0x6f, 0x72, 0x7d, 0xa5, 0x7f, 0x15, 0x28, 0xb6, 0x61, 0xd1, 0x72, 0x27, 0x11, 0x1a, 0x09, - 0xc7, 0x3b, 0xf4, 0x47, 0xbe, 0xe7, 0x9e, 0xd3, 0x03, 0x37, 0xd7, 0xaf, 0x3d, 0x7f, 0xb6, 0xf4, - 0x96, 0xea, 0xdc, 0xf6, 0x0e, 0xfd, 0x3d, 0xcf, 0x3d, 0xcf, 0xad, 0xbf, 0x30, 0xd5, 0x25, 0xbe, - 0x07, 0x9d, 0x43, 0x3f, 0xb4, 0xe4, 0x28, 0x25, 0x59, 0x87, 0xd6, 0xe9, 0x3f, 0x7f, 0xb6, 0x74, - 0x85, 0x7a, 0x1e, 0x5e, 0xa0, 0xdb, 0x7c, 0x1e, 0xaf, 0xff, 0x6b, 0x19, 0x6a, 0xd4, 0x16, 0xf7, - 0xa0, 0x31, 0xa6, 0x27, 0x49, 0xf4, 0xe4, 0x15, 0xe4, 0x21, 0xea, 0x5b, 0xe1, 0xb7, 0x52, 0x62, - 0x9b, 0x0c, 0xc3, 0x19, 0xb1, 0x79, 0xe0, 0xca, 0x38, 0x52, 0x3c, 0x9f, 0x9b, 0x31, 0xe4, 0x0e, - 0x35, 0x43, 0x0d, 0x9b, 0xe6, 0x9b, 0xca, 0x05, 0xbe, 0xe9, 0x43, 0xd3, 0x3a, 0x96, 0xd6, 0x49, - 0x34, 0x19, 0x2b, 0xae, 0x4a, 0x61, 0xf1, 0x0e, 0xb4, 0xa9, 0x1d, 0xf8, 0x8e, 0x47, 0xd3, 0x6b, - 0x34, 0x60, 0x3e, 0x43, 0x0e, 0xa3, 0xfe, 0x16, 0xcc, 0xe7, 0x0f, 0x9b, 0x57, 0x1f, 0x55, 0x56, - 0x1f, 0x37, 0xf2, 0xea, 0xa3, 0xb5, 0x0a, 0x78, 0x66, 0x9e, 0x92, 0x53, 0x25, 0xb8, 0x4e, 0xfe, - 0x0a, 0x33, 0xd4, 0xd0, 0xac, 0x75, 0x78, 0x4a, 0x5e, 0x25, 0xf9, 0xd0, 0xd8, 0x71, 0x2c, 0xe9, - 0x45, 0xe4, 0x7c, 0x4c, 0x22, 0x99, 0x2a, 0x25, 0x6c, 0xe3, 0x7d, 0xc7, 0xe6, 0xd9, 0xae, 0x6f, - 0xcb, 0x48, 0xa9, 0xb3, 0x14, 0xc6, 0x3e, 0x79, 0x16, 0x38, 0xe1, 0xf9, 0x90, 0x29, 0x55, 0x31, - 0x52, 0x18, 0xb9, 0x4b, 0x7a, 0xb8, 0x99, 0x9d, 0x38, 0x12, 0x0a, 0xd4, 0xff, 0xbc, 0x0a, 0xf3, - 0x3f, 0x96, 0xa1, 0xbf, 0x1f, 0xfa, 0x81, 0x1f, 0x99, 0xae, 0x58, 0x2b, 0xd2, 0x9c, 0xdf, 0xf6, - 0x06, 0x9e, 0x36, 0x3f, 0x6c, 0x65, 0x90, 0x3e, 0x02, 0xbf, 0x59, 0xfe, 0x55, 0x74, 0xa8, 0xf3, - 0x9b, 0xcf, 0xa0, 0x99, 0xea, 0xc1, 0x31, 0xfc, 0xca, 0x74, 0xd6, 0x22, 0x3d, 0x54, 0x0f, 0x4a, - 0xe5, 0xd8, 0x3c, 0x7b, 0xb2, 0xbd, 0xa1, 0xde, 0x56, 0x41, 0x8a, 0x0a, 0xc3, 0x33, 0x6f, 0x98, - 0x3c, 0x6a, 0x0a, 0xe3, 0x4d, 0x91, 0x22, 0xd1, 0xf6, 0x46, 0x6f, 0x9e, 0xba, 0x12, 0x50, 0xbc, - 0x0d, 0xda, 0xd8, 0x3c, 0x43, 0x85, 0xb6, 0x6d, 0xb3, 0x68, 0x1a, 0x19, 0x42, 0x7c, 0x03, 0x2a, - 0xf1, 0x99, 0x47, 0xb2, 0x87, 0xde, 0x0d, 0x3a, 0xbb, 0xc3, 0x33, 0x4f, 0xa9, 0x3e, 0x03, 0xfb, - 0xf0, 0x4d, 0x2d, 0xc7, 0x26, 0x67, 0x46, 0x33, 0xb0, 0x29, 0x6e, 0x42, 0xc3, 0xe5, 0xd7, 0x22, - 0x87, 0xa5, 0xb5, 0xda, 0x62, 0x3d, 0x4a, 0x28, 0x23, 0xe9, 0x13, 0x1f, 0x42, 0x33, 0xa1, 0x4e, - 0xaf, 0x45, 0xe3, 0xba, 0x09, 0x3d, 0x13, 0x32, 0x1a, 0xe9, 0x08, 0x71, 0x0f, 0x34, 0x5b, 0xba, - 0x32, 0x96, 0x23, 0x8f, 0x15, 0x79, 0x8b, 0x1d, 0xd9, 0x0d, 0x42, 0xee, 0x46, 0x86, 0xfc, 0xe9, - 0x44, 0x46, 0xb1, 0xd1, 0xb4, 0x15, 0x42, 0xbc, 0x9b, 0x09, 0x56, 0x87, 0x9e, 0x2b, 0x4f, 0xcc, - 0xa4, 0xab, 0xff, 0x5d, 0x58, 0x98, 0x7a, 0xb4, 0x3c, 0x97, 0xb6, 0x5f, 0x61, 0x2c, 0x3f, 0xaf, - 0x36, 0x9b, 0x5d, 0x4d, 0xff, 0xcf, 0x0a, 0x2c, 0x28, 0x81, 0x39, 0x76, 0x82, 0x41, 0xac, 0x54, - 0x17, 0x19, 0x26, 0xc5, 0xab, 0x55, 0x23, 0x01, 0xc5, 0xef, 0x40, 0x9d, 0x34, 0x4d, 0x22, 0xf0, - 0x4b, 0x19, 0x23, 0xa4, 0xd3, 0x59, 0x01, 0x28, 0x2e, 0x52, 0xc3, 0xc5, 0xc7, 0x50, 0xfb, 0x42, - 0x86, 0x3e, 0x1b, 0xda, 0xd6, 0xea, 0xf5, 0x59, 0xf3, 0x90, 0x7c, 0x6a, 0x1a, 0x0f, 0xfe, 0xdf, - 0xf2, 0x0b, 0x7c, 0x15, 0x7e, 0x79, 0x17, 0x8d, 0xed, 0xd8, 0x3f, 0x95, 0x76, 0xaf, 0x91, 0xd1, - 0x5c, 0x31, 0x79, 0xd2, 0x95, 0xb0, 0x4c, 0x73, 0x26, 0xcb, 0x68, 0x2f, 0x66, 0x99, 0xfe, 0x06, - 0xb4, 0x72, 0x74, 0x99, 0xf1, 0x50, 0x4b, 0x45, 0x75, 0xa2, 0xa5, 0xaa, 0x34, 0xaf, 0x95, 0x36, - 0x00, 0x32, 0x2a, 0x7d, 0x5d, 0xdd, 0xa6, 0xff, 0xbc, 0x04, 0x0b, 0x0f, 0x7c, 0xcf, 0x93, 0x14, - 0x32, 0xf0, 0x9b, 0x67, 0x22, 0x5e, 0x7a, 0xa1, 0x88, 0x7f, 0x00, 0xb5, 0x08, 0x07, 0xab, 0xd5, - 0x2f, 0xcd, 0x78, 0x44, 0x83, 0x47, 0xa0, 0xa2, 0x1f, 0x9b, 0x67, 0xa3, 0x40, 0x7a, 0xb6, 0xe3, - 0x1d, 0x25, 0x8a, 0x7e, 0x6c, 0x9e, 0xed, 0x33, 0x46, 0xff, 0xeb, 0x32, 0xc0, 0x67, 0xd2, 0x74, - 0xe3, 0x63, 0x34, 0x66, 0xf8, 0xa2, 0x8e, 0x17, 0xc5, 0xa6, 0x67, 0x25, 0x01, 0x5b, 0x0a, 0xe3, - 0x8b, 0xa2, 0x4d, 0x97, 0x11, 0xab, 0x48, 0xcd, 0x48, 0x40, 0xe4, 0x0f, 0xdc, 0x6e, 0x12, 0x29, - 0xdb, 0xaf, 0xa0, 0xcc, 0x91, 0xa9, 0x12, 0x5a, 0x39, 0x32, 0x3d, 0x68, 0x60, 0x00, 0xe4, 0xf8, - 0x1e, 0x31, 0x8d, 0x66, 0x24, 0x20, 0xae, 0x33, 0x09, 0x62, 0x67, 0xcc, 0x16, 0xbe, 0x62, 0x28, - 0x08, 0x4f, 0x85, 0x16, 0x7d, 0xd3, 0x3a, 0xf6, 0x49, 0x91, 0x54, 0x8c, 0x14, 0xc6, 0xd5, 0x7c, - 0xef, 0xc8, 0xc7, 0xdb, 0x35, 0xc9, 0x79, 0x4c, 0x40, 0xbe, 0x8b, 0x2d, 0xcf, 0xb0, 0x4b, 0xa3, - 0xae, 0x14, 0x46, 0xba, 0x48, 0x39, 0x3a, 0x94, 0x66, 0x3c, 0x09, 0x65, 0xd4, 0x03, 0xea, 0x06, - 0x29, 0xb7, 0x14, 0x46, 0x7c, 0x03, 0xe6, 0x91, 0x70, 0x66, 0x14, 0x39, 0x47, 0x9e, 0xb4, 0x49, - 0xbd, 0x54, 0x0d, 0x24, 0xe6, 0x9a, 0x42, 0xe9, 0x7f, 0x5b, 0x86, 0x3a, 0xeb, 0x82, 0x82, 0xb3, - 0x54, 0x7a, 0x2d, 0x67, 0xe9, 0x6d, 0xd0, 0x82, 0x50, 0xda, 0x8e, 0x95, 0xbc, 0xa3, 0x66, 0x64, - 0x08, 0x8a, 0xb2, 0xd0, 0x3b, 0x20, 0x7a, 0x36, 0x0d, 0x06, 0x84, 0x0e, 0x6d, 0xdf, 0x1b, 0xd9, - 0x4e, 0x74, 0x32, 0x3a, 0x38, 0x8f, 0x65, 0xa4, 0x68, 0xd1, 0xf2, 0xbd, 0x0d, 0x27, 0x3a, 0x59, - 0x47, 0x14, 0x92, 0x90, 0x65, 0x84, 0x64, 0xa3, 0x69, 0x28, 0x48, 0x7c, 0x04, 0x1a, 0xf9, 0xb0, - 0xe4, 0xe4, 0x68, 0xe4, 0x9c, 0x5c, 0x79, 0xfe, 0x6c, 0x49, 0x20, 0x72, 0xca, 0xbb, 0x69, 0x26, - 0x38, 0xf4, 0xd2, 0x70, 0x32, 0x9a, 0x2b, 0x92, 0x61, 0xf6, 0xd2, 0x10, 0x35, 0x8c, 0xf2, 0x5e, - 0x1a, 0x63, 0xc4, 0x1d, 0x10, 0x13, 0xcf, 0xf2, 0xc7, 0x01, 0x32, 0x85, 0xb4, 0xd5, 0x21, 0x5b, - 0x74, 0xc8, 0xc5, 0x7c, 0x0f, 0x1d, 0x55, 0xff, 0x97, 0x32, 0xcc, 0x6f, 0x38, 0xa1, 0xb4, 0x62, - 0x69, 0x6f, 0xda, 0x47, 0x12, 0xcf, 0x2e, 0xbd, 0xd8, 0x89, 0xcf, 0x95, 0x1b, 0xaa, 0xa0, 0x34, - 0x8a, 0x28, 0x17, 0xa3, 0x7e, 0x96, 0xb0, 0x0a, 0x25, 0x2a, 0x18, 0x10, 0xab, 0x00, 0x1c, 0xe7, - 0x51, 0xb2, 0xa2, 0xfa, 0xe2, 0x64, 0x85, 0x46, 0xc3, 0xb0, 0x29, 0xde, 0xa2, 0xf4, 0xc6, 0x44, - 0xe2, 0xdb, 0xd5, 0x68, 0xdf, 0x06, 0xc1, 0xec, 0xd1, 0x52, 0xf8, 0xd9, 0xe0, 0x8d, 0xb1, 0x2d, - 0xde, 0x81, 0xb2, 0x1f, 0x10, 0x71, 0xd5, 0xd2, 0xf9, 0x2b, 0xac, 0xec, 0x05, 0x46, 0xd9, 0x0f, - 0x50, 0x8a, 0x39, 0x06, 0x27, 0xc6, 0x43, 0x29, 0x46, 0xbb, 0x47, 0x91, 0x9f, 0xa1, 0x7a, 0x84, - 0x0e, 0xf3, 0xa6, 0xeb, 0xfa, 0x3f, 0x93, 0xf6, 0x7e, 0x28, 0xed, 0x84, 0x07, 0x0b, 0x38, 0xe4, - 0x12, 0xcf, 0x1c, 0xcb, 0x28, 0x30, 0x2d, 0xa9, 0x58, 0x30, 0x43, 0xe8, 0x57, 0xa0, 0xbc, 0x17, - 0x88, 0x06, 0x54, 0x06, 0x9b, 0xc3, 0xee, 0x1c, 0x36, 0x36, 0x36, 0x77, 0xba, 0x68, 0x51, 0xea, - 0xdd, 0x86, 0xfe, 0x65, 0x19, 0xb4, 0xc7, 0x93, 0xd8, 0x44, 0xdd, 0x12, 0xe1, 0x2d, 0x8b, 0x1c, - 0x9a, 0xb1, 0xe2, 0x5b, 0xd0, 0x8c, 0x62, 0x33, 0x24, 0xaf, 0x84, 0xad, 0x53, 0x83, 0xe0, 0x61, - 0x24, 0xde, 0x83, 0x9a, 0xb4, 0x8f, 0x64, 0x62, 0x2e, 0xba, 0xd3, 0xf7, 0x35, 0xb8, 0x5b, 0x2c, - 0x43, 0x3d, 0xb2, 0x8e, 0xe5, 0xd8, 0xec, 0x55, 0xb3, 0x81, 0x03, 0xc2, 0xb0, 0x1b, 0x6e, 0xa8, - 0x7e, 0xf1, 0x2e, 0xd4, 0xf0, 0x6d, 0x22, 0x15, 0xdf, 0x52, 0x44, 0x8c, 0xcf, 0xa0, 0x86, 0x71, - 0x27, 0x32, 0x9e, 0x1d, 0xfa, 0xc1, 0xc8, 0x0f, 0x88, 0xf6, 0x9d, 0xd5, 0xcb, 0xa4, 0xe3, 0x92, - 0xdb, 0xac, 0x6c, 0x84, 0x7e, 0xb0, 0x17, 0x18, 0x75, 0x9b, 0x7e, 0x31, 0xca, 0xa1, 0xe1, 0xcc, - 0x11, 0x6c, 0x14, 0x34, 0xc4, 0x70, 0x4a, 0x6b, 0x19, 0x9a, 0x63, 0x19, 0x9b, 0xb6, 0x19, 0x9b, - 0xca, 0x36, 0xcc, 0xb3, 0xca, 0x64, 0x9c, 0x91, 0xf6, 0xea, 0x77, 0xa1, 0xce, 0x4b, 0x8b, 0x26, - 0x54, 0x77, 0xf7, 0x76, 0x37, 0x99, 0xac, 0x6b, 0x3b, 0x3b, 0xdd, 0x12, 0xa2, 0x36, 0xd6, 0x86, - 0x6b, 0xdd, 0x32, 0xb6, 0x86, 0x3f, 0xda, 0xdf, 0xec, 0x56, 0xf4, 0x7f, 0x2c, 0x41, 0x33, 0x59, - 0x47, 0x7c, 0x0a, 0x80, 0x22, 0x3c, 0x3a, 0x76, 0xbc, 0xd4, 0xc1, 0xbb, 0x9a, 0xdf, 0x69, 0x05, - 0x5f, 0xf5, 0x33, 0xec, 0x65, 0xf3, 0x4a, 0x12, 0x4f, 0x70, 0x7f, 0x00, 0x9d, 0x62, 0xe7, 0x0c, - 0x4f, 0xf7, 0x76, 0xde, 0xaa, 0x74, 0x56, 0xdf, 0x28, 0x2c, 0x8d, 0x33, 0x89, 0xb5, 0x73, 0x06, - 0xe6, 0x0e, 0x34, 0x13, 0xb4, 0x68, 0x41, 0x63, 0x63, 0x73, 0x6b, 0xed, 0xc9, 0x0e, 0xb2, 0x0a, - 0x40, 0x7d, 0xb0, 0xbd, 0xfb, 0x70, 0x67, 0x93, 0xaf, 0xb5, 0xb3, 0x3d, 0x18, 0x76, 0xcb, 0xfa, - 0x1f, 0x96, 0xa0, 0x99, 0x78, 0x32, 0xe2, 0x03, 0x74, 0x3e, 0xc8, 0x49, 0x53, 0x96, 0x88, 0x32, - 0x53, 0xb9, 0xb0, 0xd5, 0x48, 0xfa, 0x51, 0x16, 0x49, 0xb1, 0x26, 0xbe, 0x0d, 0x01, 0xf9, 0xa8, - 0xb9, 0x52, 0x48, 0x2c, 0x09, 0xa8, 0xda, 0xbe, 0x27, 0x95, 0xc3, 0x4c, 0x6d, 0xe2, 0x41, 0xc7, - 0xb3, 0x64, 0x16, 0x4e, 0x34, 0x08, 0x1e, 0x46, 0x7a, 0xcc, 0x7e, 0x74, 0x7a, 0xb0, 0x74, 0xb7, - 0x52, 0x7e, 0xb7, 0x0b, 0x41, 0x49, 0xf9, 0x62, 0x50, 0x92, 0x19, 0xce, 0xda, 0xab, 0x0c, 0xa7, - 0xfe, 0xf3, 0x3a, 0x74, 0x0c, 0x19, 0xc5, 0x7e, 0x28, 0x95, 0x5f, 0xf8, 0x32, 0x11, 0xba, 0x06, - 0x10, 0xf2, 0xe0, 0x6c, 0x6b, 0x4d, 0x61, 0x38, 0x9a, 0x72, 0x7d, 0x8b, 0x78, 0x57, 0x59, 0xc8, - 0x14, 0x16, 0x57, 0x41, 0x3b, 0x30, 0xad, 0x13, 0x5e, 0x96, 0xed, 0x64, 0x93, 0x11, 0xbc, 0xae, - 0x69, 0x59, 0x32, 0x8a, 0x46, 0xc8, 0x0a, 0x6c, 0x2d, 0x35, 0xc6, 0x3c, 0x92, 0xe7, 0xe2, 0x1e, - 0x40, 0x24, 0xad, 0x50, 0xc6, 0xd4, 0x8d, 0x36, 0x53, 0x5b, 0x5f, 0xfc, 0xd5, 0xb3, 0xa5, 0xb9, - 0x7f, 0x7e, 0xb6, 0xa4, 0x0d, 0xa4, 0x17, 0x39, 0xb1, 0x73, 0x2a, 0x0d, 0x8d, 0x07, 0xe1, 0x8c, - 0x6f, 0x41, 0x3b, 0x92, 0x11, 0x1a, 0xdb, 0x51, 0xec, 0x9f, 0x48, 0xf6, 0xcb, 0x67, 0x4e, 0x9a, - 0x57, 0xe3, 0x86, 0x38, 0x0c, 0x15, 0x91, 0xe9, 0xf9, 0xde, 0xf9, 0xd8, 0x9f, 0x44, 0xca, 0xb2, - 0x64, 0x08, 0xb1, 0x02, 0x97, 0xa4, 0x67, 0x85, 0xe7, 0x01, 0xde, 0x08, 0xcf, 0x32, 0x3a, 0x74, - 0x5c, 0xa9, 0x1c, 0xfa, 0xc5, 0xac, 0xeb, 0x91, 0x3c, 0xdf, 0x72, 0x5c, 0x89, 0xd7, 0x3a, 0x35, - 0x27, 0x6e, 0x3c, 0xa2, 0x7c, 0x01, 0xf0, 0xb5, 0x08, 0xb3, 0x66, 0xdb, 0xa1, 0xb8, 0x05, 0x8b, - 0xdc, 0x1d, 0xfa, 0xae, 0x74, 0x6c, 0x5e, 0xac, 0x45, 0xa3, 0x16, 0xa8, 0xc3, 0x20, 0x3c, 0x2d, - 0xb5, 0x02, 0x97, 0x78, 0x2c, 0xdf, 0x31, 0x19, 0x3d, 0xcf, 0x5b, 0x53, 0xd7, 0x40, 0xf5, 0x14, - 0xb7, 0x0e, 0xcc, 0xf8, 0x98, 0xa2, 0x80, 0x64, 0xeb, 0x7d, 0x33, 0x3e, 0x46, 0xbf, 0x80, 0xbb, - 0x0f, 0x1d, 0xe9, 0x72, 0x14, 0xaf, 0x19, 0x3c, 0x63, 0x0b, 0x31, 0xe8, 0x17, 0xa8, 0x01, 0x7e, - 0x38, 0x36, 0x39, 0x0d, 0xaa, 0x19, 0x3c, 0x69, 0x8b, 0x50, 0xb8, 0x85, 0x7a, 0x51, 0x6f, 0x32, - 0xa6, 0x84, 0x68, 0xd5, 0x50, 0x6f, 0xbc, 0x3b, 0x19, 0x8b, 0x0f, 0xa0, 0xeb, 0x78, 0x56, 0x28, - 0xc7, 0xd2, 0x8b, 0x4d, 0x77, 0x74, 0x18, 0xfa, 0xe3, 0xde, 0x22, 0x0d, 0x5a, 0xc8, 0xe1, 0xb7, - 0x42, 0x7f, 0xac, 0xb2, 0x37, 0x81, 0x19, 0xc6, 0x8e, 0xe9, 0xf6, 0x44, 0x92, 0xbd, 0xd9, 0x67, - 0x84, 0x78, 0x17, 0xda, 0x38, 0x7b, 0x37, 0xb5, 0x10, 0x97, 0x68, 0x99, 0x22, 0x52, 0x7c, 0x1b, - 0xde, 0x74, 0xa2, 0x14, 0x5c, 0xfb, 0x99, 0x89, 0x1c, 0x4d, 0x9c, 0xd9, 0xbb, 0x4c, 0x2b, 0xbe, - 0xa8, 0x5b, 0x7f, 0x5e, 0x81, 0x66, 0x1a, 0xbe, 0xde, 0x06, 0x6d, 0x9c, 0xe8, 0x5f, 0xe5, 0x78, - 0xb6, 0x0b, 0x4a, 0xd9, 0xc8, 0xfa, 0xc5, 0x35, 0x28, 0x9f, 0x9c, 0x2a, 0x5b, 0xd0, 0x5e, 0xe1, - 0x02, 0x46, 0x70, 0xf0, 0xf1, 0xca, 0xa3, 0xa7, 0x46, 0xf9, 0xe4, 0xf4, 0x2b, 0xc8, 0xa1, 0x78, - 0x1f, 0x16, 0x2c, 0x57, 0x9a, 0xde, 0x28, 0xf3, 0x96, 0x88, 0xcf, 0x8d, 0x0e, 0xa1, 0xf7, 0x53, - 0x97, 0xe9, 0x26, 0xd4, 0x6c, 0xe9, 0xc6, 0x66, 0x3e, 0x8f, 0xbe, 0x17, 0x9a, 0x96, 0x2b, 0x37, - 0x10, 0x6d, 0x70, 0x2f, 0xda, 0x82, 0x34, 0x64, 0xcc, 0xd9, 0x82, 0x19, 0xe1, 0x62, 0xaa, 0x67, - 0x20, 0xaf, 0x67, 0x6e, 0xc3, 0xa2, 0x3c, 0x0b, 0xc8, 0x00, 0x8e, 0xd2, 0x0c, 0x09, 0x5b, 0xe6, - 0x6e, 0xd2, 0xf1, 0x20, 0xc9, 0x94, 0x7c, 0x88, 0x2a, 0x90, 0x49, 0x3d, 0x4f, 0x7b, 0x09, 0x95, - 0x88, 0xcd, 0xa9, 0x15, 0x23, 0x19, 0x22, 0x3e, 0x00, 0xcd, 0xb2, 0xad, 0x11, 0x53, 0xa6, 0x9d, - 0x9d, 0xed, 0xc1, 0xc6, 0x03, 0x26, 0x49, 0xd3, 0xb2, 0x2d, 0x8e, 0x12, 0x0a, 0xa1, 0x6c, 0xe7, - 0x75, 0x42, 0xd9, 0xbc, 0x91, 0xef, 0x16, 0x8c, 0xfc, 0xe7, 0xd5, 0x66, 0xa3, 0xdb, 0xd4, 0xdf, - 0x81, 0x66, 0xb2, 0x11, 0xaa, 0xee, 0x48, 0x7a, 0x2a, 0x4d, 0x41, 0xaa, 0x1b, 0xc1, 0x61, 0xa4, - 0x5b, 0x50, 0x79, 0xf4, 0x74, 0x40, 0x1a, 0x1c, 0x8d, 0x69, 0x8d, 0x7c, 0x2f, 0x6a, 0xa7, 0x5a, - 0xbd, 0x9c, 0xd3, 0xea, 0xd7, 0xd9, 0x20, 0xd2, 0x03, 0x25, 0xb9, 0xdd, 0x1c, 0x06, 0x49, 0xcc, - 0xce, 0x40, 0x95, 0xd3, 0xbe, 0x04, 0xe8, 0xff, 0x55, 0x81, 0x86, 0xf2, 0xd7, 0xd0, 0x08, 0x4e, - 0xd2, 0xb4, 0x24, 0x36, 0x8b, 0x81, 0x74, 0xea, 0xf8, 0xe5, 0x6b, 0x54, 0x95, 0x57, 0xd7, 0xa8, - 0xc4, 0xa7, 0x30, 0x1f, 0x70, 0x5f, 0xde, 0x55, 0x7c, 0x33, 0x3f, 0x47, 0xfd, 0xd2, 0xbc, 0x56, - 0x90, 0x01, 0x48, 0x4a, 0x4a, 0xd4, 0xc7, 0xe6, 0x91, 0xa2, 0x40, 0x03, 0xe1, 0xa1, 0x79, 0xf4, - 0x5a, 0x7e, 0x5f, 0x87, 0x1c, 0xc8, 0x79, 0x32, 0x20, 0xe8, 0x2b, 0xe6, 0x5f, 0xa6, 0x5d, 0x74, - 0xbf, 0xae, 0x82, 0x66, 0xf9, 0xe3, 0xb1, 0x43, 0x7d, 0x1d, 0x95, 0x86, 0x23, 0xc4, 0x30, 0xd2, - 0xff, 0xa0, 0x04, 0x0d, 0x75, 0xaf, 0x0b, 0xc6, 0x7d, 0x7d, 0x7b, 0x77, 0xcd, 0xf8, 0x51, 0xb7, - 0x84, 0xce, 0xcb, 0xf6, 0xee, 0xb0, 0x5b, 0x16, 0x1a, 0xd4, 0xb6, 0x76, 0xf6, 0xd6, 0x86, 0xdd, - 0x0a, 0x1a, 0xfc, 0xf5, 0xbd, 0xbd, 0x9d, 0x6e, 0x55, 0xcc, 0x43, 0x73, 0x63, 0x6d, 0xb8, 0x39, - 0xdc, 0x7e, 0xbc, 0xd9, 0xad, 0xe1, 0xd8, 0x87, 0x9b, 0x7b, 0xdd, 0x3a, 0x36, 0x9e, 0x6c, 0x6f, - 0x74, 0x1b, 0xd8, 0xbf, 0xbf, 0x36, 0x18, 0xfc, 0x60, 0xcf, 0xd8, 0xe8, 0x36, 0xc9, 0x69, 0x18, - 0x1a, 0xdb, 0xbb, 0x0f, 0xbb, 0x1a, 0xb6, 0xf7, 0xd6, 0x3f, 0xdf, 0x7c, 0x30, 0xec, 0x02, 0xb6, - 0x9f, 0xf2, 0xda, 0xf3, 0xfa, 0x7d, 0x68, 0xe5, 0xe8, 0x86, 0x2b, 0x19, 0x9b, 0x5b, 0xdd, 0x39, - 0xdc, 0xfe, 0xe9, 0xda, 0xce, 0x13, 0xf4, 0x37, 0x3a, 0x00, 0xd4, 0x1c, 0xed, 0xac, 0xed, 0x3e, - 0xec, 0x96, 0x95, 0xb7, 0xfa, 0x7d, 0x68, 0x3e, 0x71, 0xec, 0x75, 0xd7, 0xb7, 0x4e, 0x90, 0x95, - 0x0e, 0xcc, 0x48, 0x2a, 0xde, 0xa3, 0x36, 0xc6, 0x06, 0x24, 0xc0, 0x91, 0x7a, 0x77, 0x05, 0x21, - 0xf5, 0xbc, 0xc9, 0x78, 0x44, 0x35, 0xcd, 0x0a, 0x1b, 0x65, 0x6f, 0x32, 0x7e, 0xe2, 0xd8, 0x91, - 0x7e, 0x02, 0x8d, 0x27, 0x8e, 0xbd, 0x6f, 0x5a, 0x27, 0xa4, 0x92, 0x71, 0xe9, 0x51, 0xe4, 0x7c, - 0x21, 0x95, 0xf1, 0xd6, 0x08, 0x33, 0x70, 0xbe, 0x90, 0xe2, 0x5d, 0xa8, 0x13, 0x90, 0xa4, 0x53, - 0x48, 0xec, 0x92, 0xe3, 0x18, 0xaa, 0x8f, 0x4a, 0x8a, 0xae, 0xeb, 0x5b, 0xa3, 0x50, 0x1e, 0xf6, - 0xde, 0xe4, 0xd7, 0x20, 0x84, 0x21, 0x0f, 0xf5, 0xdf, 0x2f, 0xa5, 0x37, 0xa7, 0xca, 0xd5, 0x12, - 0x54, 0x03, 0xd3, 0x3a, 0x51, 0xbe, 0x53, 0x4b, 0x2d, 0x88, 0x87, 0x31, 0xa8, 0x43, 0xbc, 0x0f, - 0x4d, 0xc5, 0x54, 0xc9, 0xae, 0xad, 0x1c, 0xf7, 0x19, 0x69, 0x67, 0x91, 0x09, 0x2a, 0x45, 0x26, - 0xa0, 0xc8, 0x3b, 0x70, 0x9d, 0x98, 0x45, 0x08, 0x05, 0x95, 0x20, 0xfd, 0x63, 0x80, 0xac, 0x88, - 0x38, 0xbb, 0x76, 0x63, 0xba, 0x8e, 0x99, 0x44, 0xf2, 0x0c, 0xe8, 0xbb, 0xd0, 0xca, 0x95, 0x1e, - 0x91, 0xb6, 0xa6, 0xeb, 0xa2, 0x3d, 0x67, 0x3d, 0xd0, 0x34, 0x1a, 0xa6, 0xeb, 0x3e, 0x92, 0xe7, - 0x11, 0xba, 0xf1, 0x5c, 0xb5, 0x2c, 0x4f, 0x15, 0xb6, 0x68, 0xaa, 0xc1, 0x9d, 0xfa, 0x87, 0x50, - 0xdf, 0x4a, 0x82, 0x9d, 0x44, 0x30, 0x4a, 0x2f, 0x12, 0x0c, 0xfd, 0x13, 0x75, 0x66, 0xaa, 0x8d, - 0x89, 0xdb, 0xaa, 0x3a, 0x1a, 0x71, 0x2d, 0xb6, 0x94, 0xe5, 0x82, 0x78, 0x90, 0x2a, 0x8c, 0xd2, - 0x60, 0x7d, 0x03, 0x9a, 0x2f, 0xad, 0x37, 0x2b, 0x02, 0x94, 0x33, 0x02, 0xcc, 0xa8, 0x40, 0xeb, - 0x3f, 0x01, 0xc8, 0xaa, 0xa8, 0x4a, 0x4e, 0x79, 0x15, 0x94, 0xd3, 0x5b, 0xd0, 0xb4, 0x8e, 0x1d, - 0xd7, 0x0e, 0xa5, 0x57, 0xb8, 0x75, 0x56, 0x77, 0x4d, 0xfb, 0xc5, 0x0d, 0xa8, 0x52, 0x71, 0xb8, - 0x92, 0x69, 0xf1, 0xb4, 0x32, 0x4c, 0x3d, 0xfa, 0x19, 0xb4, 0x39, 0x3e, 0x7a, 0x0d, 0xef, 0xb2, - 0xa8, 0x46, 0xcb, 0x17, 0xd4, 0xe8, 0x15, 0xa8, 0x93, 0xbb, 0x92, 0xdc, 0x46, 0x41, 0x2f, 0x50, - 0xaf, 0x7f, 0x54, 0x06, 0xe0, 0xad, 0x77, 0x7d, 0x5b, 0x16, 0x13, 0x11, 0xa5, 0xe9, 0x44, 0x84, - 0x80, 0x6a, 0x5a, 0xf7, 0xd7, 0x0c, 0x6a, 0x67, 0x86, 0x51, 0x25, 0x27, 0xd8, 0x30, 0xbe, 0x0d, - 0x1a, 0x79, 0x94, 0xce, 0x17, 0x54, 0xec, 0xc1, 0x0d, 0x33, 0x44, 0xbe, 0x0a, 0x5e, 0x2b, 0x56, - 0xc1, 0xd3, 0x52, 0x5c, 0x9d, 0x57, 0xe3, 0x52, 0xdc, 0xac, 0xea, 0x26, 0x65, 0x87, 0x22, 0x19, - 0xc6, 0x49, 0x6a, 0x83, 0xa1, 0x34, 0x4a, 0xd7, 0xd4, 0x58, 0x93, 0xf3, 0x3b, 0x9e, 0x3f, 0xb2, - 0x7c, 0xef, 0xd0, 0x75, 0xac, 0x58, 0x55, 0xbd, 0xc1, 0xf3, 0x1f, 0x28, 0x0c, 0x2d, 0xe6, 0x39, - 0x3f, 0x9d, 0xb0, 0x63, 0x89, 0x8b, 0x11, 0xa4, 0x7f, 0x0a, 0xf3, 0xc9, 0xbb, 0x50, 0x51, 0xef, - 0x56, 0x1a, 0xd9, 0x96, 0xb2, 0x37, 0xcf, 0xc8, 0xb7, 0x5e, 0xee, 0x95, 0x92, 0xd8, 0x56, 0xff, - 0xbb, 0x6a, 0x32, 0x59, 0xd5, 0x9e, 0x5e, 0x4e, 0xdb, 0x62, 0xb2, 0xa2, 0xfc, 0x5a, 0xc9, 0x8a, - 0x6f, 0x83, 0x66, 0x53, 0xfc, 0xed, 0x9c, 0x26, 0x86, 0xae, 0x3f, 0x1d, 0x6b, 0xab, 0x08, 0x9d, - 0x3c, 0xff, 0x74, 0xf0, 0x2b, 0xde, 0x27, 0x7d, 0x85, 0xda, 0xac, 0x57, 0xa8, 0x7f, 0xcd, 0x57, - 0xc8, 0x88, 0xdc, 0xc9, 0x13, 0x19, 0x9d, 0x68, 0xcf, 0xf7, 0x46, 0xde, 0xc4, 0x75, 0xcd, 0x03, - 0x57, 0xaa, 0xe7, 0x69, 0x79, 0xbe, 0xb7, 0xab, 0x50, 0x18, 0x03, 0xe4, 0x87, 0xb0, 0x12, 0xe0, - 0xa7, 0x5a, 0xc8, 0x8d, 0x23, 0x55, 0xb1, 0x0c, 0x5d, 0xff, 0xe0, 0x27, 0xd2, 0x8a, 0x89, 0x92, - 0x23, 0x92, 0x7e, 0x0e, 0x00, 0x3a, 0x8c, 0x47, 0xd2, 0xa1, 0x8b, 0x3b, 0xcd, 0x16, 0xed, 0x0b, - 0x6c, 0xf1, 0x31, 0xb4, 0x88, 0x9d, 0x47, 0x51, 0x20, 0xad, 0xa8, 0xb7, 0x40, 0x6f, 0x4e, 0x8f, - 0xc2, 0x25, 0xf1, 0x6d, 0xec, 0x1c, 0x04, 0xd2, 0x32, 0xc0, 0x49, 0x9a, 0xa8, 0xb9, 0xb4, 0x94, - 0xe6, 0xb9, 0xcc, 0x81, 0x06, 0xb5, 0xed, 0xdd, 0x8d, 0xcd, 0x1f, 0x76, 0x4b, 0x68, 0xa0, 0x8d, - 0xcd, 0xa7, 0x9b, 0xc6, 0x60, 0xb3, 0x5b, 0x46, 0x83, 0xb9, 0xb1, 0xb9, 0xb3, 0x39, 0xdc, 0xec, - 0x56, 0xd8, 0xf9, 0xa2, 0x82, 0x92, 0xeb, 0x58, 0x4e, 0xac, 0xef, 0xc1, 0xc2, 0xd4, 0x4e, 0x33, - 0x15, 0xda, 0x32, 0x34, 0xfc, 0x20, 0xf1, 0xc5, 0x53, 0xbe, 0xdc, 0x23, 0xd4, 0xbe, 0xe9, 0x84, - 0x46, 0xd2, 0x8d, 0x96, 0x20, 0x43, 0xbf, 0xaa, 0x8a, 0xaf, 0x29, 0x7f, 0x4a, 0x1f, 0x00, 0x64, - 0x59, 0x19, 0x34, 0x41, 0x19, 0x65, 0x55, 0x5a, 0x38, 0x4e, 0x68, 0xba, 0x9c, 0x6a, 0x9f, 0xf2, - 0x8b, 0x72, 0x3f, 0xdc, 0xaf, 0xaf, 0x82, 0xf6, 0xd8, 0x0c, 0x3e, 0xe3, 0x0a, 0xf0, 0x4d, 0xe8, - 0x50, 0x60, 0x93, 0x84, 0x8c, 0x6c, 0x19, 0xe6, 0x8d, 0x76, 0x8a, 0x45, 0x43, 0xa3, 0xff, 0x45, - 0x09, 0x2e, 0x3f, 0xf6, 0x4f, 0x65, 0xea, 0xe8, 0xef, 0x9b, 0xe7, 0xae, 0x6f, 0xda, 0xaf, 0x90, - 0xad, 0x6b, 0x00, 0x91, 0x3f, 0xa1, 0x8a, 0x6c, 0x52, 0xbf, 0x36, 0x34, 0xc6, 0x3c, 0x54, 0x1f, - 0x00, 0xc9, 0x28, 0xa6, 0x4e, 0xe5, 0x35, 0x20, 0x8c, 0x5d, 0x6f, 0x40, 0x3d, 0x3e, 0xf3, 0xb2, - 0x6a, 0x7a, 0x2d, 0xa6, 0x72, 0xc6, 0x4c, 0xbf, 0xbf, 0x36, 0xdb, 0xef, 0xd7, 0x1f, 0x80, 0x36, - 0x3c, 0xa3, 0x84, 0xfe, 0xa4, 0xe8, 0x79, 0x97, 0x5e, 0xe2, 0xdf, 0x95, 0xa7, 0xfc, 0xbb, 0x7f, - 0x2f, 0x41, 0x2b, 0x17, 0xc0, 0x88, 0x6f, 0x40, 0x35, 0x3e, 0xf3, 0x8a, 0x1f, 0xcf, 0x24, 0x9b, - 0x18, 0xd4, 0x75, 0x21, 0x69, 0x5d, 0xbe, 0x90, 0xb4, 0x16, 0x3b, 0xb0, 0xc0, 0x66, 0x26, 0xb9, - 0x44, 0x92, 0xdb, 0x7b, 0x67, 0x2a, 0x60, 0xe2, 0xa2, 0x47, 0x72, 0x25, 0x95, 0xb0, 0xea, 0x1c, - 0x15, 0x90, 0xfd, 0x35, 0xb8, 0x34, 0x63, 0xd8, 0x57, 0x29, 0x7f, 0xe9, 0x4b, 0xd0, 0x1e, 0x9e, - 0x79, 0x43, 0x67, 0x2c, 0xa3, 0xd8, 0x1c, 0x07, 0xe4, 0x1f, 0x2b, 0x37, 0xa1, 0x6a, 0x94, 0xe3, - 0x48, 0x7f, 0x0f, 0xe6, 0xf7, 0xa5, 0x0c, 0x0d, 0x19, 0x05, 0xbe, 0xc7, 0x9e, 0xa0, 0x2a, 0x36, - 0xb0, 0x4f, 0xa2, 0x20, 0xfd, 0xf7, 0x40, 0x33, 0xcc, 0xc3, 0x78, 0xdd, 0x8c, 0xad, 0xe3, 0xaf, - 0x92, 0xbd, 0x7a, 0x0f, 0x1a, 0x01, 0xf3, 0x94, 0x0a, 0x6b, 0xe7, 0xc9, 0x37, 0x51, 0x7c, 0x66, - 0x24, 0x9d, 0xfa, 0xb7, 0xa0, 0xa3, 0x2a, 0x7f, 0xc9, 0x49, 0x72, 0xe5, 0xc1, 0xd2, 0x0b, 0xcb, - 0x83, 0xfa, 0x11, 0xb4, 0x93, 0x79, 0x6c, 0xe9, 0x5f, 0x6b, 0xda, 0x57, 0xff, 0xfe, 0x42, 0xff, - 0x5d, 0xb8, 0x34, 0x98, 0x1c, 0x44, 0x56, 0xe8, 0x90, 0xbc, 0x27, 0xdb, 0xf5, 0xa1, 0x19, 0x84, - 0xf2, 0xd0, 0x39, 0x93, 0x89, 0x88, 0xa5, 0xb0, 0xb8, 0x05, 0x8d, 0x31, 0xd2, 0x4b, 0x66, 0xc2, - 0x9b, 0x05, 0xeb, 0x8f, 0xb1, 0xc7, 0x48, 0x06, 0xe8, 0xdf, 0x81, 0xcb, 0xc5, 0xe5, 0x15, 0x15, - 0xde, 0x81, 0xca, 0xc9, 0x69, 0xa4, 0xc8, 0xbc, 0x58, 0x08, 0xf6, 0xe9, 0xc3, 0x17, 0xec, 0xd5, - 0xff, 0xa6, 0x04, 0x95, 0xdd, 0xc9, 0x38, 0xff, 0x75, 0x61, 0x95, 0xbf, 0x2e, 0xbc, 0x9a, 0x2f, - 0x4c, 0x70, 0xf0, 0x98, 0x15, 0x20, 0xde, 0x06, 0xed, 0xd0, 0x0f, 0x7f, 0x66, 0x86, 0xb6, 0xb4, - 0x95, 0xbb, 0x91, 0x21, 0x28, 0x4e, 0x98, 0x8c, 0x03, 0x65, 0xb3, 0xa8, 0x2d, 0x6e, 0x2a, 0x87, - 0x85, 0x03, 0xba, 0x45, 0xa4, 0xec, 0xee, 0x64, 0xbc, 0xe2, 0x4a, 0x33, 0x22, 0x0b, 0xca, 0x3e, - 0x8c, 0x7e, 0x1b, 0xb4, 0x14, 0x85, 0x7a, 0x7a, 0x77, 0x30, 0xda, 0xde, 0xe0, 0x64, 0x2f, 0x86, - 0x3e, 0x25, 0xd4, 0xd1, 0xc3, 0x1f, 0xee, 0x8e, 0x86, 0x83, 0x6e, 0x59, 0xff, 0x31, 0xb4, 0x12, - 0xf9, 0xd9, 0xb6, 0xa9, 0xb2, 0x49, 0x02, 0xbc, 0x6d, 0x17, 0xe4, 0x79, 0x9b, 0x62, 0x53, 0xe9, - 0xd9, 0xdb, 0x89, 0xe0, 0x31, 0x50, 0xbc, 0xa1, 0x2a, 0x93, 0x26, 0x37, 0xd4, 0x37, 0x61, 0xd1, - 0xa0, 0x0a, 0x0d, 0x7a, 0x13, 0xc9, 0x93, 0x5d, 0x81, 0xba, 0xe7, 0xdb, 0x32, 0xdd, 0x40, 0x41, - 0xb8, 0xb3, 0x7a, 0x6c, 0xa5, 0xd2, 0xd2, 0xb7, 0x97, 0xb0, 0x88, 0x5a, 0xb2, 0xc8, 0x68, 0x85, - 0xea, 0x41, 0x69, 0xaa, 0x7a, 0x80, 0x9b, 0xa8, 0x0f, 0x05, 0x58, 0xf3, 0x27, 0x1f, 0x07, 0xf4, - 0xa1, 0x69, 0x47, 0x31, 0x89, 0xb5, 0xd2, 0x8d, 0x29, 0xac, 0xdf, 0x85, 0x4b, 0x6b, 0x41, 0xe0, - 0x9e, 0x27, 0x65, 0x55, 0xb5, 0x51, 0x2f, 0xab, 0xbd, 0x96, 0x54, 0x40, 0xcc, 0xa0, 0xbe, 0x05, - 0xf3, 0x49, 0x6a, 0xe5, 0xb1, 0x8c, 0x4d, 0xd2, 0x78, 0xae, 0x53, 0xc8, 0x2d, 0x34, 0x19, 0x31, - 0x2c, 0xd6, 0x28, 0xa6, 0xee, 0xb7, 0x02, 0x75, 0xa5, 0x4e, 0x05, 0x54, 0x2d, 0xdf, 0xe6, 0x8d, - 0x6a, 0x06, 0xb5, 0x91, 0xab, 0xc6, 0xd1, 0x51, 0xe2, 0xde, 0x8f, 0xa3, 0x23, 0xfd, 0xbf, 0xcb, - 0xd0, 0x5e, 0xa7, 0x94, 0x5b, 0x72, 0xc6, 0x5c, 0x3a, 0xba, 0x54, 0x48, 0x47, 0xe7, 0x53, 0xcf, - 0xe5, 0x42, 0xea, 0xb9, 0x70, 0xa0, 0x4a, 0xd1, 0x27, 0x7f, 0x13, 0x1a, 0x13, 0xcf, 0x39, 0x4b, - 0xec, 0x84, 0x46, 0xbe, 0xcd, 0xd9, 0x30, 0x12, 0x37, 0xa0, 0x85, 0xa6, 0xc4, 0xf1, 0x38, 0xdd, - 0xcb, 0x39, 0xdb, 0x3c, 0x6a, 0x2a, 0xa9, 0x5b, 0x7f, 0x79, 0x52, 0xb7, 0xf1, 0x75, 0x92, 0xba, - 0xcd, 0xaf, 0x91, 0xd4, 0xd5, 0xa6, 0x93, 0xba, 0xc5, 0xa8, 0x03, 0x2e, 0x44, 0x1d, 0xd7, 0x00, - 0xf8, 0x9b, 0xa7, 0xc3, 0x89, 0xeb, 0x2a, 0xd7, 0x4c, 0x23, 0xcc, 0xd6, 0xc4, 0x75, 0xf5, 0x1d, - 0xe8, 0x24, 0x0f, 0xa0, 0x14, 0xc5, 0xa7, 0xb0, 0xa0, 0x8a, 0x3a, 0x32, 0x54, 0x79, 0x44, 0xd6, - 0x7f, 0x24, 0xa5, 0x5c, 0x77, 0x51, 0x3d, 0x46, 0xc7, 0xce, 0x83, 0x91, 0xfe, 0xcb, 0x12, 0xb4, - 0x0b, 0x23, 0xc4, 0xfd, 0xac, 0x44, 0x54, 0x22, 0x59, 0xef, 0x5d, 0x58, 0xe5, 0xe5, 0x65, 0xa2, - 0xf2, 0x54, 0x99, 0x48, 0xbf, 0x93, 0x16, 0x7f, 0x54, 0xc9, 0x67, 0x2e, 0x2d, 0xf9, 0x50, 0x95, - 0x64, 0x6d, 0x38, 0x34, 0xba, 0x65, 0x51, 0x87, 0xf2, 0xee, 0xa0, 0x5b, 0xd1, 0x7f, 0x53, 0x86, - 0xf6, 0xe6, 0x59, 0x40, 0xdf, 0xff, 0xbd, 0x32, 0x84, 0xcb, 0x71, 0x5f, 0xb9, 0xc0, 0x7d, 0x39, - 0x3e, 0xaa, 0xa8, 0x9a, 0x37, 0xf3, 0x11, 0x06, 0x75, 0x9c, 0x62, 0x56, 0xfc, 0xc5, 0xd0, 0xff, - 0x1f, 0xfe, 0x2a, 0x68, 0x27, 0x98, 0xae, 0x6d, 0xee, 0x40, 0x27, 0x21, 0xae, 0x62, 0x9f, 0xd7, - 0x12, 0x7c, 0xfe, 0x3e, 0xd9, 0x4d, 0xb3, 0x8d, 0x0c, 0xe8, 0x7f, 0x56, 0x06, 0x8d, 0xb9, 0x11, - 0xef, 0xf3, 0x81, 0xb2, 0x11, 0xa5, 0xac, 0x8c, 0x96, 0x76, 0xae, 0x3c, 0x92, 0xe7, 0x99, 0x9d, - 0x98, 0x59, 0x7a, 0x56, 0x39, 0x49, 0x4e, 0xc5, 0x50, 0x4e, 0xf2, 0x2a, 0x68, 0xec, 0xe2, 0x4d, - 0x54, 0x0d, 0xa7, 0x6a, 0xb0, 0xcf, 0xf7, 0xc4, 0x21, 0x2b, 0x15, 0xcb, 0x70, 0xac, 0x5e, 0x8a, - 0xda, 0xc5, 0xa0, 0xb7, 0x9d, 0x84, 0x5b, 0x05, 0x8a, 0x34, 0xa6, 0x29, 0x72, 0x0c, 0x0d, 0x75, - 0x36, 0x8c, 0x26, 0x9e, 0xec, 0x3e, 0xda, 0xdd, 0xfb, 0xc1, 0x6e, 0x81, 0x47, 0xd3, 0x78, 0xa3, - 0x9c, 0x8f, 0x37, 0x2a, 0x88, 0x7f, 0xb0, 0xf7, 0x64, 0x77, 0xd8, 0xad, 0x8a, 0x36, 0x68, 0xd4, - 0x1c, 0x19, 0x9b, 0x4f, 0xbb, 0x35, 0x4a, 0xe9, 0x3d, 0xf8, 0x6c, 0xf3, 0xf1, 0x5a, 0xb7, 0x9e, - 0x16, 0x35, 0x1b, 0xfa, 0x9f, 0x96, 0x60, 0x91, 0x09, 0x92, 0xcf, 0x68, 0xe5, 0xff, 0x39, 0x50, - 0xe5, 0x7f, 0x0e, 0xfc, 0xdf, 0x26, 0xb1, 0x70, 0xd2, 0xc4, 0x49, 0x3e, 0x23, 0xe0, 0x4c, 0x6b, - 0x73, 0xe2, 0xa8, 0xaf, 0x07, 0xfe, 0xa1, 0x04, 0x7d, 0x8e, 0x2f, 0x1e, 0x86, 0x66, 0x70, 0xfc, - 0xfd, 0x9d, 0x0b, 0xe9, 0x94, 0x17, 0x79, 0xdd, 0x37, 0xa1, 0x43, 0xff, 0xad, 0xf8, 0xa9, 0x3b, - 0x52, 0xa1, 0x3d, 0xbf, 0x6e, 0x5b, 0x61, 0x79, 0x21, 0xf1, 0x11, 0xcc, 0xf3, 0x7f, 0x30, 0xa8, - 0xf4, 0x50, 0x28, 0x81, 0x17, 0xa2, 0x9b, 0x16, 0x8f, 0xe2, 0x82, 0xfd, 0xfd, 0x74, 0x52, 0x96, - 0x79, 0xb9, 0x58, 0xe5, 0x56, 0x53, 0x86, 0x94, 0x8f, 0xb9, 0x0b, 0x57, 0x67, 0xde, 0x43, 0xb1, - 0x7d, 0x2e, 0x03, 0xce, 0xdc, 0xa6, 0xff, 0xa6, 0x04, 0xcd, 0xf5, 0x89, 0x7b, 0x42, 0x06, 0xf5, - 0x1a, 0x80, 0xb4, 0x8f, 0xa4, 0xfa, 0x33, 0x43, 0x89, 0x54, 0x88, 0x86, 0x18, 0xfe, 0x3b, 0xc3, - 0xa7, 0x00, 0x7c, 0xc7, 0xd1, 0xd8, 0x0c, 0xd4, 0x13, 0x51, 0x49, 0x3a, 0x59, 0x40, 0xdd, 0xe5, - 0xb1, 0x19, 0xa8, 0x92, 0x74, 0x94, 0xc0, 0x59, 0xa9, 0xbe, 0xf2, 0x92, 0x52, 0x7d, 0x7f, 0x17, - 0x3a, 0xc5, 0x25, 0x66, 0xc4, 0x98, 0xef, 0x15, 0x3f, 0x87, 0xba, 0x48, 0xc3, 0x5c, 0x3c, 0xf0, - 0x39, 0x2c, 0x4c, 0x55, 0x31, 0x5e, 0xa6, 0x57, 0x0b, 0x22, 0x53, 0x9e, 0x16, 0x99, 0x0f, 0x61, - 0x71, 0x68, 0x46, 0x27, 0x2a, 0x46, 0xca, 0x1c, 0x81, 0xd8, 0x8c, 0x4e, 0x46, 0x29, 0x51, 0xeb, - 0x08, 0x6e, 0xdb, 0xfa, 0x7d, 0x10, 0xf9, 0xd1, 0x8a, 0xfe, 0x18, 0xfb, 0xe2, 0xf0, 0xb1, 0x8c, - 0xcd, 0xc4, 0x63, 0x41, 0x04, 0x12, 0x6f, 0xf5, 0xef, 0x4b, 0x50, 0xc5, 0xa0, 0x42, 0xdc, 0x01, - 0xed, 0x33, 0x69, 0x86, 0xf1, 0x81, 0x34, 0x63, 0x51, 0x08, 0x20, 0xfa, 0x44, 0xb7, 0xec, 0x13, - 0x2b, 0x7d, 0xee, 0x5e, 0x49, 0xac, 0xf0, 0x07, 0xe0, 0xc9, 0x87, 0xed, 0xed, 0x24, 0x38, 0xa1, - 0xe0, 0xa5, 0x5f, 0x98, 0xaf, 0xcf, 0x2d, 0xd3, 0xf8, 0xcf, 0x7d, 0xc7, 0x7b, 0xc0, 0x9f, 0x1d, - 0x8b, 0xe9, 0x60, 0x66, 0x7a, 0x86, 0xb8, 0x03, 0xf5, 0xed, 0x08, 0xa3, 0xa6, 0x8b, 0x43, 0x89, - 0xf8, 0xf9, 0x80, 0x4a, 0x9f, 0x5b, 0xfd, 0xcb, 0x1a, 0x54, 0x7f, 0x2c, 0x43, 0x5f, 0x7c, 0x08, - 0x0d, 0xf5, 0x41, 0x9a, 0xc8, 0x7d, 0x78, 0xd6, 0xa7, 0x04, 0xc8, 0xd4, 0x97, 0x6a, 0xb4, 0x4b, - 0x97, 0xdf, 0x2f, 0xab, 0xdd, 0x89, 0xec, 0x7b, 0xb9, 0x0b, 0x87, 0xfa, 0x04, 0xba, 0x83, 0x38, - 0x94, 0xe6, 0x38, 0x37, 0xbc, 0x48, 0xaa, 0x59, 0x85, 0x40, 0xa2, 0xd7, 0x6d, 0xa8, 0x73, 0x68, - 0x3a, 0x35, 0x61, 0xba, 0xca, 0x47, 0x83, 0xdf, 0x87, 0xd6, 0xe0, 0xd8, 0x9f, 0xb8, 0xf6, 0x40, - 0x86, 0xa7, 0x52, 0xe4, 0xa2, 0xab, 0x7e, 0xae, 0xad, 0xcf, 0x89, 0xfb, 0x50, 0xc7, 0x17, 0x09, - 0xc7, 0x62, 0x31, 0x17, 0x81, 0x31, 0x9b, 0xf4, 0x45, 0x1e, 0x95, 0x50, 0x4a, 0xbc, 0x0f, 0x1a, - 0x87, 0x02, 0x18, 0x08, 0x34, 0x54, 0x74, 0xc1, 0xc7, 0xc8, 0x85, 0x08, 0xfa, 0x9c, 0x58, 0x06, - 0xc8, 0xc5, 0xb4, 0x2f, 0x1b, 0xf9, 0x11, 0xb4, 0x1f, 0x90, 0x26, 0xdc, 0x0b, 0xd7, 0x0e, 0xfc, - 0x30, 0x16, 0xd3, 0x1f, 0xc9, 0xf6, 0xa7, 0x11, 0xfa, 0x1c, 0x46, 0x87, 0xc3, 0xf0, 0x9c, 0xc7, - 0x2f, 0xaa, 0x54, 0x40, 0xb6, 0xdf, 0x0c, 0xba, 0x88, 0x8f, 0x53, 0xb9, 0x4a, 0x23, 0x80, 0x59, - 0x25, 0x43, 0x26, 0x11, 0xcb, 0x00, 0x91, 0x08, 0xb2, 0xf0, 0x44, 0xbc, 0xc1, 0xe5, 0xcb, 0xa9, - 0x70, 0xe5, 0xe2, 0x94, 0x2c, 0x14, 0xe1, 0x29, 0x17, 0x42, 0x93, 0xa9, 0x29, 0xdf, 0x84, 0xf9, - 0x7c, 0x58, 0x21, 0xa8, 0x0e, 0x37, 0x23, 0xd0, 0x28, 0x4e, 0x5b, 0xfd, 0x8f, 0x1a, 0xd4, 0x7f, - 0xe0, 0x87, 0x27, 0x32, 0x14, 0xb7, 0xa0, 0x4e, 0x85, 0x68, 0x25, 0x4b, 0x69, 0x51, 0x7a, 0x16, - 0xed, 0xde, 0x05, 0x8d, 0x38, 0x03, 0x85, 0x9d, 0xf9, 0x95, 0xfe, 0x5c, 0xc6, 0x8b, 0x73, 0xda, - 0x97, 0x98, 0xbb, 0xc3, 0xdc, 0x9a, 0x7e, 0x78, 0x52, 0x28, 0x14, 0xf7, 0xe9, 0x49, 0x1f, 0x3d, - 0x1d, 0xa0, 0x7c, 0xde, 0x2b, 0xa1, 0x4f, 0x31, 0xe0, 0xc7, 0xc3, 0x41, 0xd9, 0x9f, 0x56, 0x58, - 0xfc, 0xb3, 0x7f, 0x89, 0xe8, 0x73, 0xe2, 0x2e, 0xd4, 0x95, 0x89, 0x59, 0xcc, 0x14, 0x61, 0x72, - 0xc3, 0x6e, 0x1e, 0xa5, 0x26, 0xdc, 0x87, 0x3a, 0x9b, 0x63, 0x9e, 0x50, 0x88, 0x6b, 0x98, 0x4f, - 0x8b, 0x9e, 0xb6, 0x3e, 0x27, 0x6e, 0x43, 0x43, 0x95, 0x99, 0xc5, 0x8c, 0x9a, 0xf3, 0x85, 0x17, - 0xab, 0xb3, 0xaf, 0xc5, 0xeb, 0x17, 0x9c, 0x5a, 0x5e, 0xbf, 0xe8, 0x8a, 0xb1, 0xe8, 0x1b, 0xd2, - 0x92, 0x4e, 0x2e, 0x31, 0x27, 0x12, 0x8a, 0xcc, 0xd0, 0x5f, 0x9f, 0x40, 0xbb, 0x90, 0xc4, 0x13, - 0xbd, 0x84, 0x2d, 0xa6, 0xf3, 0x7a, 0x17, 0xb4, 0xc6, 0x77, 0x40, 0x53, 0x69, 0x87, 0x03, 0xc5, - 0x18, 0x33, 0x92, 0x1c, 0xfd, 0x8b, 0x79, 0x07, 0x52, 0x05, 0x3f, 0x84, 0x4b, 0x33, 0x6c, 0xab, - 0xa0, 0xcf, 0x9e, 0x5f, 0xec, 0x3c, 0xf4, 0x97, 0x5e, 0xd8, 0x9f, 0x12, 0xe0, 0xeb, 0x89, 0xd3, - 0x77, 0x01, 0x32, 0x13, 0xc3, 0xb2, 0x71, 0xc1, 0x40, 0xf5, 0xaf, 0x4c, 0xa3, 0x93, 0x4d, 0xd7, - 0x7b, 0xbf, 0xfa, 0xf2, 0x7a, 0xe9, 0xd7, 0x5f, 0x5e, 0x2f, 0xfd, 0xdb, 0x97, 0xd7, 0x4b, 0xbf, - 0xfc, 0xed, 0xf5, 0xb9, 0x5f, 0xff, 0xf6, 0xfa, 0xdc, 0x3f, 0xfd, 0xf6, 0xfa, 0xdc, 0x41, 0x9d, - 0xfe, 0xe5, 0xf9, 0xd1, 0xff, 0x04, 0x00, 0x00, 0xff, 0xff, 0xb9, 0x62, 0x5c, 0xb3, 0x5b, 0x3a, - 0x00, 0x00, + // 5646 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x3b, 0x49, 0x6f, 0x1c, 0x67, + 0x76, 0xac, 0x5e, 0xab, 0x5e, 0x2f, 0x6c, 0x7e, 0x92, 0xe5, 0x76, 0xcb, 0x12, 0x39, 0x65, 0xcb, + 0xa6, 0x25, 0x8b, 0x92, 0x68, 0xcf, 0x64, 0xec, 0xc1, 0x20, 0xe1, 0xd2, 0x94, 0x69, 0x71, 0x9b, + 0xea, 0x96, 0x66, 0x01, 0x92, 0x46, 0xb1, 0xea, 0x23, 0x59, 0xc3, 0xea, 0xaa, 0x9a, 0xaa, 0x6a, + 0x0e, 0xe9, 0xdb, 0x9c, 0xe6, 0x92, 0xc3, 0x20, 0x39, 0x26, 0xc8, 0x21, 0xd7, 0xe4, 0x14, 0x04, + 0x48, 0x10, 0x20, 0x39, 0x05, 0xc1, 0x20, 0xa7, 0x39, 0x06, 0x99, 0x44, 0x08, 0x3c, 0x39, 0xe9, + 0x10, 0x20, 0xf9, 0x03, 0x09, 0xde, 0xfb, 0xbe, 0xda, 0x9a, 0x4d, 0x49, 0x76, 0x90, 0x4b, 0x4e, + 0xfd, 0xbd, 0xf7, 0xed, 0xef, 0x7b, 0xfb, 0xab, 0x06, 0x35, 0x38, 0x5c, 0x09, 0x42, 0x3f, 0xf6, + 0x59, 0x29, 0x38, 0xec, 0x69, 0x66, 0xe0, 0x08, 0xb0, 0x77, 0xf7, 0xd8, 0x89, 0x4f, 0x26, 0x87, + 0x2b, 0x96, 0x3f, 0x7e, 0x60, 0x1f, 0x87, 0x66, 0x70, 0x72, 0xdf, 0xf1, 0x1f, 0x1c, 0x9a, 0xf6, + 0x31, 0x0f, 0x1f, 0x9c, 0x7d, 0xfc, 0x20, 0x38, 0x7c, 0x90, 0x4c, 0xed, 0xdd, 0xcf, 0x8d, 0x3d, + 0xf6, 0x8f, 0xfd, 0x07, 0x84, 0x3e, 0x9c, 0x1c, 0x11, 0x44, 0x00, 0xb5, 0xc4, 0x70, 0xbd, 0x07, + 0x95, 0x1d, 0x27, 0x8a, 0x19, 0x83, 0xca, 0xc4, 0xb1, 0xa3, 0xae, 0xb2, 0x54, 0x5e, 0xae, 0x19, + 0xd4, 0xd6, 0x77, 0x41, 0x1b, 0x9a, 0xd1, 0xe9, 0x33, 0xd3, 0x9d, 0x70, 0xd6, 0x81, 0xf2, 0x99, + 0xe9, 0x76, 0x95, 0x25, 0x65, 0xb9, 0x69, 0x60, 0x93, 0xad, 0x80, 0x7a, 0x66, 0xba, 0xa3, 0xf8, + 0x22, 0xe0, 0xdd, 0xd2, 0x92, 0xb2, 0xdc, 0x5e, 0xbd, 0xb6, 0x12, 0x1c, 0xae, 0x1c, 0xf8, 0x51, + 0xec, 0x78, 0xc7, 0x2b, 0xcf, 0x4c, 0x77, 0x78, 0x11, 0x70, 0xa3, 0x7e, 0x26, 0x1a, 0xfa, 0x3e, + 0x34, 0x06, 0xa1, 0xb5, 0x35, 0xf1, 0xac, 0xd8, 0xf1, 0x3d, 0xdc, 0xd1, 0x33, 0xc7, 0x9c, 0x56, + 0xd4, 0x0c, 0x6a, 0x23, 0xce, 0x0c, 0x8f, 0xa3, 0x6e, 0x79, 0xa9, 0x8c, 0x38, 0x6c, 0xb3, 0x2e, + 0xd4, 0x9d, 0x68, 0xc3, 0x9f, 0x78, 0x71, 0xb7, 0xb2, 0xa4, 0x2c, 0xab, 0x46, 0x02, 0xea, 0x7f, + 0x55, 0x86, 0xea, 0xf7, 0x26, 0x3c, 0xbc, 0xa0, 0x79, 0x71, 0x1c, 0x26, 0x6b, 0x61, 0x9b, 0x5d, + 0x87, 0xaa, 0x6b, 0x7a, 0xc7, 0x51, 0xb7, 0x44, 0x8b, 0x09, 0x80, 0xdd, 0x04, 0xcd, 0x3c, 0x8a, + 0x79, 0x38, 0x9a, 0x38, 0x76, 0xb7, 0xbc, 0xa4, 0x2c, 0xd7, 0x0c, 0x95, 0x10, 0x4f, 0x1d, 0x9b, + 0xbd, 0x05, 0xaa, 0xed, 0x8f, 0xac, 0xfc, 0x5e, 0xb6, 0x4f, 0x7b, 0xb1, 0x77, 0x40, 0x9d, 0x38, + 0xf6, 0xc8, 0x75, 0xa2, 0xb8, 0x5b, 0x5d, 0x52, 0x96, 0x1b, 0xab, 0x2a, 0x5e, 0x16, 0x69, 0x67, + 0xd4, 0x27, 0x8e, 0x4d, 0x44, 0xbc, 0x0b, 0x6a, 0x14, 0x5a, 0xa3, 0xa3, 0x89, 0x67, 0x75, 0x6b, + 0x34, 0x68, 0x1e, 0x07, 0xe5, 0x6e, 0x6d, 0xd4, 0x23, 0x01, 0xe0, 0xb5, 0x42, 0x7e, 0xc6, 0xc3, + 0x88, 0x77, 0xeb, 0x62, 0x2b, 0x09, 0xb2, 0x87, 0xd0, 0x38, 0x32, 0x2d, 0x1e, 0x8f, 0x02, 0x33, + 0x34, 0xc7, 0x5d, 0x35, 0x5b, 0x68, 0x0b, 0xd1, 0x07, 0x88, 0x8d, 0x0c, 0x38, 0x4a, 0x01, 0xf6, + 0x11, 0xb4, 0x08, 0x8a, 0x46, 0x47, 0x8e, 0x1b, 0xf3, 0xb0, 0xab, 0xd1, 0x9c, 0x36, 0xcd, 0x21, + 0xcc, 0x30, 0xe4, 0xdc, 0x68, 0x8a, 0x41, 0x02, 0xc3, 0x6e, 0x01, 0xf0, 0xf3, 0xc0, 0xf4, 0xec, + 0x91, 0xe9, 0xba, 0x5d, 0xa0, 0x33, 0x68, 0x02, 0xb3, 0xe6, 0xba, 0xec, 0x4d, 0x3c, 0x9f, 0x69, + 0x8f, 0xe2, 0xa8, 0xdb, 0x5a, 0x52, 0x96, 0x2b, 0x46, 0x0d, 0xc1, 0x61, 0x84, 0x74, 0xb5, 0x4c, + 0xeb, 0x84, 0x77, 0xdb, 0x4b, 0xca, 0x72, 0xd5, 0x10, 0x00, 0x62, 0x8f, 0x9c, 0x30, 0x8a, 0xbb, + 0xf3, 0x02, 0x4b, 0x00, 0xbb, 0x01, 0x35, 0xff, 0xe8, 0x28, 0xe2, 0x71, 0xb7, 0x43, 0x68, 0x09, + 0xe9, 0xab, 0xa0, 0x11, 0x57, 0x11, 0xd5, 0xee, 0x40, 0xed, 0x0c, 0x01, 0xc1, 0x7c, 0x8d, 0xd5, + 0x16, 0x1e, 0x3b, 0x65, 0x3c, 0x43, 0x76, 0xea, 0xb7, 0x41, 0xdd, 0x31, 0xbd, 0xe3, 0x84, 0x5b, + 0xf1, 0x39, 0x69, 0x82, 0x66, 0x50, 0x5b, 0xff, 0xe3, 0x32, 0xd4, 0x0c, 0x1e, 0x4d, 0xdc, 0x98, + 0xbd, 0x0f, 0x80, 0x8f, 0x35, 0x36, 0xe3, 0xd0, 0x39, 0x97, 0xab, 0x66, 0xcf, 0xa5, 0x4d, 0x1c, + 0x7b, 0x97, 0xba, 0xd8, 0x43, 0x68, 0xd2, 0xea, 0xc9, 0xd0, 0x52, 0x76, 0x80, 0xf4, 0x7c, 0x46, + 0x83, 0x86, 0xc8, 0x19, 0x37, 0xa0, 0x46, 0xfc, 0x21, 0x78, 0xb4, 0x65, 0x48, 0x88, 0xdd, 0x81, + 0xb6, 0xe3, 0xc5, 0xf8, 0x7e, 0x56, 0x3c, 0xb2, 0x79, 0x94, 0x30, 0x50, 0x2b, 0xc5, 0x6e, 0xf2, + 0x28, 0x66, 0x8f, 0x40, 0x3c, 0x42, 0xb2, 0x61, 0x95, 0x36, 0x6c, 0xa7, 0x8f, 0x1b, 0x89, 0x1d, + 0x69, 0x8c, 0xdc, 0xf1, 0x3e, 0x34, 0xf0, 0x7e, 0xc9, 0x8c, 0x1a, 0xcd, 0x68, 0xd2, 0x6d, 0x24, + 0x39, 0x0c, 0xc0, 0x01, 0x72, 0x38, 0x92, 0x06, 0x99, 0x54, 0x30, 0x15, 0xb5, 0xd9, 0x1a, 0xb4, + 0xf8, 0x79, 0x1c, 0x9a, 0xa3, 0x31, 0x8f, 0x43, 0xc7, 0x8a, 0xba, 0x2a, 0x2d, 0xf2, 0x36, 0x2e, + 0x22, 0x48, 0xb6, 0xd2, 0xc7, 0xfe, 0x5d, 0xd1, 0xdd, 0xf7, 0xe2, 0xf0, 0xc2, 0x68, 0xf2, 0x1c, + 0xaa, 0xf7, 0xdb, 0xb0, 0x70, 0x69, 0x08, 0xea, 0x84, 0x53, 0x7e, 0x21, 0xa5, 0x0e, 0x9b, 0xc8, + 0x06, 0x44, 0x2d, 0x52, 0x08, 0x15, 0x43, 0x00, 0x9f, 0x96, 0xbe, 0xad, 0xe8, 0x7d, 0xa8, 0xee, + 0x87, 0x36, 0x0f, 0x67, 0xca, 0x2a, 0x83, 0x8a, 0xcd, 0x23, 0x8b, 0x66, 0xa9, 0x06, 0xb5, 0x33, + 0xf9, 0x2d, 0xe7, 0xe4, 0x57, 0xff, 0x13, 0x05, 0x1a, 0x03, 0x3f, 0x8c, 0x77, 0x79, 0x14, 0x99, + 0xc7, 0x9c, 0x2d, 0x42, 0xd5, 0xc7, 0x65, 0xe5, 0x2b, 0x6b, 0x78, 0x25, 0xda, 0xc7, 0x10, 0xf8, + 0x29, 0x5e, 0x28, 0x5d, 0xcd, 0x0b, 0xc8, 0xd7, 0x24, 0xf9, 0x65, 0xc9, 0xd7, 0x24, 0xf7, 0x19, + 0x07, 0x57, 0xf2, 0x1c, 0x7c, 0xa5, 0x78, 0xe8, 0xdf, 0x04, 0xc0, 0xf3, 0x7d, 0x45, 0x4e, 0xd4, + 0x7f, 0xae, 0x40, 0xc3, 0x30, 0x8f, 0xe2, 0x0d, 0xdf, 0x8b, 0xf9, 0x79, 0xcc, 0xda, 0x50, 0x72, + 0x6c, 0xa2, 0x51, 0xcd, 0x28, 0x39, 0x36, 0x9e, 0xee, 0x38, 0xf4, 0x27, 0x01, 0x91, 0xa8, 0x65, + 0x08, 0x80, 0x68, 0x69, 0xdb, 0x21, 0x1d, 0x19, 0x69, 0x69, 0xdb, 0x21, 0x5b, 0x84, 0x46, 0xe4, + 0x99, 0x41, 0x74, 0xe2, 0xc7, 0x78, 0xba, 0x0a, 0x9d, 0x0e, 0x12, 0xd4, 0x30, 0x42, 0xc1, 0x77, + 0xa2, 0x91, 0xcb, 0xcd, 0xd0, 0xe3, 0x21, 0x29, 0x33, 0xd5, 0xd0, 0x9c, 0x68, 0x47, 0x20, 0xf4, + 0x9f, 0x97, 0xa1, 0xb6, 0xcb, 0xc7, 0x87, 0x3c, 0xbc, 0x74, 0x88, 0x87, 0xa0, 0xd2, 0xbe, 0x23, + 0xc7, 0x16, 0xe7, 0x58, 0x7f, 0xe3, 0xc5, 0xf3, 0xc5, 0x05, 0xc2, 0x6d, 0xdb, 0x1f, 0xfa, 0x63, + 0x27, 0xe6, 0xe3, 0x20, 0xbe, 0x30, 0xea, 0x12, 0x35, 0xf3, 0x80, 0x37, 0xa0, 0xe6, 0x72, 0x13, + 0xdf, 0x4c, 0x88, 0x88, 0x84, 0xd8, 0x7d, 0xa8, 0x9b, 0xe3, 0x91, 0xcd, 0x4d, 0x5b, 0x1c, 0x6a, + 0xfd, 0xfa, 0x8b, 0xe7, 0x8b, 0x1d, 0x73, 0xbc, 0xc9, 0xcd, 0xfc, 0xda, 0x35, 0x81, 0x61, 0x9f, + 0xa0, 0x5c, 0x44, 0xf1, 0x68, 0x12, 0xd8, 0x66, 0xcc, 0x49, 0xdf, 0x56, 0xd6, 0xbb, 0x2f, 0x9e, + 0x2f, 0x5e, 0x47, 0xf4, 0x53, 0xc2, 0xe6, 0xa6, 0x41, 0x86, 0x45, 0xdd, 0x9b, 0x5c, 0x5f, 0xea, + 0x5e, 0x09, 0xb2, 0x6d, 0x58, 0xb0, 0xdc, 0x49, 0x84, 0x06, 0xc2, 0xf1, 0x8e, 0xfc, 0x91, 0xef, + 0xb9, 0x17, 0xf4, 0xc0, 0xea, 0xfa, 0xad, 0x17, 0xcf, 0x17, 0xdf, 0x92, 0x9d, 0xdb, 0xde, 0x91, + 0xbf, 0xef, 0xb9, 0x17, 0xb9, 0xf5, 0xe7, 0xa7, 0xba, 0xd8, 0xef, 0x40, 0xfb, 0xc8, 0x0f, 0x2d, + 0x3e, 0x4a, 0x49, 0xd6, 0xa6, 0x75, 0x7a, 0x2f, 0x9e, 0x2f, 0xde, 0xa0, 0x9e, 0xc7, 0x97, 0xe8, + 0xd6, 0xcc, 0xe3, 0xf5, 0x7f, 0x2d, 0x41, 0x95, 0xda, 0xec, 0x21, 0xd4, 0xc7, 0xf4, 0x24, 0x89, + 0x8e, 0xbc, 0x81, 0x3c, 0x44, 0x7d, 0x2b, 0xe2, 0xad, 0xa4, 0xd0, 0x26, 0xc3, 0x70, 0x46, 0x6c, + 0x1e, 0xba, 0x3c, 0x8e, 0x24, 0xcf, 0xe7, 0x66, 0x0c, 0x45, 0x87, 0x9c, 0x21, 0x87, 0x4d, 0xf3, + 0x4d, 0xf9, 0x12, 0xdf, 0xf4, 0x40, 0xb5, 0x4e, 0xb8, 0x75, 0x1a, 0x4d, 0xc6, 0x92, 0xab, 0x52, + 0x98, 0xbd, 0x03, 0x2d, 0x6a, 0x07, 0xbe, 0xe3, 0xd1, 0xf4, 0x2a, 0x0d, 0x68, 0x66, 0xc8, 0x61, + 0xd4, 0xdb, 0x82, 0x66, 0xfe, 0xb0, 0x79, 0xf5, 0x51, 0x11, 0xea, 0x63, 0x29, 0xaf, 0x3e, 0x1a, + 0xab, 0x80, 0x67, 0x16, 0x53, 0x72, 0xaa, 0x04, 0xd7, 0xc9, 0x5f, 0x61, 0x86, 0x1a, 0x9a, 0xb5, + 0x8e, 0x98, 0x92, 0x57, 0x49, 0x3e, 0xd4, 0x77, 0x1c, 0x8b, 0x7b, 0x11, 0x39, 0x1e, 0x93, 0x88, + 0xa7, 0x4a, 0x09, 0xdb, 0x78, 0xdf, 0xb1, 0x79, 0xbe, 0xe7, 0xdb, 0x3c, 0x92, 0xea, 0x2c, 0x85, + 0xb1, 0x8f, 0x9f, 0x07, 0x4e, 0x78, 0x31, 0x14, 0x94, 0x2a, 0x1b, 0x29, 0x8c, 0xdc, 0xc5, 0x3d, + 0xdc, 0xcc, 0x4e, 0x9c, 0x08, 0x09, 0xea, 0x7f, 0x5e, 0x81, 0xe6, 0x8f, 0x78, 0xe8, 0x1f, 0x84, + 0x7e, 0xe0, 0x47, 0xa6, 0xcb, 0xd6, 0x8a, 0x34, 0x17, 0x6f, 0xbb, 0x84, 0xa7, 0xcd, 0x0f, 0x5b, + 0x19, 0xa4, 0x8f, 0x20, 0xde, 0x2c, 0xff, 0x2a, 0x3a, 0xd4, 0xc4, 0x9b, 0xcf, 0xa0, 0x99, 0xec, + 0xc1, 0x31, 0xe2, 0x95, 0xe9, 0xac, 0x45, 0x7a, 0xc8, 0x1e, 0x94, 0xca, 0xb1, 0x79, 0xfe, 0x74, + 0x7b, 0x53, 0xbe, 0xad, 0x84, 0x24, 0x15, 0x86, 0xe7, 0xde, 0x30, 0x79, 0xd4, 0x14, 0xc6, 0x9b, + 0x22, 0x45, 0xa2, 0xed, 0xcd, 0x6e, 0x93, 0xba, 0x12, 0x90, 0xbd, 0x0d, 0xda, 0xd8, 0x3c, 0x47, + 0x85, 0xb6, 0x6d, 0x0b, 0xd1, 0x34, 0x32, 0x04, 0xfb, 0x06, 0x94, 0xe3, 0x73, 0x8f, 0x64, 0x0f, + 0x3d, 0x1b, 0x74, 0x74, 0x87, 0xe7, 0x9e, 0x54, 0x7d, 0x06, 0xf6, 0xe1, 0x9b, 0x5a, 0x8e, 0x4d, + 0x8e, 0x8c, 0x66, 0x60, 0x93, 0xdd, 0x81, 0xba, 0x2b, 0x5e, 0x8b, 0x9c, 0x95, 0xc6, 0x6a, 0x43, + 0xe8, 0x51, 0x42, 0x19, 0x49, 0x1f, 0xfb, 0x10, 0xd4, 0x84, 0x3a, 0xdd, 0x06, 0x8d, 0xeb, 0x24, + 0xf4, 0x4c, 0xc8, 0x68, 0xa4, 0x23, 0xd8, 0x43, 0xd0, 0x6c, 0xee, 0xf2, 0x98, 0x8f, 0x3c, 0xa1, + 0xc8, 0x1b, 0xc2, 0x89, 0xdd, 0x24, 0xe4, 0x5e, 0x64, 0xf0, 0x9f, 0x4c, 0x78, 0x14, 0x1b, 0xaa, + 0x2d, 0x11, 0xec, 0xdd, 0x4c, 0xb0, 0xda, 0xf4, 0x5c, 0x79, 0x62, 0x26, 0x5d, 0xbd, 0xef, 0xc2, + 0xfc, 0xd4, 0xa3, 0xe5, 0xb9, 0xb4, 0xf5, 0x0a, 0x63, 0xf9, 0x79, 0x45, 0x55, 0x3b, 0x9a, 0xfe, + 0x9f, 0x65, 0x98, 0x97, 0x02, 0x73, 0xe2, 0x04, 0x83, 0x58, 0xaa, 0x2e, 0x32, 0x4c, 0x92, 0x57, + 0x2b, 0x46, 0x02, 0xb2, 0xdf, 0x82, 0x1a, 0x69, 0x9a, 0x44, 0xe0, 0x17, 0x33, 0x46, 0x48, 0xa7, + 0x0b, 0x05, 0x20, 0xb9, 0x48, 0x0e, 0x67, 0x1f, 0x43, 0xf5, 0x0b, 0x1e, 0xfa, 0xc2, 0xd0, 0x36, + 0x56, 0x6f, 0xcf, 0x9a, 0x87, 0xe4, 0x93, 0xd3, 0xc4, 0xe0, 0xff, 0x2d, 0xbf, 0xc0, 0x57, 0xe1, + 0x97, 0x77, 0xd1, 0xd8, 0x8e, 0xfd, 0x33, 0x6e, 0x77, 0xeb, 0x19, 0xcd, 0x25, 0x93, 0x27, 0x5d, + 0x09, 0xcb, 0xa8, 0x33, 0x59, 0x46, 0xbb, 0x9a, 0x65, 0x7a, 0x9b, 0xd0, 0xc8, 0xd1, 0x65, 0xc6, + 0x43, 0x2d, 0x16, 0xd5, 0x89, 0x96, 0xaa, 0xd2, 0xbc, 0x56, 0xda, 0x04, 0xc8, 0xa8, 0xf4, 0x75, + 0x75, 0x9b, 0xfe, 0x33, 0x05, 0xe6, 0x37, 0x7c, 0xcf, 0xe3, 0x14, 0x2e, 0x88, 0x37, 0xcf, 0x44, + 0x5c, 0xb9, 0x52, 0xc4, 0x3f, 0x80, 0x6a, 0x84, 0x83, 0xe5, 0xea, 0xd7, 0x66, 0x3c, 0xa2, 0x21, + 0x46, 0xa0, 0xa2, 0x1f, 0x9b, 0xe7, 0xa3, 0x80, 0x7b, 0xb6, 0xe3, 0x1d, 0x27, 0x8a, 0x7e, 0x6c, + 0x9e, 0x1f, 0x08, 0x8c, 0xfe, 0xd7, 0x25, 0x80, 0xcf, 0xb8, 0xe9, 0xc6, 0x27, 0x68, 0xcc, 0xf0, + 0x45, 0x1d, 0x2f, 0x8a, 0x4d, 0xcf, 0x4a, 0x82, 0xb5, 0x14, 0xc6, 0x17, 0x45, 0x9b, 0xce, 0x23, + 0xa1, 0x22, 0x35, 0x23, 0x01, 0x91, 0x3f, 0x70, 0xbb, 0x49, 0x24, 0x6d, 0xbf, 0x84, 0x32, 0x47, + 0xa6, 0x42, 0x68, 0xe9, 0xc8, 0x74, 0xa1, 0x8e, 0xc1, 0x8f, 0xe3, 0x7b, 0xc4, 0x34, 0x9a, 0x91, + 0x80, 0xb8, 0xce, 0x24, 0x88, 0x9d, 0xb1, 0xb0, 0xf0, 0x65, 0x43, 0x42, 0x78, 0x2a, 0xb4, 0xe8, + 0x7d, 0xeb, 0xc4, 0x27, 0x45, 0x52, 0x36, 0x52, 0x18, 0x57, 0xf3, 0xbd, 0x63, 0x1f, 0x6f, 0xa7, + 0x92, 0xf3, 0x98, 0x80, 0xe2, 0x2e, 0x36, 0x3f, 0xc7, 0x2e, 0x8d, 0xba, 0x52, 0x18, 0xe9, 0xc2, + 0xf9, 0xe8, 0x88, 0x9b, 0xf1, 0x24, 0xe4, 0x51, 0x17, 0xa8, 0x1b, 0x38, 0xdf, 0x92, 0x18, 0xf6, + 0x0d, 0x68, 0x22, 0xe1, 0xcc, 0x28, 0x72, 0x8e, 0x3d, 0x6e, 0x93, 0x7a, 0xa9, 0x18, 0x48, 0xcc, + 0x35, 0x89, 0xd2, 0xff, 0xb6, 0x04, 0x35, 0xa1, 0x0b, 0x0a, 0xce, 0x92, 0xf2, 0x5a, 0xce, 0xd2, + 0xdb, 0xa0, 0x05, 0x21, 0xb7, 0x1d, 0x2b, 0x79, 0x47, 0xcd, 0xc8, 0x10, 0x14, 0x61, 0xa1, 0x77, + 0x40, 0xf4, 0x54, 0x0d, 0x01, 0x30, 0x1d, 0x5a, 0xbe, 0x37, 0xb2, 0x9d, 0xe8, 0x74, 0x74, 0x78, + 0x11, 0xf3, 0x48, 0xd2, 0xa2, 0xe1, 0x7b, 0x9b, 0x4e, 0x74, 0xba, 0x8e, 0x28, 0x24, 0xa1, 0x90, + 0x11, 0x92, 0x0d, 0xd5, 0x90, 0x10, 0xfb, 0x08, 0x34, 0xf2, 0x61, 0xc9, 0xc9, 0xd1, 0xc8, 0x39, + 0xb9, 0xf1, 0xe2, 0xf9, 0x22, 0x43, 0xe4, 0x94, 0x77, 0xa3, 0x26, 0x38, 0xf4, 0xd2, 0x70, 0x32, + 0x9a, 0x2b, 0x92, 0x61, 0xe1, 0xa5, 0x21, 0x6a, 0x18, 0xe5, 0xbd, 0x34, 0x81, 0x61, 0xf7, 0x81, + 0x4d, 0x3c, 0xcb, 0x1f, 0x07, 0xc8, 0x14, 0xdc, 0x96, 0x87, 0x6c, 0xd0, 0x21, 0x17, 0xf2, 0x3d, + 0x74, 0x54, 0xfd, 0x5f, 0x4a, 0xd0, 0xdc, 0x74, 0x42, 0x6e, 0xc5, 0xdc, 0xee, 0xdb, 0xc7, 0x1c, + 0xcf, 0xce, 0xbd, 0xd8, 0x89, 0x2f, 0xa4, 0x1b, 0x2a, 0xa1, 0x34, 0x8a, 0x28, 0x15, 0x23, 0x7e, + 0x21, 0x61, 0x65, 0x4a, 0x52, 0x08, 0x80, 0xad, 0x02, 0x88, 0x18, 0x8f, 0x12, 0x15, 0x95, 0xab, + 0x13, 0x15, 0x1a, 0x0d, 0xc3, 0x26, 0x7b, 0x8b, 0x52, 0x1b, 0x13, 0x8e, 0x6f, 0x57, 0xa5, 0x7d, + 0xeb, 0x04, 0x0b, 0x8f, 0x96, 0x42, 0xcf, 0xba, 0xd8, 0x18, 0xdb, 0xec, 0x1d, 0x28, 0xf9, 0x01, + 0x11, 0x57, 0x2e, 0x9d, 0xbf, 0xc2, 0xca, 0x7e, 0x60, 0x94, 0xfc, 0x00, 0xa5, 0x58, 0xc4, 0xdf, + 0xc4, 0x78, 0x28, 0xc5, 0x68, 0xf7, 0x28, 0xea, 0x33, 0x64, 0x0f, 0xd3, 0xa1, 0x69, 0xba, 0xae, + 0xff, 0x53, 0x6e, 0x1f, 0x84, 0xdc, 0x4e, 0x78, 0xb0, 0x80, 0x43, 0x2e, 0xf1, 0xcc, 0x31, 0x8f, + 0x02, 0xd3, 0xe2, 0x92, 0x05, 0x33, 0x84, 0x7e, 0x03, 0x4a, 0xfb, 0x01, 0xab, 0x43, 0x79, 0xd0, + 0x1f, 0x76, 0xe6, 0xb0, 0xb1, 0xd9, 0xdf, 0xe9, 0xa0, 0x45, 0xa9, 0x75, 0xea, 0xfa, 0x97, 0x25, + 0xd0, 0x76, 0x27, 0xb1, 0x89, 0xba, 0x25, 0xc2, 0x5b, 0x16, 0x39, 0x34, 0x63, 0xc5, 0xb7, 0x40, + 0x8d, 0x62, 0x33, 0x24, 0xaf, 0x44, 0x58, 0xa7, 0x3a, 0xc1, 0xc3, 0x88, 0xbd, 0x07, 0x55, 0x6e, + 0x1f, 0xf3, 0xc4, 0x5c, 0x74, 0xa6, 0xef, 0x6b, 0x88, 0x6e, 0xb6, 0x0c, 0xb5, 0xc8, 0x3a, 0xe1, + 0x63, 0xb3, 0x5b, 0xc9, 0x06, 0x0e, 0x08, 0x23, 0xdc, 0x70, 0x43, 0xf6, 0xb3, 0x77, 0xa1, 0x8a, + 0x6f, 0x13, 0xc9, 0xd8, 0x96, 0xa2, 0x61, 0x7c, 0x06, 0x39, 0x4c, 0x74, 0x22, 0xe3, 0xd9, 0xa1, + 0x1f, 0x8c, 0xfc, 0x80, 0x68, 0xdf, 0x5e, 0xbd, 0x4e, 0x3a, 0x2e, 0xb9, 0xcd, 0xca, 0x66, 0xe8, + 0x07, 0xfb, 0x81, 0x51, 0xb3, 0xe9, 0x17, 0xa3, 0x1c, 0x1a, 0x2e, 0x38, 0x42, 0x18, 0x05, 0x0d, + 0x31, 0x22, 0x9d, 0xb5, 0x0c, 0xea, 0x98, 0xc7, 0xa6, 0x6d, 0xc6, 0xa6, 0xb4, 0x0d, 0x4d, 0xa1, + 0x32, 0x05, 0xce, 0x48, 0x7b, 0xf5, 0x07, 0x50, 0x13, 0x4b, 0x33, 0x15, 0x2a, 0x7b, 0xfb, 0x7b, + 0x7d, 0x41, 0xd6, 0xb5, 0x9d, 0x9d, 0x8e, 0x82, 0xa8, 0xcd, 0xb5, 0xe1, 0x5a, 0xa7, 0x84, 0xad, + 0xe1, 0x0f, 0x0f, 0xfa, 0x9d, 0xb2, 0xfe, 0x8f, 0x0a, 0xa8, 0xc9, 0x3a, 0xec, 0x53, 0x00, 0x14, + 0xe1, 0xd1, 0x89, 0xe3, 0xa5, 0x0e, 0xde, 0xcd, 0xfc, 0x4e, 0x2b, 0xf8, 0xaa, 0x9f, 0x61, 0xaf, + 0x30, 0xaf, 0x24, 0xf1, 0x04, 0xf7, 0x06, 0xd0, 0x2e, 0x76, 0xce, 0xf0, 0x74, 0xef, 0xe5, 0xad, + 0x4a, 0x7b, 0xf5, 0x8d, 0xc2, 0xd2, 0x38, 0x93, 0x58, 0x3b, 0x67, 0x60, 0xee, 0x83, 0x9a, 0xa0, + 0x59, 0x03, 0xea, 0x9b, 0xfd, 0xad, 0xb5, 0xa7, 0x3b, 0xc8, 0x2a, 0x00, 0xb5, 0xc1, 0xf6, 0xde, + 0xe3, 0x9d, 0xbe, 0xb8, 0xd6, 0xce, 0xf6, 0x60, 0xd8, 0x29, 0xe9, 0x7f, 0xa8, 0x80, 0x9a, 0x78, + 0x32, 0xec, 0x03, 0x74, 0x3e, 0xc8, 0x49, 0x93, 0x96, 0x88, 0xb2, 0x52, 0xb9, 0xb0, 0xd5, 0x48, + 0xfa, 0x51, 0x16, 0x49, 0xb1, 0x26, 0xbe, 0x0d, 0x01, 0xf9, 0xa8, 0xb9, 0x5c, 0x48, 0x2a, 0x31, + 0xa8, 0xd8, 0xbe, 0xc7, 0xa5, 0xc3, 0x4c, 0x6d, 0xe2, 0x41, 0xc7, 0xb3, 0x78, 0x16, 0x4e, 0xd4, + 0x09, 0x1e, 0x46, 0x7a, 0x2c, 0xfc, 0xe8, 0xf4, 0x60, 0xe9, 0x6e, 0x4a, 0x7e, 0xb7, 0x4b, 0x41, + 0x49, 0xe9, 0x72, 0x50, 0x92, 0x19, 0xce, 0xea, 0xab, 0x0c, 0xa7, 0xfe, 0xb3, 0x1a, 0xb4, 0x0d, + 0x1e, 0xc5, 0x7e, 0xc8, 0xa5, 0x5f, 0xf8, 0x32, 0x11, 0xba, 0x05, 0x10, 0x8a, 0xc1, 0xd9, 0xd6, + 0x9a, 0xc4, 0x88, 0x68, 0xca, 0xf5, 0x2d, 0xe2, 0x5d, 0x69, 0x21, 0x53, 0x98, 0xdd, 0x04, 0xed, + 0xd0, 0xb4, 0x4e, 0xc5, 0xb2, 0xc2, 0x4e, 0xaa, 0x02, 0x21, 0xd6, 0x35, 0x2d, 0x8b, 0x47, 0xd1, + 0x08, 0x59, 0x41, 0x58, 0x4b, 0x4d, 0x60, 0x9e, 0xf0, 0x0b, 0xf6, 0x10, 0x20, 0xe2, 0x56, 0xc8, + 0x63, 0xea, 0x46, 0x9b, 0xa9, 0xad, 0x2f, 0xfc, 0xf2, 0xf9, 0xe2, 0xdc, 0x3f, 0x3f, 0x5f, 0xd4, + 0x06, 0xdc, 0x8b, 0x9c, 0xd8, 0x39, 0xe3, 0x86, 0x26, 0x06, 0xe1, 0x8c, 0x6f, 0x41, 0x2b, 0xe2, + 0x11, 0x1a, 0xdb, 0x51, 0xec, 0x9f, 0x72, 0xe1, 0x97, 0xcf, 0x9c, 0xd4, 0x94, 0xe3, 0x86, 0x38, + 0x0c, 0x15, 0x91, 0xe9, 0xf9, 0xde, 0xc5, 0xd8, 0x9f, 0x44, 0xd2, 0xb2, 0x64, 0x08, 0xb6, 0x02, + 0xd7, 0xb8, 0x67, 0x85, 0x17, 0x01, 0xde, 0x08, 0xcf, 0x32, 0x3a, 0x72, 0x5c, 0x2e, 0x1d, 0xfa, + 0x85, 0xac, 0xeb, 0x09, 0xbf, 0xd8, 0x72, 0x5c, 0x8e, 0xd7, 0x3a, 0x33, 0x27, 0x6e, 0x3c, 0xa2, + 0x7c, 0x01, 0x88, 0x6b, 0x11, 0x66, 0xcd, 0xb6, 0x43, 0x76, 0x17, 0x16, 0x44, 0x77, 0xe8, 0xbb, + 0xdc, 0xb1, 0xc5, 0x62, 0x0d, 0x1a, 0x35, 0x4f, 0x1d, 0x06, 0xe1, 0x69, 0xa9, 0x15, 0xb8, 0x26, + 0xc6, 0x8a, 0x3b, 0x26, 0xa3, 0x9b, 0x62, 0x6b, 0xea, 0x1a, 0xc8, 0x9e, 0xe2, 0xd6, 0x81, 0x19, + 0x9f, 0x50, 0x14, 0x90, 0x6c, 0x7d, 0x60, 0xc6, 0x27, 0xe8, 0x17, 0x88, 0xee, 0x23, 0x87, 0xbb, + 0x22, 0x8a, 0xd7, 0x0c, 0x31, 0x63, 0x0b, 0x31, 0xe8, 0x17, 0xc8, 0x01, 0x7e, 0x38, 0x36, 0x45, + 0x0a, 0x54, 0x33, 0xc4, 0xa4, 0x2d, 0x42, 0xe1, 0x16, 0xf2, 0x45, 0xbd, 0xc9, 0x98, 0x92, 0xa1, + 0x15, 0x43, 0xbe, 0xf1, 0xde, 0x64, 0xcc, 0x3e, 0x80, 0x8e, 0xe3, 0x59, 0x21, 0x1f, 0x73, 0x2f, + 0x36, 0xdd, 0xd1, 0x51, 0xe8, 0x8f, 0xbb, 0x0b, 0x34, 0x68, 0x3e, 0x87, 0xdf, 0x0a, 0xfd, 0xb1, + 0xcc, 0xde, 0x04, 0x66, 0x18, 0x3b, 0xa6, 0xdb, 0x65, 0x49, 0xf6, 0xe6, 0x40, 0x20, 0xd8, 0xbb, + 0xd0, 0xc2, 0xd9, 0x7b, 0xa9, 0x85, 0xb8, 0x46, 0xcb, 0x14, 0x91, 0xec, 0xdb, 0xf0, 0xa6, 0x13, + 0xa5, 0xe0, 0xda, 0x4f, 0x4d, 0xe4, 0x68, 0xe2, 0xcc, 0xee, 0x75, 0x5a, 0xf1, 0xaa, 0x6e, 0xfd, + 0x45, 0x19, 0xd4, 0x34, 0x7c, 0xbd, 0x07, 0xda, 0x38, 0xd1, 0xbf, 0xd2, 0xf1, 0x6c, 0x15, 0x94, + 0xb2, 0x91, 0xf5, 0xb3, 0x5b, 0x50, 0x3a, 0x3d, 0x93, 0xb6, 0xa0, 0xb5, 0x22, 0x8a, 0x17, 0xc1, + 0xe1, 0xc7, 0x2b, 0x4f, 0x9e, 0x19, 0xa5, 0xd3, 0xb3, 0xaf, 0x20, 0x87, 0xec, 0x7d, 0x98, 0xb7, + 0x5c, 0x6e, 0x7a, 0xa3, 0xcc, 0x5b, 0x22, 0x3e, 0x37, 0xda, 0x84, 0x3e, 0x48, 0x5d, 0xa6, 0x3b, + 0x50, 0xb5, 0xb9, 0x1b, 0x9b, 0xf9, 0x1c, 0xfa, 0x7e, 0x68, 0x5a, 0x2e, 0xdf, 0x44, 0xb4, 0x21, + 0x7a, 0xd1, 0x16, 0xa4, 0x21, 0x63, 0xce, 0x16, 0xcc, 0x08, 0x17, 0x53, 0x3d, 0x03, 0x79, 0x3d, + 0x73, 0x0f, 0x16, 0xf8, 0x79, 0x40, 0x06, 0x70, 0x94, 0x66, 0x48, 0x84, 0x65, 0xee, 0x24, 0x1d, + 0x1b, 0x49, 0xa6, 0xe4, 0x43, 0x54, 0x81, 0x82, 0xd4, 0x4d, 0xda, 0x8b, 0xc9, 0x2c, 0x6c, 0x4e, + 0xad, 0x18, 0xc9, 0x10, 0xf6, 0x01, 0x68, 0x96, 0x6d, 0x8d, 0x04, 0x65, 0x5a, 0xd9, 0xd9, 0x36, + 0x36, 0x37, 0x04, 0x49, 0x54, 0xcb, 0xb6, 0x44, 0x94, 0x50, 0x08, 0x65, 0xdb, 0xaf, 0x13, 0xca, + 0xe6, 0x8d, 0x7c, 0xa7, 0x60, 0xe4, 0x3f, 0xaf, 0xa8, 0xf5, 0x8e, 0xaa, 0xbf, 0x03, 0x6a, 0xb2, + 0x11, 0xaa, 0xee, 0x88, 0x7b, 0x32, 0x4d, 0x41, 0xaa, 0x1b, 0xc1, 0x61, 0xa4, 0x5b, 0x50, 0x7e, + 0xf2, 0x6c, 0x40, 0x1a, 0x1c, 0x8d, 0x69, 0x95, 0x7c, 0x2f, 0x6a, 0xa7, 0x5a, 0xbd, 0x94, 0xd3, + 0xea, 0xb7, 0x85, 0x41, 0xa4, 0x07, 0x4a, 0x72, 0xbb, 0x39, 0x0c, 0x92, 0x58, 0x38, 0x03, 0x15, + 0x91, 0xf6, 0x25, 0x40, 0xff, 0xaf, 0x32, 0xd4, 0xa5, 0xbf, 0x86, 0x46, 0x70, 0x92, 0xa6, 0x25, + 0xb1, 0x59, 0x0c, 0xa4, 0x53, 0xc7, 0x2f, 0x5f, 0x9f, 0x2a, 0xbf, 0xba, 0x3e, 0xc5, 0x3e, 0x85, + 0x66, 0x20, 0xfa, 0xf2, 0xae, 0xe2, 0x9b, 0xf9, 0x39, 0xf2, 0x97, 0xe6, 0x35, 0x82, 0x0c, 0x40, + 0x52, 0x52, 0x92, 0x3e, 0x36, 0x8f, 0x25, 0x05, 0xea, 0x08, 0x0f, 0xcd, 0xe3, 0xd7, 0xf2, 0xfb, + 0xda, 0xe4, 0x40, 0x36, 0xc9, 0x80, 0xa0, 0xaf, 0x98, 0x7f, 0x99, 0x56, 0xd1, 0xfd, 0xba, 0x09, + 0x9a, 0xe5, 0x8f, 0xc7, 0x0e, 0xf5, 0xb5, 0x65, 0x1a, 0x8e, 0x10, 0xc3, 0x48, 0xff, 0x03, 0x05, + 0xea, 0xf2, 0x5e, 0x97, 0x8c, 0xfb, 0xfa, 0xf6, 0xde, 0x9a, 0xf1, 0xc3, 0x8e, 0x82, 0xce, 0xcb, + 0xf6, 0xde, 0xb0, 0x53, 0x62, 0x1a, 0x54, 0xb7, 0x76, 0xf6, 0xd7, 0x86, 0x9d, 0x32, 0x1a, 0xfc, + 0xf5, 0xfd, 0xfd, 0x9d, 0x4e, 0x85, 0x35, 0x41, 0xdd, 0x5c, 0x1b, 0xf6, 0x87, 0xdb, 0xbb, 0xfd, + 0x4e, 0x15, 0xc7, 0x3e, 0xee, 0xef, 0x77, 0x6a, 0xd8, 0x78, 0xba, 0xbd, 0xd9, 0xa9, 0x63, 0xff, + 0xc1, 0xda, 0x60, 0xf0, 0xfd, 0x7d, 0x63, 0xb3, 0xa3, 0x92, 0xd3, 0x30, 0x34, 0xb6, 0xf7, 0x1e, + 0x77, 0x34, 0x6c, 0xef, 0xaf, 0x7f, 0xde, 0xdf, 0x18, 0x76, 0x00, 0xdb, 0xcf, 0xc4, 0xda, 0x4d, + 0xfd, 0x11, 0x34, 0x72, 0x74, 0xc3, 0x95, 0x8c, 0xfe, 0x56, 0x67, 0x0e, 0xb7, 0x7f, 0xb6, 0xb6, + 0xf3, 0x14, 0xfd, 0x8d, 0x36, 0x00, 0x35, 0x47, 0x3b, 0x6b, 0x7b, 0x8f, 0x3b, 0x25, 0xe9, 0xad, + 0x7e, 0x0f, 0xd4, 0xa7, 0x8e, 0xbd, 0xee, 0xfa, 0xd6, 0x29, 0xb2, 0xd2, 0xa1, 0x19, 0x71, 0xc9, + 0x7b, 0xd4, 0xc6, 0xd8, 0x80, 0x04, 0x38, 0x92, 0xef, 0x2e, 0x21, 0xa4, 0x9e, 0x37, 0x19, 0x8f, + 0xa8, 0x9e, 0x59, 0x16, 0x46, 0xd9, 0x9b, 0x8c, 0x9f, 0x3a, 0x76, 0xa4, 0x9f, 0x42, 0xfd, 0xa9, + 0x63, 0x1f, 0x98, 0xd6, 0x29, 0xa9, 0x64, 0x5c, 0x7a, 0x14, 0x39, 0x5f, 0x70, 0x69, 0xbc, 0x35, + 0xc2, 0x0c, 0x9c, 0x2f, 0x38, 0x7b, 0x17, 0x6a, 0x04, 0x24, 0xe9, 0x14, 0x12, 0xbb, 0xe4, 0x38, + 0x86, 0xec, 0xa3, 0x72, 0xa2, 0xeb, 0xfa, 0xd6, 0x28, 0xe4, 0x47, 0xdd, 0x37, 0xc5, 0x6b, 0x10, + 0xc2, 0xe0, 0x47, 0xfa, 0xef, 0x2b, 0xe9, 0xcd, 0xa9, 0x6a, 0xb5, 0x08, 0x95, 0xc0, 0xb4, 0x4e, + 0xa5, 0xef, 0xd4, 0x90, 0x0b, 0xe2, 0x61, 0x0c, 0xea, 0x60, 0xef, 0x83, 0x2a, 0x99, 0x2a, 0xd9, + 0xb5, 0x91, 0xe3, 0x3e, 0x23, 0xed, 0x2c, 0x32, 0x41, 0xb9, 0xc8, 0x04, 0x14, 0x79, 0x07, 0xae, + 0x13, 0x0b, 0x11, 0x42, 0x41, 0x25, 0x48, 0xff, 0x18, 0x20, 0x2b, 0x20, 0xce, 0xae, 0xdd, 0x98, + 0xae, 0x63, 0x26, 0x91, 0xbc, 0x00, 0xf4, 0x3d, 0x68, 0xe4, 0xca, 0x8e, 0x48, 0x5b, 0xd3, 0x75, + 0xd1, 0x9e, 0x0b, 0x3d, 0xa0, 0x1a, 0x75, 0xd3, 0x75, 0x9f, 0xf0, 0x8b, 0x08, 0xdd, 0x78, 0x51, + 0xb1, 0x2c, 0x4d, 0x15, 0xb5, 0x68, 0xaa, 0x21, 0x3a, 0xf5, 0x0f, 0xa1, 0xb6, 0x95, 0x04, 0x3b, + 0x89, 0x60, 0x28, 0x57, 0x09, 0x86, 0xfe, 0x89, 0x3c, 0x33, 0xd5, 0xc5, 0xd8, 0x3d, 0x59, 0x19, + 0x8d, 0x44, 0x1d, 0x56, 0xc9, 0x72, 0x41, 0x62, 0x90, 0x2c, 0x8a, 0xd2, 0x60, 0x7d, 0x13, 0xd4, + 0x97, 0xd6, 0x9a, 0x25, 0x01, 0x4a, 0x19, 0x01, 0x66, 0x54, 0x9f, 0xf5, 0x1f, 0x03, 0x64, 0x15, + 0x54, 0x29, 0xa7, 0x62, 0x15, 0x94, 0xd3, 0xbb, 0xa0, 0x5a, 0x27, 0x8e, 0x6b, 0x87, 0xdc, 0x2b, + 0xdc, 0x3a, 0xab, 0xb9, 0xa6, 0xfd, 0x6c, 0x09, 0x2a, 0x54, 0x18, 0x2e, 0x67, 0x5a, 0x3c, 0xad, + 0x0a, 0x53, 0x8f, 0x7e, 0x0e, 0x2d, 0x11, 0x1f, 0xbd, 0x86, 0x77, 0x59, 0x54, 0xa3, 0xa5, 0x4b, + 0x6a, 0xf4, 0x06, 0xd4, 0xc8, 0x5d, 0x49, 0x6e, 0x23, 0xa1, 0x2b, 0xd4, 0xeb, 0x1f, 0x95, 0x00, + 0xc4, 0xd6, 0x7b, 0xbe, 0xcd, 0x8b, 0x89, 0x08, 0x65, 0x3a, 0x11, 0xc1, 0xa0, 0x92, 0xd6, 0xfc, + 0x35, 0x83, 0xda, 0x99, 0x61, 0x94, 0xc9, 0x09, 0x61, 0x18, 0xdf, 0x06, 0x8d, 0x3c, 0x4a, 0xe7, + 0x0b, 0x2a, 0xf6, 0xe0, 0x86, 0x19, 0x22, 0x5f, 0x01, 0xaf, 0x16, 0x2b, 0xe0, 0x69, 0x29, 0xae, + 0x26, 0x56, 0x13, 0xa5, 0xb8, 0x59, 0x95, 0x4d, 0xca, 0x0e, 0x45, 0x3c, 0x8c, 0x93, 0xd4, 0x86, + 0x80, 0xd2, 0x28, 0x5d, 0x93, 0x63, 0x4d, 0x91, 0xdf, 0xf1, 0xfc, 0x91, 0xe5, 0x7b, 0x47, 0xae, + 0x63, 0xc5, 0xb2, 0xe2, 0x0d, 0x9e, 0xbf, 0x21, 0x31, 0xb4, 0x98, 0xe7, 0xfc, 0x64, 0x22, 0x1c, + 0x4b, 0x5c, 0x8c, 0x20, 0xfd, 0x53, 0x68, 0x26, 0xef, 0x42, 0x45, 0xbd, 0xbb, 0x69, 0x64, 0xab, + 0x64, 0x6f, 0x9e, 0x91, 0x6f, 0xbd, 0xd4, 0x55, 0x92, 0xd8, 0x56, 0xff, 0xbb, 0x4a, 0x32, 0x59, + 0xd6, 0x9e, 0x5e, 0x4e, 0xdb, 0x62, 0xb2, 0xa2, 0xf4, 0x5a, 0xc9, 0x8a, 0x6f, 0x83, 0x66, 0x53, + 0xfc, 0xed, 0x9c, 0x25, 0x86, 0xae, 0x37, 0x1d, 0x6b, 0xcb, 0x08, 0x9d, 0x3c, 0xff, 0x74, 0xf0, + 0x2b, 0xde, 0x27, 0x7d, 0x85, 0xea, 0xac, 0x57, 0xa8, 0x7d, 0xcd, 0x57, 0xc8, 0x88, 0xdc, 0xce, + 0x13, 0x19, 0x9d, 0x68, 0xcf, 0xf7, 0x46, 0xde, 0xc4, 0x75, 0xcd, 0x43, 0x97, 0xcb, 0xe7, 0x69, + 0x78, 0xbe, 0xb7, 0x27, 0x51, 0x18, 0x03, 0xe4, 0x87, 0x08, 0x25, 0x20, 0x9e, 0x6a, 0x3e, 0x37, + 0x8e, 0x54, 0xc5, 0x32, 0x74, 0xfc, 0xc3, 0x1f, 0x73, 0x2b, 0x26, 0x4a, 0x8e, 0x48, 0xfa, 0x45, + 0x00, 0xd0, 0x16, 0x78, 0x24, 0x1d, 0xba, 0xb8, 0xd3, 0x6c, 0xd1, 0xba, 0xc4, 0x16, 0x8f, 0xa0, + 0x79, 0xc6, 0xad, 0xd8, 0x0f, 0x47, 0x51, 0xc0, 0xad, 0xa8, 0x3b, 0x9f, 0x3d, 0xfa, 0x33, 0xc2, + 0x0f, 0x02, 0x6e, 0x19, 0x8d, 0xb3, 0xb4, 0x8d, 0x6a, 0x4b, 0x4b, 0x09, 0x9e, 0x4b, 0x1b, 0x68, + 0x50, 0xdd, 0xde, 0xdb, 0xec, 0xff, 0xa0, 0xa3, 0xa0, 0x75, 0x36, 0xfa, 0xcf, 0xfa, 0xc6, 0xa0, + 0xdf, 0x29, 0xa1, 0xb5, 0xdc, 0xec, 0xef, 0xf4, 0x87, 0xfd, 0x4e, 0x59, 0x78, 0x5e, 0x54, 0x4d, + 0x72, 0x1d, 0xcb, 0x89, 0xf5, 0xcf, 0x01, 0xb2, 0x5d, 0x66, 0x2a, 0xb2, 0x65, 0xa8, 0xfb, 0x41, + 0xe2, 0x83, 0xa7, 0x47, 0xdb, 0x27, 0xd4, 0x81, 0xe9, 0x84, 0x46, 0xd2, 0x8d, 0x16, 0x20, 0x43, + 0xbf, 0xaa, 0x7a, 0xaf, 0x49, 0x3f, 0x4a, 0x1f, 0x00, 0x64, 0xd9, 0x18, 0x34, 0x3d, 0x19, 0x45, + 0x65, 0x3a, 0x38, 0x4e, 0x68, 0xb9, 0x9c, 0x6a, 0x9d, 0xd2, 0x55, 0x39, 0x1f, 0xd1, 0xaf, 0xaf, + 0x82, 0xb6, 0x6b, 0x06, 0x9f, 0x89, 0xca, 0xef, 0x1d, 0x68, 0x53, 0x40, 0x93, 0x84, 0x8a, 0xc2, + 0x22, 0x34, 0x8d, 0x56, 0x8a, 0x45, 0x03, 0xa3, 0xff, 0x85, 0x02, 0xd7, 0x77, 0xfd, 0x33, 0x9e, + 0x3a, 0xf8, 0x07, 0xe6, 0x85, 0xeb, 0x9b, 0xf6, 0x2b, 0x64, 0xea, 0x16, 0x40, 0xe4, 0x4f, 0xa8, + 0x12, 0x9b, 0xd4, 0xad, 0x0d, 0x4d, 0x60, 0x1e, 0xcb, 0x8f, 0x7e, 0x78, 0x14, 0x53, 0xa7, 0xf4, + 0x16, 0x10, 0xc6, 0xae, 0x37, 0xa0, 0x16, 0x9f, 0x7b, 0x59, 0x15, 0xbd, 0x1a, 0x53, 0x19, 0x63, + 0xa6, 0xbf, 0x5f, 0x9d, 0xed, 0xef, 0xeb, 0x1b, 0xa0, 0x0d, 0xcf, 0x29, 0x91, 0x3f, 0x29, 0x7a, + 0xdc, 0xca, 0x4b, 0xfc, 0xba, 0xd2, 0x94, 0x5f, 0xf7, 0xef, 0x0a, 0x34, 0x72, 0x81, 0x0b, 0xfb, + 0x06, 0x54, 0xe2, 0x73, 0xaf, 0xf8, 0xc1, 0x4c, 0xb2, 0x89, 0x41, 0x5d, 0x97, 0x92, 0xd5, 0xa5, + 0x4b, 0xc9, 0x6a, 0xb6, 0x03, 0xf3, 0xc2, 0xbc, 0x24, 0x97, 0x48, 0x72, 0x7a, 0xef, 0x4c, 0x05, + 0x4a, 0xa2, 0xd8, 0x91, 0x5c, 0x49, 0x26, 0xaa, 0xda, 0xc7, 0x05, 0x64, 0x6f, 0x0d, 0xae, 0xcd, + 0x18, 0xf6, 0x55, 0xca, 0x5e, 0xfa, 0x22, 0xb4, 0x86, 0xe7, 0xde, 0xd0, 0x19, 0xf3, 0x28, 0x36, + 0xc7, 0x01, 0xf9, 0xc5, 0xd2, 0x3d, 0xa8, 0x18, 0xa5, 0x38, 0xd2, 0xdf, 0x83, 0xe6, 0x01, 0xe7, + 0xa1, 0xc1, 0xa3, 0xc0, 0xf7, 0x84, 0x07, 0x28, 0x8b, 0x0c, 0xc2, 0x17, 0x91, 0x90, 0xfe, 0x7b, + 0xa0, 0x19, 0xe6, 0x51, 0xbc, 0x6e, 0xc6, 0xd6, 0xc9, 0x57, 0xc9, 0x5a, 0xbd, 0x07, 0xf5, 0x40, + 0xf0, 0x94, 0x0c, 0x67, 0x9b, 0xe4, 0x93, 0x48, 0x3e, 0x33, 0x92, 0x4e, 0xfd, 0x5b, 0xd0, 0x96, + 0x15, 0xbf, 0xe4, 0x24, 0xb9, 0xb2, 0xa0, 0x72, 0x65, 0x59, 0x50, 0x3f, 0x86, 0x56, 0x32, 0x4f, + 0x58, 0xf8, 0xd7, 0x9a, 0xf6, 0xd5, 0xbf, 0xbb, 0xd0, 0x7f, 0x17, 0xae, 0x0d, 0x26, 0x87, 0x91, + 0x15, 0x3a, 0x24, 0xef, 0xc9, 0x76, 0x3d, 0x50, 0x83, 0x90, 0x1f, 0x39, 0xe7, 0x3c, 0x11, 0xb1, + 0x14, 0x66, 0x77, 0xa1, 0x3e, 0x46, 0x7a, 0xf1, 0x4c, 0x78, 0xb3, 0x20, 0x7d, 0x17, 0x7b, 0x8c, + 0x64, 0x80, 0xfe, 0x1d, 0xb8, 0x5e, 0x5c, 0x5e, 0x52, 0xe1, 0x1d, 0x28, 0x9f, 0x9e, 0x45, 0x92, + 0xcc, 0x0b, 0x85, 0x20, 0x9f, 0x3e, 0x78, 0xc1, 0x5e, 0xfd, 0x6f, 0x14, 0x28, 0xef, 0x4d, 0xc6, + 0xf9, 0x2f, 0x0a, 0x2b, 0xe2, 0x8b, 0xc2, 0x9b, 0xf9, 0x82, 0x84, 0x08, 0x1a, 0xb3, 0xc2, 0xc3, + 0xdb, 0xa0, 0x1d, 0xf9, 0xe1, 0x4f, 0xcd, 0xd0, 0xe6, 0xb6, 0x74, 0x33, 0x32, 0x04, 0xc5, 0x07, + 0x93, 0x71, 0x20, 0x6d, 0x15, 0xb5, 0xd9, 0x1d, 0xe9, 0xa8, 0x88, 0x40, 0x6e, 0x01, 0x29, 0xbb, + 0x37, 0x19, 0xaf, 0xb8, 0xdc, 0x8c, 0xc8, 0x72, 0x0a, 0xdf, 0x45, 0xbf, 0x07, 0x5a, 0x8a, 0x42, + 0x15, 0xbd, 0x37, 0x18, 0x6d, 0x6f, 0x8a, 0x24, 0x2f, 0x86, 0x3c, 0x0a, 0xaa, 0xe7, 0xe1, 0x0f, + 0xf6, 0x46, 0xc3, 0x41, 0xa7, 0xa4, 0xff, 0x08, 0x1a, 0x89, 0xfc, 0x6c, 0xdb, 0x54, 0xd1, 0x24, + 0x01, 0xde, 0xb6, 0x0b, 0xf2, 0xbc, 0x4d, 0x31, 0x29, 0xf7, 0xec, 0xed, 0x44, 0xf0, 0x04, 0x50, + 0xbc, 0xa1, 0x2c, 0x8f, 0x26, 0x37, 0xd4, 0xfb, 0xb0, 0x60, 0x50, 0x65, 0x06, 0xbd, 0x88, 0xe4, + 0xc9, 0x6e, 0x40, 0xcd, 0xf3, 0x6d, 0x9e, 0x6e, 0x20, 0x21, 0xdc, 0x59, 0x3e, 0xb6, 0x54, 0x69, + 0xe9, 0xdb, 0x73, 0x58, 0x40, 0x2d, 0x59, 0x64, 0xb4, 0x42, 0xd5, 0x40, 0x99, 0xaa, 0x1a, 0xe0, + 0x26, 0xf2, 0x03, 0x01, 0xa1, 0xf9, 0x93, 0x8f, 0x02, 0x7a, 0xa0, 0xda, 0x51, 0x4c, 0x62, 0x2d, + 0x75, 0x63, 0x0a, 0xeb, 0x0f, 0xe0, 0xda, 0x5a, 0x10, 0xb8, 0x17, 0x49, 0x39, 0x55, 0x6e, 0xd4, + 0xcd, 0x6a, 0xae, 0x8a, 0x0c, 0x84, 0x05, 0xa8, 0x6f, 0x41, 0x33, 0x49, 0xa9, 0xec, 0xf2, 0xd8, + 0x24, 0x8d, 0xe7, 0x3a, 0x85, 0x9c, 0x82, 0x2a, 0x10, 0xc3, 0x62, 0x6d, 0x62, 0xea, 0x7e, 0x2b, + 0x50, 0x93, 0xea, 0x94, 0x41, 0xc5, 0xf2, 0x6d, 0xb1, 0x51, 0xd5, 0xa0, 0x36, 0x72, 0xd5, 0x38, + 0x3a, 0x4e, 0xdc, 0xfa, 0x71, 0x74, 0xac, 0xff, 0x77, 0x09, 0x5a, 0xeb, 0x94, 0x6a, 0x4b, 0xce, + 0x98, 0x4b, 0x43, 0x2b, 0x85, 0x34, 0x74, 0x3e, 0xe5, 0x5c, 0x2a, 0xa4, 0x9c, 0x0b, 0x07, 0x2a, + 0x17, 0x7d, 0xf1, 0x37, 0xa1, 0x3e, 0xf1, 0x9c, 0xf3, 0xc4, 0x4e, 0x68, 0xe4, 0xd3, 0x9c, 0x0f, + 0x23, 0xb6, 0x04, 0x0d, 0x34, 0x25, 0x8e, 0x27, 0xd2, 0xbc, 0x22, 0x57, 0x9b, 0x47, 0x4d, 0x25, + 0x73, 0x6b, 0x2f, 0x4f, 0xe6, 0xd6, 0xbf, 0x4e, 0x32, 0x57, 0xfd, 0x1a, 0xc9, 0x5c, 0x6d, 0x3a, + 0x99, 0x5b, 0x8c, 0x36, 0xe0, 0x52, 0xb4, 0x71, 0x0b, 0x40, 0x7c, 0xeb, 0x74, 0x34, 0x71, 0x5d, + 0xe9, 0x92, 0x69, 0x84, 0xd9, 0x9a, 0xb8, 0xae, 0xbe, 0x03, 0xed, 0xe4, 0x01, 0xa4, 0xa2, 0xf8, + 0x14, 0xe6, 0x65, 0x31, 0x87, 0x87, 0x32, 0x7f, 0x28, 0xf4, 0x1f, 0x49, 0xa9, 0xa8, 0xb7, 0xc8, + 0x1e, 0xa3, 0x6d, 0xe7, 0xc1, 0x48, 0xff, 0x85, 0x02, 0xad, 0xc2, 0x08, 0xf6, 0x28, 0x2b, 0x0d, + 0x29, 0x24, 0xeb, 0xdd, 0x4b, 0xab, 0xbc, 0xbc, 0x3c, 0x54, 0x9a, 0x2a, 0x0f, 0xe9, 0xf7, 0xd3, + 0xa2, 0x8f, 0x2c, 0xf5, 0xcc, 0xa5, 0xa5, 0x1e, 0xaa, 0x8e, 0xac, 0x0d, 0x87, 0x46, 0xa7, 0xc4, + 0x6a, 0x50, 0xda, 0x1b, 0x74, 0xca, 0xfa, 0xaf, 0x4b, 0xd0, 0xea, 0x9f, 0x07, 0xf4, 0xdd, 0xdf, + 0x2b, 0x43, 0xb7, 0x1c, 0xf7, 0x95, 0x0a, 0xdc, 0x97, 0xe3, 0xa3, 0xb2, 0xac, 0x75, 0x0b, 0x3e, + 0xc2, 0x60, 0x4e, 0xa4, 0x96, 0x25, 0x7f, 0x09, 0xe8, 0xff, 0x0f, 0x7f, 0x15, 0xb4, 0x13, 0x4c, + 0xd7, 0x34, 0x77, 0xa0, 0x9d, 0x10, 0x57, 0xb2, 0xcf, 0x6b, 0x09, 0xbe, 0xf8, 0x26, 0xd9, 0x4d, + 0xb3, 0x8c, 0x02, 0xd0, 0xff, 0xac, 0x04, 0x9a, 0xe0, 0x46, 0xbc, 0xcf, 0x07, 0xd2, 0x46, 0x28, + 0x59, 0xf9, 0x2c, 0xed, 0x5c, 0x79, 0xc2, 0x2f, 0x32, 0x3b, 0x31, 0xb3, 0xe4, 0x2c, 0x73, 0x91, + 0x22, 0x05, 0x43, 0xb9, 0xc8, 0x9b, 0xa0, 0x09, 0x17, 0x6f, 0x22, 0x6b, 0x37, 0x15, 0x43, 0xf8, + 0x7c, 0x4f, 0x1d, 0xb2, 0x52, 0x31, 0x0f, 0xc7, 0xf2, 0xa5, 0xa8, 0x5d, 0x0c, 0x76, 0x5b, 0x49, + 0x98, 0x55, 0xa0, 0x48, 0x7d, 0x9a, 0x22, 0x27, 0x50, 0x97, 0x67, 0xc3, 0x40, 0xe2, 0xe9, 0xde, + 0x93, 0xbd, 0xfd, 0xef, 0xef, 0x15, 0x78, 0x34, 0x0d, 0x35, 0x4a, 0xf9, 0x50, 0xa3, 0x8c, 0xf8, + 0x8d, 0xfd, 0xa7, 0x7b, 0xc3, 0x4e, 0x85, 0xb5, 0x40, 0xa3, 0xe6, 0xc8, 0xe8, 0x3f, 0xeb, 0x54, + 0x29, 0x95, 0xb7, 0xf1, 0x59, 0x7f, 0x77, 0xad, 0x53, 0x4b, 0x8b, 0x99, 0x75, 0xfd, 0x4f, 0x15, + 0x58, 0x10, 0x04, 0xc9, 0x67, 0xb2, 0xf2, 0xff, 0x16, 0xa8, 0x88, 0x7f, 0x0b, 0xfc, 0xdf, 0x26, + 0xaf, 0x70, 0xd2, 0xc4, 0x49, 0x3e, 0x1f, 0x10, 0x19, 0x56, 0x75, 0xe2, 0xc8, 0xaf, 0x06, 0xfe, + 0x41, 0x81, 0x9e, 0x88, 0x2f, 0x1e, 0x87, 0x66, 0x70, 0xf2, 0xbd, 0x9d, 0x4b, 0x69, 0x94, 0xab, + 0xbc, 0xee, 0x3b, 0xd0, 0xa6, 0xff, 0x53, 0xfc, 0xc4, 0x1d, 0xc9, 0x90, 0x5e, 0xbc, 0x6e, 0x4b, + 0x62, 0xc5, 0x42, 0xec, 0x23, 0x68, 0x8a, 0xff, 0x5d, 0x50, 0xc9, 0xa1, 0x50, 0xfa, 0x2e, 0x44, + 0x37, 0x0d, 0x31, 0x4a, 0x14, 0xea, 0x1f, 0xa5, 0x93, 0xb2, 0x8c, 0xcb, 0xe5, 0xea, 0xb6, 0x9c, + 0x32, 0xa4, 0x3c, 0xcc, 0x03, 0xb8, 0x39, 0xf3, 0x1e, 0x92, 0xed, 0x73, 0x99, 0x6f, 0xc1, 0x6d, + 0xfa, 0xaf, 0x15, 0x50, 0xd7, 0x27, 0xee, 0x29, 0x19, 0xd4, 0x5b, 0x00, 0xdc, 0x3e, 0xe6, 0xf2, + 0x0f, 0x0c, 0x0a, 0xa9, 0x10, 0x0d, 0x31, 0xe2, 0x2f, 0x0c, 0x9f, 0x02, 0x88, 0x3b, 0x8e, 0xc6, + 0x66, 0x20, 0x9f, 0x88, 0x4a, 0xd1, 0xc9, 0x02, 0xf2, 0x2e, 0xbb, 0x66, 0x20, 0x4b, 0xd1, 0x51, + 0x02, 0x67, 0x25, 0xfa, 0xf2, 0x4b, 0x4a, 0xf4, 0xbd, 0x3d, 0x68, 0x17, 0x97, 0x98, 0x11, 0x63, + 0xbe, 0x57, 0xfc, 0x0c, 0xea, 0x32, 0x0d, 0x73, 0xf1, 0xc0, 0xe7, 0x30, 0x3f, 0x55, 0xbd, 0x78, + 0x99, 0x5e, 0x2d, 0x88, 0x4c, 0x69, 0x5a, 0x64, 0x3e, 0x84, 0x85, 0xa1, 0x19, 0x9d, 0xca, 0x18, + 0x29, 0x73, 0x04, 0x62, 0x33, 0x3a, 0x1d, 0xa5, 0x44, 0xad, 0x21, 0xb8, 0x6d, 0xeb, 0x8f, 0x80, + 0xe5, 0x47, 0x4b, 0xfa, 0x63, 0xec, 0x8b, 0xc3, 0xc7, 0x3c, 0x36, 0x13, 0x8f, 0x05, 0x11, 0x48, + 0xbc, 0xd5, 0xbf, 0x57, 0xa0, 0x82, 0x41, 0x05, 0xbb, 0x0f, 0xda, 0x67, 0xdc, 0x0c, 0xe3, 0x43, + 0x6e, 0xc6, 0xac, 0x10, 0x40, 0xf4, 0x88, 0x6e, 0xd9, 0xa7, 0x55, 0xfa, 0xdc, 0x43, 0x85, 0xad, + 0x88, 0x0f, 0xbf, 0x93, 0x0f, 0xda, 0x5b, 0x49, 0x70, 0x42, 0xc1, 0x4b, 0xaf, 0x30, 0x5f, 0x9f, + 0x5b, 0xa6, 0xf1, 0x9f, 0xfb, 0x8e, 0xb7, 0x21, 0x3e, 0x37, 0x66, 0xd3, 0xc1, 0xcc, 0xf4, 0x0c, + 0x76, 0x1f, 0x6a, 0xdb, 0x11, 0x46, 0x4d, 0x97, 0x87, 0x12, 0xf1, 0xf3, 0x01, 0x95, 0x3e, 0xb7, + 0xfa, 0x97, 0x55, 0xa8, 0xfc, 0x88, 0x87, 0x3e, 0xfb, 0x10, 0xea, 0xf2, 0x43, 0x34, 0x96, 0xfb, + 0xe0, 0xac, 0x47, 0xd9, 0xa8, 0xa9, 0x2f, 0xd4, 0x68, 0x97, 0x8e, 0x78, 0xbf, 0xac, 0x66, 0xc7, + 0xb2, 0xef, 0xe4, 0x2e, 0x1d, 0xea, 0x13, 0xe8, 0x0c, 0xe2, 0x90, 0x9b, 0xe3, 0xdc, 0xf0, 0x22, + 0xa9, 0x66, 0x15, 0x00, 0x89, 0x5e, 0xf7, 0xa0, 0x26, 0x42, 0xd3, 0xa9, 0x09, 0xd3, 0xd5, 0x3d, + 0x1a, 0xfc, 0x3e, 0x34, 0x06, 0x27, 0xfe, 0xc4, 0xb5, 0x07, 0x3c, 0x3c, 0xe3, 0x2c, 0x17, 0x5d, + 0xf5, 0x72, 0x6d, 0x7d, 0x8e, 0x3d, 0x82, 0x1a, 0xbe, 0x48, 0x38, 0x66, 0x0b, 0xb9, 0x08, 0x4c, + 0xb0, 0x49, 0x8f, 0xe5, 0x51, 0x09, 0xa5, 0xd8, 0xfb, 0xa0, 0x89, 0x50, 0x00, 0x03, 0x81, 0xba, + 0x8c, 0x2e, 0xc4, 0x31, 0x72, 0x21, 0x82, 0x3e, 0xc7, 0x96, 0x01, 0x72, 0x31, 0xed, 0xcb, 0x46, + 0x7e, 0x04, 0xad, 0x0d, 0xd2, 0x84, 0xfb, 0xe1, 0xda, 0xa1, 0x1f, 0xc6, 0x6c, 0xfa, 0xe3, 0xd8, + 0xde, 0x34, 0x42, 0x9f, 0xc3, 0xe8, 0x70, 0x18, 0x5e, 0x88, 0xf1, 0x0b, 0x32, 0x15, 0x90, 0xed, + 0x37, 0x83, 0x2e, 0xec, 0xe3, 0x54, 0xae, 0xd2, 0x08, 0x60, 0x56, 0xa9, 0x50, 0x90, 0x48, 0xc8, + 0x00, 0x91, 0x08, 0xb2, 0xf0, 0x84, 0xbd, 0x21, 0xca, 0x96, 0x53, 0xe1, 0xca, 0xe5, 0x29, 0x59, + 0x28, 0x22, 0xa6, 0x5c, 0x0a, 0x4d, 0xa6, 0xa6, 0x7c, 0x13, 0x9a, 0xf9, 0xb0, 0x82, 0x51, 0xfd, + 0x6d, 0x46, 0xa0, 0x51, 0x9c, 0xb6, 0xfa, 0x1f, 0x55, 0xa8, 0x7d, 0xdf, 0x0f, 0x4f, 0x79, 0xc8, + 0xee, 0x42, 0x8d, 0x0a, 0xd0, 0x52, 0x96, 0xd2, 0x62, 0xf4, 0x2c, 0xda, 0xbd, 0x0b, 0x1a, 0x71, + 0x06, 0x0a, 0xbb, 0xe0, 0x57, 0xfa, 0x43, 0x99, 0x58, 0x5c, 0xa4, 0x7b, 0x89, 0xb9, 0xdb, 0x82, + 0x5b, 0xd3, 0x0f, 0x4e, 0x0a, 0x05, 0xe2, 0x1e, 0x3d, 0xe9, 0x93, 0x67, 0x03, 0x94, 0xcf, 0x87, + 0x0a, 0xfa, 0x14, 0x03, 0xf1, 0x78, 0x38, 0x28, 0xfb, 0xb3, 0x8a, 0x10, 0xff, 0xec, 0xdf, 0x21, + 0xfa, 0x1c, 0x7b, 0x00, 0x35, 0x69, 0x62, 0x16, 0x32, 0x45, 0x98, 0xdc, 0xb0, 0x93, 0x47, 0xc9, + 0x09, 0x8f, 0xa0, 0x26, 0xcc, 0xb1, 0x98, 0x50, 0x88, 0x6b, 0x04, 0x9f, 0x16, 0x3d, 0x6d, 0x7d, + 0x8e, 0xdd, 0x83, 0xba, 0x2c, 0x2f, 0xb3, 0x19, 0xb5, 0xe6, 0x4b, 0x2f, 0x56, 0x13, 0xbe, 0x96, + 0x58, 0xbf, 0xe0, 0xd4, 0x8a, 0xf5, 0x8b, 0xae, 0x98, 0x10, 0x7d, 0x83, 0x5b, 0xdc, 0xc9, 0x25, + 0xe6, 0x58, 0x42, 0x91, 0x19, 0xfa, 0xeb, 0x13, 0x68, 0x15, 0x92, 0x78, 0xac, 0x9b, 0xb0, 0xc5, + 0x74, 0x5e, 0xef, 0x92, 0xd6, 0xf8, 0x0e, 0x68, 0x32, 0xed, 0x70, 0x28, 0x19, 0x63, 0x46, 0x92, + 0xa3, 0x77, 0x39, 0xef, 0x40, 0xaa, 0xe0, 0x07, 0x70, 0x6d, 0x86, 0x6d, 0x65, 0xf4, 0xb9, 0xf3, + 0xd5, 0xce, 0x43, 0x6f, 0xf1, 0xca, 0xfe, 0x94, 0x00, 0x5f, 0x4f, 0x9c, 0xbe, 0x0b, 0x90, 0x99, + 0x18, 0x21, 0x1b, 0x97, 0x0c, 0x54, 0xef, 0xc6, 0x34, 0x3a, 0xd9, 0x74, 0xbd, 0xfb, 0xcb, 0x2f, + 0x6f, 0x2b, 0xbf, 0xfa, 0xf2, 0xb6, 0xf2, 0x6f, 0x5f, 0xde, 0x56, 0x7e, 0xf1, 0x9b, 0xdb, 0x73, + 0xbf, 0xfa, 0xcd, 0xed, 0xb9, 0x7f, 0xfa, 0xcd, 0xed, 0xb9, 0xc3, 0x1a, 0xfd, 0xb3, 0xf3, 0xa3, + 0xff, 0x09, 0x00, 0x00, 0xff, 0xff, 0x52, 0x83, 0x35, 0x13, 0x4f, 0x3a, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -8013,9 +8012,9 @@ func (m *Result) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - if len(m.VectorMetrics) > 0 { - for k := range m.VectorMetrics { - v := m.VectorMetrics[k] + if len(m.ExtraMetrics) > 0 { + for k := range m.ExtraMetrics { + v := m.ExtraMetrics[k] baseI := i i = encodeVarintPb(dAtA, i, uint64(v)) i-- @@ -10563,10 +10562,10 @@ func (m *SchemaUpdate) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - if len(m.IndexSpecs) > 0 { - for iNdEx := len(m.IndexSpecs) - 1; iNdEx >= 0; iNdEx-- { + if len(m.VectorSpecs) > 0 { + for iNdEx := len(m.VectorSpecs) - 1; iNdEx >= 0; iNdEx-- { { - size, err := m.IndexSpecs[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + size, err := m.VectorSpecs[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } @@ -10693,7 +10692,7 @@ func (m *SchemaUpdate) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } -func (m *VectorIndexSpec) Marshal() (dAtA []byte, err error) { +func (m *VectorSpec) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) @@ -10703,12 +10702,12 @@ func (m *VectorIndexSpec) Marshal() (dAtA []byte, err error) { return dAtA[:n], nil } -func (m *VectorIndexSpec) MarshalTo(dAtA []byte) (int, error) { +func (m *VectorSpec) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } -func (m *VectorIndexSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) { +func (m *VectorSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int @@ -12437,8 +12436,8 @@ func (m *Result) Size() (n int) { if m.List { n += 2 } - if len(m.VectorMetrics) > 0 { - for k, v := range m.VectorMetrics { + if len(m.ExtraMetrics) > 0 { + for k, v := range m.ExtraMetrics { _ = k _ = v mapEntrySize := 1 + len(k) + sovPb(uint64(len(k))) + 1 + sovPb(uint64(v)) @@ -13573,8 +13572,8 @@ func (m *SchemaUpdate) Size() (n int) { if m.Unique { n += 2 } - if len(m.IndexSpecs) > 0 { - for _, e := range m.IndexSpecs { + if len(m.VectorSpecs) > 0 { + for _, e := range m.VectorSpecs { l = e.Size() n += 1 + l + sovPb(uint64(l)) } @@ -13582,7 +13581,7 @@ func (m *SchemaUpdate) Size() (n int) { return n } -func (m *VectorIndexSpec) Size() (n int) { +func (m *VectorSpec) Size() (n int) { if m == nil { return 0 } @@ -15473,7 +15472,7 @@ func (m *Result) Unmarshal(dAtA []byte) error { m.List = bool(v != 0) case 8: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field VectorMetrics", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field ExtraMetrics", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -15500,8 +15499,8 @@ func (m *Result) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - if m.VectorMetrics == nil { - m.VectorMetrics = make(map[string]uint64) + if m.ExtraMetrics == nil { + m.ExtraMetrics = make(map[string]uint64) } var mapkey string var mapvalue uint64 @@ -15582,7 +15581,7 @@ func (m *Result) Unmarshal(dAtA []byte) error { iNdEx += skippy } } - m.VectorMetrics[mapkey] = mapvalue + m.ExtraMetrics[mapkey] = mapvalue iNdEx = postIndex default: iNdEx = preIndex @@ -23124,7 +23123,7 @@ func (m *SchemaUpdate) Unmarshal(dAtA []byte) error { m.Unique = bool(v != 0) case 15: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field IndexSpecs", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field VectorSpecs", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -23151,8 +23150,8 @@ func (m *SchemaUpdate) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.IndexSpecs = append(m.IndexSpecs, &VectorIndexSpec{}) - if err := m.IndexSpecs[len(m.IndexSpecs)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + m.VectorSpecs = append(m.VectorSpecs, &VectorSpec{}) + if err := m.VectorSpecs[len(m.VectorSpecs)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex @@ -23177,7 +23176,7 @@ func (m *SchemaUpdate) Unmarshal(dAtA []byte) error { } return nil } -func (m *VectorIndexSpec) Unmarshal(dAtA []byte) error { +func (m *VectorSpec) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -23200,10 +23199,10 @@ func (m *VectorIndexSpec) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: VectorIndexSpec: wiretype end group for non-group") + return fmt.Errorf("proto: VectorSpec: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: VectorIndexSpec: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: VectorSpec: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: diff --git a/schema/parse.go b/schema/parse.go index 29b85942a14..e0f4ed1aad1 100644 --- a/schema/parse.go +++ b/schema/parse.go @@ -63,12 +63,13 @@ func parseDirective(it *lex.ItemIterator, schema *pb.SchemaUpdate, t types.TypeI } schema.Directive = pb.SchemaUpdate_REVERSE case "index": - tokenizer, err := parseIndexDirective(it, schema.Predicate, t) + tokenizer, vectorSpecs, err := parseIndexDirective(it, schema.Predicate, t) if err != nil { return err } schema.Directive = pb.SchemaUpdate_INDEX schema.Tokenizer = tokenizer + schema.VectorSpecs = vectorSpecs case "count": schema.Count = true case "upsert": @@ -175,77 +176,249 @@ func parseScalarPair(it *lex.ItemIterator, predicate string, ns uint64) (*pb.Sch return schema, nil } -// parseIndexDirective works on "@index" or "@index(customtokenizer)". +// parseIndexDirective works on "@index" or "@index(customtokenizer)" +// or @index(tok1(opt1:"opt1val",opt2:"opt2val"), tok2, tok3) +// We assume that the "@index" has already been found, so we just need +// to parse the rest. +// Syntax EBNF (after '@index' has been found): +// +// Tokens ::= '(' TokenList ')' +// TokenList ::= Token [',' TokenList]* +// +// This function will specifically handle this as: +// +// Tokens ::= '(' TokenList ')' +// TokenList ::= Token [',' TokeniList] +// +// It then defers to parseTokenOrVectorSpec to parse Token. func parseIndexDirective(it *lex.ItemIterator, predicate string, - typ types.TypeID) ([]string, error) { - var tokenizers []string + typ types.TypeID) ([]string, []*pb.VectorSpec, error) { + tokenizers := []string{} + var vectorSpecs []*pb.VectorSpec var seen = make(map[string]bool) var seenSortableTok bool if typ == types.UidID || typ == types.DefaultID || typ == types.PasswordID { - return tokenizers, it.Item().Errorf("Indexing not allowed on predicate %s of type %s", - predicate, typ.Name()) + return tokenizers, vectorSpecs, + it.Item().Errorf("Indexing not allowed on predicate %s of type %s", + predicate, typ.Name()) } if !it.Next() { // Nothing to read. - return []string{}, it.Item().Errorf("Invalid ending.") + return tokenizers, vectorSpecs, it.Item().Errorf("Invalid ending.") } next := it.Item() if next.Typ != itemLeftRound { it.Prev() // Backup. - return []string{}, it.Item().Errorf("Require type of tokenizer for pred: %s for indexing.", - predicate) + return tokenizers, vectorSpecs, + it.Item().Errorf("Require type of tokenizer for pred: %s for indexing.", + predicate) } - expectArg := true - // Look for tokenizers. + // Look for tokenizers and IndexFactories (vectorSpecs). for { + tokenText, vectorSpec, sortable, err := parseTokenOrVectorSpec(it, predicate, typ) + if err != nil { + return tokenizers, vectorSpecs, err + } + if sortable && seenSortableTok { + return tokenizers, vectorSpecs, + next.Errorf("Only one index tokenizer can be sortable for %s", + predicate) + } + seenSortableTok = sortable + if tokenText != "" { + if _, found := seen[tokenText]; found { + return tokenizers, vectorSpecs, + next.Errorf("Duplicate tokenizers defined for predicate %v", + predicate) + } + tokenizers = append(tokenizers, tokenText) + seen[tokenText] = true + } else { + // parseTokenOrVectorSpec should have returned either + // non-empty tokenText or non-nil vectorsSpec or an error. + x.AssertTrue(vectorSpec != nil) + // At the moment, we cannot accept two VectorSpecs of + // the same name. Later, we may reconsider this as we + // develop a simple means to distinguish how their keys + // are formed based on the specified options. The notion + // of "seen" still applies, but we just use the tokenizer name. + seen[vectorSpec.Name] = true + vectorSpecs = append(vectorSpecs, vectorSpec) + } + it.Next() next = it.Item() if next.Typ == itemRightRound { break } - if next.Typ == itemComma { - if expectArg { - return nil, next.Errorf("Expected a tokenizer but got comma") - } - expectArg = true - continue + if next.Typ != itemComma { + return tokenizers, vectorSpecs, next.Errorf( + "Expected ',' or ')' but found '%s' for predicate '%s'", + next.Val, predicate) } - if next.Typ != itemText { - return tokenizers, next.Errorf("Expected directive arg but got: %v", next.Val) + } + return tokenizers, vectorSpecs, nil +} + +// parseTokenOrVectorSpec(it, predicate, typ) will parse a "Token" according to the +// grammar specification below. +// +// Token ::= TokenName [ TokenOptions ] +// TokenName ::= {itemText from Lexer} +// +// For TokenOptions, it defers to parseTokenOptions parsing. +// We expect either to find the name of a Tokenizer or else the name of an IndexFactory +// along with its options. We also return a boolean value indicating whether or +// not the found index is Sortable. +func parseTokenOrVectorSpec( + it *lex.ItemIterator, + predicate string, + typ types.TypeID) (string, *pb.VectorSpec, bool, error) { + it.Next() + next := it.Item() + if next.Typ != itemText { + return "", nil, false, next.Errorf( + "Expected token or VectorFactory name, but found '%s'", + next.Val) + } + tokenOrFactoryName := strings.ToLower(next.Val) + factory, found := tok.GetIndexFactory(tokenOrFactoryName) + if found { + // TODO: Consider allowing IndexFactory types not related to + // VectorIndex objects. + if typ != types.VFloatID { + return "", nil, false, + next.Errorf("IndexFactory: %s isn't valid for predicate: %s of type: %s", + factory.Name(), x.ParseAttr(predicate), typ.Name()) } - if !expectArg { - return tokenizers, next.Errorf("Expected a comma but got: %v", next) + tokenOpts, err := parseTokenOptions(it, factory) + if err != nil { + return "", nil, false, err } - // Look for custom tokenizer. - tokenizer, has := tok.GetTokenizer(strings.ToLower(next.Val)) - if !has { - return tokenizers, next.Errorf("Invalid tokenizer %s", next.Val) + allowedOpts := factory.AllowedOptions() + for _, pair := range tokenOpts { + _, err := allowedOpts.GetParsedOption(pair.Key, pair.Value) + if err != nil { + return "", nil, false, + next.Errorf("IndexFactory: %s issues this error: '%s'", + factory.Name(), err) + } } - tokenizerType, ok := types.TypeForName(tokenizer.Type()) - x.AssertTrue(ok) // Type is validated during tokenizer loading. - if tokenizerType != typ { - return tokenizers, - next.Errorf("Tokenizer: %s isn't valid for predicate: %s of type: %s", - tokenizer.Name(), x.ParseAttr(predicate), typ.Name()) + vs := &pb.VectorSpec{ + Name: tokenOrFactoryName, + Options: tokenOpts, } - if _, found := seen[tokenizer.Name()]; found { - return tokenizers, next.Errorf("Duplicate tokenizers defined for pred %v", - predicate) + return "", vs, factory.IsSortable(), err + } + + // Look for custom tokenizer, and validate its type. + tokenizer, has := tok.GetTokenizer(tokenOrFactoryName) + if !has { + return tokenOrFactoryName, nil, false, + next.Errorf("Invalid tokenizer %s", next.Val) + } + tokenizerType, ok := types.TypeForName(tokenizer.Type()) + x.AssertTrue(ok) // Type is validated during tokenizer loading. + if tokenizerType != typ { + return tokenOrFactoryName, nil, false, + next.Errorf("Tokenizer: %s isn't valid for predicate: %s of type: %s", + tokenizer.Name(), x.ParseAttr(predicate), typ.Name()) + } + return tokenOrFactoryName, nil, tokenizer.IsSortable(), nil +} + +// parseTokenOptions(it, factory) will parse "TokenOptions" according to the +// following grammar: +// +// TokenOptions ::= ['(' TokenOptionList ')'] +// TokenOptionList ::= TokenOption [',' TokenOptionList ] +// +// TODO: TokenOptionList could be made optional so that "hnsw()" is treated as +// an hnsw index with default search options +// +// For Parsing TokenOption, it defers to parseTokenOption +// Note that specifying TokenOptions is optional! The result is considered +// valid even if no token options are found as long as the first character +// discovered by it is a comma or end-parenthesis. (In the context where we +// invoke this, a comma indicates another tokenizer, and an end-parenthesis +// indicates the end of a list of tokenizers. +// TokenOptions provide the OptionKey-OptionValue pairs needed for building +// a VectorIndex. The factory is used to validate that any option name given +// is specified as an AllowedOption. +func parseTokenOptions(it *lex.ItemIterator, factory tok.IndexFactory) ([]*pb.OptionPair, error) { + retVal := []*pb.OptionPair{} + nextItem, found := it.PeekOne() + if !found { + return nil, nextItem.Errorf( + "unexpected end of stream when looking for IndexFactory options") + } + if nextItem.Typ == itemComma || nextItem.Typ == itemRightRound { + return []*pb.OptionPair{}, nil + } + if nextItem.Typ != itemLeftRound { + return nil, nextItem.Errorf( + "unexpected '%s' found when expecting '('", nextItem.Val) + } + it.Next() // Reads initial '(' + for { + optPair, err := parseTokenOption(it, factory) + if err != nil { + return retVal, err } - if tokenizer.IsSortable() { - if seenSortableTok { - return nil, next.Errorf("More than one sortable index encountered for: %v", - predicate) - } - seenSortableTok = true + retVal = append(retVal, optPair) + it.Next() + nextItem = it.Item() + if nextItem.Typ == itemRightRound { + return retVal, nil } - tokenizers = append(tokenizers, tokenizer.Name()) - seen[tokenizer.Name()] = true - expectArg = false + if nextItem.Typ != itemComma { + return nil, nextItem.Errorf( + "unexpected '%s' found when expecting ',' or ')'", + nextItem.Val) + } + } +} + +// parseTokenOption(it, factory) constructs OptionPair instances +// and validates that the options are okay via the factory. +// +// TokenOption ::= OptionName ':' OptionValue +// OptionName ::= {itemText from Lexer} +// OptionValue ::= {itemQuotedText from Lexer} +func parseTokenOption(it *lex.ItemIterator, factory tok.IndexFactory) (*pb.OptionPair, error) { + it.Next() + nextItem := it.Item() + if nextItem.Typ != itemText { + return nil, nextItem.Errorf( + "unexpected '%s' found when expecting option name", + nextItem.Val) } - return tokenizers, nil + optName := nextItem.Val + it.Next() + nextItem = it.Item() + if nextItem.Typ != itemColon { + return nil, nextItem.Errorf( + "unexpected '%s' found when expecting ':'", + nextItem.Val) + } + it.Next() + nextItem = it.Item() + if nextItem.Typ != itemQuotedText { + return nil, nextItem.Errorf( + "unexpected '%s' found when expecting quoted text", + nextItem.Val) + } + optVal := nextItem.Val[1 : len(nextItem.Val)-1] + return &pb.OptionPair{Key: optName, Value: optVal}, nil +} + +func HasTokenizerOrVectorSpec(update *pb.SchemaUpdate) bool { + if update == nil { + return false + } + return len(update.Tokenizer) > 0 || len(update.VectorSpecs) > 0 } // resolveTokenizers resolves default tokenizers and verifies tokenizers definitions. @@ -263,13 +436,17 @@ func resolveTokenizers(updates []*pb.SchemaUpdate) error { continue } - if len(schema.Tokenizer) == 0 && schema.Directive == pb.SchemaUpdate_INDEX { - return errors.Errorf("Require type of tokenizer for pred: %s of type: %s for indexing.", + if !HasTokenizerOrVectorSpec(schema) && + schema.Directive == pb.SchemaUpdate_INDEX { + return errors.Errorf( + "Require type of tokenizer for pred: %s of type: %s for indexing.", schema.Predicate, typ.Name()) - } else if len(schema.Tokenizer) > 0 && schema.Directive != pb.SchemaUpdate_INDEX { - return errors.Errorf("Tokenizers present without indexing on attr %s", x.ParseAttr(schema.Predicate)) + } else if HasTokenizerOrVectorSpec(schema) && + schema.Directive != pb.SchemaUpdate_INDEX { + return errors.Errorf("Tokenizers present without indexing on attr %s", + x.ParseAttr(schema.Predicate)) } - // check for valid tokeniser types and duplicates + // check for valid tokenizer types and duplicates var seen = make(map[string]bool) var seenSortableTok bool for _, t := range schema.Tokenizer { diff --git a/schema/parse_test.go b/schema/parse_test.go index 4aaab7edede..f5d6b5f0dad 100644 --- a/schema/parse_test.go +++ b/schema/parse_test.go @@ -42,7 +42,8 @@ func checkSchema(t *testing.T, h map[string]*pb.SchemaUpdate, expected []nameTyp for _, nt := range expected { typ, found := h[nt.name] require.True(t, found, nt) - require.EqualValuesf(t, *nt.typ, *typ, "found in map: %+v\n expected: %+v", *typ, *nt.typ) + require.EqualValuesf(t, *nt.typ, *typ, "found in map: %+v\n expected: %+v", + *typ, *nt.typ) } } @@ -52,7 +53,7 @@ age:int . name: string . address: string . : string . -coordinates: float32vector . +coordinates: vfloat . ` func TestSchema(t *testing.T) { diff --git a/schema/schema.go b/schema/schema.go index e04d67649aa..7a41be8155c 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -34,6 +34,7 @@ import ( "github.com/dgraph-io/dgraph/tok" "github.com/dgraph-io/dgraph/types" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/dgraph/tok/hnsw" ) var ( @@ -294,13 +295,14 @@ func (s *state) IsIndexed(ctx context.Context, pred string) bool { defer s.RUnlock() if isWrite { // TODO(Aman): we could return the query schema if it is a delete. - if schema, ok := s.mutSchema[pred]; ok && len(schema.Tokenizer) > 0 { + if schema, ok := s.mutSchema[pred]; ok && + (len(schema.Tokenizer) > 0 || len(schema.VectorSpecs) > 0) { return true } } if schema, ok := s.predicate[pred]; ok { - return len(schema.Tokenizer) > 0 + return len(schema.Tokenizer) > 0 || len(schema.VectorSpecs) > 0 } return false @@ -367,6 +369,42 @@ func (s *state) Tokenizer(ctx context.Context, pred string) []tok.Tokenizer { return tokenizers } +// FactoryCreateSpec(ctx, pred) returns the list of versioned +// FactoryCreateSpec instances for given predicate. +// The FactoryCreateSpec type defines the IndexFactory instance(s) +// for given predicate along with their options, if specified. +func (s *state) FactoryCreateSpec(ctx context.Context, pred string) ([]*tok.FactoryCreateSpec, error) { + isWrite, _ := ctx.Value(IsWrite).(bool) + s.RLock() + defer s.RUnlock() + var su *pb.SchemaUpdate + if isWrite { + if schema, ok := s.mutSchema[pred]; ok { + su = schema + } + } + if su == nil { + if schema, ok := s.predicate[pred]; ok { + su = schema + } + } + if su == nil { + // This may happen when some query that needs indexing over this predicate is executing + // while the predicate is dropped from the state (using drop operation). + glog.Errorf("Schema state not found for %s.", pred) + return nil, errors.Errorf("Schema state not found for %s.", pred) + } + creates := make([]*tok.FactoryCreateSpec, 0, len(su.VectorSpecs)) + for _, vs := range su.VectorSpecs { + c, err := tok.GetFactoryCreateSpecFromSpec(vs) + if err != nil { + return nil, err + } + creates = append(creates, c) + } + return creates, nil +} + // TokenizerNames returns the tokenizer names for given predicate func (s *state) TokenizerNames(ctx context.Context, pred string) []string { var names []string @@ -420,6 +458,22 @@ func (s *state) HasCount(ctx context.Context, pred string) bool { return false } +func (s *state) PredicatesToDelete(pred string) []string { + s.RLock() + defer s.RUnlock() + preds := make([]string, 0) + if schema, ok := s.predicate[pred]; ok { + preds = append(preds, pred) + + if schema.ValueType == pb.Posting_VFLOAT && len(schema.VectorSpecs) != 0 { + preds = append(preds, hnsw.ConcatStrings(pred, hnsw.VecEntry)) + preds = append(preds, hnsw.ConcatStrings(pred, hnsw.VecKeyword)) + preds = append(preds, hnsw.ConcatStrings(pred, hnsw.VecDead)) + } + } + return preds +} + // IsList returns whether the predicate is of list type. func (s *state) IsList(pred string) bool { s.RLock() diff --git a/schema/state.go b/schema/state.go index d754828182e..fe525e5a37c 100644 --- a/schema/state.go +++ b/schema/state.go @@ -22,20 +22,22 @@ import ( // Constants representing type of different graphql lexed items. const ( - itemText lex.ItemType = 5 + iota // plain text - itemNumber // number - itemLeftCurl // left curly bracket - itemRightCurl // right curly bracket - itemColon // colon - itemLeftRound // left round bracket - itemRightRound // right round bracket - itemAt - itemComma - itemNewLine - itemDot - itemLeftSquare - itemRightSquare - itemExclamationMark + itemText lex.ItemType = 5 + iota // plain text + itemNumber // number + itemLeftCurl // left curly bracket + itemRightCurl // right curly bracket + itemColon // colon + itemLeftRound // left round bracket + itemRightRound // right round bracket + itemAt // '@' + itemComma // ',' + itemNewLine // carriage-return or line-feed. + itemDot // '.' + itemLeftSquare // '[' + itemRightSquare // ']' + itemExclamationMark // '!' + itemQuote // double quote char: '"' + itemQuotedText // See Lexer.LexQuotedString() ) func lexText(l *lex.Lexer) lex.StateFn { @@ -91,6 +93,11 @@ Loop: l.Backup() return lexNumber } + case r == '"': + if err := l.LexQuotedString(); err != nil { + return l.Errorf("Invalid schema: %v", err) + } + l.Emit(itemQuotedText) default: return l.Errorf("Invalid schema. Unexpected %s", l.Input[l.Start:l.Pos]) } diff --git a/tok/constraints/constraints.go b/tok/constraints/constraints.go new file mode 100644 index 00000000000..965423af291 --- /dev/null +++ b/tok/constraints/constraints.go @@ -0,0 +1,49 @@ +/* + * Copyright 2023 DGraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package constraints + +// Signed interface taken from https://pkg.go.dev/golang.org/x/exp/constraints. +// We copy it here since constraints package future is uncertain. +type Signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +// Unsigned interface taken from https://pkg.go.dev/golang.org/x/exp/constraints. +// We copy it here since constraints package future is uncertain. +type Unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 +} + +// Integer interface taken from https://pkg.go.dev/golang.org/x/exp/constraints. +// We copy it here since constraints package future is uncertain. +type Integer interface { + Signed | Unsigned +} + +// created for values of vfloat to support []float32 or []float64 +type Float interface { + float32 | float64 +} + +// Float interface +type AnyFloat interface { + ~float32 | ~float64 +} + +type Simple interface { + Integer | AnyFloat | ~bool | ~string +} diff --git a/tok/hnsw/heap.go b/tok/hnsw/heap.go new file mode 100644 index 00000000000..24c6edbe8fe --- /dev/null +++ b/tok/hnsw/heap.go @@ -0,0 +1,63 @@ +package hnsw + +import ( + "container/heap" + + c "github.com/dgraph-io/dgraph/tok/constraints" +) + +const notAUid uint64 = 0 + +type minPersistentHeapElement[T c.Float] struct { + value T + index uint64 + // An element that is "filteredOut" is one that should be removed + // from final consideration due to it not matching the passed in + // filter criteria. + filteredOut bool +} + +func initPersistentHeapElement[T c.Float]( + val T, i uint64, filteredOut bool) *minPersistentHeapElement[T] { + return &minPersistentHeapElement[T]{ + value: val, + index: i, + filteredOut: filteredOut, + } +} + +type minPersistentTupleHeap[T c.Float] []minPersistentHeapElement[T] + +func (h minPersistentTupleHeap[T]) Len() int { + return len(h) +} + +func (h minPersistentTupleHeap[T]) Less(i, j int) bool { + return h[i].value < h[j].value +} + +func (h minPersistentTupleHeap[T]) Swap(i, j int) { + h[i], h[j] = h[j], h[i] +} + +func (h *minPersistentTupleHeap[T]) Push(x interface{}) { + *h = append(*h, x.(minPersistentHeapElement[T])) +} + +func (h *minPersistentTupleHeap[T]) Pop() interface{} { + old := *h + n := len(old) + x := old[n-1] + *h = old[:n-1] + return x +} + +// buildPersistentHeapByInit will create a tuple heap using the array of minPersistentHeapElements +// in time O(n), where n = length of array +func buildPersistentHeapByInit[T c.Float](array []minPersistentHeapElement[T]) *minPersistentTupleHeap[T] { + // initialize the MinTupleHeap that has implement the heap.Interface + minPersistentTupleHeap := &minPersistentTupleHeap[T]{} + *minPersistentTupleHeap = array + heap.Init(minPersistentTupleHeap) + return minPersistentTupleHeap +} diff --git a/tok/hnsw/helper.go b/tok/hnsw/helper.go new file mode 100644 index 00000000000..03be45906e3 --- /dev/null +++ b/tok/hnsw/helper.go @@ -0,0 +1,684 @@ +package hnsw + +import ( + "context" + "encoding/binary" + "encoding/json" + "log" + "math" + "math/rand" + "sort" + "strconv" + "strings" + + "github.com/chewxy/math32" + c "github.com/dgraph-io/dgraph/tok/constraints" + "github.com/dgraph-io/dgraph/tok/index" + "github.com/getsentry/sentry-go" + "github.com/pkg/errors" +) + +const ( + Euclidian = "euclidian" + Cosine = "cosine" + DotProd = "dotproduct" + plError = "\nerror fetching posting list for data key: " + dataError = "\nerror fetching data for data key: " + VecKeyword = "__vector_" + visitedVectorsLevel = "visited_vectors_level_" + distanceComputations = "vector_distance_computations" + searchTime = "vector_search_time" + VecEntry = "__vector_entry" + VecDead = "__vector_dead" + VectorIndexMaxLevels = 5 + EfConstruction = 16 + EfSearch = 12 + numEdgesConst = 2 + // ByteData indicates the key stores data. + ByteData = byte(0x00) + // DefaultPrefix is the prefix used for data, index and reverse keys so that relative + DefaultPrefix = byte(0x00) + // NsSeparator is the separator between the namespace and attribute. + NsSeparator = "-" +) + +type SearchResult struct { + nnUids []uint64 + traversalPath []uint64 + extraMetrics map[string]uint64 +} + +func newSearchResult(nnUids []uint64, traversalPath []uint64, + extraMetrics map[string]uint64) *SearchResult { + return &SearchResult{ + nnUids: nnUids, + traversalPath: traversalPath, + extraMetrics: extraMetrics, + } +} + +func (s *SearchResult) GetNnUids() []uint64 { + return s.nnUids +} + +func (s *SearchResult) GetTraversalPath() []uint64 { + return s.traversalPath +} + +func (s *SearchResult) GetExtraMetrics() map[string]uint64 { + return s.extraMetrics +} + +func norm[T c.Float](v []T, floatBits int) T { + vectorNorm, _ := dotProduct(v, v, floatBits) + if floatBits == 32 { + return T(math32.Sqrt(float32(vectorNorm))) + } + if floatBits == 64 { + return T(math.Sqrt(float64(vectorNorm))) + } + panic("Invalid floatBits") +} + +// This needs to implement signature of SimilarityType[T].distanceScore +// function, hence it takes in a floatBits parameter, +// but doesn't actually use it. +func dotProduct[T c.Float](a, b []T, floatBits int) (T, error) { + var dotProduct T + if len(a) != len(b) { + err := errors.New("can not compute dot product on vectors of different lengths") + return dotProduct, err + } + for i := range a { + dotProduct += a[i] * b[i] + } + return dotProduct, nil +} + +// This needs to implement signature of SimilarityType[T].distanceScore +// function, hence it takes in a floatBits parameter. +func cosineSimilarity[T c.Float](a, b []T, floatBits int) (T, error) { + dotProd, err := dotProduct(a, b, floatBits) + if err != nil { + return 0, err + } + normA := norm[T](a, floatBits) + normB := norm[T](b, floatBits) + if normA == 0 || normB == 0 { + err := errors.New("can not compute cosine similarity on zero vector") + var empty T + return empty, err + } + return dotProd / (normA * normB), nil +} + +// This needs to implement signature of SimilarityType[T].distanceScore +// function, hence it takes in a floatBits parameter, +// but doesn't actually use it. +func euclidianDistanceSq[T c.Float](a, b []T, floatBits int) (T, error) { + if len(a) != len(b) { + return 0, errors.New("can not subtract vectors of different lengths") + } + var distSq T + for i := range a { + val := a[i] - b[i] + distSq += val * val + } + return distSq, nil +} + +func contains[T c.Float](slice []minPersistentHeapElement[T], uuid uint64) bool { + for _, e := range slice { + if e.index == uuid { + return true + } + } + return false +} + +// Used for distance, since shorter distance is better +func insortPersistentHeapAscending[T c.Float]( + slice []minPersistentHeapElement[T], + val minPersistentHeapElement[T]) []minPersistentHeapElement[T] { + i := sort.Search(len(slice), func(i int) bool { return slice[i].value > val.value }) + var empty T + slice = append(slice, *initPersistentHeapElement(empty, notAUid, false)) + copy(slice[i+1:], slice[i:]) + slice[i] = val + return slice +} + +// Used for cosine similarity, since higher similarity score is better +func insortPersistentHeapDescending[T c.Float]( + slice []minPersistentHeapElement[T], + val minPersistentHeapElement[T]) []minPersistentHeapElement[T] { + i := sort.Search(len(slice), func(i int) bool { return slice[i].value < val.value }) + var empty T + slice = append(slice, *initPersistentHeapElement(empty, notAUid, false)) + copy(slice[i+1:], slice[i:]) + slice[i] = val + return slice +} + +func isBetterScoreForDistance[T c.Float](a, b T) bool { + return a < b +} + +func isBetterScoreForSimilarity[T c.Float](a, b T) bool { + return a > b +} + +func ParseEdges(s string) ([]uint64, error) { + s = strings.ReplaceAll(s, "\n", " ") + s = strings.ReplaceAll(s, "\t", " ") + s = strings.TrimSpace(s) + if len(s) == 0 { + return []uint64{}, nil + } + trimmedPre := strings.TrimPrefix(s, "[") + if len(trimmedPre) == len(s) { + return nil, cannotConvertToUintSlice(s) + } + trimmed := strings.TrimRight(trimmedPre, "]") + if len(trimmed) == len(trimmedPre) { + return nil, cannotConvertToUintSlice(s) + } + if len(trimmed) == 0 { + return []uint64{}, nil + } + if strings.Contains(trimmed, ",") { + // Splitting based on comma-separation. + values := strings.Split(trimmed, ",") + result := make([]uint64, len(values)) + for i := 0; i < len(values); i++ { + trimmedVal := strings.TrimSpace(values[i]) + val, err := strconv.ParseUint(trimmedVal, 10, 64) + if err != nil { + return nil, cannotConvertToUintSlice(s) + } + result[i] = val + } + return result, nil + } + values := strings.Split(trimmed, " ") + result := make([]uint64, 0, len(values)) + for i := 0; i < len(values); i++ { + if len(values[i]) == 0 { + // skip if we have an empty string. This can naturally + // occur if input s was "[1.0 2.0]" + // notice the extra whitespace in separation! + continue + } + if len(values[i]) > 0 { + val, err := strconv.ParseUint(values[i], 10, 64) + if err != nil { + return nil, cannotConvertToUintSlice(s) + } + result = append(result, val) + } + } + return result, nil +} + +func cannotConvertToUintSlice(s string) error { + return errors.Errorf("Cannot convert %s to uint slice", s) +} + +func diff(a []uint64, b []uint64) []uint64 { + // Turn b into a map + m := make(map[uint64]bool, len(b)) + for _, s := range b { + m[s] = false + } + // Append values from the longest slice that don't exist in the map + var diff []uint64 + for _, s := range a { + if _, ok := m[s]; !ok { + diff = append(diff, s) + continue + } + m[s] = true + } + return diff +} + +// TODO: Move SimilarityType to index package. +// +// Remove "hnsw-isms". +type SimilarityType[T c.Float] struct { + indexType string + distanceScore func(v, w []T, floatBits int) (T, error) + insortHeap func(slice []minPersistentHeapElement[T], val minPersistentHeapElement[T]) []minPersistentHeapElement[T] + isBetterScore func(a, b T) bool +} + +func GetSimType[T c.Float](indexType string, floatBits int) SimilarityType[T] { + switch { + case indexType == Euclidian: + return SimilarityType[T]{indexType: Euclidian, distanceScore: euclidianDistanceSq[T], + insortHeap: insortPersistentHeapAscending[T], isBetterScore: isBetterScoreForDistance[T]} + case indexType == Cosine: + return SimilarityType[T]{indexType: Cosine, distanceScore: cosineSimilarity[T], + insortHeap: insortPersistentHeapDescending[T], isBetterScore: isBetterScoreForSimilarity[T]} + case indexType == DotProd: + return SimilarityType[T]{indexType: DotProd, distanceScore: dotProduct[T], + insortHeap: insortPersistentHeapDescending[T], isBetterScore: isBetterScoreForSimilarity[T]} + default: + return SimilarityType[T]{indexType: Euclidian, distanceScore: euclidianDistanceSq[T], + insortHeap: insortPersistentHeapAscending[T], isBetterScore: isBetterScoreForDistance[T]} + } +} + +// implements CacheType interface +type TxnCache struct { + txn index.Txn + startTs uint64 +} + +func (tc *TxnCache) Get(key []byte) (rval index.Value, rerr error) { + return tc.txn.Get(key) +} + +func (tc *TxnCache) Ts() uint64 { + return tc.startTs +} + +func (tc *TxnCache) Find(prefix []byte, filter func([]byte) bool) (uint64, error) { + return tc.txn.Find(prefix, filter) +} + +func NewTxnCache(txn index.Txn, startTs uint64) *TxnCache { + return &TxnCache{ + txn: txn, + startTs: startTs, + } +} + +// implements index.CacheType interface +type QueryCache struct { + cache index.LocalCache + readTs uint64 +} + +func (qc *QueryCache) Find(prefix []byte, filter func([]byte) bool) (uint64, error) { + return qc.cache.Find(prefix, filter) +} + +func (qc *QueryCache) Get(key []byte) (rval index.Value, rerr error) { + return qc.cache.Get(key) +} + +func (qc *QueryCache) Ts() uint64 { + return qc.readTs +} + +func NewQueryCache(cache index.LocalCache, readTs uint64) *QueryCache { + return &QueryCache{ + cache: cache, + readTs: readTs, + } +} + +// getDataFromKeyWithCacheType(keyString, uid, c) looks up data in c +// associated with keyString and uid. +func getDataFromKeyWithCacheType(keyString string, uid uint64, c index.CacheType) (index.Value, error) { + key := DataKey(keyString, uid) + data, err := c.Get(key) + if err != nil { + return nil, errors.New(err.Error() + plError + keyString + " with uid" + strconv.FormatUint(uid, 10)) + } + return data, nil +} + +// populateEdgeDataFromStore(keyString, uid, c, edgeData) +// will fill edgeData with the contents of the neighboring edges for +// a given DataKey by looking into the given cache (which may result +// in a call to the underlying persistent storage). +// If data is found for the key, this returns true, otherwise, it +// returns false. If the data was found (and there were no errors), +// it populates edgeData with the found contents. +func populateEdgeDataFromKeyWithCacheType( + keyString string, + uid uint64, + c index.CacheType, + edgeData *[][]uint64) (bool, error) { + data, err := getDataFromKeyWithCacheType(keyString, uid, c) + // Note that "dataError" errors are treated as just not having + // found the data -- no harm, no foul, as it is probably a + // dead reference that we can ignore. + if err != nil && !strings.Contains(err.Error(), dataError) { + return false, err + } + if data == nil { + return false, nil + } + err = json.Unmarshal(data.([]byte), &edgeData) + return true, err +} + +// entryUuidInsert adds the entry uuid to the given key +func entryUuidInsert( + ctx context.Context, + key []byte, + txn index.Txn, + predEntryKey string, + entryUuid []byte) (*index.KeyValue, error) { + edge := &index.KeyValue{ + Entity: 1, + Attr: predEntryKey, + Value: entryUuid, + } + err := txn.AddMutationWithLockHeld(ctx, key, edge) + return edge, err +} + +func ConcatStrings(strs ...string) string { + total := "" + for _, s := range strs { + total += s + } + return total +} + +func getInsertLayer(maxLevels int) int { + // multFactor is a multiplicative factor used to normalize the distribution + var level int + randFloat := rand.Float64() + for i := 0; i < maxLevels; i++ { + // calculate level based on section 3.1 here + if randFloat < math.Pow(1.0/float64(5), float64(maxLevels-1-i)) { + level = i + break + } + } + return level +} + +// adds the data corresponding to a uid to the given vec variable in the form of []T +// this does not allocate memory for vec, so it must be allocated before calling this function +func (ph *persistentHNSW[T]) getVecFromUid(uid uint64, c index.CacheType, vec *[]T) error { + data, err := getDataFromKeyWithCacheType(ph.pred, uid, c) + if err != nil { + if strings.Contains(err.Error(), plError) { + // no vector. Return empty array of floats + index.BytesAsFloatArray([]byte{}, vec, ph.floatBits) + return errors.New("Nil vector returned") + } + return err + } + if data != nil { + index.BytesAsFloatArray(data.([]byte), vec, ph.floatBits) + return nil + + } else { + index.BytesAsFloatArray([]byte{}, vec, ph.floatBits) + return errors.New("Nil vector returned") + } +} + +// chooses whether to create the entry and start nodes based on if it already +// exists, and if it hasnt been created yet, it adds the startNode to all +// levels. +func (ph *persistentHNSW[T]) createEntryAndStartNodes( + ctx context.Context, + c *TxnCache, + inUuid uint64, + vec *[]T) (uint64, []*index.KeyValue, error) { + txn := c.txn + edges := []*index.KeyValue{} + entryKey := DataKey(ph.vecEntryKey, 1) // 0-profile_vector_entry + txn.LockKey(entryKey) + defer txn.UnlockKey(entryKey) + data, _ := txn.GetWithLockHeld(entryKey) + + create_edges := func(inUuid uint64) (uint64, []*index.KeyValue, error) { + startEdges, err := ph.addStartNodeToAllLevels(ctx, entryKey, txn, inUuid) + if err != nil { + return 0, []*index.KeyValue{}, err + } + // return entry node at all levels + edges = append(edges, startEdges...) + return 0, edges, nil + } + + if data == nil { + // no entries in vector index yet b/c no entry exists, so put in all levels + return create_edges(inUuid) + } + + entry := BytesToUint64(data.([]byte)) // convert entry Uuid returned from Get to uint64 + err := ph.getVecFromUid(entry, c, vec) + if err != nil || len(*vec) == 0 { + // The entry vector has been deleted. We have to create a new entry vector. + entry, err := ph.PickStartNode(ctx, c, vec) + if err != nil { + return 0, []*index.KeyValue{}, err + } + return create_edges(entry) + } + + return entry, edges, nil +} + +// adds empty layers to all levels +func (ph *persistentHNSW[T]) addStartNodeToAllLevels( + ctx context.Context, + entryKey []byte, + txn index.Txn, + inUuid uint64) ([]*index.KeyValue, error) { + edges := []*index.KeyValue{} + key := DataKey(ph.vecKey, inUuid) + emptyEdges := make([][]uint64, ph.maxLevels) + emptyEdgesBytes, err := json.Marshal(emptyEdges) + if err != nil { + return []*index.KeyValue{}, err + } + // creates empty at all levels only for entry node + edge, err := ph.newPersistentEdgeKeyValueEntry(ctx, key, txn, inUuid, emptyEdgesBytes) + if err != nil { + return []*index.KeyValue{}, err + } + edges = append(edges, edge) + inUuidByte := Uint64ToBytes(inUuid) + // add inUuid as entry for this structure from now on + edge, err = entryUuidInsert(ctx, entryKey, txn, ph.vecEntryKey, inUuidByte) + if err != nil { + return []*index.KeyValue{}, err + } + edges = append(edges, edge) + return edges, nil +} + +// creates a new edge with the given uuid and edges. Lock must be held before calling this function +func (ph *persistentHNSW[T]) newPersistentEdgeKeyValueEntry(ctx context.Context, key []byte, + txn index.Txn, uuid uint64, edges []byte) (*index.KeyValue, error) { + txn.LockKey(key) + defer txn.UnlockKey(key) + edge := &index.KeyValue{ + Entity: uuid, + Attr: ph.vecKey, + Value: edges, + } + if err := txn.AddMutationWithLockHeld(ctx, key, edge); err != nil { + return nil, err + } + return edge, nil +} + +// addNeighbors adds the neighbors of the given uuid to the given level. +// It returns the edge created and the error if any. +func (ph *persistentHNSW[T]) addNeighbors(ctx context.Context, tc *TxnCache, + uuid uint64, allLayerNeighbors [][]uint64) (*index.KeyValue, error) { + + txn := tc.txn + keyPred := ph.vecKey + key := DataKey(keyPred, uuid) + txn.LockKey(key) + defer txn.UnlockKey(key) + var nnEdgesErr error + var allLayerEdges [][]uint64 + var ok bool + allLayerEdges, ok = ph.nodeAllEdges[uuid] + if !ok { + data, _ := txn.GetWithLockHeld(key) + if data == nil { + allLayerEdges = allLayerNeighbors + } else { + // all edges of nearest neighbor + err := json.Unmarshal(data.([]byte), &allLayerEdges) + if err != nil { + return nil, err + } + } + } + for level := 0; level < ph.maxLevels; level++ { + allLayerEdges[level], nnEdgesErr = ph.removeDeadNodes(allLayerEdges[level], tc) + if nnEdgesErr != nil { + return nil, nnEdgesErr + } + // This adds at most efConstruction number of edges for each layer for this node + allLayerEdges[level] = append(allLayerEdges[level], allLayerNeighbors[level]...) + } + + // on every modification of the layer edges, add it to in mem map so you dont have to always be reading + // from persistent storage + ph.nodeAllEdges[uuid] = allLayerEdges + inboundEdgesBytes, marshalErr := json.Marshal(allLayerEdges) + if marshalErr != nil { + return nil, marshalErr + } + + edge := &index.KeyValue{ + Entity: uuid, + Attr: ph.vecKey, + Value: inboundEdgesBytes, + } + if err := txn.AddMutationWithLockHeld(ctx, key, edge); err != nil { + return nil, err + } + return edge, nil +} + +// removeDeadNodes(nnEdges, tc) removes dead nodes from nnEdges and returns the new nnEdges +func (ph *persistentHNSW[T]) removeDeadNodes(nnEdges []uint64, tc *TxnCache) ([]uint64, error) { + data, err := getDataFromKeyWithCacheType(ph.vecDead, 1, tc) + if err != nil && err.Error() == plError { + return []uint64{}, err + } + var deadNodes []uint64 + if data != nil { // if dead nodes exist, convert to []uint64 + deadNodes, err = ParseEdges(string(data.([]byte))) + if err != nil { + return []uint64{}, err + } + nnEdges = diff(nnEdges, deadNodes) // set nnEdges to be all elements not contained in deadNodes + } + return nnEdges, nil +} + +func Uint64ToBytes(key uint64) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, key) + return b +} + +func BytesToUint64(bytes []byte) uint64 { + return binary.BigEndian.Uint64(bytes) +} + +func isEqual[T c.Float](a []T, b []T) bool { + if len(a) != len(b) { + return false + } + for i, val := range a { + if val != b[i] { + return false + } + } + return true +} + +// DataKey generates a data key with the given attribute and UID. +// The structure of a data key is as follows: +// +// byte 0: key type prefix (set to DefaultPrefix or ByteSplit if part of a multi-part list) +// byte 1-2: length of attr +// next len(attr) bytes: value of attr +// next byte: data type prefix (set to ByteData) +// next eight bytes: value of uid +// next eight bytes (optional): if the key corresponds to a split list, the startUid of +// the split stored in this key and the first byte will be sets to ByteSplit. +func DataKey(attr string, uid uint64) []byte { + extra := 1 + 8 // ByteData + UID + buf, prefixLen := generateKey(DefaultPrefix, attr, extra) + + rest := buf[prefixLen:] + rest[0] = ByteData + + rest = rest[1:] + binary.BigEndian.PutUint64(rest, uid) + return buf +} + +// genKey creates the key and writes the initial bytes (type byte, length of attribute, +// and the attribute itself). It leaves the rest of the key empty for further processing +// if necessary. It also returns next index from where further processing should be done. +func generateKey(typeByte byte, attr string, extra int) ([]byte, int) { + // Separate namespace and attribute from attr and write namespace in the first 8 bytes of key. + namespace, attr := ParseNamespaceBytes(attr) + prefixLen := 1 + 8 + 2 + len(attr) // byteType + ns + len(pred) + pred + buf := make([]byte, prefixLen+extra) + buf[0] = typeByte + AssertTrue(copy(buf[1:], namespace) == 8) + rest := buf[9:] + + writeAttr(rest, attr) + return buf, prefixLen +} + +func ParseNamespaceBytes(attr string) ([]byte, string) { + splits := strings.SplitN(attr, NsSeparator, 2) + ns := make([]byte, 8) + binary.BigEndian.PutUint64(ns, strToUint(splits[0])) + return ns, splits[1] +} + +// AssertTrue asserts that b is true. Otherwise, it would log fatal. +func AssertTrue(b bool) { + if !b { + log.Fatalf("%+v", errors.Errorf("Assert failed")) + } +} + +func writeAttr(buf []byte, attr string) []byte { + AssertTrue(len(attr) < math.MaxUint16) + binary.BigEndian.PutUint16(buf[:2], uint16(len(attr))) + + rest := buf[2:] + AssertTrue(len(attr) == copy(rest, attr)) + + return rest[len(attr):] +} + +// For consistency, use base16 to encode/decode the namespace. +func strToUint(s string) uint64 { + ns, err := strconv.ParseUint(s, 16, 64) + Check(err) + return ns +} + +// Check logs fatal if err != nil. +func Check(err error) { + if err != nil { + err = errors.Wrap(err, "") + CaptureSentryException(err) + log.Fatalf("%+v", err) + } +} + +// CaptureSentryException sends the error report to Sentry. +func CaptureSentryException(err error) { + if err != nil { + sentry.CaptureException(err) + } +} diff --git a/tok/hnsw/persistent_factory.go b/tok/hnsw/persistent_factory.go new file mode 100644 index 00000000000..8de9467f8f4 --- /dev/null +++ b/tok/hnsw/persistent_factory.go @@ -0,0 +1,161 @@ +package hnsw + +import ( + "sync" + + c "github.com/dgraph-io/dgraph/tok/constraints" + "github.com/dgraph-io/dgraph/tok/index" + opt "github.com/dgraph-io/dgraph/tok/options" + "github.com/pkg/errors" +) + +const ( + ExponentOpt string = "exponent" + MaxLevelsOpt string = "maxLevels" + EfConstructionOpt string = "efConstruction" + EfSearchOpt string = "efSearch" + MetricOpt string = "metric" + Hnsw string = "hnsw" +) + +// persistentIndexFactory is an in memory implementation of the IndexFactory interface. +// indexMap is an in memory map that corresponds an index name with a corresponding VectorIndex. +// In persistentIndexFactory, the VectorIndex will be of type HNSW. +type persistentIndexFactory[T c.Float] struct { + // TODO: Can we kill the map?? This should disappear once server + // restarts, correct? So at the very least, this is not dependable. + indexMap map[string]index.VectorIndex[T] + floatBits int + mu sync.RWMutex +} + +// CreateFactory creates an instance of the private struct persistentIndexFactory. +// NOTE: if T and floatBits do not match in # of bits, there will be consequences. +func CreateFactory[T c.Float](floatBits int) index.IndexFactory[T] { + f := &persistentIndexFactory[T]{ + indexMap: map[string]index.VectorIndex[T]{}, + floatBits: floatBits, + } + return f +} + +// Implements NamedFactory interface for use as a plugin. +func (hf *persistentIndexFactory[T]) Name() string { return Hnsw } + +func (hf *persistentIndexFactory[T]) isNameAvailableWithLock(name string) bool { + _, nameUsed := hf.indexMap[name] + return !nameUsed +} +func (hf *persistentIndexFactory[T]) isNameAvailable(name string) bool { + hf.mu.Lock() + defer hf.mu.Unlock() + return hf.isNameAvailableWithLock(name) +} + +// hf.AllowedOptions() allows persistentIndexFactory to implement the +// IndexFactory interface (see vector-indexer/index/index.go for details). +// We define here options for exponent, maxLevels, efSearch, efConstruction, +// and metric. +func (hf *persistentIndexFactory[T]) AllowedOptions() opt.AllowedOptions { + retVal := opt.NewAllowedOptions() + retVal.AddIntOption(ExponentOpt). + AddIntOption(MaxLevelsOpt). + AddIntOption(EfConstructionOpt). + AddIntOption(EfSearchOpt) + getSimFunc := func(optValue string) (any, error) { + return GetSimType[T](optValue, hf.floatBits), nil + } + + retVal.AddCustomOption(MetricOpt, getSimFunc) + return retVal +} + +// Create is an implementation of the IndexFactory interface function, invoked by an HNSWIndexFactory +// instance. It takes in a string name and a VectorSource implementation, and returns a VectorIndex and error +// flag. It creates an HNSW instance using the index name and populates other parts of the HNSW struct such as +// multFactor, maxLevels, efConstruction, maxNeighbors, and efSearch using struct parameters. +// It then populates the HNSW graphs using the InsertChunk function until there are no more items to populate. +// Finally, the function adds the name and hnsw object to the in memory map and returns the object. +func (hf *persistentIndexFactory[T]) Create( + name string, + o opt.Options, + floatBits int) (index.VectorIndex[T], error) { + hf.mu.Lock() + defer hf.mu.Unlock() + return hf.createWithLock(name, o, floatBits) +} + +func (hf *persistentIndexFactory[T]) createWithLock( + name string, + o opt.Options, + floatBits int) (index.VectorIndex[T], error) { + if !hf.isNameAvailableWithLock(name) { + err := errors.New("index with name " + name + " already exists") + return nil, err + } + retVal := &persistentHNSW[T]{ + pred: name, + vecEntryKey: ConcatStrings(name, VecEntry), + vecKey: ConcatStrings(name, VecKeyword), + vecDead: ConcatStrings(name, VecDead), + floatBits: floatBits, + nodeAllEdges: map[uint64][][]uint64{}, + } + err := retVal.applyOptions(o) + if err != nil { + return nil, err + } + hf.indexMap[name] = retVal + return retVal, nil +} + +// Find is an implementation of the IndexFactory interface function, invoked by an persistentIndexFactory +// instance. It returns the VectorIndex corresponding with a string name using the in memory map. +func (hf *persistentIndexFactory[T]) Find(name string) (index.VectorIndex[T], error) { + hf.mu.RLock() + defer hf.mu.RUnlock() + return hf.findWithLock(name) +} + +func (hf *persistentIndexFactory[T]) findWithLock(name string) (index.VectorIndex[T], error) { + vecInd := hf.indexMap[name] + return vecInd, nil +} + +// Remove is an implementation of the IndexFactory interface function, invoked by an persistentIndexFactory +// instance. It removes the VectorIndex corresponding with a string name using the in memory map. +func (hf *persistentIndexFactory[T]) Remove(name string) error { + hf.mu.Lock() + defer hf.mu.Unlock() + return hf.removeWithLock(name) +} + +func (hf *persistentIndexFactory[T]) removeWithLock(name string) error { + delete(hf.indexMap, name) + return nil +} + +// CreateOrReplace is an implementation of the IndexFactory interface funciton, +// invoked by an persistentIndexFactory. It checks if a VectorIndex +// correpsonding with name exists. If it does, it removes it, and replaces it +// via the Create function using the passed VectorSource. If the VectorIndex +// does not exist, it creates that VectorIndex corresponding with the name using +// the VectorSource. +func (hf *persistentIndexFactory[T]) CreateOrReplace( + name string, + o opt.Options, + floatBits int) (index.VectorIndex[T], error) { + hf.mu.Lock() + defer hf.mu.Unlock() + vi, err := hf.findWithLock(name) + if err != nil { + return nil, err + } + if vi != nil { + err = hf.removeWithLock(name) + if err != nil { + return nil, err + } + } + return hf.createWithLock(name, o, floatBits) +} diff --git a/tok/hnsw/persistent_hnsw.go b/tok/hnsw/persistent_hnsw.go new file mode 100644 index 00000000000..2a3fec0f09e --- /dev/null +++ b/tok/hnsw/persistent_hnsw.go @@ -0,0 +1,448 @@ +package hnsw + +import ( + "context" + "fmt" + "strings" + "time" + + c "github.com/dgraph-io/dgraph/tok/constraints" + "github.com/dgraph-io/dgraph/tok/index" + opt "github.com/dgraph-io/dgraph/tok/options" + "github.com/pkg/errors" +) + +type persistentHNSW[T c.Float] struct { + maxLevels int + efConstruction int + efSearch int + pred string + vecEntryKey string + vecKey string + vecDead string + simType SimilarityType[T] + floatBits int + // nodeAllEdges[65443][1][3] indicates the 3rd neighbor in the first + // layer for uuid 65443. The result will be a neighboring uuid. + nodeAllEdges map[uint64][][]uint64 + visitedUids []uint64 +} + +func (ph *persistentHNSW[T]) applyOptions(o opt.Options) error { + if o.Specifies(ExponentOpt) { + // Adjust defaults based on exponent. + exponent, _, _ := opt.GetOpt(o, ExponentOpt, 3) + + if !o.Specifies(MaxLevelsOpt) { + o.SetOpt(MaxLevelsOpt, exponent) + } + + if !o.Specifies(EfConstructionOpt) { + o.SetOpt(EfConstructionOpt, 6*exponent) + } + + if !o.Specifies(EfSearchOpt) { + o.SetOpt(EfConstructionOpt, 9*exponent) + } + } + + var err error + ph.maxLevels, _, err = opt.GetOpt(o, MaxLevelsOpt, 3) + if err != nil { + return err + } + ph.efConstruction, _, err = opt.GetOpt(o, EfConstructionOpt, 18) + if err != nil { + return err + } + ph.efSearch, _, err = opt.GetOpt(o, EfSearchOpt, 27) + if err != nil { + return err + } + simType, foundSimType := opt.GetInterfaceOpt(o, MetricOpt) + if foundSimType { + okSimType, ok := simType.(SimilarityType[T]) + if !ok { + return fmt.Errorf("cannot cast %T to SimilarityType", simType) + } + ph.simType = okSimType + } else { + ph.simType = SimilarityType[T]{indexType: Euclidian, distanceScore: euclidianDistanceSq[T], + insortHeap: insortPersistentHeapAscending[T], isBetterScore: isBetterScoreForDistance[T]} + } + return nil +} + +func (ph *persistentHNSW[T]) emptyFinalResultWithError(e error) ( + *index.SearchPathResult, error) { + return index.NewSearchPathResult(), e +} + +func (ph *persistentHNSW[T]) emptySearchResultWithError(e error) (*searchLayerResult[T], error) { + return newLayerResult[T](0), e +} + +// fillNeighborEdges(uuid, c, edges) will "fill" edges with the neighbors for +// all levels associated with given uuid and CacheType. +// It returns true when we were able to find the node (either in cache or +// in persistent store) and false otherwise. +// (Of course, it may also return an error if a problem was encountered). +func (ph *persistentHNSW[T]) fillNeighborEdges(uuid uint64, c index.CacheType, edges *[][]uint64) (bool, error) { + var ok bool + *edges, ok = ph.nodeAllEdges[uuid] + if ok { + return true, nil + } + + ok, err := populateEdgeDataFromKeyWithCacheType(ph.vecKey, uuid, c, edges) + if err != nil { + return false, err + } + if !ok { + return false, nil + } + + // add this to in mem storage of uid -> edges + ph.nodeAllEdges[uuid] = *edges + return true, nil +} + +// searchPersistentLayer searches a layer of the hnsw graph for the nearest +// neighbors of the query vector and returns the traversal path and the nearest +// neighbors +func (ph *persistentHNSW[T]) searchPersistentLayer( + c index.CacheType, + level int, + entry uint64, + startVec, query []T, + entryIsFilteredOut bool, + expectedNeighbors int, + filter index.SearchFilter[T]) (*searchLayerResult[T], error) { + r := newLayerResult[T](level) + + bestDist, err := ph.simType.distanceScore(startVec, query, ph.floatBits) + r.markFirstDistanceComputation() + if err != nil { + return ph.emptySearchResultWithError(err) + } + best := minPersistentHeapElement[T]{ + value: bestDist, + index: entry, + filteredOut: entryIsFilteredOut, + } + r.setFirstPathNode(best) + //create set using map to append to on future visited nodes + ph.visitedUids = append(ph.visitedUids, best.index) + candidateHeap := *buildPersistentHeapByInit([]minPersistentHeapElement[T]{best}) + for candidateHeap.Len() != 0 { + currCandidate := candidateHeap.Pop().(minPersistentHeapElement[T]) + if r.numNeighbors() < expectedNeighbors && + ph.simType.isBetterScore(r.lastNeighborScore(), currCandidate.value) { + // If the "worst score" in our neighbors list is deemed to have + // a better score than the current candidate -- and if we have at + // least our expected number of nearest results -- we discontinue + // the search. + // Note that while this is faithful to the published + // HNSW algorithms insofar as we stop when we reach a local + // minimum, it leaves something to be desired in terms of + // guarantees of getting best results. + break + } + var allLayerEdges [][]uint64 + + found, err := ph.fillNeighborEdges(currCandidate.index, c, &allLayerEdges) + if err != nil { + return ph.emptySearchResultWithError(err) + } + if !found { + continue + } + currLayerEdges := allLayerEdges[level] + currLayerEdges = diff(currLayerEdges, ph.visitedUids) + var eVec []T + for i := range currLayerEdges { + // iterate over candidate's neighbors distances to get + // best ones + _ = ph.getVecFromUid(currLayerEdges[i], c, &eVec) + // intentionally ignoring error -- we catch it + // indirectly via eVec == nil check. + if len(eVec) == 0 { + continue + } + currDist, err := ph.simType.distanceScore(eVec, query, ph.floatBits) + ph.visitedUids = append(ph.visitedUids, currLayerEdges[i]) + r.incrementDistanceComputations() + if err != nil { + return ph.emptySearchResultWithError(err) + } + filteredOut := !filter(query, eVec, currLayerEdges[i]) + currElement := initPersistentHeapElement( + currDist, currLayerEdges[i], filteredOut) + nodeVisited := r.nodeVisited(*currElement) + if !nodeVisited { + r.addToVisited(*currElement) + + // If we have not yet found k candidates, we can consider + // any candidate. Otherwise, only consider those that + // are better than our current k nearest neighbors. + // Note that the "numNeighbors" function is a bit tricky: + // If we previously added to the heap M elements that should + // be filtered out, we ignore M elements in the numNeighbors + // check! In this way, we can make sure to allow in up to + // expectedNeighbors "unfiltered" elements. + if ph.simType.isBetterScore(currDist, r.lastNeighborScore()) || + r.numNeighbors() < expectedNeighbors { + candidateHeap.Push(*currElement) + r.addPathNode(*currElement, ph.simType, expectedNeighbors) + } + } + } + } + return r, nil +} + +// Search searches the hnsw graph for the nearest neighbors of the query vector +// and returns the traversal path and the nearest neighbors +func (ph *persistentHNSW[T]) Search(ctx context.Context, c index.CacheType, query []T, + maxResults int, filter index.SearchFilter[T]) (nnUids []uint64, err error) { + r, err := ph.SearchWithPath(ctx, c, query, maxResults, filter) + return r.Neighbors, err +} + +// Search searches the hnsw graph for the nearest neighbors of the query uid +// and returns the traversal path and the nearest neighbors +func (ph *persistentHNSW[T]) SearchWithUid(ctx context.Context, c index.CacheType, queryUid uint64, + maxResults int, filter index.SearchFilter[T]) (nnUids []uint64, err error) { + var queryVec []T + err = ph.getVecFromUid(queryUid, c, &queryVec) + if err != nil { + if strings.Contains(err.Error(), plError) { + // No vector. return empty result + return []uint64{}, nil + } + return []uint64{}, err + } + + if len(queryVec) == 0 { + // No vector. return empty result + return []uint64{}, nil + } + + shouldFilterOutQueryVec := !filter(queryVec, queryVec, queryUid) + + // how normal search works is by cotinuously searching higher layers + // for the best entry node to the last layer since we already know the + // best entry node (since it already exists in the lowest level), we + // can just search the last layer and return the results. + r, err := ph.searchPersistentLayer( + c, ph.maxLevels-1, queryUid, queryVec, queryVec, + shouldFilterOutQueryVec, maxResults, filter) + for _, n := range r.neighbors { + nnUids = append(nnUids, n.index) + } + return nnUids, err +} + +// There will be times when the entry node has been deleted. In that case, we want to make a new node +// the first vector. +func (ph *persistentHNSW[T]) calculateNewEntryVec( + ctx context.Context, + c index.CacheType, + startVec *[]T) (uint64, error) { + + itr, err := c.Find([]byte(ph.pred), func(value []byte) bool { + index.BytesAsFloatArray(value, startVec, ph.floatBits) + return len(*startVec) != 0 + }) + + if err != nil { + return 0, errors.Wrapf(err, "HNSW tree has no elements") + } + if itr == 0 { + return itr, errors.New("HNSW tree has no elements") + } + + return itr, nil +} + +func (ph *persistentHNSW[T]) PickStartNode( + ctx context.Context, + c index.CacheType, + startVec *[]T) (uint64, error) { + + data, err := getDataFromKeyWithCacheType(ph.vecEntryKey, 1, c) + if err != nil { + if strings.Contains(err.Error(), plError) { + // The index might be empty + return ph.calculateNewEntryVec(ctx, c, startVec) + } + return 0, err + } + + entry := BytesToUint64(data.([]byte)) + err = ph.getVecFromUid(entry, c, startVec) + if err != nil { + fmt.Println(err) + } + + if len(*startVec) == 0 { + return ph.calculateNewEntryVec(ctx, c, startVec) + } + return entry, err +} + +// SearchWithPath allows persistentHNSW to implement index.OptionalIndexSupport. +// See index.OptionalIndexSupport.SearchWithPath for more info. +func (ph *persistentHNSW[T]) SearchWithPath( + ctx context.Context, + c index.CacheType, + query []T, + maxResults int, + filter index.SearchFilter[T]) (r *index.SearchPathResult, err error) { + start := time.Now().UnixMilli() + r = index.NewSearchPathResult() + + // 0-profile_vector_entry + var startVec []T + entry, err := ph.PickStartNode(ctx, c, &startVec) + if err != nil { + return ph.emptyFinalResultWithError(err) + } + + // Calculates best entry for last level (maxLevels-1) by searching each + // layer and using new best entry. + for level := 0; level < ph.maxLevels-1; level++ { + if isEqual(startVec, query) { + break + } + filterOut := !filter(query, startVec, entry) + layerResult, err := ph.searchPersistentLayer( + c, level, entry, startVec, query, filterOut, ph.efSearch, filter) + if err != nil { + return ph.emptyFinalResultWithError(err) + } + layerResult.updateFinalMetrics(r) + entry = layerResult.bestNeighbor().index + layerResult.updateFinalPath(r) + err = ph.getVecFromUid(entry, c, &startVec) + if err != nil { + return ph.emptyFinalResultWithError(err) + } + } + filterOut := !filter(query, startVec, entry) + layerResult, err := ph.searchPersistentLayer( + c, ph.maxLevels-1, entry, startVec, query, filterOut, maxResults, filter) + if err != nil { + return ph.emptyFinalResultWithError(err) + } + layerResult.updateFinalMetrics(r) + layerResult.updateFinalPath(r) + layerResult.addFinalNeighbors(r) + t := time.Now().UnixMilli() + elapsed := t - start + r.Metrics[searchTime] = uint64(elapsed) + return r, nil +} + +// InsertToPersistentStorage inserts a node into the hnsw graph and returns the +// traversal path and the edges created +func (ph *persistentHNSW[T]) Insert(ctx context.Context, c index.CacheType, + inUuid uint64, inVec []T) ([]*index.KeyValue, error) { + tc, ok := c.(*TxnCache) + if !ok { + return []*index.KeyValue{}, nil + } + _, edges, err := ph.insertHelper(ctx, tc, inUuid, inVec) + return edges, err +} + +// InsertToPersistentStorage inserts a node into the hnsw graph and returns the +// traversal path and the edges created +func (ph *persistentHNSW[T]) insertHelper(ctx context.Context, tc *TxnCache, + inUuid uint64, inVec []T) ([]minPersistentHeapElement[T], []*index.KeyValue, error) { + + // return all the new edges created at all HNSW levels + var startVec []T + entry, edges, err := ph.createEntryAndStartNodes(ctx, tc, inUuid, &startVec) + if err != nil || len(edges) > 0 { + return []minPersistentHeapElement[T]{}, edges, err + } + + if entry == inUuid { + // something interesting is you physically cannot add duplicate nodes, + // it'll just overwrite w the same info + // only situation where you can add duplicate nodes is if your + // mutation adds the same node as entry + return []minPersistentHeapElement[T]{}, []*index.KeyValue{}, nil + } + + // startVecs: vectors used to calc where to start up until inLevel, + // nns: nearest neighbors to return, + // visited: all visited nodes + // var nns []minPersistentHeapElement[T] + visited := []minPersistentHeapElement[T]{} + inLevel := getInsertLayer(ph.maxLevels) // calculate layer to insert node at (randomized every time) + var layerErr error + + for level := 0; level < inLevel; level++ { + // perform insertion for layers [level, max_level) only, when level < inLevel just find better start + err := ph.getVecFromUid(entry, tc, &startVec) + if err != nil { + return []minPersistentHeapElement[T]{}, []*index.KeyValue{}, err + } + layerResult, err := ph.searchPersistentLayer(tc, level, entry, startVec, + inVec, false, 1, index.AcceptAll[T]) + if err != nil { + return []minPersistentHeapElement[T]{}, []*index.KeyValue{}, err + } + entry = layerResult.bestNeighbor().index + } + + emptyEdges := make([][]uint64, ph.maxLevels) + _, err = ph.addNeighbors(ctx, tc, inUuid, emptyEdges) + if err != nil { + return []minPersistentHeapElement[T]{}, []*index.KeyValue{}, err + } + + var outboundEdgesAllLayers = make([][]uint64, ph.maxLevels) + var inboundEdgesAllLayersMap = make(map[uint64][][]uint64) + nnUidArray := []uint64{} + for level := inLevel; level < ph.maxLevels; level++ { + err := ph.getVecFromUid(entry, tc, &startVec) + if err != nil { + return []minPersistentHeapElement[T]{}, []*index.KeyValue{}, err + } + layerResult, err := ph.searchPersistentLayer(tc, level, entry, startVec, + inVec, false, ph.efConstruction, index.AcceptAll[T]) + if err != nil { + return []minPersistentHeapElement[T]{}, []*index.KeyValue{}, layerErr + } + + nns := layerResult.neighbors + for i := 0; i < len(nns); i++ { + nnUidArray = append(nnUidArray, nns[i].index) + inboundEdgesAllLayersMap[nns[i].index] = make([][]uint64, ph.maxLevels) + inboundEdgesAllLayersMap[nns[i].index][level] = + append(inboundEdgesAllLayersMap[nns[i].index][level], inUuid) + // add nn to outboundEdges. + // These should already be correctly ordered. + outboundEdgesAllLayers[level] = + append(outboundEdgesAllLayers[level], nns[i].index) + } + } + edge, err := ph.addNeighbors(ctx, tc, inUuid, outboundEdgesAllLayers) + for i := 0; i < len(nnUidArray); i++ { + edge, err := ph.addNeighbors( + ctx, tc, nnUidArray[i], inboundEdgesAllLayersMap[nnUidArray[i]]) + if err != nil { + return []minPersistentHeapElement[T]{}, []*index.KeyValue{}, err + } + edges = append(edges, edge) + } + if err != nil { + return []minPersistentHeapElement[T]{}, []*index.KeyValue{}, err + } + edges = append(edges, edge) + + return visited, edges, nil +} diff --git a/tok/hnsw/persistent_hnsw_test.go b/tok/hnsw/persistent_hnsw_test.go new file mode 100644 index 00000000000..a4c813e7a5f --- /dev/null +++ b/tok/hnsw/persistent_hnsw_test.go @@ -0,0 +1,614 @@ +package hnsw + +import ( + "context" + "fmt" + "sync" + "testing" + + c "github.com/dgraph-io/dgraph/tok/constraints" + "github.com/dgraph-io/dgraph/tok/index" + opt "github.com/dgraph-io/dgraph/tok/options" + "golang.org/x/exp/slices" +) + +type createpersistentHNSWTest[T c.Float] struct { + maxLevels int + efSearch int + efConstruction int + pred string + indexType string + expectedIndexType string + floatBits int +} + +var createpersistentHNSWTests = []createpersistentHNSWTest[float64]{ + { + maxLevels: 1, + efSearch: 1, + efConstruction: 1, + pred: "a", + indexType: "b", + expectedIndexType: Euclidian, + floatBits: 64, + }, + { + maxLevels: 1, + efSearch: 1, + efConstruction: 1, + pred: "a", + indexType: Euclidian, + expectedIndexType: Euclidian, + floatBits: 64, + }, + { + maxLevels: 1, + efSearch: 1, + efConstruction: 1, + pred: "a", + indexType: Cosine, + expectedIndexType: Cosine, + floatBits: 64, + }, + { + maxLevels: 1, + efSearch: 1, + efConstruction: 1, + pred: "a", + indexType: DotProd, + expectedIndexType: DotProd, + floatBits: 64, + }, +} + +func optionsFromCreateTestCase[T c.Float](tc createpersistentHNSWTest[T]) opt.Options { + retVal := opt.NewOptions() + retVal.SetOpt(MaxLevelsOpt, tc.maxLevels) + retVal.SetOpt(EfSearchOpt, tc.efSearch) + retVal.SetOpt(EfConstructionOpt, tc.efConstruction) + retVal.SetOpt(MetricOpt, GetSimType[T](tc.indexType, tc.floatBits)) + return retVal +} + +func TestRaceCreateOrReplace(t *testing.T) { + f := CreateFactory[float64](64) + test := createpersistentHNSWTests[0] + opts := optionsFromCreateTestCase(test) + + var wg sync.WaitGroup + run := func() { + for i := 0; i < 10; i++ { + vIndex, err := f.CreateOrReplace(test.pred, opts, nil, 64) + if err != nil { + t.Errorf("Error creating index: %s for test case %d (%+v)", + err, i, test) + } + if vIndex == nil { + t.Errorf("TestCreatepersistentHNSW test case %d (%+v) generated nil index", + i, test) + } + } + wg.Done() + } + + for i := 0; i < 5; i++ { + wg.Add(1) + go run() + } + + wg.Wait() +} + +func TestCreatepersistentHNSW(t *testing.T) { + f := CreateFactory[float64](64) + for i, test := range createpersistentHNSWTests { + opts := optionsFromCreateTestCase(test) + vIndex, err := f.CreateOrReplace(test.pred, opts, nil, 64) + if err != nil { + t.Errorf("Error creating index: %s for test case %d (%+v)", + err, i, test) + return + } + if vIndex == nil { + t.Errorf("TestCreatepersistentHNSW test case %d (%+v) generated nil index", + i, test) + return + } + flatPh := vIndex.(*persistentHNSW[float64]) + if flatPh.simType.indexType != test.expectedIndexType { + t.Errorf("output %q not equal to expected %q", flatPh.simType.indexType, test.expectedIndexType) + return + } + } +} + +type flatInMemListAddMutationTest struct { + key string + startTs uint64 + finishTs uint64 + t *index.KeyValue + expectedErr error +} + +var flatInMemListAddMutationTests = []flatInMemListAddMutationTest{ + {key: "a", startTs: 0, finishTs: 5, t: &index.KeyValue{Value: []byte("abc")}, expectedErr: nil}, + {key: "b", startTs: 1, finishTs: 2, t: &index.KeyValue{Value: []byte("123")}, expectedErr: nil}, + {key: "c", startTs: 0, finishTs: 99, t: &index.KeyValue{Value: []byte("xyz")}, expectedErr: nil}, +} + +// TODO: It seriously seems wrong to have a transactional concept so tightly coupled with +// +// Dgraph product here! We should expect that we are using this module for completely +// independent use, possibly having nothing to do with Dgraph. +func flatInMemListWriteMutation(test flatInMemListAddMutationTest, t *testing.T) { + l := newInMemList(test.key, test.startTs, test.finishTs) + err := l.AddMutation(context.TODO(), nil, test.t) + if err != nil { + if err.Error() != test.expectedErr.Error() { + t.Errorf("Output %q not equal to expected %q", err.Error(), test.expectedErr.Error()) + } + } else { + if err != test.expectedErr { + t.Errorf("Output %q not equal to expected %q", err, test.expectedErr) + } + } + // should not modify db [test.startTs, test.finishTs) + if tsDbs[test.finishTs-1].inMemTestDb[test.key] != tsDbs[test.startTs].inMemTestDb[test.key] { + t.Errorf( + "Database at time %q not equal to expected database at time %q. Expected: %q, Got: %q", + test.finishTs-1, test.startTs, + tsDbs[test.startTs].inMemTestDb[test.key], + tsDbs[test.finishTs-1].inMemTestDb[test.key]) + } + if string(tsDbs[test.finishTs].inMemTestDb[test.key].([]byte)[:]) != string(test.t.Value[:]) { + t.Errorf("The database at time %q for key %q gave value of %q instead of %q", test.finishTs, + test.key, string(tsDbs[test.finishTs].inMemTestDb[test.key].([]byte)[:]), string(test.t.Value[:])) + } + if string(tsDbs[test.finishTs].inMemTestDb[test.key].([]byte)[:]) != + string(tsDbs[99].inMemTestDb[test.key].([]byte)[:]) { + t.Errorf("The database at time %q for key %q gave value of %q instead of %q", test.finishTs, + test.key, string(tsDbs[99].inMemTestDb[test.key].([]byte)[:]), + string(tsDbs[test.finishTs].inMemTestDb[test.key].([]byte)[:])) + } +} + +func TestFlatInMemListAddMutation(t *testing.T) { + emptyTsDbs() + for _, test := range flatInMemListAddMutationTests { + flatInMemListWriteMutation(test, t) + } +} + +var flatInMemListAddMutationOverwriteTests = []flatInMemListAddMutationTest{ + {key: "a", startTs: 0, finishTs: 5, t: &index.KeyValue{Value: []byte("abc")}, expectedErr: nil}, + {key: "a", startTs: 0, finishTs: 5, t: &index.KeyValue{Value: []byte("123")}, expectedErr: nil}, + {key: "a", startTs: 0, finishTs: 5, t: &index.KeyValue{Value: []byte("xyz")}, expectedErr: nil}, +} + +func TestFlatInMemListAddOverwriteMutation(t *testing.T) { + emptyTsDbs() + for _, test := range flatInMemListAddMutationOverwriteTests { + flatInMemListWriteMutation(test, t) + } +} + +type flatInMemListAddMutationTestBranchDependent struct { + key string + startTs uint64 + finishTs uint64 + t *index.KeyValue + expectedErr error + currIteration int +} + +var flatInMemListAddMultipleWritesMutationTests = []flatInMemListAddMutationTestBranchDependent{ + {key: "a", startTs: 0, finishTs: 2, t: &index.KeyValue{Value: []byte("abc")}, expectedErr: nil, currIteration: 0}, + {key: "a", startTs: 1, finishTs: 3, t: &index.KeyValue{Value: []byte("123")}, expectedErr: nil, currIteration: 1}, + {key: "a", startTs: 2, finishTs: 4, t: &index.KeyValue{Value: []byte("xyz")}, expectedErr: nil, currIteration: 2}, +} + +func TestFlatInMemListAddMultipleWritesMutation(t *testing.T) { + emptyTsDbs() + for _, test := range flatInMemListAddMultipleWritesMutationTests { + l := newInMemList(test.key, test.startTs, test.finishTs) + err := l.AddMutation(context.TODO(), nil, test.t) + if err != nil { + if err.Error() != test.expectedErr.Error() { + t.Errorf("Output %q not equal to expected %q", err.Error(), test.expectedErr.Error()) + } + } else { + if err != test.expectedErr { + t.Errorf("Output %q not equal to expected %q", err, test.expectedErr) + } + } + if test.currIteration == 0 { + conv := flatInMemListAddMutationTest{test.key, test.startTs, test.finishTs, test.t, test.expectedErr} + flatInMemListWriteMutation(conv, t) + } else { + if string(tsDbs[test.finishTs-1].inMemTestDb[test.key].([]byte)[:]) != + string(flatInMemListAddMultipleWritesMutationTests[test.currIteration-1].t.Value[:]) { + t.Errorf("The database at time %q for key %q gave value of %q instead of %q", test.finishTs, + test.key, string(tsDbs[test.finishTs].inMemTestDb[test.key].([]byte)[:]), string(test.t.Value[:])) + } + if string(tsDbs[test.finishTs].inMemTestDb[test.key].([]byte)[:]) != string(test.t.Value[:]) { + t.Errorf("The database at time %q for key %q gave value of %q instead of %q", test.finishTs, + test.key, string(tsDbs[test.finishTs].inMemTestDb[test.key].([]byte)[:]), string(test.t.Value[:])) + } + } + } +} + +type insertToPersistentFlatStorageTest struct { + tc *TxnCache + inUuid uint64 + inVec []float64 + expectedErr error + expectedEdgesList []string + minExpectedEdge string +} + +var flatPhs = []*persistentHNSW[float64]{ + { + maxLevels: 5, + efConstruction: 16, + efSearch: 12, + pred: "0-a", + vecEntryKey: ConcatStrings("0-a", VecEntry), + vecKey: ConcatStrings("0-a", VecKeyword), + vecDead: ConcatStrings("0-a", VecDead), + floatBits: 64, + simType: GetSimType[float64](Euclidian, 64), + nodeAllEdges: make(map[uint64][][]uint64), + }, + { + maxLevels: 5, + efConstruction: 16, + efSearch: 12, + pred: "0-a", + vecEntryKey: ConcatStrings("0-a", VecEntry), + vecKey: ConcatStrings("0-a", VecKeyword), + vecDead: ConcatStrings("0-a", VecDead), + floatBits: 64, + simType: GetSimType[float64](Cosine, 64), + nodeAllEdges: make(map[uint64][][]uint64), + }, + { + maxLevels: 5, + efConstruction: 16, + efSearch: 12, + pred: "0-a", + vecEntryKey: ConcatStrings("0-a", VecEntry), + vecKey: ConcatStrings("0-a", VecKeyword), + vecDead: ConcatStrings("0-a", VecDead), + floatBits: 64, + simType: GetSimType[float64](DotProd, 64), + nodeAllEdges: make(map[uint64][][]uint64), + }, +} + +var flatPh = &persistentHNSW[float64]{ + maxLevels: 5, + efConstruction: 16, + efSearch: 12, + pred: "0-a", + vecEntryKey: ConcatStrings("0-a", VecEntry), + vecKey: ConcatStrings("0-a", VecKeyword), + vecDead: ConcatStrings("0-a", VecDead), + floatBits: 64, + simType: GetSimType[float64](Euclidian, 64), + nodeAllEdges: make(map[uint64][][]uint64), +} + +var flatEntryInsertToPersistentFlatStorageTests = []insertToPersistentFlatStorageTest{ + { + tc: NewTxnCache(&inMemTxn{startTs: 12, commitTs: 40}, 12), + inUuid: uint64(123), + inVec: []float64{0.824, 0.319, 0.111}, + expectedErr: nil, + expectedEdgesList: []string{"0-a__vector__123", "0-a__vector_entry_1"}, + minExpectedEdge: "", + }, + { + tc: NewTxnCache(&inMemTxn{startTs: 11, commitTs: 37}, 11), + inUuid: uint64(1), + inVec: []float64{0.3, 0.5, 0.7}, + expectedErr: nil, + expectedEdgesList: []string{"0-a__vector__1", "0-a__vector_entry_1"}, + minExpectedEdge: "", + }, + { + tc: NewTxnCache(&inMemTxn{startTs: 0, commitTs: 1}, 0), + inUuid: uint64(5), + inVec: []float64{0.1, 0.1, 0.1}, + expectedErr: nil, + expectedEdgesList: []string{"0-a__vector__5", "0-a__vector_entry_1"}, + minExpectedEdge: "", + }, +} + +func TestFlatEntryInsertToPersistentFlatStorage(t *testing.T) { + emptyTsDbs() + for _, test := range flatEntryInsertToPersistentFlatStorageTests { + emptyTsDbs() + key := DataKey(flatPh.pred, test.inUuid) + for i := range tsDbs { + tsDbs[i].inMemTestDb[string(key[:])] = floatArrayAsBytes(test.inVec) + } + edges, err := flatPh.Insert(context.TODO(), test.tc, test.inUuid, test.inVec) + if err != nil { + if err.Error() != test.expectedErr.Error() { + t.Errorf("Output %q not equal to expected %q", err.Error(), test.expectedErr.Error()) + } + } else { + if err != test.expectedErr { + t.Errorf("Output %q not equal to expected %q", err, test.expectedErr) + } + } + var float1, float2 = []float64{}, []float64{} + index.BytesAsFloatArray(tsDbs[0].inMemTestDb[string(key[:])].([]byte), &float1, 64) + index.BytesAsFloatArray(tsDbs[99].inMemTestDb[string(key[:])].([]byte), &float2, 64) + if !equalFloat64Slice(float1, float2) { + t.Errorf("Vector value for predicate %q at beginning and end of database were "+ + "not equivalent. Start Value: %v, End Value: %v", flatPh.pred, tsDbs[0].inMemTestDb[flatPh.pred].([]float64), + tsDbs[99].inMemTestDb[flatPh.pred].([]float64)) + } + edgesNameList := []string{} + for _, edge := range edges { + edgeName := edge.Attr + "_" + fmt.Sprint(edge.Entity) + edgesNameList = append(edgesNameList, edgeName) + } + if !equalStringSlice(edgesNameList, test.expectedEdgesList) { + t.Errorf("Edges created during insert is incorrect. Expected: %v, Got: %v", test.expectedEdgesList, edgesNameList) + } + entryKey := DataKey(ConcatStrings(flatPh.pred, VecEntry), 1) + entryVal := BytesToUint64(tsDbs[99].inMemTestDb[string(entryKey[:])].([]byte)) + if entryVal != test.inUuid { + t.Errorf("entry value stored is incorrect. Expected: %q, Got: %q", test.inUuid, entryVal) + } + } +} + +var flatEntryInsert = insertToPersistentFlatStorageTest{ + tc: NewTxnCache(&inMemTxn{startTs: 0, commitTs: 1}, 0), + inUuid: uint64(5), + inVec: []float64{0.1, 0.1, 0.1}, + expectedErr: nil, + expectedEdgesList: []string{ + "0-a__vector__5", + "0-a__vector__5", + "0-a__vector__5", + "0-a__vector__5", + "0-a__vector__5", + "0-a__vector_entry_1", + }, + minExpectedEdge: "", +} + +var nonflatEntryInsertToPersistentFlatStorageTests = []insertToPersistentFlatStorageTest{ + { + tc: NewTxnCache(&inMemTxn{startTs: 12, commitTs: 40}, 12), + inUuid: uint64(123), + inVec: []float64{0.824, 0.319, 0.111}, + expectedErr: nil, + expectedEdgesList: []string{}, + minExpectedEdge: "0-a__vector__123", + }, + { + tc: NewTxnCache(&inMemTxn{startTs: 11, commitTs: 37}, 11), + inUuid: uint64(1), + inVec: []float64{0.3, 0.5, 0.7}, + expectedErr: nil, + expectedEdgesList: []string{}, + minExpectedEdge: "0-a__vector__1", + }, +} + +func TestNonflatEntryInsertToPersistentFlatStorage(t *testing.T) { + emptyTsDbs() + key := DataKey(flatPh.pred, flatEntryInsert.inUuid) + for i := range tsDbs { + tsDbs[i].inMemTestDb[string(key[:])] = floatArrayAsBytes(flatEntryInsert.inVec) + } + _, err := flatPh.Insert(context.TODO(), + flatEntryInsert.tc, + flatEntryInsert.inUuid, + flatEntryInsert.inVec) + if err != nil { + t.Errorf("Encountered error on initial insert: %s", err) + return + } + // testKey := DataKey(BuildDataKeyPred(flatPh.pred, VecKeyword, fmt.Sprint(0)), flatEntryInsert.inUuid) + // fmt.Print(tsDbs[1].inMemTestDb[string(testKey[:])]) + for _, test := range nonflatEntryInsertToPersistentFlatStorageTests { + entryKey := DataKey(ConcatStrings(flatPh.pred, VecEntry), 1) + entryVal := BytesToUint64(tsDbs[99].inMemTestDb[string(entryKey[:])].([]byte)) + if entryVal != 5 { + t.Errorf("entry value stored is incorrect. Expected: %q, Got: %q", 5, entryVal) + } + for i := range tsDbs { + key := DataKey(flatPh.pred, test.inUuid) + tsDbs[i].inMemTestDb[string(key[:])] = floatArrayAsBytes(test.inVec) + } + edges, err := flatPh.Insert(context.TODO(), test.tc, test.inUuid, test.inVec) + if err != nil && test.expectedErr != nil { + if err.Error() != test.expectedErr.Error() { + t.Errorf("Output %q not equal to expected %q", err.Error(), test.expectedErr.Error()) + } + } else { + if err != test.expectedErr { + t.Errorf("Output %q not equal to expected %q", err, test.expectedErr) + } + } + var float1, float2 = []float64{}, []float64{} + index.BytesAsFloatArray(tsDbs[0].inMemTestDb[string(key[:])].([]byte), &float1, 64) + index.BytesAsFloatArray(tsDbs[99].inMemTestDb[string(key[:])].([]byte), &float2, 64) + if !equalFloat64Slice(float1, float2) { + t.Errorf("Vector value for predicate %q at beginning and end of database were "+ + "not equivalent. Start Value: %v, End Value: %v", flatPh.pred, tsDbs[0].inMemTestDb[flatPh.pred].([]float64), + tsDbs[99].inMemTestDb[flatPh.pred].([]float64)) + } + edgesNameList := []string{} + for _, edge := range edges { + edgeName := edge.Attr + "_" + fmt.Sprint(edge.Entity) + edgesNameList = append(edgesNameList, edgeName) + } + if !slices.Contains(edgesNameList, test.minExpectedEdge) { + t.Errorf("Expected at least %q in list of edges %v", test.minExpectedEdge, edgesNameList) + } + } +} + +type searchPersistentFlatStorageTest struct { + qc *QueryCache + query []float64 + maxResults int + expectedErr error + expectedNns []uint64 +} + +var searchPersistentFlatStorageTests = []searchPersistentFlatStorageTest{ + { + qc: NewQueryCache(&inMemLocalCache{readTs: 45}, 45), + query: []float64{0.3, 0.5, 0.7}, + maxResults: 1, + expectedErr: nil, + expectedNns: []uint64{1}, + }, + { + qc: NewQueryCache(&inMemLocalCache{readTs: 93}, 93), + query: []float64{0.824, 0.319, 0.111}, + maxResults: 1, + expectedErr: nil, + expectedNns: []uint64{5}, + }, +} + +var flatPopulateBasicInsertsForSearch = []insertToPersistentFlatStorageTest{ + { + tc: NewTxnCache(&inMemTxn{startTs: 0, commitTs: 1}, 0), + inUuid: uint64(5), + inVec: []float64{0.1, 0.1, 0.1}, + expectedErr: nil, + expectedEdgesList: nil, + minExpectedEdge: "", + }, + { + tc: NewTxnCache(&inMemTxn{startTs: 11, commitTs: 15}, 11), + inUuid: uint64(123), + inVec: []float64{0.824, 0.319, 0.111}, + expectedErr: nil, + expectedEdgesList: nil, + minExpectedEdge: "", + }, + { + tc: NewTxnCache(&inMemTxn{startTs: 20, commitTs: 37}, 20), + inUuid: uint64(1), + inVec: []float64{0.3, 0.5, 0.7}, + expectedErr: nil, + expectedEdgesList: nil, + minExpectedEdge: "", + }, +} + +func flatPopulateInserts(insertArr []insertToPersistentFlatStorageTest) error { + emptyTsDbs() + for _, in := range insertArr { + for i := range tsDbs { + key := DataKey(flatPh.pred, in.inUuid) + tsDbs[i].inMemTestDb[string(key[:])] = floatArrayAsBytes(in.inVec) + } + _, err := flatPh.Insert(context.TODO(), in.tc, in.inUuid, in.inVec) + if err != nil { + return err + } + } + return nil +} + +func RunFlatSearchTests(t *testing.T, test searchPersistentFlatStorageTest, flatPh *persistentHNSW[float64]) { + nns, err := flatPh.Search(context.TODO(), test.qc, test.query, test.maxResults, index.AcceptAll[float64]) + if err != nil && test.expectedErr != nil { + if err.Error() != test.expectedErr.Error() { + t.Errorf("Output %q not equal to expected %q", err.Error(), test.expectedErr.Error()) + } + } else { + if err != test.expectedErr { + t.Errorf("Output %q not equal to expected %q", err, test.expectedErr) + } + } + if !equalUint64Slice(nns, test.expectedNns) { + t.Errorf("Nearest neighbors expected value: %v, Got: %v", test.expectedNns, nns) + } +} + +func TestBasicSearchPersistentFlatStorage(t *testing.T) { + for _, flatPh := range flatPhs { + emptyTsDbs() + err := flatPopulateInserts(flatPopulateBasicInsertsForSearch) + if err != nil { + t.Errorf("Error populating inserts: %s", err) + return + } + for _, test := range searchPersistentFlatStorageTests { + RunFlatSearchTests(t, test, flatPh) + } + } +} + +var flatPopulateOverlappingInserts = []insertToPersistentFlatStorageTest{ + { + tc: NewTxnCache(&inMemTxn{startTs: 0, commitTs: 5}, 0), + inUuid: uint64(5), + inVec: []float64{0.1, 0.1, 0.1}, + expectedErr: nil, + expectedEdgesList: nil, + minExpectedEdge: "", + }, + { + tc: NewTxnCache(&inMemTxn{startTs: 3, commitTs: 9}, 3), + inUuid: uint64(123), + inVec: []float64{0.824, 0.319, 0.111}, + expectedErr: nil, + expectedEdgesList: nil, + minExpectedEdge: "", + }, + { + tc: NewTxnCache(&inMemTxn{startTs: 8, commitTs: 37}, 8), + inUuid: uint64(1), + inVec: []float64{0.3, 0.5, 0.7}, + expectedErr: nil, + expectedEdgesList: nil, + minExpectedEdge: "", + }, +} + +var overlappingSearchPersistentFlatStorageTests = []searchPersistentFlatStorageTest{ + { + qc: NewQueryCache(&inMemLocalCache{readTs: 45}, 45), + query: []float64{0.3, 0.5, 0.7}, + maxResults: 1, + expectedErr: nil, + expectedNns: []uint64{123}, + }, + { + qc: NewQueryCache(&inMemLocalCache{readTs: 93}, 93), + query: []float64{0.824, 0.319, 0.111}, + maxResults: 1, + expectedErr: nil, + expectedNns: []uint64{123}, + }, +} + +func TestOverlappingInsertsAndSearchPersistentFlatStorage(t *testing.T) { + for _, flatPh := range flatPhs { + emptyTsDbs() + err := flatPopulateInserts(flatPopulateOverlappingInserts) + if err != nil { + t.Errorf("Error from flatPopulateInserts: %s", err) + return + } + for _, test := range overlappingSearchPersistentFlatStorageTests { + RunFlatSearchTests(t, test, flatPh) + } + } +} diff --git a/tok/hnsw/search_layer.go b/tok/hnsw/search_layer.go new file mode 100644 index 00000000000..49f129648bb --- /dev/null +++ b/tok/hnsw/search_layer.go @@ -0,0 +1,120 @@ +package hnsw + +import ( + c "github.com/dgraph-io/dgraph/tok/constraints" + "github.com/dgraph-io/dgraph/tok/index" + + "fmt" +) + +type searchLayerResult[T c.Float] struct { + // neighbors represents the candidates with the best scores so far. + neighbors []minPersistentHeapElement[T] + // visited represents elements seen (so we don't try to re-visit). + visited []minPersistentHeapElement[T] + path []uint64 + metrics map[string]uint64 + level int + // filtered represents num elements of meighbors that don't + // belong in final return set since they should be filtered out. + // When we encounter a node that we consider a "best" node, but where + // it should be filtered out, we allow it to enter the "neighbors" + // attribute as an element. However, we then allow neighbors to + // grow by this extra "filtered" amount. Theoretically, it could be + // pushed out, but that will be okay! At the end, we grab all + // non-filtered elements up to the limit of what is expected. + filtered int +} + +func newLayerResult[T c.Float](level int) *searchLayerResult[T] { + return &searchLayerResult[T]{ + neighbors: []minPersistentHeapElement[T]{}, + visited: []minPersistentHeapElement[T]{}, + path: []uint64{}, + metrics: make(map[string]uint64), + level: level, + } +} + +func (slr *searchLayerResult[T]) setFirstPathNode(n minPersistentHeapElement[T]) { + slr.neighbors = []minPersistentHeapElement[T]{n} + slr.visited = []minPersistentHeapElement[T]{n} + slr.path = []uint64{n.index} +} + +func (slr *searchLayerResult[T]) addPathNode( + n minPersistentHeapElement[T], + simType SimilarityType[T], + maxResults int) { + slr.neighbors = simType.insortHeap(slr.neighbors, n) + if n.filteredOut { + slr.filtered++ + } + effectiveMaxLen := maxResults + slr.filtered + if len(slr.neighbors) > effectiveMaxLen { + slr.neighbors = slr.neighbors[:effectiveMaxLen] + } + + if slr.neighbors[0].index == n.index { + slr.path = append(slr.path, slr.neighbors[0].index) + } +} + +func (slr *searchLayerResult[T]) numNeighbors() int { + return len(slr.neighbors) - slr.filtered +} + +func (slr *searchLayerResult[T]) markFirstDistanceComputation() { + slr.metrics[distanceComputations] = 1 +} + +func (slr *searchLayerResult[T]) incrementDistanceComputations() { + slr.metrics[distanceComputations]++ +} + +// slr.lastNeighborScore() returns the "score" (based on similarity type) +// of the last neighbor being tracked. The score is reflected as a value +// of the minPersistentHeapElement. +// If slr is empty, this will panic. +func (slr *searchLayerResult[T]) lastNeighborScore() T { + return slr.neighbors[len(slr.neighbors)-1].value +} + +// slr.bestNeighbor() returns the heap element with the "best" score. +// panics if there is no such element. +func (slr *searchLayerResult[T]) bestNeighbor() minPersistentHeapElement[T] { + return slr.neighbors[0] +} + +func (slr *searchLayerResult[T]) nodeVisited(n minPersistentHeapElement[T]) bool { + for _, visitedNode := range slr.visited { + if visitedNode.index == n.index { + return true + } + } + return false +} + +func (slr *searchLayerResult[T]) addToVisited(n minPersistentHeapElement[T]) { + slr.visited = append(slr.visited, n) +} + +func (slr *searchLayerResult[T]) updateFinalMetrics(r *index.SearchPathResult) { + visitName := ConcatStrings(visitedVectorsLevel, fmt.Sprint(slr.level)) + r.Metrics[visitName] += uint64(len(slr.visited)) + for k, v := range slr.metrics { + r.Metrics[k] += v + } +} + +func (slr *searchLayerResult[T]) updateFinalPath(r *index.SearchPathResult) { + r.Path = append(r.Path, slr.path...) +} + +func (slr *searchLayerResult[T]) addFinalNeighbors(r *index.SearchPathResult) { + for _, n := range slr.neighbors { + if !n.filteredOut { + r.Neighbors = append(r.Neighbors, n.index) + } + } +} diff --git a/tok/hnsw/test_helper.go b/tok/hnsw/test_helper.go new file mode 100644 index 00000000000..036ac3f03e4 --- /dev/null +++ b/tok/hnsw/test_helper.go @@ -0,0 +1,281 @@ +package hnsw + +import ( + "context" + "encoding/binary" + "math" + "strings" + "sync" + + "github.com/dgraph-io/dgraph/tok/index" + "github.com/pkg/errors" +) + +// holds an map in memory that is a string (which will be []bytes as string) +// as the key, with an index.Val as the value +type indexStorage struct { + inMemTestDb map[string]index.Value + + //Two locks allow for lock promotion when writing, so we promote a read lock + //between the start and finish times to a full lock on the finish time + + // readMu acquires read locks when accessing values + readMu sync.RWMutex + // writeMu acquires write locks on mutations + writeMu sync.Mutex +} + +// datastructure visualization of persistent db over 100 units of time +// within this, we will conduct all testing, i.e. reads at 1 Ts = tsDbs[1], +// writes at 4 Ts = tsDbs[4] +var tsDbs [100]indexStorage + +func emptyTsDbs() { + for i := range tsDbs { + tsDbs[i] = indexStorage{inMemTestDb: make(map[string]index.Value)} + } +} + +type inMemList struct { + key string + startTs uint64 + finishTs uint64 +} + +// creates a new inMem list with the list's corresponding key, +// when it's action was started and when it will conclude. +// for mutations startTs will be txn.StartTs and finishTs will be txn.commitTs +// for reads, they both start and finish at c.ReadTs +// finishTs is unknown in real scenarios, this is for testing purposes +func newInMemList(key string, startTs, finishTs uint64) *inMemList { + return &inMemList{ + key: key, + startTs: startTs, + finishTs: finishTs, + } +} + +// locks the posting list & invokes ValueWithLockHeld +func (l *inMemList) Value(readTs uint64) (rval index.Value, rerr error) { + // reading should only lock the db at current instance in time + tsDbs[readTs].readMu.RLock() + defer tsDbs[readTs].readMu.RUnlock() + return l.ValueWithLockHeld(readTs) +} + +// reads value from the database at readTs corresponding to List's key +func (l *inMemList) ValueWithLockHeld(readTs uint64) (rval index.Value, rerr error) { + val, ok := tsDbs[readTs].inMemTestDb[l.key] + if !ok { + return nil, errors.New("Could not find data with key " + l.key) + } + return val, nil +} + +// locks the posting list and invokes AddMutationWithLockHeld +func (l *inMemList) AddMutation(ctx context.Context, txn index.Txn, t *index.KeyValue) error { + // locks from the txn.StartTs up to txn.CommitTs + l.Lock() + defer l.Unlock() + return l.AddMutationWithLockHeld(ctx, txn, t) +} + +// adds mutation to the database at the txn's commitTs +func (l *inMemList) AddMutationWithLockHeld(ctx context.Context, txn index.Txn, t *index.KeyValue) error { + // creates key from directedEdge + //builds value + val := t.Value + // a mutation persists from the moment the txn gets committed until the "rest of time" + for i := l.finishTs; i < uint64(len(tsDbs)); i++ { + tsDbs[i].inMemTestDb[l.key] = val + } + return nil +} + +// if youre locking at a certain point in time, the lock should be held for this moment +// and all future moments until your commitTs +func (l *inMemList) Lock() { + if !strings.Contains(l.key, "entry") { + for i := l.startTs; i <= l.finishTs; i++ { + tsDbs[i].readMu.RLock() + } + for i := l.finishTs; i < uint64(len(tsDbs)); i++ { + tsDbs[i].writeMu.Lock() + } + } +} + +// undoes lock +func (l *inMemList) Unlock() { + if !strings.Contains(l.key, "entry") { + for i := l.startTs; i <= l.finishTs; i++ { + tsDbs[i].readMu.RUnlock() + } + for i := l.finishTs; i < uint64(len(tsDbs)); i++ { + tsDbs[i].writeMu.Unlock() + } + } +} + +// a txn has a startTs (when the txn started) and commitTs (when the txn changes were committed) +type inMemTxn struct { + startTs uint64 + commitTs uint64 +} + +func (t *inMemTxn) Find(prefix []byte, filter func([]byte) bool) (uint64, error) { + tsDb := tsDbs[t.startTs] + tsDb.readMu.RLock() + defer tsDb.readMu.RUnlock() + for _, b := range tsDb.inMemTestDb { + if filter(b.([]byte)) { + return 1, nil + } + } + return 0, nil +} + +func (t *inMemTxn) StartTs() uint64 { + return t.startTs +} + +// locks the txn and invokes GetWithLockHeld +func (t *inMemTxn) Get(key []byte) (rval index.Value, rerr error) { + tsDbs[t.startTs].readMu.RLock() + defer tsDbs[t.startTs].readMu.RUnlock() + return t.GetWithLockHeld(key) +} + +// reads value from the database at txn's startTs +func (t *inMemTxn) GetWithLockHeld(key []byte) (rval index.Value, rerr error) { + val, ok := tsDbs[t.startTs].inMemTestDb[string(key[:])] + if !ok { + return nil, errors.New("Could not find data with key " + string(key[:])) + } + return val, nil +} + +// locks the txn and invokes AddMutationWithLockHeld +func (t *inMemTxn) AddMutation(ctx context.Context, key []byte, t1 *index.KeyValue) error { + tsDbs[t.startTs].writeMu.Lock() + defer tsDbs[t.startTs].writeMu.Unlock() + return t.AddMutationWithLockHeld(ctx, key, t1) +} + +// adds mutation to the database at the txn's commitTs +func (t *inMemTxn) AddMutationWithLockHeld(ctx context.Context, key []byte, t1 *index.KeyValue) error { + val := t1.Value + for i := t.commitTs; i < uint64(len(tsDbs)); i++ { + tsDbs[i].inMemTestDb[string(key[:])] = val + } + return nil +} + +// locks the txn +func (t *inMemTxn) LockKey(key []byte) { + if !strings.Contains(string(key[:]), "entry") { + // locks from the txn.StartTs up to txn.CommitTs + for i := t.startTs; i <= t.commitTs; i++ { + tsDbs[i].readMu.RLock() + } + for i := t.commitTs; i < uint64(len(tsDbs)); i++ { + tsDbs[i].writeMu.Lock() + } + } +} + +// undoes lock +func (t *inMemTxn) UnlockKey(key []byte) { + if !strings.Contains(string(key[:]), "entry") { + // locks from the txn.StartTs up to txn.CommitTs + for i := t.startTs; i <= t.commitTs; i++ { + tsDbs[i].readMu.RUnlock() + } + for i := t.commitTs; i < uint64(len(tsDbs)); i++ { + tsDbs[i].writeMu.Unlock() + } + } +} + +type inMemLocalCache struct { + readTs uint64 +} + +// locks the local cache and invokes GetWithLockHeld +func (c *inMemLocalCache) Get(key []byte) (rval index.Value, rerr error) { + tsDbs[c.readTs].readMu.RLock() + defer tsDbs[c.readTs].readMu.RUnlock() + return c.GetWithLockHeld(key) +} + +func (c *inMemLocalCache) Find(prefix []byte, filter func([]byte) bool) (uint64, error) { + tsDb := tsDbs[c.readTs] + tsDb.readMu.RLock() + defer tsDb.readMu.RUnlock() + for _, b := range tsDb.inMemTestDb { + if filter(b.([]byte)) { + return 1, nil + } + } + return 0, nil +} + +// reads value from the database at c's readTs +func (c *inMemLocalCache) GetWithLockHeld(key []byte) (rval index.Value, rerr error) { + val, ok := tsDbs[c.readTs].inMemTestDb[string(key[:])] + if !ok { + return nil, errors.New("Could not find data with key " + string(key[:])) + } + return val, nil +} + +func equalFloat64Slice(a, b []float64) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func equalStringSlice(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func equalUint64Slice(a, b []uint64) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +// floatArrayAsBytes(v) will create a byte array encoding +// v using LittleEndian format. This is sort of the inverse +// of BytesAsFloatArray, but note that we can always be successful +// converting to bytes, but the inverse is not feasible. +func floatArrayAsBytes(v []float64) []byte { + retVal := make([]byte, 8*len(v)) + offset := retVal + for i := 0; i < len(v); i++ { + bits := math.Float64bits(v[i]) + binary.LittleEndian.PutUint64(offset, bits) + offset = offset[8:] + } + return retVal +} diff --git a/tok/index/helper.go b/tok/index/helper.go new file mode 100644 index 00000000000..200645d9fdc --- /dev/null +++ b/tok/index/helper.go @@ -0,0 +1,79 @@ +/* + * Copyright 2023 Hypermode, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package index + +import ( + "encoding/binary" + "math" + + c "github.com/dgraph-io/dgraph/tok/constraints" +) + +// BytesAsFloatArray[T c.Float](encoded) converts encoded into a []T, +// where T is either float32 or float64, depending on the value of floatBits. +// Let floatBytes = floatBits/8. If len(encoded) % floatBytes is +// not 0, it will ignore any trailing bytes, and simply convert floatBytes +// bytes at a time to generate the entries. +// The result is appended to the given retVal slice. If retVal is nil +// then a new slice is created and appended to. +func BytesAsFloatArray[T c.Float](encoded []byte, retVal *[]T, floatBits int) { + // Unfortunately, this is not as simple as casting the result, + // and it is also not possible to directly use the + // golang "unsafe" library to directly do the conversion. + // The machine where this operation gets run might prefer + // BigEndian/LittleEndian, but the machine that sent it may have + // preferred the other, and there is no way to tell! + // + // The solution below, unfortunately, requires another memory + // allocation. + // TODO Potential optimization: If we detect that current machine is + // using LittleEndian format, there might be a way of making this + // work with the golang "unsafe" library. + floatBytes := floatBits / 8 + + *retVal = (*retVal)[:0] + resultLen := len(encoded) / floatBytes + if resultLen == 0 { + return + } + for i := 0; i < resultLen; i++ { + // Assume LittleEndian for encoding since this is + // the assumption elsewhere when reading from client. + // See dgraph-io/dgo/protos/api.pb.go + // See also dgraph-io/dgraph/types/conversion.go + // This also seems to be the preference from many examples + // I have found via Google search. It's unclear why this + // should be a preference. + if retVal == nil { + retVal = &[]T{} + } + *retVal = append(*retVal, BytesToFloat[T](encoded, floatBits)) + + encoded = encoded[(floatBytes):] + } +} + +func BytesToFloat[T c.Float](encoded []byte, floatBits int) T { + if floatBits == 32 { + bits := binary.LittleEndian.Uint32(encoded) + return T(math.Float32frombits(bits)) + } else if floatBits == 64 { + bits := binary.LittleEndian.Uint64(encoded) + return T(math.Float64frombits(bits)) + } + panic("Invalid floatBits") +} diff --git a/tok/index/index.go b/tok/index/index.go new file mode 100644 index 00000000000..65caf65a8b5 --- /dev/null +++ b/tok/index/index.go @@ -0,0 +1,167 @@ +/* + * Copyright 2023 Hypermode, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package index + +import ( + "context" + + c "github.com/dgraph-io/dgraph/tok/constraints" + opts "github.com/dgraph-io/dgraph/tok/options" +) + +// IndexFactory is responsible for being able to create, find, and remove +// VectorIndexes. There is no "update" as of now; just remove and create. +// +// It is expected that the IndexFactory has some notion of persistence, but +// it is perfectly happy to support a total in-memory solution. To achieve +// persistence, it is the responsibility of the implementations of IndexFactory +// to reference the persistent storage. +type IndexFactory[T c.Float] interface { + // The Name returned represents the name of the factory rather than the + // name of any particular index. + Name() string + + // Specifies the set of allowed options and a corresponding means to + // parse a string version of those options. + AllowedOptions() opts.AllowedOptions + + // Create is expected to create a VectorIndex, or generate an error + // if the name already has a corresponding VectorIndex or other problems. + // The name will be associated with with the generated VectorIndex + // such that if we Create an index with the name "foo", then later + // attempt to find the index with name "foo", it will refer to the + // same object. + // The set of vectors to use in the index process is defined by + // source. + Create(name string, o opts.Options, floatBits int) (VectorIndex[T], error) + + // Find is expected to retrieve the VectorIndex corresponding with the + // name. If it attempts to find a name that does not exist, the VectorIndex + // will return as a nil value. It should throw an error in persistent storage + // issues when accessing information. + Find(name string) (VectorIndex[T], error) + + // Remove is expected to delete the VectorIndex corresponding with the name. + // If removing a name that doesn't exist, nothing will happen and no errors + // are thrown. An error should only be thrown if there is issues accessing + // persistent storage information. + Remove(name string) error + + // CreateOrReplace will create a new index -- as defined by the Create + // function -- if it does not yet exist, otherwise, it will replace any + // index with the given name. + CreateOrReplace(name string, o opts.Options, floatBits int) (VectorIndex[T], error) +} + +// SearchFilter defines a predicate function that we will use to determine +// whether or not a given vector is "interesting". When used in the context +// of VectorIndex.Search, a true result means that we want to keep the result +// in the returned list, and a false result implies we should skip. +type SearchFilter[T c.Float] func(query, resultVal []T, resultUID uint64) bool + +// AcceptAll implements SearchFilter by way of accepting all results. +func AcceptAll[T c.Float](_, _ []T, _ uint64) bool { return true } + +// AcceptNone implements SearchFilter by way of rejecting all results. +func AcceptNone[T c.Float](_, _ []T, _ uint64) bool { return false } + +// OptionalIndexSupport defines abilities that might not be universally +// supported by all VectorIndex types. A VectorIndex will technically +// define the functions required by OptionalIndexSupport, but may do so +// by way of simply returning an errors.ErrUnsupported result. +type OptionalIndexSupport[T c.Float] interface { + // SearchWithPath(ctx, c, query, maxResults, filter) is similar to + // Search(ctx, c, query, maxResults, filter), but returns an extended + // set of content in the search results. + // The full contents returned are indicated by the SearchPathResult. + // See the description there for more info. + SearchWithPath( + ctx context.Context, + c CacheType, + query []T, + maxResults int, + filter SearchFilter[T]) (*SearchPathResult, error) +} + +// A VectorIndex can be used to Search for vectors and add vectors to an index. +type VectorIndex[T c.Float] interface { + OptionalIndexSupport[T] + + // Search will find the uids for a given set of vectors based on the + // input query, limiting to the specified maximum number of results. + // The filter parameter indicates that we might discard certain parameters + // based on some input criteria. The maxResults count is counted *after* + // being filtered. In other words, we only count those results that had not + // been filtered out. + Search(ctx context.Context, c CacheType, query []T, + maxResults int, + filter SearchFilter[T]) ([]uint64, error) + + // SearchWithUid will find the uids for a given set of vectors based on the + // input queryUid, limiting to the specified maximum number of results. + // The filter parameter indicates that we might discard certain parameters + // based on some input criteria. The maxResults count is counted *after* + // being filtered. In other words, we only count those results that had not + // been filtered out. + SearchWithUid(ctx context.Context, c CacheType, queryUid uint64, + maxResults int, + filter SearchFilter[T]) ([]uint64, error) + + // Insert will add a vector and uuid into the existing VectorIndex. If + // uuid already exists, it should throw an error to not insert duplicate uuids + Insert(ctx context.Context, c CacheType, uuid uint64, vec []T) ([]*KeyValue, error) +} + +// A Txn is an interface representation of a persistent storage transaction, +// where multiple operations are performed on a database +type Txn interface { + // StartTs gets the exact time that the transaction started, returned in uint64 format + StartTs() uint64 + // Get uses a []byte key to return the Value corresponding to the key + Get(key []byte) (rval Value, rerr error) + // GetWithLockHeld uses a []byte key to return the Value corresponding to the key with a mutex lock held + GetWithLockHeld(key []byte) (rval Value, rerr error) + Find(prefix []byte, filter func(val []byte) bool) (uint64, error) + // Adds a mutation operation on a index.Txn interface, where the mutation + // is represented in the form of an index.DirectedEdge + AddMutation(ctx context.Context, key []byte, t *KeyValue) error + // Same as AddMutation but with a mutex lock held + AddMutationWithLockHeld(ctx context.Context, key []byte, t *KeyValue) error + // mutex lock + LockKey(key []byte) + // mutex unlock + UnlockKey(key []byte) +} + +// Local cache is an interface representation of the local cache of a persistent storage system +type LocalCache interface { + // Get uses a []byte key to return the Value corresponding to the key + Get(key []byte) (rval Value, rerr error) + // GetWithLockHeld uses a []byte key to return the Value corresponding to the key with a mutex lock held + GetWithLockHeld(key []byte) (rval Value, rerr error) + Find(prefix []byte, filter func(val []byte) bool) (uint64, error) +} + +// Value is an interface representation of the value of a persistent storage system +type Value interface{} + +// CacheType is an interface representation of the cache of a persistent storage system +type CacheType interface { + Get(key []byte) (rval Value, rerr error) + Ts() uint64 + Find(prefix []byte, filter func(val []byte) bool) (uint64, error) +} diff --git a/tok/index/search_path.go b/tok/index/search_path.go new file mode 100644 index 00000000000..ccd41f732fb --- /dev/null +++ b/tok/index/search_path.go @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Hypermode, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package index + +// SearchPathResult is the return-type for the optional +// SearchWithPath function for a VectorIndex +// (by way of extending OptionalIndexSupport). +type SearchPathResult struct { + // The collection of nearest-neighbors in sorted order after filtlering + // out neighbors that fail any Filter criteria. + Neighbors []uint64 + // The path from the start of search to the closest neighbor vector. + Path []uint64 + // A collection of captured named counters that occurred for the + // particular search. + Metrics map[string]uint64 +} + +// NewSearchPathResult() provides an initialized (empty) *SearchPathResult. +// The attributes will be non-nil, but empty. +func NewSearchPathResult() *SearchPathResult { + return &SearchPathResult{ + Neighbors: []uint64{}, + Path: []uint64{}, + Metrics: make(map[string]uint64), + } +} diff --git a/tok/index/types.go b/tok/index/types.go new file mode 100644 index 00000000000..9c5a0df2091 --- /dev/null +++ b/tok/index/types.go @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Hypermode, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package index + +// TypeID represents the type of the data. +type TypeID Posting_ValType +type Posting_ValType int32 + +type KeyValue struct { + // Entity is the uid of the key to be built + Entity uint64 `protobuf:"fixed64,1,opt,name=entity,proto3" json:"entity,omitempty"` + // Attr is the attribute of the key to be built + Attr string `protobuf:"bytes,2,opt,name=attr,proto3" json:"attr,omitempty"` + // Value is the value corresponding to the key built from entity & attr + Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` +} diff --git a/tok/index_factory.go b/tok/index_factory.go new file mode 100644 index 00000000000..9994d99f15b --- /dev/null +++ b/tok/index_factory.go @@ -0,0 +1,120 @@ +/* + * Copyright 2016-2023 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tok + +import ( + "fmt" + + "github.com/pkg/errors" + + "github.com/dgraph-io/dgraph/tok/index" + opts "github.com/dgraph-io/dgraph/tok/options" +) + +// registerIndexFactory(f) will register f as both a Tokenizer and specifically +// as an IndexFactory. +func registerIndexFactory(f IndexFactory) { + // Note: All necessary checks for duplication, etc. is done in + // registerTokenizers. Since we add IndexFactory instances + // to both tokenizers map and indexFactories map, it suffices + // to just check the tokenizers map for uniqueness. + registerTokenizer(f) + indexFactories[f.Name()] = f +} + +// IndexFactory combines the notion of a Tokenizer with +// index.IndexFactory. We register IndexFactory instances just +// like we register Tokenizers. +type IndexFactory interface { + Tokenizer + // TODO: Distinguish between float64 and float32, allowing either. + // Default should be float32. + index.IndexFactory[float32] +} + +// FactoryCreateSpec includes an IndexFactory and the options required +// to instantiate a VectorIndex of the given type. +// In short, everything that is needed in order to create a VectorIndex! +type FactoryCreateSpec struct { + factory IndexFactory + opts opts.Options +} + +func (fcs *FactoryCreateSpec) CreateIndex(name string) (index.VectorIndex[float32], error) { + if fcs == nil || fcs.factory == nil { + return nil, + errors.Errorf( + "cannot create Index for '%s' with nil factory", + name) + } + + // TODO: What we should really be doing here is a "Find it, and if found, + // replace it *only if* the options have changed!" However, there + // is currently no way to introspect the options. + // We cheat for the moment and simply do a CreateOrReplace. + // This avoids us getting into duplicate create conflicts, but + // has the downside of not allowing us to reuse the pre-existing + // index. + // nil VectorSource at the moment. + return fcs.factory.CreateOrReplace(name, fcs.opts, 32) +} + +func createIndexFactory(f index.IndexFactory[float32]) IndexFactory { + return &indexFactory{delegate: f} +} + +type indexFactory struct { + delegate index.IndexFactory[float32] +} + +func (f *indexFactory) Name() string { return f.delegate.Name() } +func (f *indexFactory) AllowedOptions() opts.AllowedOptions { + return f.delegate.AllowedOptions() +} +func (f *indexFactory) Create( + name string, + o opts.Options, + floatBits int) (index.VectorIndex[float32], error) { + return f.delegate.Create(name, o, floatBits) +} +func (f *indexFactory) Find(name string) (index.VectorIndex[float32], error) { + return f.delegate.Find(name) +} +func (f *indexFactory) Remove(name string) error { + return f.delegate.Remove(name) +} +func (f *indexFactory) CreateOrReplace( + name string, + o opts.Options, + floatBits int) (index.VectorIndex[float32], error) { + return f.delegate.CreateOrReplace(name, o, floatBits) +} + +func (f *indexFactory) Type() string { + return "float32vector" +} +func (f *indexFactory) Tokens(v interface{}) ([]string, error) { + return tokensForExpectedVFloat(v) +} +func (f *indexFactory) Identifier() byte { return IdentVFloat } +func (f *indexFactory) IsSortable() bool { return false } +func (f *indexFactory) IsLossy() bool { return true } + +func tokensForExpectedVFloat(v interface{}) ([]string, error) { + // If there is a vfloat, we can only allow one mutation at a time + return []string{fmt.Sprint("float")}, nil +} diff --git a/tok/options/README.md b/tok/options/README.md new file mode 100644 index 00000000000..c13ba4e3e6e --- /dev/null +++ b/tok/options/README.md @@ -0,0 +1,71 @@ +// TODO: Consider moving this to a shared location, if it ends up getting used +// by more than one package. Currently, the target is primarily the +// vector-indexer, but it might also be applicable to the planned +// vector-compression algorithms. + +For a plugin mechanism, it is critical that all of the starting points for the +plugin have exactly the same function signature so that we can reasonably find +it and invoke it. How then, do we allow for supporting different construction +parameters given that different plugins might have different needs? The options +package is intended to be an answer to that question. + +It's best to understand this package by looking at the first target use-case, +which is to specify a Vector Index plugin. Our intent is to start with the +factory: for a given kind of Vector Index, you need to have a factory to Create +and Remove VectorIndexes or to find an index that already exists. + +If we only cared about the HNSW-type of VectorIndex, this would not be an issue, +but if we want the ability to support different kinds of VectorIndex +implementations, or to let customers specify their own, we need to make sure +that we have a consistent mechanism for creating a factory that applies to all +of them -- despite the fact that internally, they might all care about very +different kinds of parameters. + +Our choice here is to let the factory itself be built using no arguments, but +for the VectorIndex lifetime operations (specifically, the operations involved +in creating a new VectorIndex) to specify a set of "generic" options. + +Hence, we now see the point of the "Options" type: It is basically a map +of option name to a specific value. + +In Dgraph, we have the abiltity to specify an @index directive for the type +vfloat. Here, we want to expand that capability to allow us to specify something +like: + +myAttribute @index("hnsw-euclidian","maxNeighbors":"5","maxLevels":"5","exponent":"6") + +Roughly, this should be read as: +I want to specify an HNSW-type vector index using euclidian distance as a metric +with maxNeighbors, maxLevels, and exponent being declared as written, and with +all other parameters taking on default values. (In the case of specifying an +exponent, this should affect the defaults for the other options, but that is +something that can be decided by the factory for the hnsw index)! + +But this leads to some natural questions: How do I know what option types are +allowed? And how do I interpret their types? For example, how do I know that I +should interpret "5" as an integer rather than a string? + +The solution will be to allow each factory to specify a set of "allowed option +types". Hence, we get the AllowedOptions class, which specifies a set of +named options and corresponding parsers for the given name. + +Now, if we want to populate an Options instance based on the content of +myAttribute @index(hnsw(metric:"euclidian",exponent:"6")), + +we collect the key-value pairs: + "metric":"euclidian" + "exponent":"6" + +And we can now invoke: +allowedOpts := hnswFactory.AllowedOpts() +myAttributeIndexOpts := NewOptions() + +val, err := allowedOpts.GetParsedOption("exponent", "6") +if err != nil { + return ErrBadOptionNoBiscuit +} +myAttributeIndexOpts.SetOpt("exponent", val) + +The final resolution of the "puzzle" in fact is to invoke +allowedOpts.PopulateOptions(pairs, myAttributeIndexOpts) +based on pairs being built from the collection of key-value pairs. diff --git a/tok/options/allowed_options.go b/tok/options/allowed_options.go new file mode 100644 index 00000000000..703d8b31571 --- /dev/null +++ b/tok/options/allowed_options.go @@ -0,0 +1,121 @@ +/* + * Copyright 2023 DGraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package options + +import ( + "fmt" +) + +// AllowedOptions specifies the collection of allowed option kinds, +// and specifies how to interpret the values. +// The assumption is that at some point, options are being specified +// by some kind of key-value pair in a string, such as: "dataWidth:8". +// In this case, for a given index, if we want to form the desired +// options, we need to know that: +// 1. "dataWidth" is an allowed option +// 2. how to interpret the string "8". +// +// In this case, to an AllowedOptions type, we would add the key "dataWidth" +// and an IntParser as the value (or whichever data type is appropriate for +// the dataWidth option). +type AllowedOptions map[string]OptionParser + +func NewAllowedOptions() AllowedOptions { + return AllowedOptions(make(map[string]OptionParser)) +} + +// OptionValue pair represents the use of a particular option with a +// string representation of its intended value. It is expected that one +// might originally encounter a string representation of the value, but +// then might need to interpret the intended value for the option. +type OptionValuePair struct { + Option string + Value string +} + +// AddIntOption(optName) will specify that optName is an allowed option, +// and that the values will be interepreted by the standard IntOptionParser. +func (ao AllowedOptions) AddIntOption(optName string) AllowedOptions { + ao[optName] = IntOptParser + return ao +} + +// AddStringOption(optName) will specify that optName is an allowed option, +// and that the values will be interepreted by the standard StringOptionParser. +func (ao AllowedOptions) AddStringOption(optName string) AllowedOptions { + ao[optName] = StringOptParser + return ao +} + +// AddFloat64Option(optName) will specify that optName is an allowed option, +// and that the values will be interepreted by the standard Float64OptionParser. +func (ao AllowedOptions) AddFloat64Option(optName string) AllowedOptions { + ao[optName] = Float64OptParser + return ao +} + +// AddCustomOption(optName) will specify that optName is an allowed option, +// and that the values will be interpreted by the provided parser. +func (ao AllowedOptions) AddCustomOption(optName string, parser OptionParser) AllowedOptions { + ao[optName] = parser + return ao +} + +// ao.GetOptionList() provides the list of named options for ao. +func (ao AllowedOptions) GetOptionList() []string { + keys := make([]string, len(ao)) + i := 0 + for k := range ao { + keys[i] = k + } + return keys +} + +// ao.GetParsedOption(optName, optValue) will parse optValue based on the +// parser associated with optName in ao. Note that if ao does not specify +// a parser for optName, this will panic! +func (ao AllowedOptions) GetParsedOption(optName, optValue string) (any, error) { + parser, ok := ao[optName] + if !ok { + return nil, fmt.Errorf("option %s is not allowed!", optName) + } + return parser(optValue) +} + +// ao.ParsePair(pair) is shorthand for: +// +// ao.GetParsedOption(pair.Option, pair.Value) +func (ao AllowedOptions) ParsePair(pair OptionValuePair) (any, error) { + return ao.GetParsedOption(pair.Option, pair.Value) +} + +// ao.PopulateOptions(pairs, opts) will use ao to interpret pairs and +// use the results to populate opts. This will look at each OptionValuePair in +// pairs, and if the value can be properly parsed, it will add the result to +// opts for the given option and result. If one or more of the options cannot +// be properly interpreted, this results in an error. +func (ao AllowedOptions) PopulateOptions(pairs []OptionValuePair, opts Options) error { + for _, p := range pairs { + val, err := ao.ParsePair(p) + if err != nil { + // TODO: Perhaps wrap the error with a bit more context?? + return err + } + opts.SetOpt(p.Option, val) + } + return nil +} diff --git a/tok/options/option_parser.go b/tok/options/option_parser.go new file mode 100644 index 00000000000..ad6cbba0ffe --- /dev/null +++ b/tok/options/option_parser.go @@ -0,0 +1,68 @@ +/* + * Copyright 2023 DGraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package options + +import ( + "strconv" +) + +// OptionParser translates a string to a value (assuming that the option +// value is well-formed). See AllowedOptions to see the purpose. +type OptionParser func(optValue string) (any, error) + +// TODO: Provide other built-in OptionParser implementations for other +// simple types. + +// IntOptParser implements OptionParser specifically translating +// int options. It assumes decimal representation of the value. +func IntOptParser(optValue string) (any, error) { + return strconv.Atoi(optValue) +} + +// UintOptParser implement OptionParser specifically translating +// uint options. It assumes decimal representation of the value. +func UintOptParser(optValue string) (any, error) { + // Specified originally as 64 bit for conversion, since it is + // not trivial (as far as I can tell) to determine the + // uint bit size. Simpler to just get it back as 64 bit then + // cast it. + retVal, err := strconv.ParseUint(optValue, 10, 64) + if err != nil { + return 0, err + } + return uint(retVal), nil +} + +// StringOptionParser implements OptionParser specifically translating +// string options (well, it actually just passes in the string value as-is with +// no modification, but having this allows us to simply implement the +// OptionParser interface. +func StringOptParser(optValue string) (any, error) { + return optValue, nil +} + +// Float64OptParser implements OptionParser specifically translating +// float64 options. +func Float64OptParser(optValue string) (any, error) { + return strconv.ParseFloat(optValue, 64) +} + +// Float32OptParser implements OptionParser specifically translating +// float32 options. +func Float32OptParser(optValue string) (any, error) { + return strconv.ParseFloat(optValue, 32) +} diff --git a/tok/options/options.go b/tok/options/options.go new file mode 100644 index 00000000000..a61755bb0cb --- /dev/null +++ b/tok/options/options.go @@ -0,0 +1,92 @@ +/* + * Copyright 2023 DGraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package options + +import ( + "fmt" + + c "github.com/dgraph-io/dgraph/tok/constraints" +) + +// An Options instance maps the various named options to their specific values. +// This is intended as a generic mechanism to specify construction options. +// Note that the value types can be mixed: i.e., we might store both an int +// option value and a string option value in the same map. +type Options map[string]any + +// NewOptions() creates a new instance of Options, allowing a client to +// then modify it to specify whichever packaged options desired. +func NewOptions() Options { + return make(map[string]any) +} + +// SetOpt(o, whichOpt, value) will associate whichOpt with value. +func (o Options) SetOpt(whichOpt string, value any) Options { + o[whichOpt] = value + return o +} + +// GetOpt(o, whichOpt, defaultValue) will attempt to retrieve the +// value associated with whichOpt in o. +// The values returned represent the retrieved (or default) value, +// whether or not the value was found, and +// possibly an error if there was a type conversion error. +// If o does not contain the key whichOpt: +// +// This returns (defaultValue, false, nil) +// +// If o finds a value for whichOpt, but the type does not match the type for +// defaultValue: +// +// This returns (defaultValue, true, A Non-Nil-Error) +// +// If o find a value "v" for whichOpt, and the value matches the type for +// defaultValue: +// +// This returns (v, true, nil). +func GetOpt[T c.Simple](o Options, whichOpt string, defaultValue T) (T, bool, error) { + val, ok := o[whichOpt] + if !ok { + return defaultValue, false, nil + } + assigned, ok := val.(T) + if !ok { + return defaultValue, true, + fmt.Errorf("unable to assign a value of type %T to type %T", + val, defaultValue) + } + return assigned, true, nil +} + +// GetInterfaceOpt(o, whichOpt) will retrieve the option associated with +// whichOpt from o. If the value is found, this returns the found value and true, +// otherwise, this returns nil and false. +// It is the caller's responsibility to make sure that the option type stored +// is compatible with the variable to which it is being assigned. +// Similarly, it is the client responsibility to handle possible default +// values if the expected value is not found. +func GetInterfaceOpt(o Options, whichOpt string) (any, bool) { + val, ok := o[whichOpt] + return val, ok +} + +// o.Specifies(whichOpt) returns true if whichOpt has been assigned a value +// in o, and false otherwise. +func (o Options) Specifies(whichOpt string) bool { + _, found := o[whichOpt] + return found +} diff --git a/tok/options/options_test.go b/tok/options/options_test.go new file mode 100644 index 00000000000..09a2497e077 --- /dev/null +++ b/tok/options/options_test.go @@ -0,0 +1,67 @@ +/* + * Copyright 2023 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package options + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSimpleOptions(t *testing.T) { + o := NewOptions() + o.SetOpt("fish", 17) + floatval, found, err := GetOpt(o, "fish", 32.2) + require.Equal(t, floatval, 32.2) + require.True(t, found) + require.NotNil(t, err) + intval, found, err := GetOpt(o, "fish", 0) + require.Equal(t, intval, 17) + require.True(t, found) + require.Nil(t, err) + stringval, found, err := GetOpt(o, "cat", "fishcat") + require.Equal(t, stringval, "fishcat") + require.False(t, found) + require.Nil(t, err) +} + +type fooBarType interface { + DoFoo(fooey int) int + DoBar(barry int) int +} + +type FooBarImpl struct{} + +func (fb FooBarImpl) DoFoo(fooey int) int { + return fooey +} + +func (fb FooBarImpl) DoBar(barry int) int { + return barry + 2 +} + +func TestInterfaceOptions(t *testing.T) { + o := NewOptions() + var fb fooBarType = FooBarImpl{} + o.SetOpt("foob", fb) + x, found := GetInterfaceOpt(o, "foob") + require.Equal(t, x, fb) + require.True(t, found) + y, found := GetInterfaceOpt(o, "barb") + require.Nil(t, y) + require.False(t, found) +} diff --git a/tok/tok.go b/tok/tok.go index 961a160aaad..1ee88f1ec20 100644 --- a/tok/tok.go +++ b/tok/tok.go @@ -28,8 +28,11 @@ import ( "golang.org/x/crypto/blake2b" "golang.org/x/text/collate" + "github.com/dgraph-io/dgraph/protos/pb" "github.com/dgraph-io/dgraph/types" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/dgraph/tok/hnsw" + opts "github.com/dgraph-io/dgraph/tok/options" ) // Tokenizer identifiers are unique and can't be reused. @@ -53,6 +56,8 @@ const ( IdentTrigram = 0xA IdentHash = 0xB IdentSha = 0xC + // Reserving 0xD for IdentBigFloat + IdentVFloat = 0xE IdentCustom = 0x80 IdentDelimiter = 0x1f // ASCII 31 - Unit separator ) @@ -85,8 +90,10 @@ type Tokenizer interface { } var tokenizers = make(map[string]Tokenizer) +var indexFactories = make(map[string]IndexFactory) func init() { + registerIndexFactory(createIndexFactory(hnsw.CreateFactory[float32](32))) registerTokenizer(GeoTokenizer{}) registerTokenizer(IntTokenizer{}) registerTokenizer(FloatTokenizer{}) @@ -154,6 +161,63 @@ func GetTokenizer(name string) (Tokenizer, bool) { return t, found } +// GetIndexFactory returns IndexFactory given name. +func GetIndexFactory(name string) (IndexFactory, bool) { + f, found := indexFactories[name] + return f, found +} + +func getOptsFromFactorySpec(f IndexFactory, spec *pb.VectorSpec) (opts.Options, error) { + allowedOpts := f.AllowedOptions() + retVal := opts.NewOptions() + for _, optPair := range spec.Options { + val, err := allowedOpts.GetParsedOption(optPair.Key, optPair.Value) + if err != nil { + return nil, err + } + retVal.SetOpt(optPair.Key, val) + } + return retVal, nil +} + +func GetFactoryCreateSpecFromSpec(spec *pb.VectorSpec) (*FactoryCreateSpec, error) { + factory, found := GetIndexFactoryFromSpec(spec) + if !found { + return &FactoryCreateSpec{}, errors.Errorf( + "cannot find index factory named '%s'", spec.Name) + } + opts, err := getOptsFromFactorySpec(factory, spec) + if err != nil { + return &FactoryCreateSpec{}, err + } + return &FactoryCreateSpec{factory: factory, opts: opts}, nil +} + +func GetIndexFactoryFromSpec(spec *pb.VectorSpec) (IndexFactory, bool) { + return GetIndexFactory(spec.Name) +} + +func GetIndexFactoryOptsFromSpec(spec *pb.VectorSpec) (opts.Options, error) { + factory, found := GetIndexFactoryFromSpec(spec) + if !found { + return nil, errors.Errorf( + "cannot get Options for factory named '%s' (factory not found)", + spec.Name) + } + return getOptsFromFactorySpec(factory, spec) +} + +func GetIndexFactoriesFromSpecs(specs []*pb.VectorSpec) []IndexFactory { + retVal := []IndexFactory{} + for _, spec := range specs { + f, found := GetIndexFactoryFromSpec(spec) + if found { + retVal = append(retVal, f) + } + } + return retVal +} + // GetTokenizers returns a list of tokenizer given a list of unique names. func GetTokenizers(names []string) ([]Tokenizer, error) { var tokenizers []Tokenizer