|
12 | 12 | "\n",
|
13 | 13 | "This notebook explains the steps of exporting an App Search engine together with its configurations in Elasticsearch. This is not meant to be an exhaustive example for all App Search features as those will vary based on your instance, but is meant to give a sense of how you can export, migrate, and enhance your application.\n",
|
14 | 14 | "\n",
|
| 15 | + "NOTE: This notebook is designed to work with Elasticsearch **8.18** or higher. If you are running this notebook against an older version of Elasticsearch, we note commands that will need to be modified.\n", |
| 16 | + "\n", |
15 | 17 | "We will look at:\n",
|
16 | 18 | "\n",
|
17 | 19 | "- how to export synonyms\n",
|
|
57 | 59 | "source": [
|
58 | 60 | "## Connect to Elasticsearch\n",
|
59 | 61 | "\n",
|
60 |
| - "ℹ️ We're using an Elastic Cloud deployment of Elasticsearch for this notebook. If you don't have an Elastic Cloud deployment, sign up [here](https://cloud.elastic.co/registration?onboarding_token=search&utm_source=github&utm_content=elasticsearch-labs-notebook) for a free trial. \n", |
| 62 | + "ℹ️ We're using an Elastic Cloud deployment of Elasticsearch for this notebook. If you don't have an Elastic Cloud deployment, sign up [here](https://cloud.elastic.co/registration?onboarding_token=search&utm_source=github&utm_content=elasticsearch-labs-notebook) for a free trial. This notebook is designed to be run against an Elasticsearch deployment running on version 8.18 or higher.\n", |
61 | 63 | "\n",
|
62 | 64 | "We'll use the **Cloud ID** to identify our deployment, because we are using Elastic Cloud deployment. To find the Cloud ID for your deployment, go to https://cloud.elastic.co/deployments and select your deployment. \n",
|
63 | 65 | "\n",
|
|
66 | 68 | },
|
67 | 69 | {
|
68 | 70 | "cell_type": "code",
|
69 |
| - "execution_count": 2, |
| 71 | + "execution_count": null, |
70 | 72 | "metadata": {},
|
71 | 73 | "outputs": [],
|
72 | 74 | "source": [
|
|
95 | 97 | "\n",
|
96 | 98 | "You can find your App Search endpoint and your search private key from the `Credentials` menu inside your App Search instance in Kibana.\n",
|
97 | 99 | "\n",
|
98 |
| - "Also note here, we define our `ENGINE_NAME`. For this examplem we are using the `national-parks-demo` sample engine that is available within App Search." |
| 100 | + "Also note here, we define our `ENGINE_NAME`. For this example, we are using the `national-parks-demo` sample engine that is available within App Search." |
99 | 101 | ]
|
100 | 102 | },
|
101 | 103 | {
|
102 | 104 | "cell_type": "code",
|
103 |
| - "execution_count": 3, |
| 105 | + "execution_count": null, |
104 | 106 | "metadata": {},
|
105 | 107 | "outputs": [],
|
106 | 108 | "source": [
|
|
129 | 131 | },
|
130 | 132 | {
|
131 | 133 | "cell_type": "code",
|
132 |
| - "execution_count": 4, |
| 134 | + "execution_count": null, |
133 | 135 | "metadata": {
|
134 | 136 | "id": "kpV8K5jHvRK6"
|
135 | 137 | },
|
|
173 | 175 | "\n",
|
174 | 176 | "Next, we will export any curations that may be in our App Search engine.\n",
|
175 | 177 | "\n",
|
176 |
| - "To export App Search curations we will use Elasticsearch [query rules](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-using-query-rules.html).\n", |
177 |
| - "At the moment of writing this notebook Elasticsearch query rules only allow for pinning results unlike App Search curations that also allow excluding results.\n", |
178 |
| - "For this reason we will only export pinned results. The code below will create the necessary `query_rules` to achieve this. Note that there is a default soft limit of 100 curations for `query_rules` that can be configured up to a hard limit of 1,000." |
| 178 | + "To export App Search curations we will use Elasticsearch [query rules](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-using-query-rules.html). The code below will create the necessary `query_rules` to achieve this. Note that there is a default soft limit of 100 curations for `query_rules` that can be configured up to a hard limit of 1,000.\n", |
| 179 | + "\n", |
| 180 | + "NOTE: This example outputs query rules requiring `exact` matches, which are case-sensitive. If you need typo tolerance, consider using `fuzzy`. If you need different case values consider adding multiple values to your criteria. " |
179 | 181 | ]
|
180 | 182 | },
|
181 | 183 | {
|
|
187 | 189 | "query_rules = []\n",
|
188 | 190 | "\n",
|
189 | 191 | "for curation in app_search.list_curations(engine_name=ENGINE_NAME).body[\"results\"]:\n",
|
190 |
| - " query_rules.append(\n", |
191 |
| - " {\n", |
192 |
| - " \"rule_id\": curation[\"id\"],\n", |
193 |
| - " \"type\": \"pinned\",\n", |
194 |
| - " \"criteria\": [\n", |
195 |
| - " {\n", |
196 |
| - " \"type\": \"exact\",\n", |
197 |
| - " \"metadata\": \"user_query\",\n", |
198 |
| - " \"values\": curation[\"queries\"],\n", |
199 |
| - " }\n", |
200 |
| - " ],\n", |
201 |
| - " \"actions\": {\"ids\": curation[\"promoted\"]},\n", |
202 |
| - " }\n", |
203 |
| - " )\n", |
| 192 | + " if curation[\"promoted\"]:\n", |
| 193 | + " query_rules.append(\n", |
| 194 | + " {\n", |
| 195 | + " \"rule_id\": curation[\"id\"] + \"-pinned\",\n", |
| 196 | + " \"type\": \"pinned\",\n", |
| 197 | + " \"criteria\": [\n", |
| 198 | + " {\n", |
| 199 | + " \"type\": \"exact\",\n", |
| 200 | + " \"metadata\": \"user_query\",\n", |
| 201 | + " \"values\": curation[\"queries\"],\n", |
| 202 | + " }\n", |
| 203 | + " ],\n", |
| 204 | + " \"actions\": {\"ids\": curation[\"promoted\"]},\n", |
| 205 | + " }\n", |
| 206 | + " )\n", |
| 207 | + " if curation[\"hidden\"]:\n", |
| 208 | + " query_rules.append(\n", |
| 209 | + " {\n", |
| 210 | + " \"rule_id\": curation[\"id\"] + \"-exclude\",\n", |
| 211 | + " \"type\": \"exclude\",\n", |
| 212 | + " \"criteria\": [\n", |
| 213 | + " {\n", |
| 214 | + " \"type\": \"exact\",\n", |
| 215 | + " \"metadata\": \"user_query\",\n", |
| 216 | + " \"values\": curation[\"queries\"],\n", |
| 217 | + " }\n", |
| 218 | + " ],\n", |
| 219 | + " \"actions\": {\"ids\": curation[\"hidden\"]},\n", |
| 220 | + " }\n", |
| 221 | + " )\n", |
204 | 222 | "\n",
|
205 | 223 | "elasticsearch.query_rules.put_ruleset(ruleset_id=ENGINE_NAME, rules=query_rules)"
|
206 | 224 | ]
|
207 | 225 | },
|
| 226 | + { |
| 227 | + "cell_type": "markdown", |
| 228 | + "metadata": {}, |
| 229 | + "source": [ |
| 230 | + "Let's take a quick look at the query rules we've migrated. We'll do this via the `GET _query_rules/ENGINE_NAME` endpoint. Note that curations with both pinned and hidden documents will be represented as two rules in the ruleset." |
| 231 | + ] |
| 232 | + }, |
| 233 | + { |
| 234 | + "cell_type": "code", |
| 235 | + "execution_count": null, |
| 236 | + "metadata": {}, |
| 237 | + "outputs": [], |
| 238 | + "source": [ |
| 239 | + "print(\n", |
| 240 | + " json.dumps(\n", |
| 241 | + " elasticsearch.query_rules.get_ruleset(ruleset_id=ENGINE_NAME).body, indent=2\n", |
| 242 | + " )\n", |
| 243 | + ")" |
| 244 | + ] |
| 245 | + }, |
208 | 246 | {
|
209 | 247 | "cell_type": "markdown",
|
210 | 248 | "metadata": {
|
|
215 | 253 | "\n",
|
216 | 254 | "We recommend reindexing your App Search engine data into a new Elasticsearch index instead of reusing the existing one. This allows you to update the index mapping to take advantage of modern features like semantic search and the newly created Elasticsearch synonym set.\n",
|
217 | 255 | "\n",
|
218 |
| - "App Search has the following data types: `text`, `number`, `date` and `geolocation`. Each of these types is mapped to Elasticsearch field types.\n", |
| 256 | + "App Search has the following data types:\n", |
| 257 | + "\n", |
| 258 | + "- `text`\n", |
| 259 | + "- `number`\n", |
| 260 | + "- `date`\n", |
| 261 | + "- `geolocation`\n", |
| 262 | + " \n", |
| 263 | + "Each of these types is mapped to Elasticsearch field types.\n", |
| 264 | + "\n", |
219 | 265 | "We can take a closer look at how App Search field types are mapped to Elasticsearch fields, by using the [`GET mapping API`](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-mapping.html).\n",
|
220 | 266 | "For App Search engines, the associated Elasticsearch index name is `.ent-search-engine-documents-[ENGINE_NAME]`, e.g. `.ent-search-engine-documents-national-parks-demo` for the App Search sample engine `national-parks-demo`.\n",
|
221 | 267 | "One thing to notice is how App Search uses [multi-fields](https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html) in Elasticsearch that allow for quickly changing the field type in App Search without requiring reindexing by creating subfields for each type of supported field:\n",
|
|
578 | 624 | "source": [
|
579 | 625 | "# Add semantic text fields for semantic search (optional)\n",
|
580 | 626 | "\n",
|
581 |
| - "One of the advantages of exporting our index directly to Elasticsearch is that we can easily perform semantic search with ELSER. To do this, we'll need to add an inference endpoint using ELSER, and a `semantic_text` field to our index to use it.\n", |
| 627 | + "One of the advantages of exporting our index directly to Elasticsearch is that we can easily perform semantic search with ELSER. To do this, we'll need to add a `semantic_text` field to our index to use it. We will set up a `semantic_text` field using our default ELSER endpoint.\n", |
582 | 628 | "\n",
|
583 |
| - "Note that to use this feature, your cluster must have at least one ML node set up with enough resources allocated to it.\n", |
| 629 | + "Note that to use this feature, your cluster must be running at least version 8.15.0 and have at least one ML node set up with enough resources allocated to it.\n", |
584 | 630 | "\n",
|
585 |
| - "If you have not already, be sure that your ELSER v2 model is [setup and deployed](https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html).\n", |
586 |
| - "\n", |
587 |
| - "Let's first start by creating our inference endpoint using the [Create inference API]](https://www.elastic.co/guide/en/elasticsearch/reference/current/put-inference-api.html)." |
588 |
| - ] |
589 |
| - }, |
590 |
| - { |
591 |
| - "cell_type": "code", |
592 |
| - "execution_count": null, |
593 |
| - "metadata": {}, |
594 |
| - "outputs": [], |
595 |
| - "source": [ |
596 |
| - "# delete our inference endpoint if it is already created\n", |
597 |
| - "if elasticsearch.inference.get(inference_id=\"elser_inference_endpoint\"):\n", |
598 |
| - " elasticsearch.inference.delete(inference_id=\"elser_inference_endpoint\")\n", |
599 |
| - "\n", |
600 |
| - "# and create our endpoint using the ELSER v2 model\n", |
601 |
| - "elasticsearch.inference.put(\n", |
602 |
| - " inference_id=\"elser_inference_endpoint\",\n", |
603 |
| - " inference_config={\n", |
604 |
| - " \"service\": \"elasticsearch\",\n", |
605 |
| - " \"service_settings\": {\n", |
606 |
| - " \"model_id\": \".elser_model_2_linux-x86_64\",\n", |
607 |
| - " \"num_allocations\": 1,\n", |
608 |
| - " \"num_threads\": 1,\n", |
609 |
| - " },\n", |
610 |
| - " },\n", |
611 |
| - " task_type=\"sparse_embedding\",\n", |
612 |
| - ")" |
| 631 | + "If you do not have an ELSER endpoint running, it will be automatically downloaded, deployed and started for you when you use `semantic_text`. This means the first few commands may take a while as the model loads. For Elasticsearch versions below 8.17, you will need to create an inference endpoint and add it to the `semantic_text` mapping." |
613 | 632 | ]
|
614 | 633 | },
|
615 | 634 | {
|
|
618 | 637 | "source": [
|
619 | 638 | "## Using semantic text fields for ingest and query\n",
|
620 | 639 | "\n",
|
621 |
| - "Next, we'll augment our text fields with `semantic_text` fields in our index. We'll do this by creating a `semtantic_text` field, and providing a `copy_to` directive from the original source field to copy the text into our semantic text fields.\n", |
| 640 | + "First, we'll augment our text fields with `semantic_text` fields in our index. We'll do this by creating a `semtantic_text` field, and providing a `copy_to` directive from the original source field to copy the text into our semantic text fields.\n", |
622 | 641 | "\n",
|
623 | 642 | "In the example below, we are using the `description` and `title` fields from our example index to add semantic search on those fields."
|
624 | 643 | ]
|
|
636 | 655 | "# add the semantic_text field to our mapping for each field defined\n",
|
637 | 656 | "for field_name in SEMANTIC_TEXT_FIELDS:\n",
|
638 | 657 | " semantic_field_name = field_name + \"_semantic\"\n",
|
639 |
| - " mapping[semantic_field_name] = {\n", |
640 |
| - " \"type\": \"semantic_text\",\n", |
641 |
| - " \"inference_id\": \"elser_inference_endpoint\",\n", |
642 |
| - " }\n", |
| 658 | + " mapping[semantic_field_name] = {\"type\": \"semantic_text\"}\n", |
643 | 659 | "\n",
|
644 | 660 | "# and for our text fields, add a \"copy_to\" directive to copy the text to the semantic_text field\n",
|
645 | 661 | "for field_name in SEMANTIC_TEXT_FIELDS:\n",
|
|
778 | 794 | "\n",
|
779 | 795 | "For the results, we sort on our score descending as the primary sort, with the document id as the secondary.\n",
|
780 | 796 | "\n",
|
781 |
| - "We apply highlights to our results, request a return size of the top 10 hits, and for each hit, return the result fields." |
| 797 | + "We apply highlights to returned text search descriptions, request a return size of the top 10 hits, and for each hit, return the result fields." |
782 | 798 | ]
|
783 | 799 | },
|
784 | 800 | {
|
|
826 | 842 | " \"order\": \"score\",\n",
|
827 | 843 | " \"encoder\": \"html\",\n",
|
828 | 844 | " \"require_field_match\": False,\n",
|
829 |
| - " \"fields\": {},\n", |
| 845 | + " \"fields\": {\"description\": {\"pre_tags\": [\"<em>\"], \"post_tags\": [\"</em>\"]}},\n", |
830 | 846 | " },\n",
|
831 | 847 | " \"size\": 10,\n",
|
832 | 848 | " \"_source\": result_fields,\n",
|
|
849 | 865 | "outputs": [],
|
850 | 866 | "source": [
|
851 | 867 | "results = elasticsearch.search(\n",
|
852 |
| - " index=SOURCE_INDEX,\n", |
| 868 | + " index=DEST_INDEX,\n", |
853 | 869 | " query=app_search_query_payload[\"query\"],\n",
|
854 | 870 | " highlight=app_search_query_payload[\"highlight\"],\n",
|
855 | 871 | " source=app_search_query_payload[\"_source\"],\n",
|
|
866 | 882 | "### How to do semantic search using ELSER with semantic text fields\n",
|
867 | 883 | "\n",
|
868 | 884 | "If you [enabled and reindexed your data with ELSER](#add-sparse_vector-fields-for-semantic-search-optional), we can now use this to do semantic search.\n",
|
869 |
| - "For each `semantic_text` field type, we can define a [semantic query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html) to easily perform a semantic search on these fields.\n" |
| 885 | + "For each `semantic_text` field type, we can define a [match query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html) to easily perform a semantic search on these fields.\n", |
| 886 | + "\n", |
| 887 | + "NOTE: For Elasticsearch versions prior to 8.18, a [semantic query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html) should be used to perform a semantic search on these fields.\n" |
870 | 888 | ]
|
871 | 889 | },
|
872 | 890 | {
|
|
881 | 899 | "\n",
|
882 | 900 | "for field_name in SEMANTIC_TEXT_FIELDS:\n",
|
883 | 901 | " semantic_field_name = field_name + \"_semantic\"\n",
|
884 |
| - " semantic_text_queries.append(\n", |
885 |
| - " {\n", |
886 |
| - " \"semantic\": {\n", |
887 |
| - " \"field\": semantic_field_name,\n", |
888 |
| - " \"query\": QUERY_STRING,\n", |
889 |
| - " }\n", |
890 |
| - " }\n", |
891 |
| - " )\n", |
| 902 | + " semantic_text_queries.append({\"match\": {semantic_field_name: QUERY_STRING}})\n", |
892 | 903 | "\n",
|
893 | 904 | "semantic_query = {\"bool\": {\"should\": semantic_text_queries}}\n",
|
894 | 905 | "print(f\"Elasticsearch query:\\n{json.dumps(semantic_query, indent=2)}\\n\")"
|
|
926 | 937 | " \"should\": [\n",
|
927 | 938 | " // multi_match query with best_fields from App Search generated query\n",
|
928 | 939 | " // multi_match query with cross_fields from App Search generated query\n",
|
929 |
| - " // text_expansion queries for sparse_vector fields\n", |
| 940 | + " // match queries for semantic_text fields\n", |
930 | 941 | " ]\n",
|
931 | 942 | " }\n",
|
932 | 943 | " } \n",
|
|
960 | 971 | "outputs": [],
|
961 | 972 | "source": [
|
962 | 973 | "results = elasticsearch.search(\n",
|
963 |
| - " index=SOURCE_INDEX,\n", |
| 974 | + " index=DEST_INDEX,\n", |
964 | 975 | " query=payload[\"query\"],\n",
|
965 | 976 | " highlight=payload[\"highlight\"],\n",
|
966 | 977 | " source=payload[\"_source\"],\n",
|
|
969 | 980 | " min_score=1,\n",
|
970 | 981 | ")\n",
|
971 | 982 | "\n",
|
972 |
| - "print(f\"Text expansion query results:\\n{json.dumps(results.body, indent=2)}\\n\")" |
| 983 | + "print(f\"Semantic query results:\\n{json.dumps(results.body, indent=2)}\\n\")" |
973 | 984 | ]
|
974 | 985 | }
|
975 | 986 | ],
|
|
0 commit comments