From ed38e47a9ece35d7c0b19eaf84ef9f1dc6d2b860 Mon Sep 17 00:00:00 2001 From: TQSH-Dev Date: Sat, 21 Feb 2026 12:02:02 +0000 Subject: [PATCH] MDEV-35327: Implement Manhattan distance for HNSW vector index --- mariadb-plugin-columnstore.install.generated | 2 + mysql-test/main/mdev_35327.result | 70 ++++++++++++++++++ mysql-test/main/mdev_35327.test | 58 +++++++++++++++ mysql-test/main/mysqld--help.result | 2 +- .../sys_vars/r/sysvars_server_embedded.result | 2 +- .../r/sysvars_server_notembedded.result | 2 +- sql/item_create.cc | 20 +++++ sql/item_vectorfunc.cc | 14 ++++ sql/item_vectorfunc.h | 5 +- sql/vector_mhnsw.cc | 17 +++-- .../connect/connect_jars/JdbcInterface.jar | Bin 0 -> 7602 bytes 11 files changed, 182 insertions(+), 10 deletions(-) create mode 100644 mysql-test/main/mdev_35327.result create mode 100644 mysql-test/main/mdev_35327.test create mode 100644 storage/connect/connect_jars/JdbcInterface.jar diff --git a/mariadb-plugin-columnstore.install.generated b/mariadb-plugin-columnstore.install.generated index d987525f2a671..2b1434e58b9a1 100644 --- a/mariadb-plugin-columnstore.install.generated +++ b/mariadb-plugin-columnstore.install.generated @@ -1 +1,3 @@ #File is generated by ColumnstoreLibrary.cmake, do not edit +etc/mysql/columnstore.cnf # added in dbcon/mysql/CMakeLists.txt +usr/local/mysql/lib/plugin/ha_columnstore.so # added in dbcon/mysql/CMakeLists.txt diff --git a/mysql-test/main/mdev_35327.result b/mysql-test/main/mdev_35327.result new file mode 100644 index 0000000000000..3100a51ba08dd --- /dev/null +++ b/mysql-test/main/mdev_35327.result @@ -0,0 +1,70 @@ +# +# MDEV-35327: Add VEC_DISTANCE_MANHATTAN function +# +# +# Checking for argument validity +# +SELECT VEC_DISTANCE_MANHATTAN(VEC_FromText('[1,2]')); +ERROR 42000: Incorrect parameter count in the call to native function 'VEC_DISTANCE_MANHATTAN' +SELECT VEC_DISTANCE_MANHATTAN(NULL, VEC_FromText('[1,2]')); +VEC_DISTANCE_MANHATTAN(NULL, VEC_FromText('[1,2]')) +NULL +# Checking for mismatched dimensions +SELECT VEC_DISTANCE_MANHATTAN(VEC_FromText('[1,1,1]'),VEC_FromText('[1,2]')); +VEC_DISTANCE_MANHATTAN(VEC_FromText('[1,1,1]'),VEC_FromText('[1,2]')) +NULL +# +# Basic math check +# +SELECT VEC_DISTANCE_MANHATTAN(VEC_FromText('[1,2,3]'), VEC_FromText('[2,3,4]')); +VEC_DISTANCE_MANHATTAN(VEC_FromText('[1,2,3]'), VEC_FromText('[2,3,4]')) +3 +# +# Without Vector Index +# +CREATE TABLE t1 (id INT, v VECTOR(3) NOT NULL); +INSERT INTO t1 VALUES (1, VEC_FromText('[2,2,2]')), (2, VEC_FromText('[0,0,5]')), (3, VEC_FromText('[1,1,1]')); +# Manhattan distance:- 6,5,3 Euclidean distance:- 3.46,5,1.73 +# Manhattan | Euclidean +# P3 P3 +# P2 P1 +# P1 P2 +# output should be 3,5,6 and ordering should be P3 < P2 < P1 +SELECT id, VEC_DISTANCE_MANHATTAN(v, VEC_FromText('[0,0,0]')) as dist FROM t1 ORDER BY dist; +id dist +3 3 +2 5 +1 6 +# Comparison with Euclidean distance +SELECT id, VEC_DISTANCE_EUCLIDEAN(v, VEC_FromText('[0,0,0]')) as dist FROM t1 ORDER BY dist; +id dist +3 1.7320508075688772 +1 3.4641016151377544 +2 5 +# +# With Vector Index +# +CREATE VECTOR INDEX idx ON t1(v) DISTANCE=manhattan; +# Output should be 3,5 and 6 again +SELECT id, VEC_DISTANCE_MANHATTAN(v, VEC_FromText('[0,0,0]')) as dist FROM t1 ORDER BY dist LIMIT 3; +id dist +3 3 +2 5 +1 6 +# Checking if the vector index is actually implemented using manhattan distance +EXPLAIN SELECT id FROM t1 FORCE INDEX (idx) +ORDER BY VEC_DISTANCE_MANHATTAN(v, VEC_FromText('[0,0,0]')) LIMIT 1; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 index NULL idx 14 NULL 1 +# Cleanup +DROP TABLE t1; +# Miscellaneous Tests +SELECT VEC_DISTANCE_MANHATTAN(VEC_FromText('[-1,-1]'), VEC_FromText('[1,1]')) as neg_test; +neg_test +4 +SELECT VEC_DISTANCE_MANHATTAN(VEC_FromText('[1.5, 2.5]'), VEC_FromText('[1.5, 2.5]')) as zero_dist; +zero_dist +0 +SELECT VEC_DISTANCE_MANHATTAN(VEC_FromText('[1.1]'), VEC_FromText('[2.2]')) as float_test; +float_test +1.100000023841858 diff --git a/mysql-test/main/mdev_35327.test b/mysql-test/main/mdev_35327.test new file mode 100644 index 0000000000000..e3132ac9b5d4f --- /dev/null +++ b/mysql-test/main/mdev_35327.test @@ -0,0 +1,58 @@ +--echo # +--echo # MDEV-35327: Add VEC_DISTANCE_MANHATTAN function +--echo # + +--echo # +--echo # Checking for argument validity +--echo # +--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT +SELECT VEC_DISTANCE_MANHATTAN(VEC_FromText('[1,2]')); +SELECT VEC_DISTANCE_MANHATTAN(NULL, VEC_FromText('[1,2]')); +--echo # Checking for mismatched dimensions +SELECT VEC_DISTANCE_MANHATTAN(VEC_FromText('[1,1,1]'),VEC_FromText('[1,2]')); + +--echo # +--echo # Basic math check +--echo # +SELECT VEC_DISTANCE_MANHATTAN(VEC_FromText('[1,2,3]'), VEC_FromText('[2,3,4]')); + + +--echo # +--echo # Without Vector Index +--echo # +CREATE TABLE t1 (id INT, v VECTOR(3) NOT NULL); +INSERT INTO t1 VALUES (1, VEC_FromText('[2,2,2]')), (2, VEC_FromText('[0,0,5]')), (3, VEC_FromText('[1,1,1]')); + +--echo # Manhattan distance:- 6,5,3 Euclidean distance:- 3.46,5,1.73 +--echo # Manhattan | Euclidean +--echo # P3 P3 +--echo # P2 P1 +--echo # P1 P2 +--echo # output should be 3,5,6 and ordering should be P3 < P2 < P1 + +SELECT id, VEC_DISTANCE_MANHATTAN(v, VEC_FromText('[0,0,0]')) as dist FROM t1 ORDER BY dist; +--echo # Comparison with Euclidean distance +SELECT id, VEC_DISTANCE_EUCLIDEAN(v, VEC_FromText('[0,0,0]')) as dist FROM t1 ORDER BY dist; + +--echo # +--echo # With Vector Index +--echo # +CREATE VECTOR INDEX idx ON t1(v) DISTANCE=manhattan; + +--echo # Output should be 3,5 and 6 again +SELECT id, VEC_DISTANCE_MANHATTAN(v, VEC_FromText('[0,0,0]')) as dist FROM t1 ORDER BY dist LIMIT 3; + +--echo # Checking if the vector index is actually implemented using manhattan distance +EXPLAIN SELECT id FROM t1 FORCE INDEX (idx) +ORDER BY VEC_DISTANCE_MANHATTAN(v, VEC_FromText('[0,0,0]')) LIMIT 1; + +--echo # Cleanup +DROP TABLE t1; + +--echo # Miscellaneous Tests + +SELECT VEC_DISTANCE_MANHATTAN(VEC_FromText('[-1,-1]'), VEC_FromText('[1,1]')) as neg_test; + +SELECT VEC_DISTANCE_MANHATTAN(VEC_FromText('[1.5, 2.5]'), VEC_FromText('[1.5, 2.5]')) as zero_dist; + +SELECT VEC_DISTANCE_MANHATTAN(VEC_FromText('[1.1]'), VEC_FromText('[2.2]')) as float_test; diff --git a/mysql-test/main/mysqld--help.result b/mysql-test/main/mysqld--help.result index cb87a7ba8f1e6..7b16b9d02687c 100644 --- a/mysql-test/main/mysqld--help.result +++ b/mysql-test/main/mysqld--help.result @@ -799,7 +799,7 @@ The following specify which files/extra groups are read (specified before remain Supported MDL namespaces: BACKUP --mhnsw-default-distance=name Distance function to build the vector index for. One of: - euclidean, cosine + euclidean, cosine, manhattan --mhnsw-default-m=# Larger values mean slower SELECTs and INSERTs, larger index size and higher memory consumption but more accurate results diff --git a/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result b/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result index 3344eea6148c3..7cd25cbdbe00a 100644 --- a/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result +++ b/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result @@ -2229,7 +2229,7 @@ VARIABLE_COMMENT Distance function to build the vector index for NUMERIC_MIN_VALUE NULL NUMERIC_MAX_VALUE NULL NUMERIC_BLOCK_SIZE NULL -ENUM_VALUE_LIST euclidean,cosine +ENUM_VALUE_LIST euclidean,cosine,manhattan READ_ONLY NO COMMAND_LINE_ARGUMENT REQUIRED VARIABLE_NAME MHNSW_DEFAULT_M diff --git a/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result b/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result index 95d90b797e0bf..2f071973bb822 100644 --- a/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result +++ b/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result @@ -2469,7 +2469,7 @@ VARIABLE_COMMENT Distance function to build the vector index for NUMERIC_MIN_VALUE NULL NUMERIC_MAX_VALUE NULL NUMERIC_BLOCK_SIZE NULL -ENUM_VALUE_LIST euclidean,cosine +ENUM_VALUE_LIST euclidean,cosine,manhattan READ_ONLY NO COMMAND_LINE_ARGUMENT REQUIRED VARIABLE_NAME MHNSW_DEFAULT_M diff --git a/sql/item_create.cc b/sql/item_create.cc index f707607e1e84a..13bda3165bcb2 100644 --- a/sql/item_create.cc +++ b/sql/item_create.cc @@ -6237,6 +6237,24 @@ class Create_func_vec_distance_cosine: public Create_func_arg2 Create_func_vec_distance_cosine Create_func_vec_distance_cosine::s_singleton; + +class Create_func_vec_distance_manhattan: public Create_func_arg2 +{ +public: + Item *create_2_arg(THD *thd, Item *arg1, Item *arg2) override + { return new (thd->mem_root) + Item_func_vec_distance(thd, arg1, arg2, Item_func_vec_distance::MANHATTAN); } + + static Create_func_vec_distance_manhattan s_singleton; + +protected: + Create_func_vec_distance_manhattan() = default; + virtual ~Create_func_vec_distance_manhattan() = default; +}; + +Create_func_vec_distance_manhattan Create_func_vec_distance_manhattan::s_singleton; + + class Create_func_vec_distance: public Create_func_arg2 { public: @@ -6251,6 +6269,7 @@ class Create_func_vec_distance: public Create_func_arg2 virtual ~Create_func_vec_distance() = default; }; + Create_func_vec_distance Create_func_vec_distance::s_singleton; class Create_func_vec_totext: public Create_func_arg1 @@ -6516,6 +6535,7 @@ const Native_func_registry func_array[] = { { STRING_WITH_LEN("UUID_SHORT") }, BUILDER(Create_func_uuid_short)}, { { STRING_WITH_LEN("VEC_DISTANCE_EUCLIDEAN") }, BUILDER(Create_func_vec_distance_euclidean)}, { { STRING_WITH_LEN("VEC_DISTANCE_COSINE") }, BUILDER(Create_func_vec_distance_cosine)}, + { { STRING_WITH_LEN("VEC_DISTANCE_MANHATTAN") }, BUILDER(Create_func_vec_distance_manhattan)}, { { STRING_WITH_LEN("VEC_DISTANCE") }, BUILDER(Create_func_vec_distance)}, { { STRING_WITH_LEN("VEC_FROMTEXT") }, BUILDER(Create_func_vec_fromtext)}, { { STRING_WITH_LEN("VEC_TOTEXT") }, BUILDER(Create_func_vec_totext)}, diff --git a/sql/item_vectorfunc.cc b/sql/item_vectorfunc.cc index 354405c3b5fe2..6a716817d9691 100644 --- a/sql/item_vectorfunc.cc +++ b/sql/item_vectorfunc.cc @@ -48,6 +48,17 @@ static double calc_distance_cosine(float *v1, float *v2, size_t v_len) return 1 - dotp/sqrt(abs1*abs2); } +static double calc_distance_manhattan(float *v1, float *v2, size_t v_len) +{ + double d= 0; + for (size_t i= 0; i < v_len; i++, v1++, v2++) + { + double dist= abs(get_float(v1) - get_float(v2)); + d+= dist; + } + return d; +} + Item_func_vec_distance::Item_func_vec_distance(THD *thd, Item *a, Item *b, distance_kind kind) :Item_real_func(thd, a, b), kind(kind) @@ -59,6 +70,7 @@ bool Item_func_vec_distance::fix_length_and_dec(THD *thd) switch (kind) { case EUCLIDEAN: calc_distance= calc_distance_euclidean; break; case COSINE: calc_distance= calc_distance_cosine; break; + case MANHATTAN: calc_distance= calc_distance_manhattan; break; case AUTO: for (uint i=0; i < 2; i++) if (auto *item= dynamic_cast(args[i]->real_item())) @@ -90,10 +102,12 @@ key_map Item_func_vec_distance::part_of_sortkey() const Field *f= item->field; KEY *keyinfo= f->table->s->key_info; for (uint i= f->table->s->keys; i < f->table->s->total_keys; i++) + { if (!keyinfo[i].is_ignored && keyinfo[i].algorithm == HA_KEY_ALG_VECTOR && f->key_start.is_set(i) && mhnsw_uses_distance(f->table, keyinfo + i) == kind) map.set_bit(i); + } } return map; } diff --git a/sql/item_vectorfunc.h b/sql/item_vectorfunc.h index bcff3daa7dfb2..72f2c76d227f9 100644 --- a/sql/item_vectorfunc.h +++ b/sql/item_vectorfunc.h @@ -39,13 +39,14 @@ class Item_func_vec_distance: public Item_real_func double (*calc_distance)(float *v1, float *v2, size_t v_len); public: - enum distance_kind { EUCLIDEAN, COSINE, AUTO } kind; + enum distance_kind { EUCLIDEAN, COSINE, MANHATTAN, AUTO } kind; Item_func_vec_distance(THD *thd, Item *a, Item *b, distance_kind kind); LEX_CSTRING func_name_cstring() const override { - static LEX_CSTRING name[3]= { + static LEX_CSTRING name[4]= { { STRING_WITH_LEN("VEC_DISTANCE_EUCLIDEAN") }, { STRING_WITH_LEN("VEC_DISTANCE_COSINE") }, + { STRING_WITH_LEN("VEC_DISTANCE_MANHATTAN")}, { STRING_WITH_LEN("VEC_DISTANCE") } }; return name[kind]; diff --git a/sql/vector_mhnsw.cc b/sql/vector_mhnsw.cc index d640363b6e76a..add99f84b5373 100644 --- a/sql/vector_mhnsw.cc +++ b/sql/vector_mhnsw.cc @@ -104,8 +104,8 @@ static MYSQL_THDVAR_UINT(default_m, PLUGIN_VAR_RQCMDARG, "and higher memory consumption but more accurate results", nullptr, nullptr, 6, 3, 200, 1); -enum metric_type : uint { EUCLIDEAN, COSINE }; -static const char *distance_names[]= { "euclidean", "cosine", nullptr }; +enum metric_type : uint { EUCLIDEAN, COSINE, MANHATTAN }; +static const char *distance_names[]= { "euclidean", "cosine", "manhattan", nullptr }; static TYPELIB distances= CREATE_TYPELIB_FOR(distance_names); static MYSQL_THDVAR_ENUM(default_distance, PLUGIN_VAR_RQCMDARG, "Distance function to build the vector index for", @@ -1749,9 +1749,16 @@ const LEX_CSTRING mhnsw_hlindex_table_def(THD *thd, uint ref_length) Item_func_vec_distance::distance_kind mhnsw_uses_distance(const TABLE *table, KEY *keyinfo) { - if (keyinfo->option_struct->metric == EUCLIDEAN) - return Item_func_vec_distance::EUCLIDEAN; - return Item_func_vec_distance::COSINE; + switch (keyinfo->option_struct->metric) { + case EUCLIDEAN: + return Item_func_vec_distance::EUCLIDEAN; + case MANHATTAN: + return Item_func_vec_distance::MANHATTAN; + case COSINE: + return Item_func_vec_distance::COSINE; + default: + return Item_func_vec_distance::COSINE; + } } /* diff --git a/storage/connect/connect_jars/JdbcInterface.jar b/storage/connect/connect_jars/JdbcInterface.jar new file mode 100644 index 0000000000000000000000000000000000000000..548d821cb789b7aa012051d94683ce325a8e5677 GIT binary patch literal 7602 zcmaKxWl$VS7p5UV2qZuP1PPWPgAOi(hY%Q?!6CT24Ge=5TnCrnA!u+XNYLPJgS)%C zZSHRE_ubm9-P6_8r|P^%e)Ny-dLXiB=-4PIFHlfmN@_YN|1vBTbQF0BRWYEnf+X8x z9|{WOuapqw#fumJmz3~t+JB_-VhYld5-O^!@{(ur1O0NcK-N(lSs>HUz;Kl^`vlkW z29#Aso<&-I6joTR%>FEFoAe!nMF1dSGa=sU6nX+h{o4uih3vhu5a@4RzQ6mwot^)W zA<6$n;ZQ>d2UDmMn~aH(v9z7DDb&o+*p$`S#?Z+rT^q|lb+Z2PQ7~?RwqEPo>rX`N z`_fV$-=NN*<0pO$`G|r+OdjhSLCa-m_G4L(3TA0*%?E5kewmZGSSgh%XVsmk9k#M9 zQa`AwDN?_iwVG5{TDr>gFz@%fwy1{>@ju-RUmZQwSBu^(AB&?9X`63$_;q8JA;{OU zCDW;1US0+no$N3R*11%F#?P9rH2_OR3yP9s(Gy`e)uit6ZgxZoQJumxe7~>HFT1tv zEVT7p?&RAE^G(FOcD4E&kAY_)s0m*}Q<7L`%5~nS!dwCnUx$deF$h*%J-`|T^xNH1 zzAeBn1Yw;&IC9^aW|L>TU5;rM(>LqBwveZq$Os9fncHI2%|$S&5_|!3fmSU3ypF7KB)5{9uCK{gfg}KIZ~=bW{wE{55bA;!oEuj%0`{2olh1e(?~(!b zis%Gc84y~-ZNS_&zb}2$d#}~=v8Mif`rW2kz~v$S8}ZZPwPuAY(HtzxRUiICcpV~N z#X-@1cCD#1s*Wd?gbIJIaBi4~h+}8&e}B-pCY1a<^pZ}w0Sg?;LM{yn%`zBn@*HUE z6&a}Ez&pdp-C2-@!vfsZeEjYY-1USg=M~^2TH1WmL^qltDxR7q#vF;XGbU7Y#}~T= z+6StF-vS$u*5EFQA+C<*@A0m95*Wt?ZH^MaCAz7OzCPlt?e$7Q8^wH;og zFp?i-YShg{$)6u4_PDhjI+)}5-Z=(+Kdsi}uG9_Ynfpv6ZBci;*OmkD z%tK(UvwbVjsL20K7ndWU9pCl54Wp))9478d!;hzGg!+&d`h*iWM+|5#Z{{% zn0obaTf@J{?$=b{J@q=wWhjLBan!)T(nq^~`c;l-%?ea{ltwONe}o%qO`R4XT8qK< zFRY%E6arH;i4MY@^PoAL4|xdtl{r5lvn`XEcj=6dVJSqHhN8kWUW>EHi9)TTqm z4f%xEMlx+ms9YLsAD112N?GF|KRlPiBt>&Z$Y8CpuM3cBn)Mm1;c@K;L8*6@x)Lew zb^cYdRCVHY|5ba9RJ(@Ea`fZL$_KRpV_wScC0VLE*p{o2`()d2&d+iU#*4ORZpd zOXZ1>-uG;11&>vjrft8B`N0(LBPJU^SrQEFKh$M9v}$kL6tTpAos#XP-6(EWGo$t3 zj}To|jFg!Q_V~mC5dPg2JmxMvnA&uQt#`4)#;ez{Cl?S@!1m@aquEegmWwPvlYNZz z!11}S$#w3F&7H@mniIbJ^y)dI(Ttov0Iq%Nxt+v)5Q6VS5u9O?om_Hcg39}>*>;U$ zvTnBovEVoKFxS<%>lc`%n-$so*+Sf&ez1TUY|~UWXrlmzb5owKi6aqt201hn3-TX% zPIBsZ5Y)ArGO2ThFYZ@!|H#kh$8!+8=W)bA9xaFHd-}cU?UdsMwMTrosU@_y0jkDE zyC(r?S_sX`_oYmQVNR7SS=hvI_!LQTItG5Ua%MzzuAFHR3Ga~-t$b{ouKeg>bB0tC zU8RUr*~r62ZG=M5FZ}2R!3JGx4{L@>;n7^~gj!X`L5AS2ow+LMu4ztn={|D&4w2L! zU5SpQa^$o#v;;QE1rJLWvDMDXS}V3I5F1;4)wuMAV0`&n)36Bvub<43%ll8}GXhpn zsk#mBf^xSddKC;~>tJeWwQtWX<1Iy!?fOrcY&VW`>=X?@TNsH+7dUg-@GF^(P1c*l zORd!2iw2VY_(`GMfFk(gd&8BD_GVA-D{?-mpq)uR<^v(^&7!YN{seN8K`5stAF3MC zIGv68vYgu9d`JZ#5E#vY0K~#7ckmXiAyMAoT6+Pn1*=C5%)kyjy`@9W08pGcU zYL*vWYxvn7>$?1X?jo@UprEP9ywPj#>+{AS=VJb9-y7ed$-{%$$eC3=e?bE z^Rx83K%bfR(8z6)O8{~i38||T>`$zEEBRRKkTb3|PO&wOA79ejEN`A3RsybJ5X%RC zpo2(o{&WslOA-E6zL3bOJCafNo2FoNiD7HDQk?#*!*MWv*TH+mwuBuq)oYGlldF+c zXxz}&GxKf8WxFf@=+(yvZ)AiDz?v0mcFGoNb>0&T6eI=I@e!yimTP-#=ZK!86X=eQ z%K}zek-AAQc7yrWTWxQk$)4@>Npq)EmBCu}4AgxigH{b3(+gYG0?ZfX%r^DPrOn6|1)lv8Zr_B1zcr^r_ZOTDEQ-<=&xb!=;< z{tB=Z!UbrVUHuVFhsbY6{llp!I*3vro;WAIY% z0?-}DcJo21*}fldJvj0pytS&1H!Vc^K+fUZJu7Va-;h+ZXMVT;h=C9 z9DGLV+SuXPo~|zVDpl?>+_+--P<^b*RrrJGMQ>cKkSWw7C2z9)tZT+=tAG5|Df7NP zQNNE%nwniIjGLV9vXk9i`~>oj1CGLJjMcSSuZZv`oR$(QSHIs=Wh_fbGL%Lk3& z@u9CTPqnr6)LA<7Y(aeQ=A|8t7}fSn^S4=aIyVlvsdU zqgSyiK|hj3X8md^W{6#KAl zxBfT{+06S?_}Yg)!QTw9xEun)Vs4{UN@cS94=%^6{e4;!CU)_?0M%gyvm2f^l07l+ z#@^kbRy#Nw^AN-`S+^s(AApgUlpe4fAv??UX*QW@c_OIU!Q4Hkvxfo*3u`c_4apt?&-R-f9g9>uKRYXk!)BYKj9Jk&%=nU6uQZ%n;I7K-NOlhHG`e_ z!P)rN!@OtRdWRMkADN{$rwWmN! zkC`2~gy5R@9=aa-7iu1@2`6iT!M2xw?9-)A&hIko&ytT~y!U8t$AVxb4MM52d*RHM z@<%u60v!M^=i4|<#6*EvE$0?IP~d?mkUaBNPATsD)FjJ$dGKuqBJu(GEG3J-8dzYv z$P1f$H+VEO%Q#ydG(^SS0@`@Mb_tU^(YBdmmNVEc@3L(d+(Ld%qmX61Z%}2^8h;x> z#ZWHhptBx1>uH@nd02%!!#B2lEhp#k83fgH2KT^TZF$^R8i1(_NmrPY)t2a- z2U@5}c1+BYr{5tCxA%l^JAu~OX5mr4Ch0^yUYU&&?jPFW(O2ut0MFyT-8hqm-LWi6 z0w@SnGdnHFJ)D&hr~r%fFs=UeEd< zkv5gUe{t2Jaa502D|tam6LDAKiUoF)R;$yT63T&U$JWU~8DNqnc&TOH0u zmmu+vAqmIs<4BB^`_5_=S{Bn{1__#pZ!gjh)}`&&rA@fFYF`X*Z****@c-uZ`sMD8 z9d%k`#O5OByR&yUUK6b^phc~rr?tFm(Qizya5o#}$aM1pO}-#^rFbre2sfz@QOuO6 zoC@0FdU&|qRuCF%Ite1C1;NDe3Q%;-?dC9=BO!L$Okt1(cwJpmNfQoPHGs>3e*TjZ|i6hSP zAXHgL?8L0Vr=4~g8mO?R1ekr6{;7|7Y1@DDPYV@vvA+(l=%VG6?ZGm%spyRRi9GBn zgWS>ij}{EG2QE~5Wc~of zasI3E$~1w1;iCK$`HlHM`fneuW0%=W;g|ee`fm!jrZl4XL(0aoud)s86ucyc#O6%~ ze^7J*z;smG^9A-N%VuY|kgUiR5!t2Z>T!pvlJoCMYsc@V z#$CyFO!G%`@{Ql}8dD6l1JV!&6ApwF5$&%<5VqIP%$q~m-yF5lY`3k2EK3v~yuIBZ z`xQWcwL*~=eB~(Oe~(W+O~*eC#Q60wj30OT6KQC84nA?erj8kT4tC%<9%D+hAXM*t z88i-$Z;aW_?#xgD_4mqi&jr}EXgzKVc((A$DBj8@VC&*dM$5&TI^OJM_C&HfkY|IT zDHhv)ACs-vbFoOEMYO|KQlKap_9(`sUqie`R&;w364*v7%1w)a=NTDhNk@8+bZU&q zmYEdjBLtZCTy^~GuV1Cy=tT}^;Y=C5d^_#SHi}u6JyzPiajlsBjEo-)S?QqYH16w8 zW$2-6NtGvWVzad?{k53TGPL%qf5KbHYE&O^0Cv;%%D(OKdnixOq@!Xf5HX0A#ggfj zSu$4fW=G7RpW}xkS=vf+u8Evh>aZToX4lk}5A*dsgWVp8Zo5#?pxi=Ku_J4LNd*6#Nl4 z1WIvF%)m_;ILU zJ;E?5I=L3qUQ_RjEag+2AZ1`HT^>sKH&8}gced9n;jQhm-KQS8)VSEIt-YBj!ynDJ zCv0t=fYiwg9Cs0)*`eWByiER!Xkma+gcjDaws8mvTstW`AqT0!Hkcq<~y(?>82ekQyQc zCl8_yilTR6Gk*KFlNH?XE%`Dp@70;;L1JaBX#4))`8e zzeM)e@WyfTbs{r6k=j0x(`vLrlJYheUe$A1CFFA-fKmlfAKLkyQ0*YW&+g>&RUS|K zHddXUs~Q#x%<5y3-+EvQ@jNGPfA@6h7NO9~OoSsR9)T(Dg&QZ3H>p7{GUhHwEc_DVI<;Os%Hw>-!~(tjGl=06X7#enM9AX+{;9(v&8QJN~w5 zM?jz3#Q<3h%~yH(U&8xmekuB0m)dk@k6G^XA}v3vj~YEH-JU1sxAX~xTeb=*3*ugJ z;uQR(+0}x;n3uGx)wc8z6OMW1&(5yl7*BTJo4d~YU!eA%%9*qGy~+dmV{;WaKc6&% zY`==+^7k`QD|30~#oSn)lg(;~F5Uo|6~nkRNcrsGcdqOxSOHnp{D4&)dsZ$slt1QT zT%;2ets~c|>qh})bYF+kcw*gyds;U$o9IegHuFpA(@R?tWsA^kn!YFOj5uQHsS+j* zMx<6dnLYTNy?KG9q4)3hgC!%JNx|k zG*OvLE4Uo(><>XNmlnA`Lju)0s>z`+bIC$X#Zvw>racOE*sKZPDcrb)n?#~+5!@23E-=5M3{S`GksxKQw7D;6*T_nc)%BWeEC58vb(t;258F~vH80g&fc;3G{$Tk!O% z=m5!m^Qag~^HqK5RO4zzb_C@_eVFpYeySbm;)_dC2VU)j3ESA#ev4C24sXTQyR01VIRf3% z*xs8*#A;{;>zh(~BiycGc`V;aKTCFamhkinSKCU}*fFW(2k?o1dQ;7!&0VP#~= zOAFp`c#6=XVBfTsM%7*VpU3mhm(k(HI>jau`qmpOEM|S_>Wuj6AI{X?uzt5?TfJ5; zLQm47IhJPrT2#!*cS7pJ;Eh+rnZQl`7Dv<}q1ydxth%YBcu@kO@Xk>_rGFY1$6OD8 z+@?aGWod~z21^mPn>x>i5H1fABv;X8oTnBwgZw;D0WkFCAy@j1*E0*;V4Ya%0}OwiWl3(jZTcwYVUL z=^--KfXFI^cF>-Qle0+EMz~2#s)~%(NxJwjRJ?GOUsV);LhVAfF%V4wD0dw&53owvfFmsdezbUiy1nqi1&)qL4z{1K$2ep%b~y93!N+U zW3&1ja#$M?tn_LNU-75hsEN|L3**L`z&}pUJ~xm1mjSsOjFbi z>|q328m?9KOd6~8l@e2sCAtC<&y*7{$Vr~}QySgSR_I8xXuRp4`~yX_DI1ha;Y-)> zbV-~7iKWjBXt_9}_xnCWV*+ctx}%p|A*`krTML`5eT{>yDc##9jxa5$sT8Ch?%I!!) z=PMq+Cg1n=v_19^IG+-HX=o&!{-@wzD%Hzgo@A-g8iz*zoaIZy==o2yb@;ug)b$7{ zJNnp0&S*_(+O-laD!g7|t{9!G(MM;h7;@TJ@y}z5w85e4ZCUr0MtxQGj$EJn4QQ*l zqCM#ddNn9~nDRuuymCB?wwP4dD$9ZGtEdVW!bW!@6%WzVl$}O<90C(-nsyJg{Xgx5 z{P0DHd!rS2cGlXP`3Ww8meZJ#q?d644}^xI^3$U9`}(gh*|6^FS*Ghqr|WRgJWjg< zy}*EdkFjz*O0Iokth=dz2Ykb)pmHDCX*PmOs!D?;dxIs>SNn`M3?k}GhrlktR- zx;m^KicjuI(Tm1KF!ZY8D~=_98%@~-@9f2tU6;{rC9zw!8Vz?M%Ge`{Sn!s65p&bS z>5`xLX_Xd#hE~04y(vGRp>|*$OPzj3&2fm3=oBApfHF$;`gTL2P3#8dVRcXD+w7fx za7yKS-kw_tf!elzU~24Iy^B>qsJQ; z=2bW$Tys{$a&ebj#veL5iQtBo`fIc#+?)%dl)Y}E75VVYvj@WmnHB|S=DLSGWx0>U zf`grZ`lw8K7k#)G9xxuzAbk%$jCqoOB6BEBF8$hVrFxCzwiZ~@o{=ohDvjbBCf(#8 z7cx)YB0o#B&Nv%%%5r1n9H!fxvB~Hka0+xQ_DX!tMt54rqbkQ4P8A