From 3c7b7427c9101ebe0169308b5b93da67892782a5 Mon Sep 17 00:00:00 2001 From: Jaewook Lee <11328376+jaewooklee93@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:27:08 +0900 Subject: [PATCH] new elasticsearch manual --- elasticsearch-streamlit/README.md | 310 +++++++++++++++++++++++++++++ elasticsearch-streamlit/image4.png | Bin 0 -> 10058 bytes 2 files changed, 310 insertions(+) create mode 100644 elasticsearch-streamlit/README.md create mode 100644 elasticsearch-streamlit/image4.png diff --git a/elasticsearch-streamlit/README.md b/elasticsearch-streamlit/README.md new file mode 100644 index 0000000..39389c2 --- /dev/null +++ b/elasticsearch-streamlit/README.md @@ -0,0 +1,310 @@ +# 1. elasticsearch server 가동 + +Ref: [Getting started with the Elastic Stack and Docker Compose](https://www.elastic.co/blog/getting-started-with-the-elastic-stack-and-docker-compose) + +## `.env` +``` +# Project namespace (defaults to the current folder name if not set) +#COMPOSE_PROJECT_NAME=myproject + + +# Password for the 'elastic' user (at least 6 characters) +ELASTIC_PASSWORD=changeme + + +# Password for the 'kibana_system' user (at least 6 characters) +KIBANA_PASSWORD=changeme + + +# Version of Elastic products +STACK_VERSION=8.7.1 + + +# Set the cluster name +CLUSTER_NAME=docker-cluster + + +# Set to 'basic' or 'trial' to automatically start the 30-day trial +LICENSE=basic +#LICENSE=trial + + +# Port to expose Elasticsearch HTTP API to the host +ES_PORT=9200 + + +# Port to expose Kibana to the host +KIBANA_PORT=5601 + + +# Increase or decrease based on the available host memory (in bytes) +ES_MEM_LIMIT=1073741824 +KB_MEM_LIMIT=1073741824 +LS_MEM_LIMIT=1073741824 + + +# SAMPLE Predefined Key only to be used in POC environments +ENCRYPTION_KEY=c34d38b3a14956121ff2170e5030b471551370178f43e5626eec58b04a30fae2 +``` + +## `docker-compose.yml` +```yaml +version: "3.8" + + +volumes: + certs: + driver: local + esdata01: + driver: local + kibanadata: + driver: local + metricbeatdata01: + driver: local + filebeatdata01: + driver: local + logstashdata01: + driver: local + + +networks: + default: + name: elastic + external: false + + +services: + setup: + image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION} + volumes: + - certs:/usr/share/elasticsearch/config/certs + user: "0" + command: > + bash -c ' + if [ x${ELASTIC_PASSWORD} == x ]; then + echo "Set the ELASTIC_PASSWORD environment variable in the .env file"; + exit 1; + elif [ x${KIBANA_PASSWORD} == x ]; then + echo "Set the KIBANA_PASSWORD environment variable in the .env file"; + exit 1; + fi; + if [ ! -f config/certs/ca.zip ]; then + echo "Creating CA"; + bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip; + unzip config/certs/ca.zip -d config/certs; + fi; + if [ ! -f config/certs/certs.zip ]; then + echo "Creating certs"; + echo -ne \ + "instances:\n"\ + " - name: es01\n"\ + " dns:\n"\ + " - es01\n"\ + " - localhost\n"\ + " ip:\n"\ + " - 127.0.0.1\n"\ + " - name: kibana\n"\ + " dns:\n"\ + " - kibana\n"\ + " - localhost\n"\ + " ip:\n"\ + " - 127.0.0.1\n"\ + > config/certs/instances.yml; + bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key; + unzip config/certs/certs.zip -d config/certs; + fi; + echo "Setting file permissions" + chown -R root:root config/certs; + find . -type d -exec chmod 750 \{\} \;; + find . -type f -exec chmod 640 \{\} \;; + echo "Waiting for Elasticsearch availability"; + until curl -s --cacert config/certs/ca/ca.crt https://es01:9200 | grep -q "missing authentication credentials"; do sleep 30; done; + echo "Setting kibana_system password"; + until curl -s -X POST --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_security/user/kibana_system/_password -d "{\"password\":\"${KIBANA_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done; + echo "All done!"; + ' + healthcheck: + test: ["CMD-SHELL", "[ -f config/certs/es01/es01.crt ]"] + interval: 1s + timeout: 5s + retries: 120 + + es01: + depends_on: + setup: + condition: service_healthy + image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION} + labels: + co.elastic.logs/module: elasticsearch + volumes: + - certs:/usr/share/elasticsearch/config/certs + - esdata01:/usr/share/elasticsearch/data + ports: + - ${ES_PORT}:9200 + environment: + - node.name=es01 + - cluster.name=${CLUSTER_NAME} + - discovery.type=single-node + - ELASTIC_PASSWORD=${ELASTIC_PASSWORD} + - bootstrap.memory_lock=true + - xpack.security.enabled=true + - xpack.security.http.ssl.enabled=true + - xpack.security.http.ssl.key=certs/es01/es01.key + - xpack.security.http.ssl.certificate=certs/es01/es01.crt + - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt + - xpack.security.transport.ssl.enabled=true + - xpack.security.transport.ssl.key=certs/es01/es01.key + - xpack.security.transport.ssl.certificate=certs/es01/es01.crt + - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt + - xpack.security.transport.ssl.verification_mode=certificate + - xpack.license.self_generated.type=${LICENSE} + mem_limit: ${ES_MEM_LIMIT} + ulimits: + memlock: + soft: -1 + hard: -1 + healthcheck: + test: + [ + "CMD-SHELL", + "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'", + ] + interval: 10s + timeout: 10s + retries: 120 +``` + +## self-signed SSL 인증서 추출 + +```sh +docker cp elasticstack_docker-es01-1:/usr/share/elasticsearch/config/certs/ca/ca.crt /tmp/. +``` +- `elasticstack_docker` 부분은 자신의 stack 이름으로 변경한다. + - portainer에서는 stack 이름 + - docker compose CLI에서는 docker-compose.yml이 있는 폴더 이름 + +## PING test + +```sh +curl -k --cacert /tmp/ca.crt -u elastic:changeme https://localhost:9200 +``` +![alt text](image4.png) + +#2. Streamlit app + +```python +``` + +--- + +# Obsidian Vault를 elasticsearch에 업로드 + +```python +from glob import glob +from elasticsearch import Elasticsearch, ConnectionError, ConnectionTimeout, helpers + +md_files = glob('/mnt/c/Users/j/Documents/obsidian-sync/**/*.md') +md_contents = [] +for file_path in md_files: + with open(file_path, 'r', encoding='utf-8') as file: + content = file.read() + md_contents.append({ + 'file_name': os.path.basename(file_path), + 'content': content + }) +md_contents + + +# Elasticsearch 클라이언트 생성 +es = Elasticsearch( + "https://localhost:9200", + ca_certs="/tmp/ca.crt", + basic_auth=("elastic", "changeme"), + verify_certs=True +) + +# 인덱스 생성 +index_name = 'md_files' +if not es.indices.exists(index=index_name): + es.indices.create(index=index_name, body={ + 'mappings': { + 'properties': { + 'file_name': {'type': 'text'}, + 'content': {'type': 'text'} + } + } + }) + +# 데이터 인덱싱 +actions = [ + { + '_index': index_name, + '_source': md_content + } + for md_content in md_contents +] + +helpers.bulk(es, actions) +``` + +# Streamlit app +```python +# pip install streamlit streamlit-keyup elasticsearch +import streamlit as st +from st_keyup import st_keyup +from elasticsearch import Elasticsearch + +st.html("""""") + +# Elasticsearch 클라이언트 생성 +es = Elasticsearch( + "https://localhost:9200", + ca_certs="/tmp/ca.crt", + basic_auth=("elastic", "changeme"), + verify_certs=True +) + +# Elasticsearch 클라이언트 설정 +index_name = 'md_files' + +st.title("🔍 *Jimm* 전용 검색 엔진") +query = st_keyup("Search-as-you-type", key="0") + +@st.experimental_fragment +def result(): + if query: + response = es.search( + index=index_name, + body={ + 'query': { + 'match': { + 'content': query + } + } + } + ) + + hits = response['hits']['hits'] + for hit in hits: + st.write(f"**File Name:** {hit['_source']['file_name']}") + st.code(f"**Content:** {hit['_source']['content'][:200]}...") # 첫 200자만 표시 + st.write('---') +# with st.container(height=1200): +result() +``` \ No newline at end of file diff --git a/elasticsearch-streamlit/image4.png b/elasticsearch-streamlit/image4.png new file mode 100644 index 0000000000000000000000000000000000000000..4e108838592205b25cbc6f285cb709cbd40d12ea GIT binary patch literal 10058 zcmY*(*Vsty{Ml@b6-u z3{y3K#(vy()lvZ7LJU)H-@0{2OH)-x4*QUik1rl+UJ(9rPCojWZpEuTJpGBY##^5u(yf5ot-^5 zHx~#5#>K_e*VjixMBKlB|G|R?c6N4ISy`o}rFnUIYHDg;US7`5&YGHjfVsUjjGI5;>ZB_*$3y~@tc z27y3#@7^^wHh%NwO-4pWb91w`wRL13G&eVAWo319b8BmB6BQM;va%{D zD5$NiefI2`g@pw_Kfk=ZypWKPoSYm!K7MFuC>|c3n3&k}=g+02q%18h1q1{f92|;@ zir{cKB_$;b3kxkREg2bEdU|?dVq!x>!`ruS8yg$7wY9yyz17v#_4M>oQc~#Y>Gk#X zgM)*kqM~GEWa#MVjEsx`0Kn(ZpF27_n3$Ld2nfQ%!@qs|CM_-f{{4Fn4i0K+YI}Qo zMMcH5w6vI*7;ImOZAz(8T00$`>EyLf2&A zdSM=;_96s{3ird6Ifp_rcH`j&wH5n)J86y{*T}{*lgw(HX>0!7<+kPJqbq5Bjiu^e zFIB897ro;df{20m^g+@7D#NBO3YOw>r5yPh>_*73iyVtDG z@q>s6-Z1w3{es&~*6%PYahE>O{}97RKW9T{smW9RR;%W-RPRqS^xmw*ZTi3fHW!7P z2U%uC^9Ev^b0Kt=JkCk@rsc$sgW0;EHdw^J-D8XGXYdhLD&ERX?e-q#;f9`bN65Q`dE~&u7eo&XV{SsNV0YSv>QxpdMGPMmo!*cn(T7!rVP6 zEbFmA+rziI<(AtrkGP9<251e+U38~*c64#<B4^$+PE_< zS(s6TWb-u4=$jiYdzZaal@=|nmfKrdY&EeliylqU7eJ--%E=sb6ifI$b*4eZ3OA1t;a^e)zVdE#|w3DrIm^;-8 zTDP`F5o)>BnZIj77&NzN9U`LX6?5YOC*5{dF0u>s*Q_Ooc!N-Qt_#PCi=TDLo|0#! zY8bp`Q>geSxNvulpEUM?GswE{aHno<1<`b(4@S+M>Edi676iGz2dB`?E%h0&gg!Kw zD%Xta@~CR&ZQRR}gxFjx_+GK&Z~HCAy~$@j2yLe3{vqtQ8^`iv^*k!_wJ^&I-<_eK z^JC)%#gqJJ`?j$;R-QXj#+K&0zMC2`K)>OmigLeJe4yhCuceCA1C3p=7pKx*y+HBG zWw*d{b4(6Dy|#qd9BP$>kD4{ysrRp+HT{vU(U8J=E36;8fcH4pO_}`acfzuEQfcKHG>Mc|E-DmGnglVx~aXaREjjxo@59gkD75 zGr?}w#tp^g&*u|}hLt8Fj>&k+pAKh=)!IFH9w87Oj>muDbR|<8XHQM%ifH3WFRPbIBMi`PBBe7wE`5iZ}aY!k~?3GYN zi{}myGG&sKG#^A-L;6ZzE)l@CKP7M_j?_HSH68T3SOB{WQZq;(R*b>wTfXArEc8BI zF2V@O^5=nu3+=AYEPP*zKH53lSi+cyyukz0`&pha@~Nl|&^kRN$!y+hd);>VI9K8G zl^C*MlPM3G%w=@UQutZ=tI>O+r6Om-rtS003Pe)WcPmW4=OGx(NjeFT z%S@5!be*BtBV59Es3S0>X!`C&OUX&x<*>ys+qu^#L1M5RlWyq+E!(LJAKlXK3xY;~ zxO-<u^0i(5-#bJx#PdbykEG*dlq zKFN8qB)1~g?2oBlNC?!OyUW!BLw#!2S#o)QdH=*SuCsN3KjiI?oEGjPyV_IKwy@7z zto3ujc7~~phMNHm`DiXrW>6T$y=nLb<`=4v(L-MHrEM~-^oSl=q_JPE2pFh z^7NgPjQCMS-pH#ffy;Z)aglrkJ9Y+CrUr=H1@8+%TaXtUEJ z+58A-cpv9si9yyk_%{)jsd%{TeuMP_S3C3Tp|Uy3zJ?%E-9-*E(lx&yEpd3tUsf}p zL-;wp7T#i#wBlHFI|)@321d5#xxYkl#)0-khku{}x3eiEEfz9B2*A({0` z$AV~UwAj*50!NY+m^lqK?-BVq&Bzq$i~!U}jr8H5U({F{1Cs3u z8LF2U&G0ELm;w&BcP`UKXF~12bDwjQMR1fC|5SN*5GG<|HtoYEIglX*0R|jTW>*}9 zdC{W10|9jkpHNo&VIG~T2}lj*RN-(6>N@YNmKaKUEI*`%ilY8~kRvP5kIJQ={p`-M<142#65) zbE%CvG`y{0T8w#vo|KYeGnlHCKYoTNmIb3t?>}ArwI@Q0MSaD>-G*OQ{)340z`#CA zhfzM@cr?3R)!X?%83pS8&+hvKhNKAE(%RS4zli_31DfrIC+r*4Rea?O7A zR!jy2QkLlh@-0lF3YD!t|KVF~T#Gd~B);J4tS#qZ$rq4YX}0+#$^#)c*y1Cl|NrkmF)W%zpcr`iOp z39=aFacL&hyEMedBKw-VOmXRJKgk@bunS(Z%xwmsZ~_UJjTF%Y z8*rsvvXPMljj$PUJ`|c>L*B!ycwp`mz(E9|~=IZe+KRR=iF1<~F z74rV}U*Ptc1JGu%o4&#G2_Jf2*1K|$(8~moWc0qp7`LGEA8^sM1T!Ijx)`mcGC=2x zTuEa&Iqny=h0XFU8<~G+n9-@Pd~ax)84;b`3uf@{0xCX%KUqrOWBfOw;?sA;2;k0$ zO9}-Y%lGYvEr`|49tc3dW-A;Y-^NHQJq-lV+wd8gbvsJ1R?N-w=eeh_xl-oH;j%%9 z5WgB9(>E47cXdG*mJ|>CtGKHA*G~(NZvJNc@iSn=s=)EZT6*Osc`MncvmJ>>l}M!c zEA9+-dTx8(jCJjkmoGF3WvY6!a3qq_8xn{y+a)CrBG1tG`}Vc`w)C}Wg-QuIyk0l! zG=|d~T8`ey98h0SR&*_4Y7r4w9AIH;R2g#fGxe$1&Ug3!tI4cxC=+^fiMj*mX^Qy# zwB9y>M11W;PC91e2hGx_^9`txc6vyD*>EDmpI^Th8tvPiOIm5))N+iYog%?Z3B*Cb z{g;^)IfZ+ep}vP$yf&QuFf6%f+D9|f>G%I($DX*i4(+|oKmBo#135)sEJD75uIIs` z^8Iw3Ei`fp!@3#CXj?##CA3hXwr`3Xk;0okZDokJ!azucHIAg-jvnNG27P{CC&AFZ zZ5_sFlea37yTaGDgP0S@(1tB?Zi;^eRbJBDZT7G3p7kce{MJmWJ>_9*W@E1c zhJ;J=FCI=XN2UWCA2eSKE3{fkEWX$b$QZ2{kv#rLSGI+1-N7sand~$udyNp9-cw-$ z{=qiAT6MMQ2MehB%@^*!!EAAiI}b58c5R16&@vqyNIM5f-`bt>-csIk3My-pfB5P=LyT+Re*wzqr7L{x7-Hl!Ta#t%lMBXqNQI?_(1(ul%I zTu^=UC*$L~sc#mdB8gh?gcrA2{wzFUT$_%oMkbOr@E%?J>x$kx);+0~5FDQTpycc{rrf0&=$#pNd!(+Dr#_h?Pt*tY#p-B+IgE z9MCsE>+i{2^WB+G2y=B8-u_$9as}o&(zaM6O7`UiA%VTDr9z_ z50vzG15}N*SwFairye}V;qY!_9WF8|>p`*gMQ z{7E30-s2fARWYc~n8cLs2g+(Lz7M^fCjoBN>N{seQpcbVh&?WgU~nk6-KAyg5o~>> zgQ8`hK`O$Kf=UsKCf!gJ)3tbgKTqBdN6`Y>M$q_I=N7^_cg}MfPJi9vwFO&B#e|~r z^NTys{$8N430c4V%Ekz?XVNEDKbm(cf0J;{Q z>1cU$!a3RS-?T)#S?<8pQg};UNhYYePEuc?p4ZM|W$Oji-0n|rjzwplk%*MaWL_t@ z-?#B_eub`p9^G=S^`vf*vyma*sb4;G;;`Y3aW0QCie!g(>hV!G!FqR}LT2DV^>$a; z{N!XZV+u{fWijL3KvghOp6$T6(@oyF`ELq7^o5BbY3fp4hKU!Nf3#US!27Y%jEH~e z_EPJzfY7}tQE0||fhwe=*!eoXPa!o;@_I!*m62a(>*490DxQh>7ED&}KJKn#SoS&; z{^5W@204}tzOWYYYp-#-G6Rtm~R!?LYJ=!=#QMXNPPF9^`&sCC)aK4YE%%avz=G%dkbN_5DMJ3&hL_9YB%S-L zAIT-8bUK#8XTjVGYO597|CdAmzSvY1-m+1?do`Z&#jx2F@r)g`tvL35{Z=y^=TY~1 z<&fX=Ue2P-{K@f)y)Q$Q!vVa5vYWZF*M|p~qfx%1WMA1g^`@1%y)>79IuDEEmT2ki z*#EC3R5uaEpzkzYqwRQ~Y=3+4Hlo(QLW~7{n07~lnH6w3#<1>I=xm5gkwQ*WG#}?^s zd*0!C@)MuUh+A?m_9Pe@ygv%@pHL=uRk@ya+X<;e-||o>7j6hv|75#y(8aw0ehKF9 z`s5lFM3ZCZVrfRaaDq1JZaqxBf2>tr!?LdCCjb5G-}4kNr-n>6XdNxQUwel600t2L z#}$S9F*D#xFFr$gvWd^V@#FgqSOJeKx^^aPRGX2s(Q0~Ni|6$ZfhA_iB1SX-+dtBs zFr9d6a4Fn$aGtEdI*eqK`o5^!_}bf=2hKT-CUxZaOFQ4M)Q3fs+}?qJ#>jt;XI@6I zUiq#DtQTlp9QN>6WUF;EKMr+?SmvPYP<1P6==sso)bOLKik*CJ?6iBgCQv`hy=cf& zJa;k(r2uG&$se}apMd|jBq#<>(V}ZuWAILl)6x~CWbuQE240hDW0wW-;CI&ZrJ1T&jqVhGkLcxF&H=REddM;EP*nGLgR_GQ}DFv-YQ{=8{87RjONFDvEp zoO2)$UDfN~Im83y>dlP_&MY+o%9R{%+OV4S9FSI!puL&$||`60m4nR3D3 zJ|Pdk`&Pb6w4W_XHgTLuYux(0)G*s-VrB?~XD@F!^({Pjo1)HHG%Ic6p(fr%-M4E- z=qNCUJr)=e#2yP&M*b^e72(IMGsVU6NLB=>2&=P{=m%szc2ZLm) z7tiUm&E6Q)Bp8J77BhG9-X%hqk%89uV2P$(d$N#-=GFA&*M^229|v8Vb!OO17aC1! zrh^l?Ej`T!_d8Wz>4X~!^=X$rW466vQ8uh^sHgVH0uFj+{KB6-qZdbf0J-9xQ$;mb z>$GHm0dSU@%fJyy5J9h`BB-o_BZ5({%gUyHDXsP+26;}fTO7#mQ*Hup5YLlV^GK7LlK9rL7j^ZF-Dcr963bZtqt9=bk@DkU3voG-M%G3szK~V zKZ;IgJ`mPFHs&@$0SQmzlp~&t|2Y!lw{}lW>CRSe29%W!`s&;4hs{6$)*GxpepVDq zt)X#voQQ6q9AY|r(6t)4q+?+2D(`~6MgSGQ$_=Lhd^T+hLIQ$}Qq^}gvFTS>lNCa7 zYIOUV_Z#v=a@wEpuRk;PAp2yz|K(XV-bPfu+s^pn_eV=trZWf6UdSYDAt(%6Q0plj zW3D%I$C>mHLz9{R%BWABf8swVE*Q8Uyj_yaw)B^j%Mx9wI1iK~pH#u6Te+u4`}$Ed z)?D-rsC7J7NP$wpjln9axvFA;iGid2&u4F={k|=Tft3sL8)B%UTIgL|vy?C#lus}jf9#3V}zglSV)$I4W z`H{}?xt%wCJRCsWJ5+Thg-Rx0vb@df)6wp{*N0GOAMch~8;!PjnTwNhZf0nH?NnE? zmmDrR)-^=?2>U4#=PW+=a=hg9^Ooup`u_a8qmW|PS59ar`VA;67EfsmI#5$t$S(xV)@)+v4(ZS&Qhna_Z!{anE`b)IvyGH1hsyu-!W;osIX;5 zp8Y&IJ=7{p`UdXftu|V#Q#@>KO}b6kOog+lbVAsF+4y}qfWFTz$g@a)OMd$yiQ)n} zu3A7O8Jqty*}cUn{(Q-$r)FIA#yL!g)WM|YxM^vpdIgH5{W z#R)m@x$??}TRzG)^Mv9cl5Ai3q301VM@u56t&~8I>RXva%kyy4H5P>SC+pouE3rBR z%w5@ZKFE#Dol`Q_-?X=2v-pZAC}_|NeUR{3vhVUqYQk6p$M%lqMQqs6b)TD{ii)Gd z%VUnOZ=9RU#Oh2HH*S)6)oB+cTz`^SE3Jcmm&1z6ECcCG(1W`xfK6UGW_$cB6)=0%mrUph+3`g6^G zhmjdLx~h#P)}9}tZ>&hTZQ#L^B$a=qza3=8rQur6|@csHxD_gYzJ7nRqQuc5`YBa`rxkn zV)Yt0qS!Y4|F;c+Z%8qys}bwfmkvIr8zD zVTK{NxyWp&SHT=2Jpv@O`1NhHeTU3Wfzu%MY**_$ShHo9mFttmkBfE}(tGf@CgRKB z%V=IKm+at_IK->W>$-a`zJBj*xCo5~Uk?0zWk=wqCHXAlNxysy#=Wd6%O*RRx5<>2 zv&1WR_d`T86D=LSD!|mAm5Vc~4U2lupT3W9%=G&stT8AJlk1hPbZn;x>_=%AQ)Chb z6(P%rynpbf_Y#t>r-Psq=di??_T+kiHN0nbwa03(9EZ6~4q#eChWn0agCda;e^3M6 z&R?AV2l3v*13zL3ZQY^^WfxSDHw(G?H5|XgDw6SI68fS#?$7M6ykDC6gQ``(p*XO_ z761&Z|M(|XN4p8@E6!Q25%2qx9D=2(12Mtt_;-%tnsys=WZK8)$=nTEE~=;fKAeR> z;Wd(CLe+%_%C;;gmCH)z?)ihtOsz;M;W4P(=eD~?HZW>pVORBwgWHA{<)u8IEO=VW zsHM$(V=1mmd%)6)`di>Jet;b(RmPb780I=EgVfHzv%RPnbq(9fVD^Q;PAN zBd8Oe?RcC)VcT>(mpl*j%?+`{)|be*Z^jEeP6FXzEttI2KIpF1D&%?5ayG241vbY0q1uj}97L-3Cq+60L*8*rU7!QM zsm6kAkCXZ+2^B`fIB1WN&_s`jKUDQr$9(wq*qj=Hj2gGmlw`b=<_`Q8HDWYJ;UtMN z`kMK_yIZq+vxu6o9*G3+U0eh@e~+tCZpN-|)M%!7B0gqHTyjtf)M5smVn@gBiQy*nC@9n|kFKv1Uuef>rGn>;X4y z2R2KnOfg_%6c&MUzh1SlZvOrA(mI?z%X`*?0*qW%(g1UNwMm1NX$>6miUT>v*97Ua z4sPALmwIy-pmmg%0P7jhv6H76$CD=&6y8h@!y&dyCx3K3Fh?H)&{lI`x8{#5;_l76 zI5$UA369kEy5MPC-K~K`#Wo}7<1{HPNd5P4j{3%iXiArdRkqvZ%oRod-2>$ z`vmUIy}z;w5FoQQl@rd8namGvCoU@WsW3REP`=8IYkkVu%@5QTILVQ=pk%8K` z0bw1;L}i0UTUh-ygc!9_JK&(sPoB4mK1lb&$%9v|B&Al_6$~aPx-bD@{(ad5q1^;< zn_vX0VPp4#y1G6~@EFJuD|m8}{1yXRx9A>hC6+k;Ho*{kNf^`v<7CL5 zMp_m8ZDuXkROVK7K`mktQZ+5sBJsmW@DVt9ysNj`wUPVESzzytD~PgI4i-6_4HO_} zsBV%)JjUk13lAogAI(IJ29u?GK5vKmoF1{#a*n!)8o_rfX92u<0$>cJ-ZSbj;Puk#iyH?pg zGU2ydTt@IWa-zLxq50bBEM|DqYY(Yz6N0Zc>x+F)d2ukt*1s$~vxhwQaz6)kd|?w2 zRy`=wCKDi7$0|ymU|~M=rns(b;2ABZKtViRBb(?H*5Y^nGT{is!sIN=S19-lY1`0Tj^K{q`lk7R|MNz96&#evP6I5kz)zL z-p7#W3Ge2MF?3{@NhbAxC5E;FHT6D}J>hWgi~J%3BMS*Tp6BzgScrXc&DZ-0OOpCWEe4qxd}Y!g2H!Ura9prT={l1Wgs l+wYsQ>ZT+HDjs0`Y$R9z=(+I+V(-D-Qj$}bMF7nL{vTPSt6l&A literal 0 HcmV?d00001