├── .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 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | " \n", 164 | " \n", 165 | " \n", 166 | " \n", 167 | " \n", 168 | " \n", 169 | " \n", 170 | " \n", 171 | " \n", 172 | " \n", 173 | " \n", 174 | " \n", 175 | " \n", 176 | " \n", 177 | " \n", 178 | " \n", 179 | " \n", 180 | " \n", 181 | " \n", 182 | " \n", 183 | " \n", 184 | " \n", 185 | " \n", 186 | " \n", 187 | " \n", 188 | " \n", 189 | " \n", 190 | " \n", 191 | " \n", 192 | " \n", 193 | " \n", 194 | " \n", 195 | " \n", 196 | " \n", 197 | " \n", 198 | " \n", 199 | " \n", 200 | " \n", 201 | "
headline_text_score
0public warned of mozzie virus threat13.311735
1cattle producers warned of virus threat13.311735
2expert plays down hendra virus threat13.295192
3residents reminded of mozzie virus threat13.213888
4mozzie virus threat sparks health alert13.213888
5report reveals lower mozzie virus threat13.213888
6hendra like virus identified as potential threat12.498677
7hendra virus poses constant threat chief vet12.498677
8public warned of mossie borne virus threat12.498677
9sunraysia fears watermelon virus threat from nt12.483357
\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 | " \n", 250 | " \n", 251 | " \n", 252 | " \n", 253 | " \n", 254 | " \n", 255 | " \n", 256 | " \n", 257 | " \n", 258 | " \n", 259 | " \n", 260 | " \n", 261 | " \n", 262 | " \n", 263 | " \n", 264 | " \n", 265 | " \n", 266 | " \n", 267 | " \n", 268 | " \n", 269 | " \n", 270 | " \n", 271 | " \n", 272 | " \n", 273 | " \n", 274 | " \n", 275 | " \n", 276 | " \n", 277 | " \n", 278 | " \n", 279 | " \n", 280 | " \n", 281 | " \n", 282 | " \n", 283 | " \n", 284 | " \n", 285 | " \n", 286 | " \n", 287 | " \n", 288 | " \n", 289 | " \n", 290 | " \n", 291 | " \n", 292 | " \n", 293 | " \n", 294 | " \n", 295 | " \n", 296 | " \n", 297 | " \n", 298 | " \n", 299 | " \n", 300 | " \n", 301 | " \n", 302 | " \n", 303 | " \n", 304 | " \n", 305 | "
headline_text_score
0hendra like virus identified as potential threat1.859408
1hendra report author warns of virus risk1.853364
2fresh concerns over hendra virus outbreak1.836927
3virus puts giteau in doubt1.823136
4hendra virus case under investigation1.817768
5who highlight dangers of vector borne diseases1.804388
6potentially deadly virus sparks mozzie warning1.799419
7who warns threat from vector borne diseases1.793783
8fears as png diseases spread1.791913
9deadly hendra virus strikes again1.788590
\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 | " \n", 364 | " \n", 365 | " \n", 366 | " \n", 367 | " \n", 368 | " \n", 369 | " \n", 370 | " \n", 371 | " \n", 372 | " \n", 373 | " \n", 374 | " \n", 375 | " \n", 376 | " \n", 377 | " \n", 378 | " \n", 379 | " \n", 380 | " \n", 381 | " \n", 382 | " \n", 383 | " \n", 384 | " \n", 385 | " \n", 386 | " \n", 387 | " \n", 388 | " \n", 389 | " \n", 390 | " \n", 391 | " \n", 392 | " \n", 393 | " \n", 394 | " \n", 395 | " \n", 396 | " \n", 397 | " \n", 398 | " \n", 399 | " \n", 400 | " \n", 401 | " \n", 402 | " \n", 403 | " \n", 404 | " \n", 405 | " \n", 406 | " \n", 407 | " \n", 408 | " \n", 409 | " \n", 410 | " \n", 411 | " \n", 412 | " \n", 413 | " \n", 414 | " \n", 415 | " \n", 416 | " \n", 417 | " \n", 418 | " \n", 419 | "
headline_text_score
5who highlight dangers of vector borne diseases1.804388
8fears as png diseases spread1.791913
21the odds of an outbreak1.773913
25flood waters carry risk of disease infection1.769219
27port uncertain of impact of viral meningitis outbreak1.767367
30human error blamed for infection scare1.764524
34oakey defence base contaminants linked to serious disease1.758969
35dangerous parasite rife in nt1.757938
40academic fears spread of mozzie borne disease1.755103
44sti symptoms dangers and treatments1.751830
\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 | " \n", 481 | " \n", 482 | " \n", 483 | " \n", 484 | " \n", 485 | " \n", 486 | " \n", 487 | " \n", 488 | " \n", 489 | " \n", 490 | " \n", 491 | " \n", 492 | " \n", 493 | " \n", 494 | " \n", 495 | " \n", 496 | " \n", 497 | " \n", 498 | " \n", 499 | " \n", 500 | " \n", 501 | " \n", 502 | " \n", 503 | " \n", 504 | " \n", 505 | " \n", 506 | " \n", 507 | " \n", 508 | " \n", 509 | " \n", 510 | " \n", 511 | " \n", 512 | " \n", 513 | " \n", 514 | " \n", 515 | " \n", 516 | " \n", 517 | " \n", 518 | " \n", 519 | " \n", 520 | " \n", 521 | " \n", 522 | " \n", 523 | " \n", 524 | " \n", 525 | " \n", 526 | " \n", 527 | " \n", 528 | " \n", 529 | " \n", 530 | " \n", 531 | " \n", 532 | " \n", 533 | " \n", 534 | " \n", 535 | " \n", 536 | "
headline_text_score
0perth storm a natural disaster16.165108
1more natural disaster planning needed16.165108
2gunnedah declared natural disaster area16.165108
3bushfire prompts natural disaster declaration16.165108
4maclean fire not natural disaster16.032738
5state helps natural disaster victims15.960692
6esperance declared natural disaster area15.960692
7flooding sparks natural disaster declarations15.960692
8government declares natural disaster areas15.960692
9nsw natural disaster zone widened15.960692
\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 | " \n", 585 | " \n", 586 | " \n", 587 | " \n", 588 | " \n", 589 | " \n", 590 | " \n", 591 | " \n", 592 | " \n", 593 | " \n", 594 | " \n", 595 | " \n", 596 | " \n", 597 | " \n", 598 | " \n", 599 | " \n", 600 | " \n", 601 | " \n", 602 | " \n", 603 | " \n", 604 | " \n", 605 | " \n", 606 | " \n", 607 | " \n", 608 | " \n", 609 | " \n", 610 | " \n", 611 | " \n", 612 | " \n", 613 | " \n", 614 | " \n", 615 | " \n", 616 | " \n", 617 | " \n", 618 | " \n", 619 | " \n", 620 | " \n", 621 | " \n", 622 | " \n", 623 | " \n", 624 | " \n", 625 | " \n", 626 | " \n", 627 | " \n", 628 | " \n", 629 | " \n", 630 | " \n", 631 | " \n", 632 | " \n", 633 | " \n", 634 | " \n", 635 | " \n", 636 | " \n", 637 | " \n", 638 | " \n", 639 | " \n", 640 | "
headline_text_score
0natural disaster declared in broken hill1.879506
1natural disaster declared in storm area1.877636
2lismore declared a natural disaster area1.867503
3broken hill declared a natural disaster area1.854052
4natural disasters take toll on austar1.848880
5perth storm a natural disaster1.836232
6call for nambucca valley natural disaster1.834766
7wagga albury declared natural disaster areas1.831764
8ballina area declared natural disaster zone1.823700
9disasters take toll on shire1.822510
\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 | " \n", 699 | " \n", 700 | " \n", 701 | " \n", 702 | " \n", 703 | " \n", 704 | " \n", 705 | " \n", 706 | " \n", 707 | " \n", 708 | " \n", 709 | " \n", 710 | " \n", 711 | " \n", 712 | " \n", 713 | " \n", 714 | " \n", 715 | " \n", 716 | " \n", 717 | " \n", 718 | " \n", 719 | " \n", 720 | " \n", 721 | " \n", 722 | " \n", 723 | " \n", 724 | " \n", 725 | " \n", 726 | " \n", 727 | " \n", 728 | " \n", 729 | " \n", 730 | " \n", 731 | " \n", 732 | " \n", 733 | " \n", 734 | " \n", 735 | " \n", 736 | " \n", 737 | " \n", 738 | " \n", 739 | " \n", 740 | " \n", 741 | " \n", 742 | " \n", 743 | " \n", 744 | " \n", 745 | " \n", 746 | " \n", 747 | " \n", 748 | " \n", 749 | " \n", 750 | " \n", 751 | " \n", 752 | " \n", 753 | " \n", 754 | "
headline_text_score
28power supply at risk if flood situation worsens1.787914
36widespread damage from freak storm1.779122
40catastrophic fire conditions for wa1.776245
43leigh creek ucg project lifeline or toxic environmental hazard1.772300
46qlds wild weather caused by freak event1.770647
48wild weather causes qld flooding1.769357
50cyclone damaged water supply fixed1.767906
53humungous effort on catastrophic day1.766371
56nsw floods receding water reveals destruction1.766130
58cyclone olwyn carnarvon water supply problems1.765932
\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 | " \n", 816 | " \n", 817 | " \n", 818 | " \n", 819 | " \n", 820 | " \n", 821 | " \n", 822 | " \n", 823 | " \n", 824 | " \n", 825 | " \n", 826 | " \n", 827 | " \n", 828 | " \n", 829 | " \n", 830 | " \n", 831 | " \n", 832 | " \n", 833 | " \n", 834 | " \n", 835 | " \n", 836 | " \n", 837 | " \n", 838 | " \n", 839 | " \n", 840 | " \n", 841 | " \n", 842 | " \n", 843 | " \n", 844 | " \n", 845 | " \n", 846 | " \n", 847 | " \n", 848 | " \n", 849 | " \n", 850 | " \n", 851 | " \n", 852 | " \n", 853 | " \n", 854 | " \n", 855 | " \n", 856 | " \n", 857 | " \n", 858 | " \n", 859 | " \n", 860 | " \n", 861 | " \n", 862 | " \n", 863 | " \n", 864 | " \n", 865 | " \n", 866 | " \n", 867 | " \n", 868 | " \n", 869 | " \n", 870 | " \n", 871 | "
headline_text_score
0regulatory madness in the banking world18.955673
1china pushes through banking sector reform14.721983
2swan to announce banking reform package14.582440
3open banking more choice or data risk13.001936
4swan wraps up meeting on banking rules reform12.904054
5govt internet regulatory plan criticised12.000524
6regulatory duplication strangling aquaculture development12.000524
7us flags financial regulatory reforms11.530772
8mcconnell a regulatory train wreck11.530772
9billabong rescue package clears regulatory hurdle11.220221
\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 | " \n", 920 | " \n", 921 | " \n", 922 | " \n", 923 | " \n", 924 | " \n", 925 | " \n", 926 | " \n", 927 | " \n", 928 | " \n", 929 | " \n", 930 | " \n", 931 | " \n", 932 | " \n", 933 | " \n", 934 | " \n", 935 | " \n", 936 | " \n", 937 | " \n", 938 | " \n", 939 | " \n", 940 | " \n", 941 | " \n", 942 | " \n", 943 | " \n", 944 | " \n", 945 | " \n", 946 | " \n", 947 | " \n", 948 | " \n", 949 | " \n", 950 | " \n", 951 | " \n", 952 | " \n", 953 | " \n", 954 | " \n", 955 | " \n", 956 | " \n", 957 | " \n", 958 | " \n", 959 | " \n", 960 | " \n", 961 | " \n", 962 | " \n", 963 | " \n", 964 | " \n", 965 | " \n", 966 | " \n", 967 | " \n", 968 | " \n", 969 | " \n", 970 | " \n", 971 | " \n", 972 | " \n", 973 | " \n", 974 | " \n", 975 | "
headline_text_score
0us flags financial regulatory reforms1.863391
1the banking royal commissions recommendations1.850401
2what can we expect from the banking inquiry1.841991
3banking royal commission superannuation hearings1.836119
4banking royal commission anz financial advice clients interest1.833776
5rba considers cap on credit card surcharges1.832314
6rba on banks interest rate moves1.831732
7will changes to financial advice laws see the1.831395
8commonwealth bank responds to financial planning inquiry1.831379
9reserve bank financial stability review1.830978
\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": "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Sa2A64A9gQpgrKRREfFaQbXjgDkR0V3SQOAK4Ihs2X8jYvsViqp4HKu6iRbB58nMzKzl8Od+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 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | "
publish_dateheadline_text
020030219aba decides against community broadcasting lic...
120030219act fire witnesses must be aware of defamation
220030219a g calls for infrastructure protection summit
320030219air nz staff in aust strike for pay rise
420030219air nz strike to affect australian travellers
\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 | --------------------------------------------------------------------------------