├── .gitignore
├── LICENSE
├── README.md
├── assets
├── architecture.png
└── blinking_elastic2.gif
├── notebooks
├── Searching_with_ElasticTransformers.ipynb
└── Setting_up_ElasticTransformers.ipynb
├── requirements.txt
└── src
├── database.py
└── logger.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .ipynb_checkpoints/
2 | .DS_store
3 | .vscode/
4 | __pycache__/
5 | index_spec
6 |
7 | logs/
8 | data/
9 |
10 | notebooks/Experiments*
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ElasticTransformers
2 | Semantic Elasticsearch with Sentence Transformers. We will use the power of Elastic and the magic of BERT to index a million articles and perform lexical and semantic search on them.
3 |
4 | The purpose is to provide an ease-of-use way of setting up your own Elasticsearch with near state of the art capabilities of contextual embeddings / semantic search using NLP transformers.
5 |
6 | ## Overview
7 |
8 |
9 |
10 |
11 |
12 | The above setup works as follows
13 | - Set up an Elasticsearch server with Dockers
14 | - Collect the dataset
15 | - Use sentence-transformers to index them onto Elastic (takes about 3 hrs on 4 CPU cores)
16 | - Look at some comparison examples between lexical and semantic search
17 |
18 | ## Setup
19 | ### Set up your environment
20 | My environment is called `et` and I use conda for this. Navigate inside the project directory
21 | ```python
22 | conda create --name et python=3.7
23 | conda install -n et nb_conda_kernels
24 | conda activate et
25 | pip install -r requirements.txt
26 | ```
27 |
28 | ### Get the data
29 | For this tutorial I am using [A Million News Headlines](https://www.kaggle.com/therohk/million-headlines "Kaggle A Million News Headlines") by Rohk and place it in the data folder inside the project dir.
30 |
31 | elastic_transformers/
32 | ├── data/
33 |
34 | You will find that the steps are otherwise pretty abstracted so you can also do this with your dataset of choice
35 |
36 | ### Elasticsearch with Docker
37 | Follow the instructions on setting up Elastic with Docker from Elastic's page [here](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html)
38 | For this tutorial, you only need to run the two steps:
39 | - [Pulling the image](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#_pulling_the_image)
40 | - [Starting a single node cluster with Docker](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#docker-cli-run-dev-mode)
41 |
42 | ## Features
43 |
44 | The repo introduces the ElasiticTransformers class. Utilities which help create, index and query Elasticsearch indices which include embeddings
45 |
46 | Initiate the connection links as well as (optionally) the name of the index to work with
47 | ```python
48 | et=ElasticTransformers(url='http://localhost:9300',index_name='et-tiny')
49 | ```
50 | *create_index_spec* define mapping for the index. Lists of relevant fields can
51 | be provided for keyword search or semantic (dense vector) search.
52 | It also has parameters for the size of the dense vector as those can vary
53 | *create_index* - uses the spec created earlier to create an index ready for search
54 |
55 | ```py
56 | et.create_index_spec(
57 | text_fields=['publish_date','headline_text'],
58 | dense_fields=['headline_text_embedding'],
59 | dense_fields_dim=768
60 | )
61 | et.create_index()
62 | ```
63 |
64 | *write_large_csv* - breaks up a large csv file into chunks and iteratively uses a predefined
65 | embedding utility to create the embeddings list for each chunk and subsequently feed results to the index
66 | ```py
67 | et.write_large_csv('data/tiny_sample.csv',
68 | chunksize=1000,
69 | embedder=embed_wrapper,
70 | field_to_embed='headline_text')
71 | ```
72 | *search* - allows to select either keyword (‘match’ in Elastic) or semantic (dense in Elastic)
73 | search. Notably it requires the same embedding function used in write_large_csv
74 | ```py
75 | et.search(query='search these terms',
76 | field='headline_text',
77 | type='match',
78 | embedder=embed_wrapper,
79 | size = 1000)
80 | ```
81 |
82 | ## Usage
83 | After successful setup, use the folling notebooks to make this all work
84 | - [Setting up the index](../master/notebooks/Setting_up_ElasticTransformers.ipynb)
85 | - [Searching](../master/notebooks/Searching_with_ElasticTransformers.ipynb)
86 |
87 | ## References
88 | This repo combines together the following amazing works by brilliant people. Please check out their work if you haven't done so yet...
89 |
90 | ### The ML part
91 | - [sentence-transformers](https://github.com/UKPLab/sentence-transformers)
92 | - [transformers](https://github.com/huggingface/transformers)
93 | - [BERT](https://github.com/google-research/bert)
94 | ### The engineering part
95 | - [Elasticsearch](https://www.elastic.co/home)
96 | - [Docker](https://hub.docker.com)
97 |
--------------------------------------------------------------------------------
/assets/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/md-experiments/elastic_transformers/9f5920ab14d814739138544f4711567b8b762e5a/assets/architecture.png
--------------------------------------------------------------------------------
/assets/blinking_elastic2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/md-experiments/elastic_transformers/9f5920ab14d814739138544f4711567b8b762e5a/assets/blinking_elastic2.gif
--------------------------------------------------------------------------------
/notebooks/Searching_with_ElasticTransformers.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "%load_ext autoreload\n",
10 | "import os\n",
11 | "os.chdir(os.path.abspath(os.curdir).replace('notebooks',''))"
12 | ]
13 | },
14 | {
15 | "cell_type": "code",
16 | "execution_count": 2,
17 | "metadata": {},
18 | "outputs": [],
19 | "source": [
20 | "import datetime\n",
21 | "from tqdm import trange\n",
22 | "import pandas as pd\n",
23 | "import matplotlib.pyplot as plt\n",
24 | "pd.set_option('display.max_colwidth', 120)"
25 | ]
26 | },
27 | {
28 | "cell_type": "code",
29 | "execution_count": 3,
30 | "metadata": {},
31 | "outputs": [],
32 | "source": [
33 | "%autoreload 2\n",
34 | "\n",
35 | "from src.database import ElasticTransformers"
36 | ]
37 | },
38 | {
39 | "cell_type": "code",
40 | "execution_count": 4,
41 | "metadata": {},
42 | "outputs": [],
43 | "source": [
44 | "from sentence_transformers import SentenceTransformer\n",
45 | "\n",
46 | "bert_embedder = SentenceTransformer('bert-base-nli-mean-tokens')"
47 | ]
48 | },
49 | {
50 | "cell_type": "code",
51 | "execution_count": 5,
52 | "metadata": {},
53 | "outputs": [],
54 | "source": [
55 | "def embed_wrapper(ls):\n",
56 | " \"\"\"\n",
57 | " Helper function which simplifies the embedding call and helps lading data into elastic easier\n",
58 | " \"\"\"\n",
59 | " results=bert_embedder.encode(ls, convert_to_tensor=True)\n",
60 | " results = [r.tolist() for r in results]\n",
61 | " return results\n"
62 | ]
63 | },
64 | {
65 | "cell_type": "code",
66 | "execution_count": 6,
67 | "metadata": {},
68 | "outputs": [
69 | {
70 | "data": {
71 | "text/plain": [
72 | "True"
73 | ]
74 | },
75 | "execution_count": 6,
76 | "metadata": {},
77 | "output_type": "execute_result"
78 | }
79 | ],
80 | "source": [
81 | "et=ElasticTransformers(index_name='et-large')\n",
82 | "et.ping()"
83 | ]
84 | },
85 | {
86 | "cell_type": "markdown",
87 | "metadata": {},
88 | "source": [
89 | "# Search Experiments\n",
90 | "\n",
91 | "To analyse results, I compared top results side by side on a few searches. \n",
92 | "\n",
93 | "Approach is to take the top 10 hits, after removing some of the noisy results (duplicates or “headlines” of just one word).\n"
94 | ]
95 | },
96 | {
97 | "cell_type": "code",
98 | "execution_count": 7,
99 | "metadata": {},
100 | "outputs": [],
101 | "source": [
102 | "def select_search_results(df,top_n=10):\n",
103 | " # four tokens or more (filtering out some meaningless headlines)\n",
104 | " df=df[df.headline_text.apply(lambda x: len(x.split())>4)].copy()\n",
105 | " # remove exact duplicates\n",
106 | " df=df.groupby('headline_text', as_index=False).first()\n",
107 | " df=df.sort_values('_score',ascending=False)\n",
108 | " df=df.reset_index(drop=True)\n",
109 | " return df.head(top_n)"
110 | ]
111 | },
112 | {
113 | "cell_type": "code",
114 | "execution_count": 31,
115 | "metadata": {},
116 | "outputs": [
117 | {
118 | "name": "stdout",
119 | "output_type": "stream",
120 | "text": [
121 | "KEYWORD SEARCH RESULTS\n"
122 | ]
123 | },
124 | {
125 | "data": {
126 | "text/html": [
127 | "\n",
128 | "\n",
141 | "
\n",
142 | " \n",
143 | " \n",
144 | " \n",
145 | " headline_text \n",
146 | " _score \n",
147 | " \n",
148 | " \n",
149 | " \n",
150 | " \n",
151 | " 0 \n",
152 | " public warned of mozzie virus threat \n",
153 | " 13.311735 \n",
154 | " \n",
155 | " \n",
156 | " 1 \n",
157 | " cattle producers warned of virus threat \n",
158 | " 13.311735 \n",
159 | " \n",
160 | " \n",
161 | " 2 \n",
162 | " expert plays down hendra virus threat \n",
163 | " 13.295192 \n",
164 | " \n",
165 | " \n",
166 | " 3 \n",
167 | " residents reminded of mozzie virus threat \n",
168 | " 13.213888 \n",
169 | " \n",
170 | " \n",
171 | " 4 \n",
172 | " mozzie virus threat sparks health alert \n",
173 | " 13.213888 \n",
174 | " \n",
175 | " \n",
176 | " 5 \n",
177 | " report reveals lower mozzie virus threat \n",
178 | " 13.213888 \n",
179 | " \n",
180 | " \n",
181 | " 6 \n",
182 | " hendra like virus identified as potential threat \n",
183 | " 12.498677 \n",
184 | " \n",
185 | " \n",
186 | " 7 \n",
187 | " hendra virus poses constant threat chief vet \n",
188 | " 12.498677 \n",
189 | " \n",
190 | " \n",
191 | " 8 \n",
192 | " public warned of mossie borne virus threat \n",
193 | " 12.498677 \n",
194 | " \n",
195 | " \n",
196 | " 9 \n",
197 | " sunraysia fears watermelon virus threat from nt \n",
198 | " 12.483357 \n",
199 | " \n",
200 | " \n",
201 | "
\n",
202 | "
"
203 | ],
204 | "text/plain": [
205 | " headline_text _score\n",
206 | "0 public warned of mozzie virus threat 13.311735\n",
207 | "1 cattle producers warned of virus threat 13.311735\n",
208 | "2 expert plays down hendra virus threat 13.295192\n",
209 | "3 residents reminded of mozzie virus threat 13.213888\n",
210 | "4 mozzie virus threat sparks health alert 13.213888\n",
211 | "5 report reveals lower mozzie virus threat 13.213888\n",
212 | "6 hendra like virus identified as potential threat 12.498677\n",
213 | "7 hendra virus poses constant threat chief vet 12.498677\n",
214 | "8 public warned of mossie borne virus threat 12.498677\n",
215 | "9 sunraysia fears watermelon virus threat from nt 12.483357"
216 | ]
217 | },
218 | "metadata": {},
219 | "output_type": "display_data"
220 | },
221 | {
222 | "name": "stdout",
223 | "output_type": "stream",
224 | "text": [
225 | "CONTEXTUAL SEARCH RESULTS\n"
226 | ]
227 | },
228 | {
229 | "data": {
230 | "text/html": [
231 | "\n",
232 | "\n",
245 | "
\n",
246 | " \n",
247 | " \n",
248 | " \n",
249 | " headline_text \n",
250 | " _score \n",
251 | " \n",
252 | " \n",
253 | " \n",
254 | " \n",
255 | " 0 \n",
256 | " hendra like virus identified as potential threat \n",
257 | " 1.859408 \n",
258 | " \n",
259 | " \n",
260 | " 1 \n",
261 | " hendra report author warns of virus risk \n",
262 | " 1.853364 \n",
263 | " \n",
264 | " \n",
265 | " 2 \n",
266 | " fresh concerns over hendra virus outbreak \n",
267 | " 1.836927 \n",
268 | " \n",
269 | " \n",
270 | " 3 \n",
271 | " virus puts giteau in doubt \n",
272 | " 1.823136 \n",
273 | " \n",
274 | " \n",
275 | " 4 \n",
276 | " hendra virus case under investigation \n",
277 | " 1.817768 \n",
278 | " \n",
279 | " \n",
280 | " 5 \n",
281 | " who highlight dangers of vector borne diseases \n",
282 | " 1.804388 \n",
283 | " \n",
284 | " \n",
285 | " 6 \n",
286 | " potentially deadly virus sparks mozzie warning \n",
287 | " 1.799419 \n",
288 | " \n",
289 | " \n",
290 | " 7 \n",
291 | " who warns threat from vector borne diseases \n",
292 | " 1.793783 \n",
293 | " \n",
294 | " \n",
295 | " 8 \n",
296 | " fears as png diseases spread \n",
297 | " 1.791913 \n",
298 | " \n",
299 | " \n",
300 | " 9 \n",
301 | " deadly hendra virus strikes again \n",
302 | " 1.788590 \n",
303 | " \n",
304 | " \n",
305 | "
\n",
306 | "
"
307 | ],
308 | "text/plain": [
309 | " headline_text _score\n",
310 | "0 hendra like virus identified as potential threat 1.859408\n",
311 | "1 hendra report author warns of virus risk 1.853364\n",
312 | "2 fresh concerns over hendra virus outbreak 1.836927\n",
313 | "3 virus puts giteau in doubt 1.823136\n",
314 | "4 hendra virus case under investigation 1.817768\n",
315 | "5 who highlight dangers of vector borne diseases 1.804388\n",
316 | "6 potentially deadly virus sparks mozzie warning 1.799419\n",
317 | "7 who warns threat from vector borne diseases 1.793783\n",
318 | "8 fears as png diseases spread 1.791913\n",
319 | "9 deadly hendra virus strikes again 1.788590"
320 | ]
321 | },
322 | "metadata": {},
323 | "output_type": "display_data"
324 | }
325 | ],
326 | "source": [
327 | "query='virus threat'\n",
328 | "print('KEYWORD SEARCH RESULTS')\n",
329 | "df0=et.search(query,'headline_text',type='match',embedder=embed_wrapper, size = 1000)\n",
330 | "display(select_search_results(df0))\n",
331 | "print('CONTEXTUAL SEARCH RESULTS')\n",
332 | "df1=et.search(query,'headline_text',type='dense',embedder=embed_wrapper, size = 1000)\n",
333 | "display(select_search_results(df1))\n",
334 | "\n"
335 | ]
336 | },
337 | {
338 | "cell_type": "code",
339 | "execution_count": 32,
340 | "metadata": {},
341 | "outputs": [
342 | {
343 | "data": {
344 | "text/html": [
345 | "\n",
346 | "\n",
359 | "
\n",
360 | " \n",
361 | " \n",
362 | " \n",
363 | " headline_text \n",
364 | " _score \n",
365 | " \n",
366 | " \n",
367 | " \n",
368 | " \n",
369 | " 5 \n",
370 | " who highlight dangers of vector borne diseases \n",
371 | " 1.804388 \n",
372 | " \n",
373 | " \n",
374 | " 8 \n",
375 | " fears as png diseases spread \n",
376 | " 1.791913 \n",
377 | " \n",
378 | " \n",
379 | " 21 \n",
380 | " the odds of an outbreak \n",
381 | " 1.773913 \n",
382 | " \n",
383 | " \n",
384 | " 25 \n",
385 | " flood waters carry risk of disease infection \n",
386 | " 1.769219 \n",
387 | " \n",
388 | " \n",
389 | " 27 \n",
390 | " port uncertain of impact of viral meningitis outbreak \n",
391 | " 1.767367 \n",
392 | " \n",
393 | " \n",
394 | " 30 \n",
395 | " human error blamed for infection scare \n",
396 | " 1.764524 \n",
397 | " \n",
398 | " \n",
399 | " 34 \n",
400 | " oakey defence base contaminants linked to serious disease \n",
401 | " 1.758969 \n",
402 | " \n",
403 | " \n",
404 | " 35 \n",
405 | " dangerous parasite rife in nt \n",
406 | " 1.757938 \n",
407 | " \n",
408 | " \n",
409 | " 40 \n",
410 | " academic fears spread of mozzie borne disease \n",
411 | " 1.755103 \n",
412 | " \n",
413 | " \n",
414 | " 44 \n",
415 | " sti symptoms dangers and treatments \n",
416 | " 1.751830 \n",
417 | " \n",
418 | " \n",
419 | "
\n",
420 | "
"
421 | ],
422 | "text/plain": [
423 | " headline_text _score\n",
424 | "5 who highlight dangers of vector borne diseases 1.804388\n",
425 | "8 fears as png diseases spread 1.791913\n",
426 | "21 the odds of an outbreak 1.773913\n",
427 | "25 flood waters carry risk of disease infection 1.769219\n",
428 | "27 port uncertain of impact of viral meningitis outbreak 1.767367\n",
429 | "30 human error blamed for infection scare 1.764524\n",
430 | "34 oakey defence base contaminants linked to serious disease 1.758969\n",
431 | "35 dangerous parasite rife in nt 1.757938\n",
432 | "40 academic fears spread of mozzie borne disease 1.755103\n",
433 | "44 sti symptoms dangers and treatments 1.751830"
434 | ]
435 | },
436 | "execution_count": 32,
437 | "metadata": {},
438 | "output_type": "execute_result"
439 | }
440 | ],
441 | "source": [
442 | "df11=select_search_results(df1,1000)\n",
443 | "\n",
444 | "df11[df11.headline_text.apply(lambda x: all([q not in x for q in query.split()]))].head(10)\n"
445 | ]
446 | },
447 | {
448 | "cell_type": "code",
449 | "execution_count": 35,
450 | "metadata": {},
451 | "outputs": [
452 | {
453 | "name": "stdout",
454 | "output_type": "stream",
455 | "text": [
456 | "KEYWORD SEARCH RESULTS\n"
457 | ]
458 | },
459 | {
460 | "data": {
461 | "text/html": [
462 | "\n",
463 | "\n",
476 | "
\n",
477 | " \n",
478 | " \n",
479 | " \n",
480 | " headline_text \n",
481 | " _score \n",
482 | " \n",
483 | " \n",
484 | " \n",
485 | " \n",
486 | " 0 \n",
487 | " perth storm a natural disaster \n",
488 | " 16.165108 \n",
489 | " \n",
490 | " \n",
491 | " 1 \n",
492 | " more natural disaster planning needed \n",
493 | " 16.165108 \n",
494 | " \n",
495 | " \n",
496 | " 2 \n",
497 | " gunnedah declared natural disaster area \n",
498 | " 16.165108 \n",
499 | " \n",
500 | " \n",
501 | " 3 \n",
502 | " bushfire prompts natural disaster declaration \n",
503 | " 16.165108 \n",
504 | " \n",
505 | " \n",
506 | " 4 \n",
507 | " maclean fire not natural disaster \n",
508 | " 16.032738 \n",
509 | " \n",
510 | " \n",
511 | " 5 \n",
512 | " state helps natural disaster victims \n",
513 | " 15.960692 \n",
514 | " \n",
515 | " \n",
516 | " 6 \n",
517 | " esperance declared natural disaster area \n",
518 | " 15.960692 \n",
519 | " \n",
520 | " \n",
521 | " 7 \n",
522 | " flooding sparks natural disaster declarations \n",
523 | " 15.960692 \n",
524 | " \n",
525 | " \n",
526 | " 8 \n",
527 | " government declares natural disaster areas \n",
528 | " 15.960692 \n",
529 | " \n",
530 | " \n",
531 | " 9 \n",
532 | " nsw natural disaster zone widened \n",
533 | " 15.960692 \n",
534 | " \n",
535 | " \n",
536 | "
\n",
537 | "
"
538 | ],
539 | "text/plain": [
540 | " headline_text _score\n",
541 | "0 perth storm a natural disaster 16.165108\n",
542 | "1 more natural disaster planning needed 16.165108\n",
543 | "2 gunnedah declared natural disaster area 16.165108\n",
544 | "3 bushfire prompts natural disaster declaration 16.165108\n",
545 | "4 maclean fire not natural disaster 16.032738\n",
546 | "5 state helps natural disaster victims 15.960692\n",
547 | "6 esperance declared natural disaster area 15.960692\n",
548 | "7 flooding sparks natural disaster declarations 15.960692\n",
549 | "8 government declares natural disaster areas 15.960692\n",
550 | "9 nsw natural disaster zone widened 15.960692"
551 | ]
552 | },
553 | "metadata": {},
554 | "output_type": "display_data"
555 | },
556 | {
557 | "name": "stdout",
558 | "output_type": "stream",
559 | "text": [
560 | "CONTEXTUAL SEARCH RESULTS\n"
561 | ]
562 | },
563 | {
564 | "data": {
565 | "text/html": [
566 | "\n",
567 | "\n",
580 | "
\n",
581 | " \n",
582 | " \n",
583 | " \n",
584 | " headline_text \n",
585 | " _score \n",
586 | " \n",
587 | " \n",
588 | " \n",
589 | " \n",
590 | " 0 \n",
591 | " natural disaster declared in broken hill \n",
592 | " 1.879506 \n",
593 | " \n",
594 | " \n",
595 | " 1 \n",
596 | " natural disaster declared in storm area \n",
597 | " 1.877636 \n",
598 | " \n",
599 | " \n",
600 | " 2 \n",
601 | " lismore declared a natural disaster area \n",
602 | " 1.867503 \n",
603 | " \n",
604 | " \n",
605 | " 3 \n",
606 | " broken hill declared a natural disaster area \n",
607 | " 1.854052 \n",
608 | " \n",
609 | " \n",
610 | " 4 \n",
611 | " natural disasters take toll on austar \n",
612 | " 1.848880 \n",
613 | " \n",
614 | " \n",
615 | " 5 \n",
616 | " perth storm a natural disaster \n",
617 | " 1.836232 \n",
618 | " \n",
619 | " \n",
620 | " 6 \n",
621 | " call for nambucca valley natural disaster \n",
622 | " 1.834766 \n",
623 | " \n",
624 | " \n",
625 | " 7 \n",
626 | " wagga albury declared natural disaster areas \n",
627 | " 1.831764 \n",
628 | " \n",
629 | " \n",
630 | " 8 \n",
631 | " ballina area declared natural disaster zone \n",
632 | " 1.823700 \n",
633 | " \n",
634 | " \n",
635 | " 9 \n",
636 | " disasters take toll on shire \n",
637 | " 1.822510 \n",
638 | " \n",
639 | " \n",
640 | "
\n",
641 | "
"
642 | ],
643 | "text/plain": [
644 | " headline_text _score\n",
645 | "0 natural disaster declared in broken hill 1.879506\n",
646 | "1 natural disaster declared in storm area 1.877636\n",
647 | "2 lismore declared a natural disaster area 1.867503\n",
648 | "3 broken hill declared a natural disaster area 1.854052\n",
649 | "4 natural disasters take toll on austar 1.848880\n",
650 | "5 perth storm a natural disaster 1.836232\n",
651 | "6 call for nambucca valley natural disaster 1.834766\n",
652 | "7 wagga albury declared natural disaster areas 1.831764\n",
653 | "8 ballina area declared natural disaster zone 1.823700\n",
654 | "9 disasters take toll on shire 1.822510"
655 | ]
656 | },
657 | "metadata": {},
658 | "output_type": "display_data"
659 | }
660 | ],
661 | "source": [
662 | "query='natural disaster'\n",
663 | "print('KEYWORD SEARCH RESULTS')\n",
664 | "df0=et.search(query,'headline_text',type='match',embedder=embed_wrapper, size = 1000)\n",
665 | "display(select_search_results(df0,10))\n",
666 | "print('CONTEXTUAL SEARCH RESULTS')\n",
667 | "df1=et.search(query,'headline_text',type='dense',embedder=embed_wrapper, size = 1000)\n",
668 | "display(select_search_results(df1,10))\n",
669 | "\n"
670 | ]
671 | },
672 | {
673 | "cell_type": "code",
674 | "execution_count": 36,
675 | "metadata": {},
676 | "outputs": [
677 | {
678 | "data": {
679 | "text/html": [
680 | "\n",
681 | "\n",
694 | "
\n",
695 | " \n",
696 | " \n",
697 | " \n",
698 | " headline_text \n",
699 | " _score \n",
700 | " \n",
701 | " \n",
702 | " \n",
703 | " \n",
704 | " 28 \n",
705 | " power supply at risk if flood situation worsens \n",
706 | " 1.787914 \n",
707 | " \n",
708 | " \n",
709 | " 36 \n",
710 | " widespread damage from freak storm \n",
711 | " 1.779122 \n",
712 | " \n",
713 | " \n",
714 | " 40 \n",
715 | " catastrophic fire conditions for wa \n",
716 | " 1.776245 \n",
717 | " \n",
718 | " \n",
719 | " 43 \n",
720 | " leigh creek ucg project lifeline or toxic environmental hazard \n",
721 | " 1.772300 \n",
722 | " \n",
723 | " \n",
724 | " 46 \n",
725 | " qlds wild weather caused by freak event \n",
726 | " 1.770647 \n",
727 | " \n",
728 | " \n",
729 | " 48 \n",
730 | " wild weather causes qld flooding \n",
731 | " 1.769357 \n",
732 | " \n",
733 | " \n",
734 | " 50 \n",
735 | " cyclone damaged water supply fixed \n",
736 | " 1.767906 \n",
737 | " \n",
738 | " \n",
739 | " 53 \n",
740 | " humungous effort on catastrophic day \n",
741 | " 1.766371 \n",
742 | " \n",
743 | " \n",
744 | " 56 \n",
745 | " nsw floods receding water reveals destruction \n",
746 | " 1.766130 \n",
747 | " \n",
748 | " \n",
749 | " 58 \n",
750 | " cyclone olwyn carnarvon water supply problems \n",
751 | " 1.765932 \n",
752 | " \n",
753 | " \n",
754 | "
\n",
755 | "
"
756 | ],
757 | "text/plain": [
758 | " headline_text _score\n",
759 | "28 power supply at risk if flood situation worsens 1.787914\n",
760 | "36 widespread damage from freak storm 1.779122\n",
761 | "40 catastrophic fire conditions for wa 1.776245\n",
762 | "43 leigh creek ucg project lifeline or toxic environmental hazard 1.772300\n",
763 | "46 qlds wild weather caused by freak event 1.770647\n",
764 | "48 wild weather causes qld flooding 1.769357\n",
765 | "50 cyclone damaged water supply fixed 1.767906\n",
766 | "53 humungous effort on catastrophic day 1.766371\n",
767 | "56 nsw floods receding water reveals destruction 1.766130\n",
768 | "58 cyclone olwyn carnarvon water supply problems 1.765932"
769 | ]
770 | },
771 | "execution_count": 36,
772 | "metadata": {},
773 | "output_type": "execute_result"
774 | }
775 | ],
776 | "source": [
777 | "df11=select_search_results(df1,1000)\n",
778 | "\n",
779 | "df11[df11.headline_text.apply(lambda x: all([q not in x for q in query.split()]))].head(10)\n"
780 | ]
781 | },
782 | {
783 | "cell_type": "code",
784 | "execution_count": 24,
785 | "metadata": {},
786 | "outputs": [
787 | {
788 | "name": "stdout",
789 | "output_type": "stream",
790 | "text": [
791 | "KEYWORD SEARCH RESULTS\n"
792 | ]
793 | },
794 | {
795 | "data": {
796 | "text/html": [
797 | "\n",
798 | "\n",
811 | "
\n",
812 | " \n",
813 | " \n",
814 | " \n",
815 | " headline_text \n",
816 | " _score \n",
817 | " \n",
818 | " \n",
819 | " \n",
820 | " \n",
821 | " 0 \n",
822 | " regulatory madness in the banking world \n",
823 | " 18.955673 \n",
824 | " \n",
825 | " \n",
826 | " 1 \n",
827 | " china pushes through banking sector reform \n",
828 | " 14.721983 \n",
829 | " \n",
830 | " \n",
831 | " 2 \n",
832 | " swan to announce banking reform package \n",
833 | " 14.582440 \n",
834 | " \n",
835 | " \n",
836 | " 3 \n",
837 | " open banking more choice or data risk \n",
838 | " 13.001936 \n",
839 | " \n",
840 | " \n",
841 | " 4 \n",
842 | " swan wraps up meeting on banking rules reform \n",
843 | " 12.904054 \n",
844 | " \n",
845 | " \n",
846 | " 5 \n",
847 | " govt internet regulatory plan criticised \n",
848 | " 12.000524 \n",
849 | " \n",
850 | " \n",
851 | " 6 \n",
852 | " regulatory duplication strangling aquaculture development \n",
853 | " 12.000524 \n",
854 | " \n",
855 | " \n",
856 | " 7 \n",
857 | " us flags financial regulatory reforms \n",
858 | " 11.530772 \n",
859 | " \n",
860 | " \n",
861 | " 8 \n",
862 | " mcconnell a regulatory train wreck \n",
863 | " 11.530772 \n",
864 | " \n",
865 | " \n",
866 | " 9 \n",
867 | " billabong rescue package clears regulatory hurdle \n",
868 | " 11.220221 \n",
869 | " \n",
870 | " \n",
871 | "
\n",
872 | "
"
873 | ],
874 | "text/plain": [
875 | " headline_text _score\n",
876 | "0 regulatory madness in the banking world 18.955673\n",
877 | "1 china pushes through banking sector reform 14.721983\n",
878 | "2 swan to announce banking reform package 14.582440\n",
879 | "3 open banking more choice or data risk 13.001936\n",
880 | "4 swan wraps up meeting on banking rules reform 12.904054\n",
881 | "5 govt internet regulatory plan criticised 12.000524\n",
882 | "6 regulatory duplication strangling aquaculture development 12.000524\n",
883 | "7 us flags financial regulatory reforms 11.530772\n",
884 | "8 mcconnell a regulatory train wreck 11.530772\n",
885 | "9 billabong rescue package clears regulatory hurdle 11.220221"
886 | ]
887 | },
888 | "metadata": {},
889 | "output_type": "display_data"
890 | },
891 | {
892 | "name": "stdout",
893 | "output_type": "stream",
894 | "text": [
895 | "CONTEXTUAL SEARCH RESULTS\n"
896 | ]
897 | },
898 | {
899 | "data": {
900 | "text/html": [
901 | "\n",
902 | "\n",
915 | "
\n",
916 | " \n",
917 | " \n",
918 | " \n",
919 | " headline_text \n",
920 | " _score \n",
921 | " \n",
922 | " \n",
923 | " \n",
924 | " \n",
925 | " 0 \n",
926 | " us flags financial regulatory reforms \n",
927 | " 1.863391 \n",
928 | " \n",
929 | " \n",
930 | " 1 \n",
931 | " the banking royal commissions recommendations \n",
932 | " 1.850401 \n",
933 | " \n",
934 | " \n",
935 | " 2 \n",
936 | " what can we expect from the banking inquiry \n",
937 | " 1.841991 \n",
938 | " \n",
939 | " \n",
940 | " 3 \n",
941 | " banking royal commission superannuation hearings \n",
942 | " 1.836119 \n",
943 | " \n",
944 | " \n",
945 | " 4 \n",
946 | " banking royal commission anz financial advice clients interest \n",
947 | " 1.833776 \n",
948 | " \n",
949 | " \n",
950 | " 5 \n",
951 | " rba considers cap on credit card surcharges \n",
952 | " 1.832314 \n",
953 | " \n",
954 | " \n",
955 | " 6 \n",
956 | " rba on banks interest rate moves \n",
957 | " 1.831732 \n",
958 | " \n",
959 | " \n",
960 | " 7 \n",
961 | " will changes to financial advice laws see the \n",
962 | " 1.831395 \n",
963 | " \n",
964 | " \n",
965 | " 8 \n",
966 | " commonwealth bank responds to financial planning inquiry \n",
967 | " 1.831379 \n",
968 | " \n",
969 | " \n",
970 | " 9 \n",
971 | " reserve bank financial stability review \n",
972 | " 1.830978 \n",
973 | " \n",
974 | " \n",
975 | "
\n",
976 | "
"
977 | ],
978 | "text/plain": [
979 | " headline_text _score\n",
980 | "0 us flags financial regulatory reforms 1.863391\n",
981 | "1 the banking royal commissions recommendations 1.850401\n",
982 | "2 what can we expect from the banking inquiry 1.841991\n",
983 | "3 banking royal commission superannuation hearings 1.836119\n",
984 | "4 banking royal commission anz financial advice clients interest 1.833776\n",
985 | "5 rba considers cap on credit card surcharges 1.832314\n",
986 | "6 rba on banks interest rate moves 1.831732\n",
987 | "7 will changes to financial advice laws see the 1.831395\n",
988 | "8 commonwealth bank responds to financial planning inquiry 1.831379\n",
989 | "9 reserve bank financial stability review 1.830978"
990 | ]
991 | },
992 | "metadata": {},
993 | "output_type": "display_data"
994 | }
995 | ],
996 | "source": [
997 | "#query='virus threat'\n",
998 | "query='regulatory risk banking reform'\n",
999 | "print('KEYWORD SEARCH RESULTS')\n",
1000 | "df0=et.search(query,'headline_text',type='match',embedder=embed_wrapper, size = 1000)\n",
1001 | "display(select_search_results(df0,10))\n",
1002 | "print('CONTEXTUAL SEARCH RESULTS')\n",
1003 | "df1=et.search(query,'headline_text',type='dense',embedder=embed_wrapper, size = 1000)\n",
1004 | "display(select_search_results(df1,10))\n",
1005 | "\n"
1006 | ]
1007 | },
1008 | {
1009 | "cell_type": "markdown",
1010 | "metadata": {},
1011 | "source": [
1012 | "# Speed comparison\n",
1013 | "\n",
1014 | "Below we perform some non-functional testing on the impact of size of index together with search parameters on time of the query. \n",
1015 | "We have tested with 3 index sizes: 1k (Tiny), 100k (Medium) & 1.1mn (Large). We have not paid particular attention to and sampling effects, meaning that for instance, the 1k index is simply the first 1000 headlines in the data, this might mean ti is not well randomized, which we have not studied"
1016 | ]
1017 | },
1018 | {
1019 | "cell_type": "code",
1020 | "execution_count": 38,
1021 | "metadata": {},
1022 | "outputs": [
1023 | {
1024 | "name": "stderr",
1025 | "output_type": "stream",
1026 | "text": [
1027 | "100%|██████████| 10/10 [26:27<00:00, 158.76s/it]\n"
1028 | ]
1029 | }
1030 | ],
1031 | "source": [
1032 | "\n",
1033 | "queries=['Amazon','news','security thread','tech news','new vaccine developed new cure','results game today all winners']\n",
1034 | "result_sizes=[1,10,100]\n",
1035 | "repeat=10\n",
1036 | "\n",
1037 | "col_names=['search index','search type','search size' , 'query', '# tokens query','repeat','time taken']\n",
1038 | "search_to_compare={'match':'Keyword Search','dense':'Contextual Search',}\n",
1039 | "indices_to_compare={'et-tiny':'Tiny','et-medium':'Medium','et-large':'Large'}\n",
1040 | "\n",
1041 | "res=[]\n",
1042 | "for i in trange(repeat):\n",
1043 | " for index in indices_to_compare:\n",
1044 | " for search_type in search_to_compare:\n",
1045 | " for query in queries:\n",
1046 | " for size in result_sizes:\n",
1047 | " t0=datetime.datetime.now()\n",
1048 | " _ = et.search(query=query,\n",
1049 | " field='headline_text',\n",
1050 | " index_name=index,\n",
1051 | " type=search_type,\n",
1052 | " embedder=embed_wrapper, \n",
1053 | " size=size)\n",
1054 | " t1=datetime.datetime.now()\n",
1055 | " time_taken=(t1-t0).total_seconds()\n",
1056 | " res.append([indices_to_compare[index], search_to_compare[search_type], size, query, len(query.split()), i,time_taken])\n",
1057 | " \n",
1058 | "result_df=pd.DataFrame(res, columns=col_names)\n",
1059 | "result_df.to_csv('data/results_search.csv')\n"
1060 | ]
1061 | },
1062 | {
1063 | "cell_type": "markdown",
1064 | "metadata": {},
1065 | "source": [
1066 | "Compare speed across different index sizes and search types for\n",
1067 | "- query token length\n",
1068 | "- result size\n",
1069 | "- index size\n",
1070 | "\n",
1071 | "Results are below"
1072 | ]
1073 | },
1074 | {
1075 | "cell_type": "code",
1076 | "execution_count": 39,
1077 | "metadata": {},
1078 | "outputs": [
1079 | {
1080 | "data": {
1081 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAskAAAEsCAYAAAAxVC5BAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/d3fzzAAAACXBIWXMAAAsTAAALEwEAmpwYAAA4iklEQVR4nO3deZgU1dn+8e8tq1vAIK6oYHBBxGhEeI3GoMTdiBoXjBoQ/bklGmPc8qqIW6JGE2NiTEhU3Dc0hiiJOxp3QBBBNC9R1HFlEwVBWZ7fH6dmbIqemQZmpme5P9fVF92nTlU9VT2cfvr0qVOKCMzMzMzM7CurlTsAMzMzM7PGxkmymZmZmVmOk2QzMzMzsxwnyWZmZmZmOU6SzczMzMxynCSbmZmZmeU4STYzq4WkrpJC0rByx9LUZedxRAPsZ3C2r371vS8za56cJJvZMiStIel0Sf+WNFvSIkkfSRqdJR6tyx2jmZlZffOHnZlVkdQdeAjYEngM+BUwE1gP+B5wE7ANcHa5YiyTt4HVgcXlDsRKditwF/BluQMxs6bJSbKZASBpdeBBYHPgBxFxf67KFZJ2AnZq8ODKRNLaEfFZpFuTLix3PI1R5Tkqdxx5EbEEWFLuOMys6fJwCzOrdDywFXB1kQQZgIgYGxF/LCyTdJCkZyXNlzQvez4gv66k6ZLGSPqmpMeyuh9LulpSa0ntJV0l6T1JCyU9LalHbhuV40y/J2mYpLclfSFpkqSBRfa5l6S7Jb0paYGkTyQ9Ium7ReqOyWLcXNJISbOBT7Nly41JLiyTdICksVncH0j6dbFhKZJ+IOmVrN47ki7MjiUkDa7mfSlcfxNJNxYc98eSnpM0KFdPkk6WNF7S59m5flLS7kW2eUp2Tt6T9GUW/22SuhapG5JGSOov6RlJ84B/FCzfQdK92fCcLyS9K+lOSd8osq2dJT2V/d3MkvRXSWvVdg6ydb8t6Z+SPszO5XvZcKD/Kaiz3Jjk7HV1jxG5fXwvOy+fZPuYJOmkUuIzs+bBPclmVunQ7N/hpa4g6RTgOuB14OKseDDwgKQTIyK/rS7Ao8DdwEhgL+AM0jCGnqQhDZcD6wJnZtvpERFLc9u5AlgTqEzYjwXulNQ+IkYU1BsMfB24BagANiZ9GXhc0u4R8e/cdtcCngKeBc4jDTOpzX7AKcCfgBuBAVnsc4BfVlaSdARwJ/Bf4KLsmAcB3y9hH2RJ96PZMfwR+A/QAdgO+A5wc0H1W4EjSef4JqAdcBTwqKRDImJUQd0zgReAa4HZwLakc7SHpF4RMSsXSm/gB8BfCvcp6QDgPmA+8FdgGrABsHe2zf8WbGN70q8WNwF3AP2A44ClwAm1nIetsvPwIfA74CNgfWBX4JvZsVTnmCJl+wMDs+1U7uME0vv5AnBZdkx7AtdL+kZEnFVTjGbWTESEH3744QfALGDuCtRfB5hHSoa+VlD+NVJC9BnQsaB8OhDAYbntjCclR38HVFB+WlZ/74KywVnZ20CHgvIOWdlsYPWC8jWLxL0+aZz16Fz5mGzblxZZp2u2bFiRsvlA14JyAZOBDwrKWgPvkRKxdQrK1wLezLYzuJbzvV1W7+xa6h2c1TshV94aGAe8lTvPxc5R/2L7ysoC+F6ufA1gBvAxsHGR7a2W28ZSoG+uzkPAImCtWo6v8u+iTy31Kv9W+tVQp3f2/j0PtM/KNiQNrbmjSP3fkYZwbF7X///88MOPxvfwcAszq/Q1UmJbqj1JvbnXRsSnlYXZ82tJCeD3cuu8FxH35sqeISWWv4+IKCiv7OXdosi+r4+IuQX7nEvq+VuH1CtZWT6/8rmktSR1IiU5LwJ9qzmuq6opr84DETG9YJ8BPAlsUDB8YEdgI2BERMwpqDsvi7sUlce7u6SaeriPJr2PD0hat/IBdCQNjehKwTmtPEeSVpPUIav7Sra/YufolYh4LFe2N6n3/+qIeC+/Qiz/S8DzEfFiruwJUiLftYZjg6/OwwBJ7WupWy1JmwCjSIn9gIioHHN+KKnn/YbC85edl3+Qhinm/67NrBnycAszq/QpsPYK1O+W/TulyLLKss1z5W8VqTunmmWV5Z2KrDO1SNlr+X1mY2EvIyVxHXP1g+XNiIhPipTX5M0iZZVDFDqRetsrz9UbReoWK1tORLwt6TLgF8AHkiYCjwP3RsTYgqo9SO/jR8tvpcr6pOEaSNoDGEpKiPNJ5zpF1v1PkbLKpHtCLYdRqbZzVpO7SF8E/hf4maQXgIeBuyLi7VJ2Lmlt0nCPNYE9I+LjgsWV4+DzXwQKrV/KfsysaXOSbGaVJgO7Sdo8IoolMXWhptkGqlumldlR1ov7NCkRugZ4ldTDupSUaO5RZLXPV2JXNR3TSsVenYg4X9KNpHG03yGNHT5L0pURcU7BPmcAP6xhU5MBlGYreYQ0ZOZc0heVBaQvEHdR/OLulTlHeSt9ziLiC2BPSX1IX352I42HHybphxHxt5rWl9SKNCZ+G+CAiMh/yavc/4+AD6rZTH39/zCzRsRJsplVuo+UcBxP6qWrTWWi0JPUo1lom1ydutaDNIa5pn32Jw1xGBIRNxVWlHRpPcVVnenZv1sVWVasrFrZF5jfA7/Phhs8DJwt6eqsR/T/SPNcv5AN56jJD4FWwL4RUdWTL2lNivciV6eyd3l7UtJd7yLiJeAlqBo6MQG4FKgxSSYNBdoXOCUiHi6y/P+yf2cWGVZiZi2IxySbWaW/kn76P1NFpnADkLRjNqMFpBkG5gOnZj9fV9ZZGziVNMzg0XqK9WRJHQr22QE4CfiENDsFfNVbuUzPpKS9qH48cn0ZR+qVHCypKvnMertLmlYsGy/cprAsG0dbOfSkcru3kNr2X1WzncKhAkXPEelL0op8PjxCuhjy55I2LLLPOutRz8YG51WQes+/Xsu6p5NmIvldRFxfTbV7gC+Ai5TmDs9vo4OkdisUtJk1Se5JNjMAIuLzbBqvh0gXfT1CSnJnAZ2B3Uk/b1+Z1f9E0tmkKeBeLJhndjDQHTix8OK6OjYz22dlD/GxwKbA8RFRORzgGdI0YVdnc/5WkHo6jyENvehVT7EtJyIWSzoTuB14SdINpCngBpPObzeKj5EutDswXNJ9pC8z80gXBB4PvBgRb2T7Gpmdl59I+hZp7O1M0vR7O5Pem8px238DfgaMljScdHe6PUkzacxcgeP7XNJxpCnnJkuqnAKuM+lv5jcs3/O/ss7Pvug8SDZTB2kava3J/jaLkbQtcDXpb+JlSUfnqvw3Ip6PiApJJ5O+NE6VdCtp5pTOpL+Zg0i/Wkyvo+Mxs0bKSbK1KFmy9BbQJiIa5S2Gs5sf3BYRXRp63xExTdIOwImkuXDPI81SMZvUGzqINK9tZf0/SvoAOAu4MCt+BTg4Ih6ox1DPIY3J/TFfXYR2VEQUxvaJpMqk/lRSezeeNK/xcTRgkpzFc4ekRcAFpHmSPwJuACYB95PGAtfklaxeP9Kcx62Ad0hzMV+d29cQSU+S5hz+BdCWLDnMXlfWe1bSD7KYLslieAz4Lmk894oc3yhJu5J6oY/jq4sH/036UlJXHiBN03Y46b1fQBoi8f9I57M665J6xzdg2TmlK91MmgqOiLhJ0n9Ic0ifSLrocybpy8kFpHNpjYCkMaT28q/ljqU6kqaTvsB7+E4To2VnXDKrXf4/vNKdzq4HDoqIp2pat9xqS5KzD/krSeNsl5B+yj49N3tAfcfYjzIlyY2d0l3pbgJ2j4gx5Y2mbkj6OWnauZ0joqYbYZjVSNIPSTfn2Zp0kepE4LKIeGYVtzsCqIiI8+sgxjrbVra9MVSTJEvqSPoVYz/SBbwfADdGxOV1se8ViHE6TpKbJI9JtlWidDvc64D9G1uCrCK3Ba6l/tdIP+H+njS2cWNSj98X5Y7Nmj5JbbOZFQrL1iL1hs8i9fKarRRJZ5BmcfklqYd9U9KdGYteX9BC/Jb0S1gP0g2HDiQNA6pTbs+bLyfJttIknUj6mXfviHguK+sg6QZJH0h6T9KlklplCcJsSb0K1l9P0ueSOkt6KvvZF0m7SApJ+2ev+2dzwlbe8OB8SW9L+ljSLZUXcEnqmq13nKR3gCeyfV8laaakN0lTZ1VnS4CIuDMilkTEgoh4JCImFcQ8RNJUSXMkPSxps4Jlv5P0rqRPJY2X9J2CZcMkjZR0m6RPSRdwfV3STZLez7b3QO78/jw7xg8kHbsy75E1KpsD0yT9StIJki4kDbXoBpwfEV+WNzxrqrI28GLgxxFxf0TMj4hFEfGPyG6hLamdpGuy9ub97Hm7bFk/SRXF2hylW3QfRZpBZZ6kf2TlG0m6T9IMSW9JOi0r/3q2re9nr9eSNE3Sj2rYVkjqXnA8I5TNQCNpHUkPZvuZkz0v9Ve2nUh3TpwTEUsj4vWIGFmwn60lPZp9Nr0h6fCCZftLmpC15+9KGlawbLnPmqz8/2WfD59Jek3pmoBK20uaJGmupLu1CjfCsYbjJNlW1smkRrl/RIwrKB9BuiCpO7ADsBfpZ6Yv+eomAJWOBB6PiBmkGQn6ZeXfJU3jtVvB68pe6sHZY3dS0rEW8IdcbN8l9RzsTRqneEAWS2/S3bSq8x9giaSbJe2rglkIAJRmfPhf4BDSRTz/Bu4sqDKWdGHY10njdu/NNYQDSBc2dSRdwHUr6Xa+PYH1SL0elTYg9XxsTBrfeV0+HmtyZgAvkJKEa0njuD8CjoiIUu+6Z1bMzqQbwdQ0/d15wP+Q2qhvAn2AwiEPRduciBhOaq+ujIi1IuL7klYj3X3wlax+f+B0SXtHxGxgCPAXpTtD/haYGBG3FNtWCce2GmmI1Wak3vEFLN/mV+cF4DJJx0pa5s6dStMcPkpqq9cDBgJ/lFQ5leR80lzZHUmdKydLOii3/arPGkmHAcOydb5G6rWeVVD3cGAf0pfi7UifY9bY1eU9rv1oGQ/SVd2fkq5WX62gfH3S0ITVC8qOBJ7MnvclXWhUORZ+HHB49rw/MCl7/i/SFfsvZK+fAg7Jnj9Omt+0cvtbAYv46na2AWxesPwJ4KSC13tldVpXc2w9SIl+BSnZHwWsny37J3BcQd3VSDdW2Kyabc0Bvpk9HwY8XbBsQ9JNLdYpsl4/0gdB64Kyj4H/Kfd774cffjS+B+mL14e11PkvsF/B672B6dnzGtucrE28tGBZX+Cd3PZ/AdxU8Pr3pAs23wM6FZQvs62sLIDuNdUpWLY9MKfg9RhSR0yxuquTOjbGZ58T00hzggMcAfw7V//PwIXVbOsa4LfZ82KfNQ8DP61m3enA0QWvrwT+VO6/Gz9qf7gn2VbWyaThCX+VquZA3QxoQ7pl7ieSPiE1OusBRMSLpKSyn6StSb3No7J1nwe2VJrDdXvSXK+bKM2J2oevrrTfiDQdU6W3SQly4dyv7xY83yj3usbb1kbE1IgYHOmiuW2z9a8pOL7fFRzbbNL0UxsDSDoz+6ltbra8A+mK+mJxbQLMjog5FDcrlr2w8HNSr7mZWd4sYF3VPDa2WNu5UeE2VqDN2QzYqLItzNq7/2XZdng4qQ0dERGzimyjJJLWkPRnpSF2n5I+CzoqN76/mEhD5n4ZETuSbnd+D+kXvq9nx9A3dwxHkXrUkdRX0pPZMI+5pPnM83N059v0/9YQTuGMKG7PmwgnybayPiL1/n6HdHEIpAbjC2DdiOiYPb4WET0L1ruZNOTiGGBkpJshEGlu2/HAT4HJkYZnPEe6Uvu/EVE5Z+v7pMat0qakHt+PCsoKp2z5gNR4FdYvSUS8TurR2Lbg+E4sOLaOEbF6RDynNP74bNJPautEREdgLsvepKEwrneBrytdfW1mtiqeJ7W9B9VQp1jb+X6J289Pg/Uu8FauLVw7IvaDqlt/Dyd1dpxSON64yLYgJY1rFLzeoOD5z0m/GPaNiK/x1TC8FbpBTUR8SrqocU3SkId3gadyx7BWRJycrXIHqRNnk4joAPypyD7zbfo3ViQma/ycJNtKi4j3SYnyPpJ+GxEfkO68dbWkryldZPcNSd8tWO024GBSonxLbpNPAT/hq/HHY3KvIY0B/pmkbkozA/wSuDuqn/P4HuA0SV2yMb3nVnc82UUcP6+8KETpVrdHksa1QWokfyGpZ7a8QzYODdKcsItJ405bSxpKGpdWVHau/kkaA7eOpDaSdquuvplZdSLdtGcoaRzxQVnva5vs2orKG6zcSboRS+fsF7qhpPa4FB/x1Q1oIN0O/DNJ50haXekC6W0l7ZQt/19SAjkE+DVwS0HPb35bkKaq+2G2nX1IY30rrU0aCvJJ1gN8ISWSdIGknZQuHG9P6oT5hDTf9YOkXy+Pyc5Vm6xuj4L9zo6IhZL6kG7hXpO/ku5WuqOS7iq4sNuaJifJtkoi4h1gD+BQSb8iXbTQFniNNCZ3JGn8bWX9d0lTXQXpwrdCT5EapqereQ1wI+mCt6dJ8x0vJN0oojp/IY0VeyXb7/011P2MNNbuRUnzScnxZFJPBhHxN+AK4K7sZ7/JwL7Zug+TxlL/h/Qz5kKW/SmumGNI4+ReJ43/O72W+mZmRUXE1aRf3s4nfVl/l9TJ8EBW5VLSdSCTSGOFX87KSnEDsE02LOGBiFhCuiB6e1I7PJOUJHaQtGMWx4+yeleQ2vtzi20rK/sp6a6Jn5CGPFSWQxrutnq2jxdI7WypgnTR30xSr/mepOlK50XEZ6RrVAZmyz7MYq285fgpwMWSPiN9obinxh1F3AtcRuqB/iw7hhpvk26Nn28mYg1O0o3A+1FHk8mbmZmZ1TVPgG0NSumOd4eQpmQzMzMza5Q83MIajKRLSEMUfh0Rb5U7HjMzM7PqeLiFmZmZmVmOe5LNzMzMzHIa3ZjkddddN7p27VruMMzMVsr48eNnRkTncsfRkNxum1lTVVOb3eiS5K5duzJu3Lhyh2FmtlIk1XhXx+bI7baZNVU1tdkebmFmZmZmluMk2czMzMwsx0mymZmZmVlOoxuTXMyiRYuoqKhg4cKF5Q6l0Wvfvj1dunShTZs25Q7FzMzM6pHzo9KtTH7UJJLkiooK1l57bbp27YqkcofTaEUEs2bNoqKigm7dupU7HDMzM6tHzo9Ks7L5UUnDLSTtI+kNSdMknVtkeTtJd2fLX8xuPYykrpIWSJqYPf5UcmQFFi5cSKdOnfwHUAtJdOrUyd8ozczMWgDnR6VZ2fyo1p5kSa2A64A9gQpgrKRREfFaQbXjgDkR0V3SQOAK4Ihs2X8jYvsViqp4HKu6iRbB58nMzKzl8Od+aVbmPJXSk9wHmBYRb0bEl8BdwIBcnQHAzdnzkUB/+V0zMzMzsyaqlCR5Y+DdgtcVWVnROhGxGJgLdMqWdZM0QdJTkr5TbAeSTpA0TtK4GTNmrNABNJQxY8ZwwAEH1Fpv6NChPPbYYyu07a5duzJz5syVDc3MzMysLJpzflTfF+59AGwaEbMk7Qg8IKlnRHxaWCkihgPDAXr37h31HFONFi9eTOvWK39aLr744jqMxqz8up770CpvY/rl+9dBJNaS+e/QrDSTKj5Z5W1s16XjcmUtMT8q5WjfAzYpeN0lKytWp0JSa6ADMCsiAvgCICLGS/ovsCVQJ/cvnT9/PocffjgVFRUsWbKECy64gCOOOILx48dzxhlnMG/ePNZdd11GjBjBhhtuyF/+8heGDx/Ol19+Sffu3bn11ltZY401GDx4MO3bt2fChAnssssunHLKKZx00knMmDGDVq1ace+99wIwb948Dj30UCZPnsyOO+7IbbfdttwYl8GDB3PAAQdw6KGH0rVrVwYNGsQ//vEPFi1axL333svWW2/NrFmzOPLII3nvvffYeeedSacpue2227j22mv58ssv6du3L3/84x95+eWXOe6443jppZdYsmQJffr04e6772bbbbeti9NoZma2wvzFpfH6/PP5nH3ysXz0wfssWbKEE356FvsceAivTZrIVRefx+efz6fjOp245DfX0Xn9Dbjvjpu57/abWbToSzbpujmjRt7l/IjShluMBbaQ1E1SW2AgMCpXZxQwKHt+KPBERISkztmFf0jaHNgCeLNOIgf+9a9/sdFGG/HKK68wefJk9tlnHxYtWsSpp57KyJEjGT9+PEOGDOG8884D4JBDDmHs2LG88sor9OjRgxtuuKFqWxUVFTz33HP85je/4aijjuLHP/4xr7zyCs899xwbbrghABMmTOCaa67htdde48033+TZZ5+tNcZ1112Xl19+mZNPPpmrrroKgIsuuohdd92VKVOmcPDBB/POO+8AMHXqVO6++26effZZJk6cSKtWrbj99tvZaaedOPDAAzn//PM5++yzOfroo50gm5mZWVHPjXmczutvyL2PPMP9jz/PLv36s2jRIi4fejZX/flm7ho9hoOOOIrfX3kpAP33/T53PPQE9z7yDJt339L5UabWnuSIWCzpJ8DDQCvgxoiYIuliYFxEjAJuAG6VNA2YTUqkAXYDLpa0CFgKnBQRs+sq+F69evHzn/+cc845hwMOOIDvfOc7TJ48mcmTJ7PnnnsCsGTJkqo3cfLkyZx//vl88sknzJs3j7333rtqW4cddhitWrXis88+47333uPggw8G0uTTlfr06UOXLl0A2H777Zk+fTq77rprjTEecsghAOy4447cf//9ADz99NNVz/fff3/WWWcdAB5//HHGjx/PTjvtBMCCBQtYb731gDSWZ6eddqJ9+/Zce+21q3DWzMzMrDnrvvU2XH3J+fz2lxfy3f57862+3+b/Xn+NaW+8zkk/TPnNkiVLWHe9DQCY9vpU/vDrS/ns07l8/vl8Vttv36ptteT8qKTBJRExGhidKxta8HwhcFiR9e4D7lvFGKu15ZZb8vLLLzN69GjOP/98+vfvz8EHH0zPnj15/vnnl6s/ePBgHnjgAb75zW8yYsQIxowZU7VszTXXrHV/7dq1q3reqlUrFi9eXPI6pdSPCAYNGsSvfvWr5ZbNmjWLefPmsWjRIhYuXFhSvGZmZtbydN28O3eNfop/P/kIf/j1ZfTZ9bv033t/vrHl1tz690eWq3/Bz0/hmr/exlbb9OLv99zBfye9VLWsJedHJd1MpLF6//33WWONNTj66KM566yzePnll9lqq62YMWNGVZK8aNEipkyZAsBnn33GhhtuyKJFi7j99tuLbnPttdemS5cuPPDAAwB88cUXfP7553Ua92677cYdd9wBwD//+U/mzJkDQP/+/Rk5ciQff/wxALNnz+btt98G4MQTT+SSSy7hqKOO4pxzzqnTeMzMzKz5+PjDD2i/+uoccMgRDDrpVF5/9RW6fmML5syaySvjUwK8aNEipr0xFYDP581j3fU2YNGiRYx+4N6i22yJ+VGTuC11dV599VXOOussVlttNdq0acP1119P27ZtGTlyJKeddhpz585l8eLFnH766fTs2ZNLLrmEvn370rlzZ/r27ctnn31WdLu33norJ554IkOHDqVNmzZVA9PryoUXXsiRRx5Jz549+fa3v82mm24KwDbbbMOll17KXnvtxdKlS2nTpg3XXXcdTz31FG3atOGHP/whS5Ys4dvf/jZPPPEEe+yxR53GZWZmZk3f/73+Gr+9bCirrbYarVu34bxfXk2btm256s83c8XQc5j32acsXrKEo487ie5b9eDHZ/4vRx/4Pdb5+rr02mFHiC+Lbrel5UcqvHKwMejdu3eMG7fs5BdTp06lR48eZYqo6fH5srrkK9hXjKTxEdG73HE0pGLtdl3z32Hj4/ek/Ip93tfXFHDNQbHzVVOb3aSHW5iZmZmZ1QcnyWZmZmZmOU6SzczMzMxynCSbmdkyJP1M0hRJkyXdKal97WuZmTUvTpLNzKyKpI2B04DeEbEt6SZSA2tey8ys+XGSbGZmea2B1SW1BtYA3i9zPGZmDa5JzpNcF9POFCplCpq11lqLefPm1el+zcwam4h4T9JVwDvAAuCRiFjuFl2STgBOAKrmMjWz8nJ+VLfck1zPSrk1o5lZYyFpHWAA0A3YCFhT0tH5ehExPCJ6R0Tvzp07N3SYZtbENYX8yEnyKvjHP/5B37592WGHHfje977HRx99BMCwYcM45phj2GWXXTjmmGOYMWMGe+65Jz179uT4449ns802Y+bMmQDcdttt9OnTh+23354TTzyRJUuWlPOQzMy+B7wVETMiYhFwP/DtMsdkZk1Ic8mPnCSvgl133ZUXXniBCRMmMHDgQK688sqqZa+99hqPPfYYd955JxdddBF77LEHU6ZM4dBDD+Wdd94B0p1f7r77bp599lkmTpxIq1atuP3228t1OGZmkIZZ/I+kNSQJ6A9MLXNMZtaENJf8qEmOSW4sKioqOOKII/jggw/48ssv6datW9WyAw88kNVXXx2AZ555hr/97W8A7LPPPqyzzjoAPP7444wfP56ddtoJgAULFrDeeus18FGYmX0lIl6UNBJ4GVgMTACGlzcqM2tKmkt+5CR5FZx66qmcccYZHHjggYwZM4Zhw4ZVLVtzzTVrXT8iGDRoEL/61a/qMUozsxUTERcCF5Y7DjNrmppLfuThFqtg7ty5bLzxxgDcfPPN1dbbZZdduOeeewB45JFHmDNnDgD9+/dn5MiRfPzxxwDMnj2bt99+u56jNjMzM6s/zSU/apI9yaVMSVLXPv/8c7p06VL1+owzzmDYsGEcdthhrLPOOuyxxx689dZbRde98MILOfLII7n11lvZeeed2WCDDVh77bVZd911ufTSS9lrr71YunQpbdq04brrrmOzzTZrqMMyMzOzZmL65fszqeKTVd7Odl06lly3OedHTTJJLoelS5cWLR8wYMByZYU/KwB06NCBhx9+mNatW/P8888zduxY2rVrB8ARRxzBEUccUefxmpmZmdW35pwfOUluAO+88w6HH344S5cupW3btvzlL38pd0hmZmZmZdXY8yMnyQ1giy22YMKECeUOw8zMzKzRaOz5kS/cMzMzMzPLcZJsZmZmZpbjJNnMzMzMLMdJspmZmZlZTtO8cG9Yhzre3txaq0jiqKOO4rbbbgNg8eLFbLjhhvTt25cHH3yw5F3169ePq666it69e7Pffvtxxx130LFjx5WN3MzMzCwZ1oHt6nR7LTs/appJchmsueaaTJ48mQULFrD66qvz6KOPVt1NZmWNHj26jqIzMzMza3jNOT/ycIsVsN9++/HQQw8BcOedd3LkkUdWLZs/fz5DhgyhT58+7LDDDvz9738HYMGCBQwcOJAePXpw8MEHs2DBgqp1unbtysyZM5k+fTrbbrttVflVV11VNeF2v379+NnPfkbv3r3p0aMHY8eO5ZBDDmGLLbbg/PPPb4CjNjMzM6tec82PnCSvgIEDB3LXXXexcOFCJk2aRN++fauWXXbZZeyxxx689NJLPPnkk5x11lnMnz+f66+/njXWWIOpU6dy0UUXMX78+BXeb9u2bRk3bhwnnXQSAwYM4LrrrmPy5MmMGDGCWbNm1eUhmpmZma2Q5pofebjFCthuu+2YPn06d955J/vtt98yyx555BFGjRrFVVddBcDChQt55513ePrppznttNOq1t9uuxUfLXTggQcC0KtXL3r27MmGG24IwOabb867775Lp06dVuWwzMzMzFZac82PnCSvoAMPPJAzzzyTMWPGLPMtJSK477772GqrrVZ4m61bt17m3ucLFy5cZnnlfcxXW221queVrxcvXrzC+zMzMzOrS80xP/JwixU0ZMgQLrzwQnr16rVM+d57783vf/97IgKg6jaLu+22G3fccQcAkydPZtKkScttc/311+fjjz9m1qxZfPHFFyt0NaiZmZlZuTXH/Khp9iSXMCVJfenSpUvVzwOFLrjgAk4//XS22247li5dSrdu3XjwwQc5+eSTOfbYY+nRowc9evRgxx13XG7dNm3aMHToUPr06cPGG2/M1ltv3RCHYmZmZs3JsLlMqvhklTezXZeOK7xOc8yPVJnZ11hJ2gf4HdAK+GtEXJ5b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\n",
1082 | "text/plain": [
1083 | ""
1084 | ]
1085 | },
1086 | "metadata": {
1087 | "needs_background": "light"
1088 | },
1089 | "output_type": "display_data"
1090 | },
1091 | {
1092 | "data": {
1093 | "image/png": "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ll133ZVTTz2Vk08+menTp1NRUcEDDzwAwNy5czniiCOYMGEC3bt3bzSxUaMdtvHoo4+yySab8OqrrzJhwgT2339/Fi5cyOmnn86wYcMYM2YMgwYN4he/+AUAhx9+OKNGjeKBx59ly6225sF771xa1kcfvM/tDz3GORdeys/OOJF+x53AA48/y+0PPkbbjTcG4I3Xx/PTIb/iwade5K233uK5556rto5t27bllVde4ZRTTuGKK64A4KKLLmK33Xbj9ddf57DDDmPq1KkATJo0ifvuu4/nnnuOcePGUVFRwV133cXOO+/MwQcfzPnnn89Pf/pTjjnmGAfOZmZmtpznRz5Ju43b88Djz/LXJ19g1959WLhwIb8Z/FOu+NNt3DtiJIf2O5rfX34JAH0O+A53//2ppbHRzTffvLSsadOm8fzzz/O73/2Oo48+mh/+8Ie8+uqrPP/887Rv3x6AsWPHcvXVVzNx4sRGFRs12p7nbt26cdZZZ3HuuefSt29fvv3tbzNhwgQmTJjAPvvsA8DixYuXvsETJkzg/PPP58PpM/n883nsssdeS8va96BDqaioYN7cOXz84Qf0OSCN4WneosXSPNvv2J2N228KwI477siUKVPYbbfdVljHww8/HIDu3bvz17/+FYBnnnlm6f2DDjqIDTbYAIAnn3ySMWPGsPPOOwMwf/58NtpoIyCNF9p5551p0aIF1157bQ1eNTMzM2usttp2O6785flc9asL2aPPfvy/Xrvw3zcmMvnNNzj5+4cBKTZqu9HXAJj8xiT+8NtLmPPZbD7/fB5rHXjA0rKOPPJIKioqmDNnDu+99x6HHZbOb1EQG/Xs2ZMOHToAjSs2arTB89Zbb80rr7zCiBEjOP/88+nTpw+HHXYYXbt25YUXXlgu/8CBA3nooYdQmy14+P67Gf3Cs0uPtVx77Wqfr2mzZkvvV1RUsGjRomrPad68ee78EcGAAQP49a9/vdyxmTNnMnfuXBYuXMiCBQto1apVtc9tZmZma5aOW27FvSOe5t//epw//PZSeu62B332O4ivb70tdzz8+HL5LzjrVK6+6U622a4bD99/N/8b//LSY3lijco4BxpXbNRoh228//77rL322hxzzDGcc845vPLKK2yzzTZMnz59afC8cOFCXn/9dQDmzJlD+/btWbhwISMeeqBkma3WWZeN22/CU4+mMXZffvEF8+d/Xqv13n333bn77rsB+Mc//sGsWbMA6NOnD8OGDePjjz8G4JNPPuGdd94B4KSTTuKXv/wlRx99NOeee26t1sfMzMwah48//IAWLVvS9/B+DDj5dN547VU6fr0zs2bO4NUxKTBeuHAhk9+cBMDnc+fSdqOvrTA2WnfddenQoQMPPfQQAF988QWff964Y6NG2/P82muvcc4557DWWmvRtGlTbrjhBpo1a8awYcM444wzmD17NosWLeLHP/4xXbt25Ze//CW9evWiVesN6bZTdz6fO7dkuZde80d+ed5PuP7KX9GkadOlEwZry4UXXshRRx1F165d2WWXXdh8880B2G677bjkkkvYd999WbJkCU2bNuW6667j6aefpmnTpnz/+99n8eLF7LLLLjz11FPstdde1TyTmZmZrUn++8ZErrp0MGuttRZNmjTlF7+6kqbNmnHFn27jssHnMnfOZyxavJhjjj+Zrbbpwg/P/jnHHLw3G2zYlm47dYf4smS5d9xxByeddBKDBw+madOmSycM1paGFhupcMZiQ9ejR48YPXr0MmmTJk2iS5cutfYcK1qqLq+qlqprCGr79TKzfCSNiYge5a5HfSrVZtc2r7bRMPl9Ka9Sf+sbe3xTE6VerxW12Y122IaZmZmZWW1z8GxmZmZmlpODZzMzMzOznHIFz5L2l/SmpMmSzitxvLmk+7LjL0nqmKXvI2mMpNeyf/cqOGdkVua47LZRrV2VmZnVOkk/kfS6pAmS7pHUovqzzMwal2qDZ0kVwHXAAcB2wFGStivKdjwwKyK2Aq4CLsvSZwDfiYhuwADgjqLzjo6IHbPbxzW4DjMzq0OSNgXOAHpExPZABdC/vLUyM6t/eXqeewKTI+KtiPgSuBc4pCjPIcBt2f1hQB9JioixEfF+lv460FJSc8zMbHXUhNSONwHWBt6vJr+ZWaOTJ3jeFHi34PG0LK1knohYBMwG2hTl+S7wSkR8UZD252zIxgWSVOrJJZ0oabSk0dOnT89R3fr3zW06lLsKZmZ1KiLeA64ApgIfALMjYvktyczMgHXWWafcVagz9bJJiqSupKEc+xYkHx0R70laF/gLcCxwe/G5ETEUGAppzdDqnqs21pYsNPy0XWu1vEKLFi2iSZNGu0+NNXJex3XNImkD0q+MnYBPgQckHRMRdxblOxE4EVi6kYGZlVdtx0Z12XavDrFRnp7n94DNCh53yNJK5sl+zmsNzMwedwAeBI6LiP9VnpD1YhARc4C7ScNDGo1HHnmEXr16sdNOO7H33nvz0UcfATBkyBCOPfZYdt11V4499limT5/OPvvsQ9euXTnhhBPYYostmDFjBgB33nknPXv2ZMcdd+Skk05i8eLF5bwkM1uz7Q28HRHTI2Ih8Fdgl+JMETE0InpERI927drVeyXNrOFqLLFRnuB5FNBZUidJzUgTRIYX5RlOmhAIcATwVESEpPWBvwPnRcRzlZklNZHUNrvfFOgLTKjRlTQwu+22Gy+++CJjx46lf//+XH755UuPTZw4kSeeeIJ77rmHiy66iL322ovXX3+dI444gqlTpwJpt5v77ruP5557jnHjxlFRUcFdd91VrssxM5sKfFPS2tkwuz7ApDLXycxWI40lNqq2XzwiFkk6DXiMNLv6loh4XdLFwOiIGA7cDNwhaTLwCV/NwD4N2AoYLGlwlrYvMA94LAucK4AngBtr8brKbtq0afTr148PPviAL7/8kk6dOi09dvDBB9OyZUsAnn32WR588EEA9t9/fzbYYAMAnnzyScaMGcPOO+8MwPz589loI6/mZ2blEREvSRoGvAIsAsaSDakzM8ujscRGuQaVRMQIYERR2uCC+wuAI0ucdwlwSRXFds9fzdXP6aefzplnnsnBBx/MyJEjGTJkyNJjrVq1qvb8iGDAgAH8+te/rsNampnlFxEXAheWux5mtnpqLLGRdxisI7Nnz2bTTdOiJLfddluV+XbddVfuv/9+AB5//HFmzZoFQJ8+fRg2bBgff5yWv/7kk09455136rjWZmZmZnWjscRGDXs642piwfzP2Wfnrksfn3vOWQwZMoQjjzySDTbYgL322ou333675LkXXnghRx11FHfccQff+ta3+NrXvsa6665L27ZtueSSS9h3331ZsmQJTZs25brrrmOLLbaor8syMzMzWyWff/45HTp8tZTvmWee2Whio0YXPNd0+ZTx0z5d6XPGTf1kmcc7dFgfgEMOKd5LhmV+ogBo3bo1jz32GE2aNOGFF15g1KhRNG+e9pHp168f/fr1W+n6mJmZmVWa8puDVim+KVYZ3+SxZMmSkumNITZqdMHz6mbq1Kl873vfY8mSJTRr1owbb2xU8ybNzMzMVkpDj40cPJdZ586dGTt2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\n",
1094 | "text/plain": [
1095 | ""
1096 | ]
1097 | },
1098 | "metadata": {
1099 | "needs_background": "light"
1100 | },
1101 | "output_type": "display_data"
1102 | }
1103 | ],
1104 | "source": [
1105 | "compare='search size' #'# tokens query'\n",
1106 | "compare='# tokens query'\n",
1107 | "comparisons=['search size', '# tokens query']\n",
1108 | "\n",
1109 | "for compare in comparisons:\n",
1110 | " fig, axes = plt.subplots(nrows=1, ncols=len(search_to_compare), figsize=(12,4))\n",
1111 | " fig.suptitle(f'Comparing {compare}',size=18)\n",
1112 | " for (c,search_type) in enumerate(search_to_compare.values()):\n",
1113 | " pvt=pd.pivot_table(result_df[(result_df['search type']==search_type)&(result_df['repeat']>2)] \\\n",
1114 | " [['search index',compare,'time taken']],\\\n",
1115 | " values='time taken',\n",
1116 | " index=compare,\n",
1117 | " columns='search index',\n",
1118 | " aggfunc='mean',\n",
1119 | " )\n",
1120 | " pvt.plot.bar(title=search_type,ax=axes[c]) \n"
1121 | ]
1122 | },
1123 | {
1124 | "cell_type": "markdown",
1125 | "metadata": {},
1126 | "source": [
1127 | "The below box plot analyzes the deviation in search times after multiple repeated calls. Although some deviations are observed, they do not seem to be significant. Only results from the Large index are shown"
1128 | ]
1129 | },
1130 | {
1131 | "cell_type": "code",
1132 | "execution_count": 17,
1133 | "metadata": {},
1134 | "outputs": [
1135 | {
1136 | "data": {
1137 | "text/plain": [
1138 | ""
1139 | ]
1140 | },
1141 | "execution_count": 17,
1142 | "metadata": {},
1143 | "output_type": "execute_result"
1144 | },
1145 | {
1146 | "data": {
1147 | "image/png": "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\n",
1148 | "text/plain": [
1149 | ""
1150 | ]
1151 | },
1152 | "metadata": {
1153 | "needs_background": "light"
1154 | },
1155 | "output_type": "display_data"
1156 | }
1157 | ],
1158 | "source": [
1159 | "box_df=result_df[(result_df['search index']=='Large')].copy()\n",
1160 | "pd.pivot_table(box_df[['search type','time taken','repeat']],\n",
1161 | " values='time taken',\n",
1162 | " index='repeat',\n",
1163 | " columns='search type',\n",
1164 | " aggfunc='mean',).plot.box()"
1165 | ]
1166 | }
1167 | ],
1168 | "metadata": {
1169 | "kernelspec": {
1170 | "display_name": "Python [conda env:et2] *",
1171 | "language": "python",
1172 | "name": "conda-env-et2-py"
1173 | },
1174 | "language_info": {
1175 | "codemirror_mode": {
1176 | "name": "ipython",
1177 | "version": 3
1178 | },
1179 | "file_extension": ".py",
1180 | "mimetype": "text/x-python",
1181 | "name": "python",
1182 | "nbconvert_exporter": "python",
1183 | "pygments_lexer": "ipython3",
1184 | "version": "3.7.7"
1185 | }
1186 | },
1187 | "nbformat": 4,
1188 | "nbformat_minor": 4
1189 | }
1190 |
--------------------------------------------------------------------------------
/notebooks/Setting_up_ElasticTransformers.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Introduction\n",
8 | "\n",
9 | "This notebook will accomplish the following\n",
10 | "\n",
11 | "- Set up an ElasticTransformers class\n",
12 | "- Instantiate an index and index the Million headlines dataset in it\n",
13 | "- Preview some search results from comparing lexical vs semantic search\n"
14 | ]
15 | },
16 | {
17 | "cell_type": "markdown",
18 | "metadata": {},
19 | "source": [
20 | "## Loading requirements"
21 | ]
22 | },
23 | {
24 | "cell_type": "code",
25 | "execution_count": 36,
26 | "metadata": {},
27 | "outputs": [
28 | {
29 | "name": "stdout",
30 | "output_type": "stream",
31 | "text": [
32 | "The autoreload extension is already loaded. To reload it, use:\n",
33 | " %reload_ext autoreload\n"
34 | ]
35 | }
36 | ],
37 | "source": [
38 | "%load_ext autoreload\n",
39 | "import os\n",
40 | "os.chdir(os.path.abspath(os.curdir).replace('notebooks',''))"
41 | ]
42 | },
43 | {
44 | "cell_type": "code",
45 | "execution_count": 37,
46 | "metadata": {},
47 | "outputs": [],
48 | "source": [
49 | "%autoreload 2\n",
50 | "from src.database import ElasticTransformers\n"
51 | ]
52 | },
53 | {
54 | "cell_type": "markdown",
55 | "metadata": {},
56 | "source": [
57 | "## Sentence Transformers\n",
58 | "\n",
59 | "This creates the sentence transformer object as well as small helper function which simplifies the embedding call and helps lading data into elastic easier"
60 | ]
61 | },
62 | {
63 | "cell_type": "code",
64 | "execution_count": 38,
65 | "metadata": {},
66 | "outputs": [],
67 | "source": [
68 | "from sentence_transformers import SentenceTransformer\n",
69 | "bert_embedder = SentenceTransformer('bert-base-nli-mean-tokens')\n"
70 | ]
71 | },
72 | {
73 | "cell_type": "code",
74 | "execution_count": 39,
75 | "metadata": {},
76 | "outputs": [],
77 | "source": [
78 | "def embed_wrapper(ls):\n",
79 | " \"\"\"\n",
80 | " Helper function which simplifies the embedding call and helps lading data into elastic easier\n",
81 | " \"\"\"\n",
82 | " results=bert_embedder.encode(ls, convert_to_tensor=True)\n",
83 | " results = [r.tolist() for r in results]\n",
84 | " return results"
85 | ]
86 | },
87 | {
88 | "cell_type": "markdown",
89 | "metadata": {},
90 | "source": [
91 | "## Quick Preview of the raw data\n",
92 | "\n",
93 | "The data contains 1.15mn news headlines (all in lower case) and their published date"
94 | ]
95 | },
96 | {
97 | "cell_type": "code",
98 | "execution_count": 40,
99 | "metadata": {},
100 | "outputs": [],
101 | "source": [
102 | "import pandas as pd\n",
103 | "df=pd.read_csv('data/abcnews-date-text.csv')"
104 | ]
105 | },
106 | {
107 | "cell_type": "code",
108 | "execution_count": 41,
109 | "metadata": {},
110 | "outputs": [
111 | {
112 | "data": {
113 | "text/html": [
114 | "\n",
115 | "\n",
128 | "
\n",
129 | " \n",
130 | " \n",
131 | " \n",
132 | " publish_date \n",
133 | " headline_text \n",
134 | " \n",
135 | " \n",
136 | " \n",
137 | " \n",
138 | " 0 \n",
139 | " 20030219 \n",
140 | " aba decides against community broadcasting lic... \n",
141 | " \n",
142 | " \n",
143 | " 1 \n",
144 | " 20030219 \n",
145 | " act fire witnesses must be aware of defamation \n",
146 | " \n",
147 | " \n",
148 | " 2 \n",
149 | " 20030219 \n",
150 | " a g calls for infrastructure protection summit \n",
151 | " \n",
152 | " \n",
153 | " 3 \n",
154 | " 20030219 \n",
155 | " air nz staff in aust strike for pay rise \n",
156 | " \n",
157 | " \n",
158 | " 4 \n",
159 | " 20030219 \n",
160 | " air nz strike to affect australian travellers \n",
161 | " \n",
162 | " \n",
163 | "
\n",
164 | "
"
165 | ],
166 | "text/plain": [
167 | " publish_date headline_text\n",
168 | "0 20030219 aba decides against community broadcasting lic...\n",
169 | "1 20030219 act fire witnesses must be aware of defamation\n",
170 | "2 20030219 a g calls for infrastructure protection summit\n",
171 | "3 20030219 air nz staff in aust strike for pay rise\n",
172 | "4 20030219 air nz strike to affect australian travellers"
173 | ]
174 | },
175 | "execution_count": 41,
176 | "metadata": {},
177 | "output_type": "execute_result"
178 | }
179 | ],
180 | "source": [
181 | "df.head()"
182 | ]
183 | },
184 | {
185 | "cell_type": "markdown",
186 | "metadata": {},
187 | "source": [
188 | "# A tiny example\n",
189 | "\n",
190 | "Let's first do this with a tiny example of 1000 headlines (the full dataset is 1.1mn headlines)"
191 | ]
192 | },
193 | {
194 | "cell_type": "code",
195 | "execution_count": 42,
196 | "metadata": {},
197 | "outputs": [],
198 | "source": [
199 | "df.head(1000).to_csv('data/tiny_sample.csv')\n"
200 | ]
201 | },
202 | {
203 | "cell_type": "markdown",
204 | "metadata": {},
205 | "source": [
206 | "# Setting up ElasticTransformers\n",
207 | "\n",
208 | "The below lines initialize the class, meaning setting the url and index name"
209 | ]
210 | },
211 | {
212 | "cell_type": "code",
213 | "execution_count": 32,
214 | "metadata": {},
215 | "outputs": [],
216 | "source": [
217 | "et=ElasticTransformers(url='http://localhost:9300',index_name='et-tiny')\n",
218 | "_ = et.ping()\n",
219 | "\n"
220 | ]
221 | },
222 | {
223 | "cell_type": "markdown",
224 | "metadata": {},
225 | "source": [
226 | "Next, we define the index specification (Elasticsearch index mapping)"
227 | ]
228 | },
229 | {
230 | "cell_type": "code",
231 | "execution_count": 33,
232 | "metadata": {},
233 | "outputs": [
234 | {
235 | "data": {
236 | "text/plain": [
237 | "{'settings': {'number_of_shards': 3, 'number_of_replicas': 1},\n",
238 | " 'mappings': {'dynamic': 'true',\n",
239 | " '_source': {'enabled': 'true'},\n",
240 | " 'properties': {'publish_date': {'type': 'text'},\n",
241 | " 'headline_text': {'type': 'text'},\n",
242 | " 'headline_text_embedding': {'type': 'dense_vector', 'dims': 768}}}}"
243 | ]
244 | },
245 | "execution_count": 33,
246 | "metadata": {},
247 | "output_type": "execute_result"
248 | }
249 | ],
250 | "source": [
251 | "et.create_index_spec(\n",
252 | " text_fields=['publish_date','headline_text'],\n",
253 | " dense_fields=['headline_text_embedding'],\n",
254 | " dense_fields_dim=768\n",
255 | ")"
256 | ]
257 | },
258 | {
259 | "cell_type": "code",
260 | "execution_count": 34,
261 | "metadata": {},
262 | "outputs": [
263 | {
264 | "name": "stdout",
265 | "output_type": "stream",
266 | "text": [
267 | "Creating 'et-tiny' index.\n"
268 | ]
269 | }
270 | ],
271 | "source": [
272 | "et.create_index()\n"
273 | ]
274 | },
275 | {
276 | "cell_type": "code",
277 | "execution_count": 35,
278 | "metadata": {},
279 | "outputs": [
280 | {
281 | "name": "stderr",
282 | "output_type": "stream",
283 | "text": [
284 | "1it [00:08, 8.52s/it]\n"
285 | ]
286 | }
287 | ],
288 | "source": [
289 | "et.write_large_csv('data/tiny_sample.csv',\n",
290 | " chunksize=1000,\n",
291 | " embedder=embed_wrapper,\n",
292 | " field_to_embed='headline_text')"
293 | ]
294 | },
295 | {
296 | "cell_type": "markdown",
297 | "metadata": {},
298 | "source": [
299 | "One sample looks like this"
300 | ]
301 | },
302 | {
303 | "cell_type": "markdown",
304 | "metadata": {},
305 | "source": [
306 | "## Indexing the entire dataset\n",
307 | "\n",
308 | "Lets do this now with 1.1mn records "
309 | ]
310 | },
311 | {
312 | "cell_type": "code",
313 | "execution_count": 44,
314 | "metadata": {},
315 | "outputs": [
316 | {
317 | "name": "stdout",
318 | "output_type": "stream",
319 | "text": [
320 | "Creating 'et-large' index.\n"
321 | ]
322 | }
323 | ],
324 | "source": [
325 | "# Initialize\n",
326 | "et=ElasticTransformers(url='http://localhost:9200',index_name='et-large')\n",
327 | "_ = et.ping()\n",
328 | "# Create index mapping\n",
329 | "et.create_index_spec(\n",
330 | " text_fields=['publish_date','headline_text'],\n",
331 | " dense_fields=['headline_text_embedding'],\n",
332 | " dense_fields_dim=768\n",
333 | ")\n",
334 | "# Create index\n",
335 | "et.create_index()"
336 | ]
337 | },
338 | {
339 | "cell_type": "markdown",
340 | "metadata": {},
341 | "source": [
342 | "### Indexing with sentence-transformers... \n",
343 | "\n",
344 | "This takes 3hrs on CPU, consumes 4CPUs & 2GB RAM for the embedding process and about 2GB RAM for Elastic"
345 | ]
346 | },
347 | {
348 | "cell_type": "code",
349 | "execution_count": 45,
350 | "metadata": {},
351 | "outputs": [
352 | {
353 | "name": "stderr",
354 | "output_type": "stream",
355 | "text": [
356 | "1187it [3:18:46, 10.05s/it]\n"
357 | ]
358 | }
359 | ],
360 | "source": [
361 | "et.write_large_csv('data/abcnews-date-text.csv',\n",
362 | " chunksize=1000,\n",
363 | " embedder=embed_wrapper,\n",
364 | " field_to_embed='headline_text')\n"
365 | ]
366 | },
367 | {
368 | "cell_type": "code",
369 | "execution_count": null,
370 | "metadata": {},
371 | "outputs": [],
372 | "source": []
373 | }
374 | ],
375 | "metadata": {
376 | "kernelspec": {
377 | "display_name": "Python [conda env:et2] *",
378 | "language": "python",
379 | "name": "conda-env-et2-py"
380 | },
381 | "language_info": {
382 | "codemirror_mode": {
383 | "name": "ipython",
384 | "version": 3
385 | },
386 | "file_extension": ".py",
387 | "mimetype": "text/x-python",
388 | "name": "python",
389 | "nbconvert_exporter": "python",
390 | "pygments_lexer": "ipython3",
391 | "version": "3.7.7"
392 | }
393 | },
394 | "nbformat": 4,
395 | "nbformat_minor": 4
396 | }
397 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pandas==1.0.5
2 | sentence-transformers==0.3.4
3 | elasticsearch==7.6.0
4 | matplotlib==3.3.1
5 | transformers==3.0.2
6 |
--------------------------------------------------------------------------------
/src/database.py:
--------------------------------------------------------------------------------
1 | from elasticsearch import Elasticsearch, helpers
2 | import datetime
3 | import json
4 | import pandas as pd
5 | import tqdm
6 | import os
7 | from src.logger import logger
8 |
9 | class ElasticTransformers(object):
10 | def __init__(self,url='http://localhost:9200', index_name=None):
11 | """
12 | Initializes class
13 |
14 | Args:
15 | url (string) full url for elastic
16 | index_name (string, optional) name of index can be used as the default index across all methods for this class instance should this apply
17 | """
18 | self.url=url
19 | self.es=Elasticsearch(self.url)
20 | self.index_name=index_name
21 | self.index_file=None
22 |
23 | def ping(self):
24 | """
25 | Checks if Elastic is healthy
26 |
27 | Returns:
28 | True if healthy, False otherwise
29 | """
30 | ping=self.es.ping()
31 | if ping:
32 | logger.debug(f'Ping successful')
33 | return ping
34 |
35 | def create_index_spec(self, index_name=None,folder='index_spec',text_fields=[], keyword_fields=[], dense_fields=[], dense_fields_dim=512, shards=3, replicas=1):
36 | """
37 | Creates mapping file for an index and stores the file
38 |
39 | Args:
40 | index_name (string, optional) name of index, defaults to index name defined when initiating the class
41 | folder (string) location to store index spec
42 | text_fields (list)
43 | keyword_fields (list)
44 | dense_fields (list) list of dense field names
45 | dense_fields_dim (int)
46 | shards (int) number of shards for index
47 | replicas (int) number of replicas for index
48 | """
49 |
50 | if not os.path.exists(folder):
51 | os.makedirs(folder)
52 |
53 | if not index_name:
54 | if self.index_name:
55 | index_name=self.index_name
56 | else:
57 | raise ValueError('index_name not provided')
58 | index_spec={}
59 |
60 | index_spec['settings']={
61 | "number_of_shards": shards,
62 | "number_of_replicas": replicas
63 | }
64 |
65 | index_spec['mappings']={
66 | "dynamic": "true",
67 | "_source": {
68 | "enabled": "true"
69 | },
70 | "properties": {},
71 | }
72 |
73 | for t in text_fields:
74 | index_spec['mappings']['properties'][t]={
75 | "type": "text"
76 | }
77 |
78 | for k in keyword_fields:
79 | index_spec['mappings']['properties'][t]={
80 | "type": "keyword"
81 | }
82 |
83 | for d in dense_fields:
84 | index_spec['mappings']['properties'][d]={
85 | "type": "dense_vector",
86 | "dims": dense_fields_dim
87 | }
88 |
89 | index_file_name=f'{folder}/spec_{index_name}.json'
90 | with open(index_file_name, 'w') as index_file:
91 | json.dump(index_spec,index_file)
92 | self.index_file=index_file_name
93 | logger.debug(f'Index spec {self.index_file} created')
94 | return index_spec
95 |
96 | def create_index(self, index_name=None, index_file=None):
97 | """
98 | Create index (index_name) based on file (index_file) containing index mapping
99 | NOTE: existing index of this name will be deleted
100 |
101 | Args:
102 | index_name (string, optional): name of index, defaults to index name defined when initiating the class
103 | index_file (string, optional): index spec file location, if none provided, will use mapping from create_index_spec else will create blank mapping
104 |
105 | """
106 | if not index_name:
107 | if self.index_name:
108 | index_name=self.index_name
109 | else:
110 | raise ValueError('index_name not provided')
111 | print(f"Creating '{index_name}' index.")
112 | self.es.indices.delete(index=index_name, ignore=[404])
113 |
114 | if index_file or self.index_file:
115 | if self.index_file:
116 | index_file=self.index_file
117 | with open(index_file) as index_file:
118 | index_spec = index_file.read().strip()
119 |
120 | else:
121 | index_spec={
122 | "number_of_shards": 3,
123 | "number_of_replicas": 1
124 | }
125 |
126 | self.es.indices.create(index=index_name, body=index_spec)
127 |
128 | def write(self,docs,index_name=None,index_field=None):
129 | """
130 | Writes entries to index
131 |
132 | Args:
133 | docs (list) list of dictionaries with keys matching index field names from index specification
134 | index_name (string, optional) name of index, defaults to index name defined when initiating the class
135 | index_field (string, optional) name of index field if present in docs. Defaults to elasicsearch indexing otherwise
136 |
137 | """
138 | if not index_name:
139 | if self.index_name:
140 | index_name=self.index_name
141 | else:
142 | raise ValueError('index_name not provided')
143 | requests = []
144 | for i, doc in enumerate(docs):
145 | request = doc
146 | request["_op_type"] = "index"
147 | if index_field:
148 | request["_id"] = doc[index_field]
149 | request["_index"] = index_name
150 | requests.append(request)
151 | helpers.bulk(self.es, requests)
152 |
153 | def write_large_csv(self, file_path, index_name=None, chunksize=10000, embedder=None, field_to_embed=None, index_field=None):
154 | """
155 | Iteratively reads through a csv file and writes it to elastic in batches
156 |
157 | Args:
158 | file_path (string) path to file
159 | index_name (string, optional) name of index, defaults to index name defined when initiating the class
160 | chunksize (int) size of the chunk to be read from file and sent to embedder
161 | embedder (function) embedder function with expected call embedded(list of strings to embed)
162 | field_to_embed (string) name of field to embed
163 | index_field (string, optional) name of index field if present in docs. Defaults to elasicsearch indexing otherwise
164 | """
165 | if not index_name:
166 | if self.index_name:
167 | index_name=self.index_name
168 | else:
169 | raise ValueError('index_name not provided')
170 | # read the large csv file with specified chunksize
171 | df_chunk = pd.read_csv(file_path, chunksize=chunksize, index_col=0)
172 |
173 | chunk_list = [] # append each chunk df here
174 |
175 | # Each chunk is in df format
176 | for chunk in tqdm.tqdm(df_chunk):
177 | if embedder:
178 | chunk[f'{field_to_embed}_embedding']=embedder(chunk[field_to_embed].values)
179 | chunk_ls=json.loads(chunk.to_json(orient='records'))
180 | self.write(chunk_ls,index_name,index_field=index_field)
181 | logger.debug(f'Successfully wrote {len(chunk_ls)} docs to {index_name}')
182 |
183 | def sample(self, index_name=None, size=3):
184 | """
185 | Provides a sample of documents from the index
186 |
187 | Args:
188 | index_name (string, optional) name of index, defaults to index name defined when initiating the class
189 | size (int, optional) number of results to retrieve, defaults to 3, max 10k, can be relaxed with elastic config
190 | """
191 | if not index_name:
192 | if self.index_name:
193 | index_name=self.index_name
194 | else:
195 | raise ValueError('index_name not provided')
196 | res=self.es.search(index=index_name, size=size)
197 | logger.debug(f"Successfully sampled {len(res['hits']['hits'])} docs from {index_name}")
198 | return res
199 |
200 | def search(self, query, field, type='match', index_name=None, embedder=None, size=10):
201 | """
202 | Search elastic
203 |
204 | Args:
205 | query (string) search query
206 | field (string) field to search
207 | type (string) type of search, takes: match, term, fuzzy, wildcard (requires "*" in query), dense (semantic search, requires embedder, index needs to be indexed with embeddings, assumes embedding field is named {field}_embedding)
208 | index_name (string, optional) name of index, defaults to index name defined when initiating the class
209 | embedder (function) embedder function with expected call embedded(list of strings to embed)
210 | size (int, optional) number of results to retrieve, defaults to 3, max 10k, can be relaxed with elastic config
211 |
212 | Returns:
213 | DataFrame with results and search score
214 | """
215 | res=[]
216 |
217 | if not index_name:
218 | if self.index_name:
219 | index_name=self.index_name
220 | else:
221 | raise ValueError('index_name not provided')
222 | if type=='dense':
223 | if not embedder:
224 | raise ValueError('Dense search requires embedder')
225 | query_vector = embedder([query])[0]
226 |
227 | script_query = {
228 | "script_score": {
229 | "query": {"match_all": {}},
230 | "script": {
231 | "source": f"cosineSimilarity(params.query_vector, doc['{field}_embedding']) + 1.0",
232 | "params": {"query_vector": query_vector}
233 | }
234 | }
235 | }
236 |
237 | res = self.es.search(
238 | index=index_name,
239 | body={
240 | "size": size,
241 | "query": script_query,
242 | "_source": {"excludes": [f'{field}_embedding']}
243 | }
244 | )
245 | else:
246 | res=self.es.search(index=index_name, body={'query':{type:{field:query}}, "_source": {"excludes": [f'{field}_embedding']}},size=size)
247 | self.search_raw_result=res
248 | hits=res['hits']['hits']
249 | if len(hits)>0:
250 | keys=list(hits[0]['_source'].keys())
251 |
252 | out=[[h['_score']]+[h['_source'][k] for k in keys] for h in hits]
253 |
254 | df=pd.DataFrame(out,columns=['_score']+keys)
255 | else:
256 | df=pd.DataFrame([])
257 | self.search_df_result=df
258 | logger.debug(f'Search {type.upper()} {query} in {index_name}.{field} returned {len(df)} results of {size} requested')
259 | return df
--------------------------------------------------------------------------------
/src/logger.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import datetime
3 | import os
4 |
5 | logs_folder='logs'
6 | if not os.path.exists(logs_folder):
7 | os.makedirs(logs_folder)
8 |
9 | # Create a custom logger
10 | logger = logging.getLogger(__name__)
11 |
12 | # Setting global logging level
13 | logger.setLevel(logging.WARNING)
14 |
15 | date=str(datetime.date.today()).replace('-','')
16 | # Initialize handlers
17 | file_hndl = logging.FileHandler(f'{logs_folder}/q_logs_{date}.log')
18 | cli_hndl = logging.StreamHandler()
19 | # Set logging level
20 | file_hndl.setLevel(level=logging.DEBUG)
21 | cli_hndl.setLevel(level=logging.DEBUG)
22 | # Add formatters to handlers
23 | logger_text_format = logging.Formatter('%(asctime)s --- %(name)s --- %(levelname)s --- %(funcName)s:%(lineno)d --- %(message)s')
24 | file_hndl.setFormatter(logger_text_format)
25 | cli_hndl.setFormatter(logger_text_format)
26 |
27 | # Add handlers to the logger
28 | logger.addHandler(file_hndl)
29 | logger.addHandler(cli_hndl)
30 |
31 |
--------------------------------------------------------------------------------