From 6ab7d572fcbe1b1e73319895a83fc83d44619878 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Mon, 28 Aug 2023 13:58:00 -0400 Subject: [PATCH 01/35] Add search phase results processor Signed-off-by: Fanit Kolchina --- _search-plugins/search-pipelines/index.md | 1 + .../search-pipelines/search-processors.md | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/_search-plugins/search-pipelines/index.md b/_search-plugins/search-pipelines/index.md index 26f663704a..7d53d0c587 100644 --- a/_search-plugins/search-pipelines/index.md +++ b/_search-plugins/search-pipelines/index.md @@ -16,6 +16,7 @@ The following is a list of search pipeline terminology: * _Search request processor_: A component that takes a search request (the query and the metadata passed in the request), performs an operation with or on the search request, and returns a search request. * _Search response processor_: A component that takes a search response and search request (the query, results, and metadata passed in the request), performs an operation with or on the search response, and returns a search response. +* _Search phase results processor_: A component that runs between search phases at the coordinating node level. A search phase results processor takes the results retrieved from one search phase and transforms them before passing them to next search phase. * _Processor_: Either a search request processor or a search response processor. * _Search pipeline_: An ordered list of processors that is integrated into OpenSearch. The pipeline intercepts a query, performs processing on the query, sends it to OpenSearch, intercepts the results, performs processing on the results, and returns them to the calling application, as shown in the following diagram. diff --git a/_search-plugins/search-pipelines/search-processors.md b/_search-plugins/search-pipelines/search-processors.md index d35f243bbf..46851e0c2b 100644 --- a/_search-plugins/search-pipelines/search-processors.md +++ b/_search-plugins/search-pipelines/search-processors.md @@ -12,10 +12,13 @@ grand_parent: Search Search processors can be of the following types: - [Search request processors](#search-request-processors) +- [Search phase results processors](#search-phase-results-processors) - [Search response processors](#search-response-processors) ## Search request processors +A search request processor takes a search request (the query and the metadata passed in the request) and performs an operation on the search request before submitting the search request to the index. + The following table lists all supported search request processors. Processor | Description | Earliest available version @@ -23,8 +26,18 @@ Processor | Description | Earliest available version [`script`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/script-processor/) | Adds a script that is run on newly indexed documents. | 2.8 [`filter_query`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/filter-query-processor/) | Adds a filtering query that is used to filter requests. | 2.8 +## Search phase results processors + +The following table lists all supported search request processors. + +Processor | Description | Earliest available version +:--- | :--- | :--- +[`phase_results_processor`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/script-processor/) | Adds a script that is run on newly indexed documents. | 2.10 + ## Search response processors +A search response processor performs an operation on the search response and returns a search response. + The following table lists all supported search response processors. Processor | Description | Earliest available version From 79e9597abd586bee70cd9ef6a89d629b863ff870 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Tue, 29 Aug 2023 12:18:47 -0400 Subject: [PATCH 02/35] Add hybrid query Signed-off-by: Fanit Kolchina --- _query-dsl/specialized/hybrid.md | 42 ++++ _query-dsl/specialized/index.md | 19 ++ _search-plugins/search-pipelines/index.md | 2 +- .../normalization-processor.md | 203 ++++++++++++++++++ .../search-pipelines/search-processors.md | 20 +- 5 files changed, 276 insertions(+), 10 deletions(-) create mode 100644 _query-dsl/specialized/hybrid.md create mode 100644 _query-dsl/specialized/index.md create mode 100644 _search-plugins/search-pipelines/normalization-processor.md diff --git a/_query-dsl/specialized/hybrid.md b/_query-dsl/specialized/hybrid.md new file mode 100644 index 0000000000..4307efa2b4 --- /dev/null +++ b/_query-dsl/specialized/hybrid.md @@ -0,0 +1,42 @@ +--- +layout: default +title: Hybrid +parent: Specialized queries +grand_parent: Query DSL +nav_order: 20 +--- + +# Hybrid query + +This is an experimental feature and is not recommended for use in a production environment. For updates on the progress of the feature or if you want to leave feedback, see the associated [GitHub issue](https://github.com/opensearch-project/neural-search/issues/244). +{: .warning} + +Use a hybrid query to combine relevance scores from multiple queries into one score for a given document. A hybrid query contains a list of one or more queries + + +```json +POST flicker-index/_search?search_pipeline=normalizationPipeline +{ + "query": { + "hybrid": { + "queries": [ + { + "neural": { + "passage_embedding": { + "query_text": "Girl with Brown Hair", + "model_id": "ABCBMODELID", + "k": 20 + } + } + }, + { + "match": { + "passage_text": "Girl Brown hair" + } + } + ] + } + } +} +``` +{% include copy-curl.html %} \ No newline at end of file diff --git a/_query-dsl/specialized/index.md b/_query-dsl/specialized/index.md new file mode 100644 index 0000000000..baa924a3f2 --- /dev/null +++ b/_query-dsl/specialized/index.md @@ -0,0 +1,19 @@ +--- +layout: default +title: Specialized queries +has_children: true +nav_order: 65 +--- + +# Specialized queries + +OpenSearch supports the following specialized queries: + +- `distance_feature` +- [`hybrid`]({{site.url}}{{site.baseurl}}/query-dsl/specialized/hybrid/) +- `more_like_this` +- `percolate` +- `rank_feature` +- `script` +- `script_score` +- `wrapper` diff --git a/_search-plugins/search-pipelines/index.md b/_search-plugins/search-pipelines/index.md index 7d53d0c587..6a5bc837ec 100644 --- a/_search-plugins/search-pipelines/index.md +++ b/_search-plugins/search-pipelines/index.md @@ -16,7 +16,7 @@ The following is a list of search pipeline terminology: * _Search request processor_: A component that takes a search request (the query and the metadata passed in the request), performs an operation with or on the search request, and returns a search request. * _Search response processor_: A component that takes a search response and search request (the query, results, and metadata passed in the request), performs an operation with or on the search response, and returns a search response. -* _Search phase results processor_: A component that runs between search phases at the coordinating node level. A search phase results processor takes the results retrieved from one search phase and transforms them before passing them to next search phase. +* _Search phase results processor_: A component that runs between search phases at the coordinating node level. A search phase results processor takes the results retrieved from one search phase and transforms them before passing them to the next search phase. * _Processor_: Either a search request processor or a search response processor. * _Search pipeline_: An ordered list of processors that is integrated into OpenSearch. The pipeline intercepts a query, performs processing on the query, sends it to OpenSearch, intercepts the results, performs processing on the results, and returns them to the calling application, as shown in the following diagram. diff --git a/_search-plugins/search-pipelines/normalization-processor.md b/_search-plugins/search-pipelines/normalization-processor.md new file mode 100644 index 0000000000..dde385c3ca --- /dev/null +++ b/_search-plugins/search-pipelines/normalization-processor.md @@ -0,0 +1,203 @@ +--- +layout: default +title: Normalization processor +nav_order: 15 +has_children: false +parent: Search processors +grand_parent: Search pipelines +--- + +# Normalization processor + +The `normalization_processor` search phase results processor intercepts the query phase results and normalizes and combines the document scores before passing the documents to the fetch phase. + +## Request fields + +The following table lists all available request fields. + +Field | Data type | Description +:--- | :--- | :--- +`normalization.technique` | String | The technique for normalizing scores. Valid values are `min_max`, `L2`. Optional. Default is `min_max`. +`combination.technique` | String | The technique for combining scores. Valid values are `harmonic_mean`, `arithmetic_mean`, `geometric_mean`. Optional. Default is `arithmetic_mean`. +`tag` | String | The processor's identifier. +`description` | String | A description of the processor. +`ignore_failure` | Boolean | If `true`, OpenSearch [ignores a failure]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/#ignoring-processor-failures) of this processor and continues to run the remaining processors in the search pipeline. Optional. Default is `false`. + +## Example + +The following example demonstrates using a search pipeline with a `normalization_processor` processor. + +### Creating a search pipeline + +The following request creates a search pipeline with a `rename_field` response processor that renames the field `message` to `notification`: + +```json +PUT /_search/pipeline/my_pipeline +{ + "phase_results_processors": [ + { + "normalization-processor": { + "normalization": { + "technique": "min_max" + }, + "combination": { + "technique": "arithmetic_mean" + } + } + } + ] +} +``` +{% include copy-curl.html %} + +### Using a search pipeline + +Search for documents in `my_index` without a search pipeline: + +```json +GET /my_index/_search +``` +{% include copy-curl.html %} + +The response contains the field `message`: + +
+ + Response + + {: .text-delta} +```json +{ + "took" : 1, + "timed_out" : false, + "_shards" : { + "total" : 1, + "successful" : 1, + "skipped" : 0, + "failed" : 0 + }, + "hits" : { + "total" : { + "value" : 2, + "relation" : "eq" + }, + "max_score" : 1.0, + "hits" : [ + { + "_index" : "my_index", + "_id" : "1", + "_score" : 1.0, + "_source" : { + "message" : "This is a public message", + "visibility" : "public" + } + } + ] + } +} +``` +
+ +To search with a pipeline, specify the pipeline name in the `search_pipeline` query parameter: + +```json +GET /my_index/_search?search_pipeline=my_pipeline +``` +{% include copy-curl.html %} + +The `message` field has been renamed to `notification`: + +
+ + Response + + {: .text-delta} +```json +{ + "took" : 2, + "timed_out" : false, + "_shards" : { + "total" : 1, + "successful" : 1, + "skipped" : 0, + "failed" : 0 + }, + "hits" : { + "total" : { + "value" : 1, + "relation" : "eq" + }, + "max_score" : 0.0, + "hits" : [ + { + "_index" : "my_index", + "_id" : "1", + "_score" : 0.0, + "_source" : { + "visibility" : "public", + "notification" : "This is a public message" + } + } + ] + } +} +``` +
+ +You can also use the `fields` option to search for specific fields in a document: + +```json +POST /my_index/_search?pretty&search_pipeline=my_pipeline +{ + "fields":["visibility", "message"] +} +``` +{% include copy-curl.html %} + +In the response, the field `message` has been renamed to `notification`: + +
+ + Response + + {: .text-delta} +```json +{ + "took" : 4, + "timed_out" : false, + "_shards" : { + "total" : 1, + "successful" : 1, + "skipped" : 0, + "failed" : 0 + }, + "hits" : { + "total" : { + "value" : 1, + "relation" : "eq" + }, + "max_score" : 0.0, + "hits" : [ + { + "_index" : "my_index", + "_id" : "1", + "_score" : 0.0, + "_source" : { + "visibility" : "public", + "notification" : "This is a public message" + }, + "fields" : { + "visibility" : [ + "public" + ], + "notification" : [ + "This is a public message" + ] + } + } + ] + } +} + +``` +
\ No newline at end of file diff --git a/_search-plugins/search-pipelines/search-processors.md b/_search-plugins/search-pipelines/search-processors.md index 46851e0c2b..c32eb4c99a 100644 --- a/_search-plugins/search-pipelines/search-processors.md +++ b/_search-plugins/search-pipelines/search-processors.md @@ -12,8 +12,8 @@ grand_parent: Search Search processors can be of the following types: - [Search request processors](#search-request-processors) -- [Search phase results processors](#search-phase-results-processors) - [Search response processors](#search-response-processors) +- [Search phase results processors](#search-phase-results-processors) ## Search request processors @@ -26,14 +26,6 @@ Processor | Description | Earliest available version [`script`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/script-processor/) | Adds a script that is run on newly indexed documents. | 2.8 [`filter_query`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/filter-query-processor/) | Adds a filtering query that is used to filter requests. | 2.8 -## Search phase results processors - -The following table lists all supported search request processors. - -Processor | Description | Earliest available version -:--- | :--- | :--- -[`phase_results_processor`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/script-processor/) | Adds a script that is run on newly indexed documents. | 2.10 - ## Search response processors A search response processor performs an operation on the search response and returns a search response. @@ -45,6 +37,16 @@ Processor | Description | Earliest available version [`rename_field`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/rename-field-processor/)| Renames an existing field. | 2.8 [`personalize_search_ranking`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/personalize-search-ranking/) | Uses [Amazon Personalize](https://aws.amazon.com/personalize/) to rerank search results (requires setting up the Amazon Personalize service). | 2.9 +## Search phase results processors + +A search phase results processor runs between search phases at the coordinating node level. It takes the results retrieved from one search phase and transforms them before passing them to the next search phase. + +The following table lists all supported search request processors. + +Processor | Description | Earliest available version +:--- | :--- | :--- +[`normalization_processor`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/script-processor/) | Intercepts the query phase results and normalizes and combines the document scores before passing the documents to the fetch phase. | 2.10 + ## Viewing available processor types You can use the Nodes Search Pipelines API to view the available processor types: From 69d42740ce258da1f51e414310bc2a27de029edb Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Tue, 5 Sep 2023 21:30:23 -0400 Subject: [PATCH 03/35] Normalization processor additions Signed-off-by: Fanit Kolchina --- .../normalization-processor.md | 179 +++--------------- 1 file changed, 31 insertions(+), 148 deletions(-) diff --git a/_search-plugins/search-pipelines/normalization-processor.md b/_search-plugins/search-pipelines/normalization-processor.md index dde385c3ca..bedeaf44aa 100644 --- a/_search-plugins/search-pipelines/normalization-processor.md +++ b/_search-plugins/search-pipelines/normalization-processor.md @@ -19,6 +19,7 @@ Field | Data type | Description :--- | :--- | :--- `normalization.technique` | String | The technique for normalizing scores. Valid values are `min_max`, `L2`. Optional. Default is `min_max`. `combination.technique` | String | The technique for combining scores. Valid values are `harmonic_mean`, `arithmetic_mean`, `geometric_mean`. Optional. Default is `arithmetic_mean`. +`combination.parameters.weights` | Array of floating-point values | Specifies the weights to use for each query. Valid values are in the [0.0, 1.0] range and signify decimal percentages. The closer the weight is to 1.0, the more weight is given to a query. The number of values in the `weights` array must equal the number of queries. The sum of the values in the array must equal 1.0. `tag` | String | The processor's identifier. `description` | String | A description of the processor. `ignore_failure` | Boolean | If `true`, OpenSearch [ignores a failure]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/#ignoring-processor-failures) of this processor and continues to run the remaining processors in the search pipeline. Optional. Default is `false`. @@ -29,19 +30,22 @@ The following example demonstrates using a search pipeline with a `normalization ### Creating a search pipeline -The following request creates a search pipeline with a `rename_field` response processor that renames the field `message` to `notification`: +The following request creates a search pipeline with a `normalization_processor` that renames the field `message` to `notification`: ```json PUT /_search/pipeline/my_pipeline { - "phase_results_processors": [ + "phase_results_processors" : [ { - "normalization-processor": { - "normalization": { - "technique": "min_max" + "normalization-processor" : { + "normalization": { + "technique": "min_max", }, - "combination": { - "technique": "arithmetic_mean" + "combination": { + "technique" : "harmonic_mean", + "parameters" : { + "weights" : [0.4, 0.7] + } } } } @@ -52,152 +56,31 @@ PUT /_search/pipeline/my_pipeline ### Using a search pipeline -Search for documents in `my_index` without a search pipeline: +Use a `hybrid` query to apply the search pipeline created in the previous section to a search request: ```json -GET /my_index/_search -``` -{% include copy-curl.html %} - -The response contains the field `message`: - -
- - Response - - {: .text-delta} -```json -{ - "took" : 1, - "timed_out" : false, - "_shards" : { - "total" : 1, - "successful" : 1, - "skipped" : 0, - "failed" : 0 - }, - "hits" : { - "total" : { - "value" : 2, - "relation" : "eq" - }, - "max_score" : 1.0, - "hits" : [ - { - "_index" : "my_index", - "_id" : "1", - "_score" : 1.0, - "_source" : { - "message" : "This is a public message", - "visibility" : "public" - } - } - ] - } -} -``` -
- -To search with a pipeline, specify the pipeline name in the `search_pipeline` query parameter: - -```json -GET /my_index/_search?search_pipeline=my_pipeline -``` -{% include copy-curl.html %} - -The `message` field has been renamed to `notification`: - -
- - Response - - {: .text-delta} -```json +POST flicker-index/_search?search_pipeline=normalizationPipeline { - "took" : 2, - "timed_out" : false, - "_shards" : { - "total" : 1, - "successful" : 1, - "skipped" : 0, - "failed" : 0 - }, - "hits" : { - "total" : { - "value" : 1, - "relation" : "eq" - }, - "max_score" : 0.0, - "hits" : [ - { - "_index" : "my_index", - "_id" : "1", - "_score" : 0.0, - "_source" : { - "visibility" : "public", - "notification" : "This is a public message" - } - } - ] - } -} -``` -
- -You can also use the `fields` option to search for specific fields in a document: - -```json -POST /my_index/_search?pretty&search_pipeline=my_pipeline -{ - "fields":["visibility", "message"] -} -``` -{% include copy-curl.html %} - -In the response, the field `message` has been renamed to `notification`: - -
- - Response - - {: .text-delta} -```json -{ - "took" : 4, - "timed_out" : false, - "_shards" : { - "total" : 1, - "successful" : 1, - "skipped" : 0, - "failed" : 0 - }, - "hits" : { - "total" : { - "value" : 1, - "relation" : "eq" - }, - "max_score" : 0.0, - "hits" : [ - { - "_index" : "my_index", - "_id" : "1", - "_score" : 0.0, - "_source" : { - "visibility" : "public", - "notification" : "This is a public message" + "query": { + "hybrid": { + "queries": [ + { + "neural": { + "passage_embedding": { + "query_text": "Girl with Brown Hair", + "model_id": "ABCBMODELID", + "k": 20 + } + } }, - "fields" : { - "visibility" : [ - "public" - ], - "notification" : [ - "This is a public message" - ] + { + "match": { + "passage_text": "Girl Brown hair" + } } - } - ] + ] + } } } - ``` -
\ No newline at end of file +{% include copy-curl.html %} \ No newline at end of file From 06dcb260126df7f6c8246cde284844c83a1ab9aa Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Wed, 6 Sep 2023 12:56:47 -0400 Subject: [PATCH 04/35] Add more details Signed-off-by: Fanit Kolchina --- .../{specialized => compound}/hybrid.md | 17 +++++++++++++---- _query-dsl/specialized/index.md | 19 ------------------- .../normalization-processor.md | 7 ++++++- 3 files changed, 19 insertions(+), 24 deletions(-) rename _query-dsl/{specialized => compound}/hybrid.md (53%) delete mode 100644 _query-dsl/specialized/index.md diff --git a/_query-dsl/specialized/hybrid.md b/_query-dsl/compound/hybrid.md similarity index 53% rename from _query-dsl/specialized/hybrid.md rename to _query-dsl/compound/hybrid.md index 4307efa2b4..2171d4b7e0 100644 --- a/_query-dsl/specialized/hybrid.md +++ b/_query-dsl/compound/hybrid.md @@ -1,9 +1,9 @@ --- layout: default title: Hybrid -parent: Specialized queries +parent: Compound queries grand_parent: Query DSL -nav_order: 20 +nav_order: 70 --- # Hybrid query @@ -11,8 +11,9 @@ nav_order: 20 This is an experimental feature and is not recommended for use in a production environment. For updates on the progress of the feature or if you want to leave feedback, see the associated [GitHub issue](https://github.com/opensearch-project/neural-search/issues/244). {: .warning} -Use a hybrid query to combine relevance scores from multiple queries into one score for a given document. A hybrid query contains a list of one or more queries +Use a hybrid query to combine relevance scores from multiple queries into one score for a given document. A hybrid query contains a list of one or more queries, which are run in parallel at the data node level. Hybrid query calculates document scores at the shard level independently for each subquery. The subquery rewriting is done at the coordinating node level to avoid duplicate computations. +## Example ```json POST flicker-index/_search?search_pipeline=normalizationPipeline @@ -39,4 +40,12 @@ POST flicker-index/_search?search_pipeline=normalizationPipeline } } ``` -{% include copy-curl.html %} \ No newline at end of file +{% include copy-curl.html %} + +## Parameters + +The following table lists all top-level parameters supported by `hybrid` queries. + +Parameter | Description +:--- | :--- +`queries` | An array of one or more query clauses that are used to match documents. A document must match at least one query clause to be returned in the results. If a document matches multiple query clauses, the relevance score is set to the highest relevance score from all matching query clauses. The maximum number of query clauses is 10. Required. \ No newline at end of file diff --git a/_query-dsl/specialized/index.md b/_query-dsl/specialized/index.md deleted file mode 100644 index baa924a3f2..0000000000 --- a/_query-dsl/specialized/index.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -layout: default -title: Specialized queries -has_children: true -nav_order: 65 ---- - -# Specialized queries - -OpenSearch supports the following specialized queries: - -- `distance_feature` -- [`hybrid`]({{site.url}}{{site.baseurl}}/query-dsl/specialized/hybrid/) -- `more_like_this` -- `percolate` -- `rank_feature` -- `script` -- `script_score` -- `wrapper` diff --git a/_search-plugins/search-pipelines/normalization-processor.md b/_search-plugins/search-pipelines/normalization-processor.md index bedeaf44aa..ef3080dd98 100644 --- a/_search-plugins/search-pipelines/normalization-processor.md +++ b/_search-plugins/search-pipelines/normalization-processor.md @@ -11,6 +11,8 @@ grand_parent: Search pipelines The `normalization_processor` search phase results processor intercepts the query phase results and normalizes and combines the document scores before passing the documents to the fetch phase. +## + ## Request fields The following table lists all available request fields. @@ -83,4 +85,7 @@ POST flicker-index/_search?search_pipeline=normalizationPipeline } } ``` -{% include copy-curl.html %} \ No newline at end of file +{% include copy-curl.html %} + +Normalization processor does not produce consistent results for a cluster with one node and one shard. +{: .warning} \ No newline at end of file From f0d166781cb6186715eb69b17976b541ad36d703 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Thu, 7 Sep 2023 10:08:02 -0400 Subject: [PATCH 05/35] Continue writing Signed-off-by: Fanit Kolchina --- _query-dsl/compound/hybrid.md | 2 +- .../normalization-processor.md | 28 +++++++++++-------- .../search-pipelines/search-processors.md | 2 +- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/_query-dsl/compound/hybrid.md b/_query-dsl/compound/hybrid.md index 2171d4b7e0..c79b23bb05 100644 --- a/_query-dsl/compound/hybrid.md +++ b/_query-dsl/compound/hybrid.md @@ -48,4 +48,4 @@ The following table lists all top-level parameters supported by `hybrid` queries Parameter | Description :--- | :--- -`queries` | An array of one or more query clauses that are used to match documents. A document must match at least one query clause to be returned in the results. If a document matches multiple query clauses, the relevance score is set to the highest relevance score from all matching query clauses. The maximum number of query clauses is 10. Required. \ No newline at end of file +`queries` | An array of one or more query clauses that are used to match documents. A document must match at least one query clause to be returned in the results. If a document matches multiple query clauses, the relevance score is set to the highest relevance score from all matching query clauses. The maximum number of query clauses is 5. Required. \ No newline at end of file diff --git a/_search-plugins/search-pipelines/normalization-processor.md b/_search-plugins/search-pipelines/normalization-processor.md index ef3080dd98..d731ee6af9 100644 --- a/_search-plugins/search-pipelines/normalization-processor.md +++ b/_search-plugins/search-pipelines/normalization-processor.md @@ -9,9 +9,15 @@ grand_parent: Search pipelines # Normalization processor -The `normalization_processor` search phase results processor intercepts the query phase results and normalizes and combines the document scores before passing the documents to the fetch phase. +The `normalization_processor` is a [search phase results processor]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/search-processors/) that runs between the query and fetch phases of search. It intercepts the query phase results and then normalizes and combines the document scores from different query clauses before passing the documents to the fetch phase. -## +## Score normalization and combination + +Many applications require both keyword matching and semantic understanding. For example, BM25 accurately provides relevant search results for a query containing keywords, and neural networks perform well when a query requires natural language understanding. Thus, you might want to combine BM25 search results with the results of k-NN or neural search. However, BM25 and k-NN search use different scales to calculate relevance scores for the matching documents. Before combining the scores from multiple queries, it is necessary to normalize those scores so they are on the same scale. For further reading about score normalization and combination, including benchmarks and discussion of various techniques, see this [semantic search blog](https://opensearch.org/blog/semantic-science-benchmarks/). + +## Flow diagram + +The following flow diagram illustrates search with the `normalization_processor`. ## Request fields @@ -19,20 +25,20 @@ The following table lists all available request fields. Field | Data type | Description :--- | :--- | :--- -`normalization.technique` | String | The technique for normalizing scores. Valid values are `min_max`, `L2`. Optional. Default is `min_max`. -`combination.technique` | String | The technique for combining scores. Valid values are `harmonic_mean`, `arithmetic_mean`, `geometric_mean`. Optional. Default is `arithmetic_mean`. -`combination.parameters.weights` | Array of floating-point values | Specifies the weights to use for each query. Valid values are in the [0.0, 1.0] range and signify decimal percentages. The closer the weight is to 1.0, the more weight is given to a query. The number of values in the `weights` array must equal the number of queries. The sum of the values in the array must equal 1.0. -`tag` | String | The processor's identifier. -`description` | String | A description of the processor. +`normalization.technique` | String | The technique for normalizing scores. Valid values are `min_max`, `L2`. Optional. Default is `min_max`. +`combination.technique` | String | The technique for combining scores. Valid values are `harmonic_mean`, `arithmetic_mean`, `geometric_mean`. Optional. Default is `arithmetic_mean`. +`combination.parameters.weights` | Array of floating-point values | Specifies the weights to use for each query. Valid values are in the [0.0, 1.0] range and signify decimal percentages. The closer the weight is to 1.0, the more weight is given to a query. The number of values in the `weights` array must equal the number of queries. The sum of the values in the array must equal 1.0. Optional. If not provided, all queries are given equal weight. +`tag` | String | The processor's identifier. Optional. +`description` | String | A description of the processor. Optional. `ignore_failure` | Boolean | If `true`, OpenSearch [ignores a failure]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/#ignoring-processor-failures) of this processor and continues to run the remaining processors in the search pipeline. Optional. Default is `false`. ## Example -The following example demonstrates using a search pipeline with a `normalization_processor` processor. +The following example demonstrates using a search pipeline with a `normalization_processor`. ### Creating a search pipeline -The following request creates a search pipeline with a `normalization_processor` that renames the field `message` to `notification`: +The following request creates a search pipeline with a `normalization_processor` that uses the `min_max` normalization technique and the `harmonic_mean` combination technique: ```json PUT /_search/pipeline/my_pipeline @@ -44,7 +50,7 @@ PUT /_search/pipeline/my_pipeline "technique": "min_max", }, "combination": { - "technique" : "harmonic_mean", + "technique" : "arithmetic_mean", "parameters" : { "weights" : [0.4, 0.7] } @@ -87,5 +93,5 @@ POST flicker-index/_search?search_pipeline=normalizationPipeline ``` {% include copy-curl.html %} -Normalization processor does not produce consistent results for a cluster with one node and one shard. +The `normalization_processor` does not produce consistent results for a cluster with one node and one shard. {: .warning} \ No newline at end of file diff --git a/_search-plugins/search-pipelines/search-processors.md b/_search-plugins/search-pipelines/search-processors.md index c32eb4c99a..dbee86e28f 100644 --- a/_search-plugins/search-pipelines/search-processors.md +++ b/_search-plugins/search-pipelines/search-processors.md @@ -39,7 +39,7 @@ Processor | Description | Earliest available version ## Search phase results processors -A search phase results processor runs between search phases at the coordinating node level. It takes the results retrieved from one search phase and transforms them before passing them to the next search phase. +A search phase results processor runs between search phases at the coordinating node level. It takes the results retrieved from one search phase and transforms them before passing them to the next search phase. The following table lists all supported search request processors. From 0ff381fb017c7e17e8da2afe00728ad17e318902 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Thu, 7 Sep 2023 14:18:04 -0400 Subject: [PATCH 06/35] Add more query then fetch details and diagram Signed-off-by: Fanit Kolchina --- _query-dsl/compound/hybrid.md | 8 ++++++-- .../search-pipelines/filter-query-processor.md | 2 +- _search-plugins/search-pipelines/index.md | 8 ++++---- .../search-pipelines/normalization-processor.md | 16 +++++++++++----- .../personalize-search-ranking.md | 2 +- .../search-pipelines/rename-field-processor.md | 2 +- .../search-pipelines/script-processor.md | 2 +- images/normalization-processor.png | Bin 0 -> 55130 bytes 8 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 images/normalization-processor.png diff --git a/_query-dsl/compound/hybrid.md b/_query-dsl/compound/hybrid.md index c79b23bb05..a2fb5e91e7 100644 --- a/_query-dsl/compound/hybrid.md +++ b/_query-dsl/compound/hybrid.md @@ -11,10 +11,12 @@ nav_order: 70 This is an experimental feature and is not recommended for use in a production environment. For updates on the progress of the feature or if you want to leave feedback, see the associated [GitHub issue](https://github.com/opensearch-project/neural-search/issues/244). {: .warning} -Use a hybrid query to combine relevance scores from multiple queries into one score for a given document. A hybrid query contains a list of one or more queries, which are run in parallel at the data node level. Hybrid query calculates document scores at the shard level independently for each subquery. The subquery rewriting is done at the coordinating node level to avoid duplicate computations. +Use a hybrid query to combine relevance scores from multiple queries into one score for a given document. A hybrid query contains a list of one or more queries, which are run in parallel at the data node level. It calculates document scores at the shard level independently for each subquery. The subquery rewriting is done at the coordinating node level to avoid duplicate computations. ## Example +The following example request combines a score from a regular `match` query clause with a score from a `neural` query clause. It uses a [search pipeline]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/) with a [normalization processor]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/script-processor/), which specifies the techniques to normalize and combine query clause relevance scores: + ```json POST flicker-index/_search?search_pipeline=normalizationPipeline { @@ -42,10 +44,12 @@ POST flicker-index/_search?search_pipeline=normalizationPipeline ``` {% include copy-curl.html %} +To learn more about the normalization processor, see [Normalization processor]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/normalization-processor/). + ## Parameters The following table lists all top-level parameters supported by `hybrid` queries. Parameter | Description :--- | :--- -`queries` | An array of one or more query clauses that are used to match documents. A document must match at least one query clause to be returned in the results. If a document matches multiple query clauses, the relevance score is set to the highest relevance score from all matching query clauses. The maximum number of query clauses is 5. Required. \ No newline at end of file +`queries` | An array of one or more query clauses that are used to match documents. A document must match at least one query clause to be returned in the results. The documents' relevance scores from all query clauses are combined into one score by applying a [search pipeline]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/). The maximum number of query clauses is 5. Required. \ No newline at end of file diff --git a/_search-plugins/search-pipelines/filter-query-processor.md b/_search-plugins/search-pipelines/filter-query-processor.md index b358bbb542..1fe396eb20 100644 --- a/_search-plugins/search-pipelines/filter-query-processor.md +++ b/_search-plugins/search-pipelines/filter-query-processor.md @@ -1,6 +1,6 @@ --- layout: default -title: Filter query processor +title: Filter query nav_order: 10 has_children: false parent: Search processors diff --git a/_search-plugins/search-pipelines/index.md b/_search-plugins/search-pipelines/index.md index 6a5bc837ec..b2a8a44e1e 100644 --- a/_search-plugins/search-pipelines/index.md +++ b/_search-plugins/search-pipelines/index.md @@ -14,10 +14,10 @@ You can use _search pipelines_ to build new or reuse existing result rerankers, The following is a list of search pipeline terminology: -* _Search request processor_: A component that takes a search request (the query and the metadata passed in the request), performs an operation with or on the search request, and returns a search request. -* _Search response processor_: A component that takes a search response and search request (the query, results, and metadata passed in the request), performs an operation with or on the search response, and returns a search response. -* _Search phase results processor_: A component that runs between search phases at the coordinating node level. A search phase results processor takes the results retrieved from one search phase and transforms them before passing them to the next search phase. -* _Processor_: Either a search request processor or a search response processor. +* [_Search request processor_]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/search-processors#search-request-processors): A component that takes a search request (the query and the metadata passed in the request), performs an operation with or on the search request, and returns a search request. +* [_Search response processor_]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/search-processors#search-response-processors): A component that takes a search response and search request (the query, results, and metadata passed in the request), performs an operation with or on the search response, and returns a search response. +* [_Search phase results processor_]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/search-processors#search-phase-results-processors): A component that runs between search phases at the coordinating node level. A search phase results processor takes the results retrieved from one search phase and transforms them before passing them to the next search phase. +* [_Processor_]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/search-processors/): Either a search request processor or a search response processor. * _Search pipeline_: An ordered list of processors that is integrated into OpenSearch. The pipeline intercepts a query, performs processing on the query, sends it to OpenSearch, intercepts the results, performs processing on the results, and returns them to the calling application, as shown in the following diagram. ![Search processor diagram]({{site.url}}{{site.baseurl}}/images/search-pipelines.png) diff --git a/_search-plugins/search-pipelines/normalization-processor.md b/_search-plugins/search-pipelines/normalization-processor.md index d731ee6af9..647fceb128 100644 --- a/_search-plugins/search-pipelines/normalization-processor.md +++ b/_search-plugins/search-pipelines/normalization-processor.md @@ -1,6 +1,6 @@ --- layout: default -title: Normalization processor +title: Normalization nav_order: 15 has_children: false parent: Search processors @@ -9,15 +9,19 @@ grand_parent: Search pipelines # Normalization processor -The `normalization_processor` is a [search phase results processor]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/search-processors/) that runs between the query and fetch phases of search. It intercepts the query phase results and then normalizes and combines the document scores from different query clauses before passing the documents to the fetch phase. +The `normalization_processor` is a search phase results processor that runs between the query and fetch phases of search. It intercepts the query phase results and then normalizes and combines the document scores from different query clauses before passing the documents to the fetch phase. ## Score normalization and combination Many applications require both keyword matching and semantic understanding. For example, BM25 accurately provides relevant search results for a query containing keywords, and neural networks perform well when a query requires natural language understanding. Thus, you might want to combine BM25 search results with the results of k-NN or neural search. However, BM25 and k-NN search use different scales to calculate relevance scores for the matching documents. Before combining the scores from multiple queries, it is necessary to normalize those scores so they are on the same scale. For further reading about score normalization and combination, including benchmarks and discussion of various techniques, see this [semantic search blog](https://opensearch.org/blog/semantic-science-benchmarks/). -## Flow diagram +## Query then fetch -The following flow diagram illustrates search with the `normalization_processor`. +OpenSearch supports two search types: `query_then_fetch` and `dfs_query_then_fetch`. The following diagram outlines the query then fetch process that includes a normalization processor. + +![Normalization processor flow diagram]({{site.url}}{{site.baseurl}}/images/normalization-processor.png) + +When you send a search request to a node, this node becomes a _coordinating node_. During the first phase of search, the _query phase_, the coordinating node routes the search request to all shards in the index, including primary and replica shards. Each shard then runs the search query locally and returns metadata about the matching documents, which includes their doc IDs and relevance scores. The `normalization_processor` then normalizes and combines scores from different query clauses. The coordinating node merges and sorts the local result lists, compiling a global list of top documents that match the query. After that, search enters a _fetch phase_, in which the coordinating node requests the documents in the global list from the shards where they reside. Each shard returns the documents' `_source` to the coordinating node. Finally, the coordinating node sends a search response containing the results back to you. ## Request fields @@ -64,7 +68,7 @@ PUT /_search/pipeline/my_pipeline ### Using a search pipeline -Use a `hybrid` query to apply the search pipeline created in the previous section to a search request: +Provide the query clauses that you want to combine in a `hybrid` query and apply the search pipeline created in the previous section so the scores are combined with the chosen techniques: ```json POST flicker-index/_search?search_pipeline=normalizationPipeline @@ -93,5 +97,7 @@ POST flicker-index/_search?search_pipeline=normalizationPipeline ``` {% include copy-curl.html %} +For more information, see [Hybrid query]({{site.url}}{{site.baseurl}}/query-dsl/compound/hybrid/). + The `normalization_processor` does not produce consistent results for a cluster with one node and one shard. {: .warning} \ No newline at end of file diff --git a/_search-plugins/search-pipelines/personalize-search-ranking.md b/_search-plugins/search-pipelines/personalize-search-ranking.md index c008bb155b..64b2ef2017 100644 --- a/_search-plugins/search-pipelines/personalize-search-ranking.md +++ b/_search-plugins/search-pipelines/personalize-search-ranking.md @@ -1,6 +1,6 @@ --- layout: default -title: Personalize search ranking processor +title: Personalize search ranking nav_order: 40 has_children: false parent: Search processors diff --git a/_search-plugins/search-pipelines/rename-field-processor.md b/_search-plugins/search-pipelines/rename-field-processor.md index 47d445f093..5ad8a367dc 100644 --- a/_search-plugins/search-pipelines/rename-field-processor.md +++ b/_search-plugins/search-pipelines/rename-field-processor.md @@ -1,6 +1,6 @@ --- layout: default -title: Rename field processor +title: Rename field nav_order: 20 has_children: false parent: Search processors diff --git a/_search-plugins/search-pipelines/script-processor.md b/_search-plugins/search-pipelines/script-processor.md index 4c25dd490e..f4bc7d43db 100644 --- a/_search-plugins/search-pipelines/script-processor.md +++ b/_search-plugins/search-pipelines/script-processor.md @@ -1,6 +1,6 @@ --- layout: default -title: Script processor +title: Script nav_order: 30 has_children: false parent: Search processors diff --git a/images/normalization-processor.png b/images/normalization-processor.png new file mode 100644 index 0000000000000000000000000000000000000000..dc8b4ef595be1220a31c9435bd9d2539c4398351 GIT binary patch literal 55130 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 literal 0 HcmV?d00001 From 32f7a6ed1e8ce2ad9b8b4022fe684457a6a8be18 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Thu, 7 Sep 2023 14:26:22 -0400 Subject: [PATCH 07/35] Small rewording Signed-off-by: Fanit Kolchina --- _search-plugins/search-pipelines/normalization-processor.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_search-plugins/search-pipelines/normalization-processor.md b/_search-plugins/search-pipelines/normalization-processor.md index 647fceb128..c055414aa0 100644 --- a/_search-plugins/search-pipelines/normalization-processor.md +++ b/_search-plugins/search-pipelines/normalization-processor.md @@ -68,7 +68,7 @@ PUT /_search/pipeline/my_pipeline ### Using a search pipeline -Provide the query clauses that you want to combine in a `hybrid` query and apply the search pipeline created in the previous section so the scores are combined with the chosen techniques: +Provide the query clauses that you want to combine in a `hybrid` query and apply the search pipeline created in the previous section so the scores are combined using the chosen techniques: ```json POST flicker-index/_search?search_pipeline=normalizationPipeline From 8b0bb3dbff9483613fb88ec8b953b34d64afaf0e Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Thu, 7 Sep 2023 14:32:17 -0400 Subject: [PATCH 08/35] Leaner left nav headers Signed-off-by: Fanit Kolchina --- _query-dsl/compound/bool.md | 2 +- _query-dsl/compound/boosting.md | 2 +- _query-dsl/compound/constant-score.md | 2 +- _query-dsl/compound/disjunction-max.md | 2 +- _query-dsl/compound/function-score.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/_query-dsl/compound/bool.md b/_query-dsl/compound/bool.md index 6e29cea382..d5d1794450 100644 --- a/_query-dsl/compound/bool.md +++ b/_query-dsl/compound/bool.md @@ -1,6 +1,6 @@ --- layout: default -title: Boolean queries +title: Boolean parent: Compound queries grand_parent: Query DSL nav_order: 10 diff --git a/_query-dsl/compound/boosting.md b/_query-dsl/compound/boosting.md index 7ccb40177b..d283a0540e 100644 --- a/_query-dsl/compound/boosting.md +++ b/_query-dsl/compound/boosting.md @@ -1,6 +1,6 @@ --- layout: default -title: Boosting queries +title: Boosting parent: Compound queries grand_parent: Query DSL nav_order: 30 diff --git a/_query-dsl/compound/constant-score.md b/_query-dsl/compound/constant-score.md index 63906e079e..15279ef50a 100644 --- a/_query-dsl/compound/constant-score.md +++ b/_query-dsl/compound/constant-score.md @@ -1,6 +1,6 @@ --- layout: default -title: Constant score queries +title: Constant score parent: Compound queries grand_parent: Query DSL nav_order: 40 diff --git a/_query-dsl/compound/disjunction-max.md b/_query-dsl/compound/disjunction-max.md index e03b8d0fb4..b6397c173c 100644 --- a/_query-dsl/compound/disjunction-max.md +++ b/_query-dsl/compound/disjunction-max.md @@ -1,6 +1,6 @@ --- layout: default -title: Disjunction max queries +title: Disjunction max parent: Compound queries grand_parent: Query DSL nav_order: 50 diff --git a/_query-dsl/compound/function-score.md b/_query-dsl/compound/function-score.md index e030f0475e..f9f7300371 100644 --- a/_query-dsl/compound/function-score.md +++ b/_query-dsl/compound/function-score.md @@ -1,6 +1,6 @@ --- layout: default -title: Function score queries +title: Function score parent: Compound queries grand_parent: Query DSL nav_order: 60 From 76e5164a124c584fe41919e5f20530feccba5646 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Thu, 7 Sep 2023 16:59:39 -0400 Subject: [PATCH 09/35] Tech review feedback Signed-off-by: Fanit Kolchina --- _query-dsl/compound/hybrid.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/_query-dsl/compound/hybrid.md b/_query-dsl/compound/hybrid.md index a2fb5e91e7..bfb9f7398e 100644 --- a/_query-dsl/compound/hybrid.md +++ b/_query-dsl/compound/hybrid.md @@ -8,10 +8,7 @@ nav_order: 70 # Hybrid query -This is an experimental feature and is not recommended for use in a production environment. For updates on the progress of the feature or if you want to leave feedback, see the associated [GitHub issue](https://github.com/opensearch-project/neural-search/issues/244). -{: .warning} - -Use a hybrid query to combine relevance scores from multiple queries into one score for a given document. A hybrid query contains a list of one or more queries, which are run in parallel at the data node level. It calculates document scores at the shard level independently for each subquery. The subquery rewriting is done at the coordinating node level to avoid duplicate computations. +Use a hybrid query to combine relevance scores from multiple queries into one score for a given document. A hybrid query contains a list of one or more queries and calculates document scores at the shard level independently for each subquery. The subquery rewriting is done at the coordinating node level to avoid duplicate computations. ## Example From 2fe3464a6e2a7681b7205c2c52e5eb85dc3d4587 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Sun, 10 Sep 2023 15:20:22 -0400 Subject: [PATCH 10/35] Add semantic search tutorial Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/cluster-settings.md | 2 +- _ml-commons-plugin/model-access-control.md | 2 +- _ml-commons-plugin/semantic-search.md | 1045 +++++++++++++++++ _query-dsl/compound/hybrid.md | 39 +- .../normalization-processor.md | 54 +- 5 files changed, 1085 insertions(+), 57 deletions(-) create mode 100644 _ml-commons-plugin/semantic-search.md diff --git a/_ml-commons-plugin/cluster-settings.md b/_ml-commons-plugin/cluster-settings.md index b5c120acb3..b60f96ecf9 100644 --- a/_ml-commons-plugin/cluster-settings.md +++ b/_ml-commons-plugin/cluster-settings.md @@ -2,7 +2,7 @@ layout: default title: ML Commons cluster settings has_children: false -nav_order: 160 +nav_order: 10 --- # ML Commons cluster settings diff --git a/_ml-commons-plugin/model-access-control.md b/_ml-commons-plugin/model-access-control.md index 19fd146293..762f83d543 100644 --- a/_ml-commons-plugin/model-access-control.md +++ b/_ml-commons-plugin/model-access-control.md @@ -2,7 +2,7 @@ layout: default title: Model access control has_children: false -nav_order: 180 +nav_order: 20 --- # Model access control diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md new file mode 100644 index 0000000000..3d57a64041 --- /dev/null +++ b/_ml-commons-plugin/semantic-search.md @@ -0,0 +1,1045 @@ +--- +layout: default +title: Semantic search +has_children: false +nav_order: 140 +--- + +# Semantic search + +By default, OpenSearch calculates document scores using the [Okapi BM25](https://en.wikipedia.org/wiki/Okapi_BM25) algorithm. BM25 is a keyword-based algorithm that performs well on a query containing keywords but fails to capture the semantic meaning of the query terms. Semantic search, unlike keyword-based search, takes into account the meaning of the query in the search context. Thus, semantic search performs well when a query requires natural language understanding. + +In this tutorial, you'll learn how to: + +- Implement semantic search in OpenSearch. +- Combine semantic search with keyword search to improve search relevance. + +## Terminology + +It's helpful to understand the following terms before starting this tutorial: + +- _Semantic search_: Employs neural search in order to determine the intention of the user's query in the search context and improve search relevance. +- _Neural search_: When indexing documents containing text, neural search uses language models to generate vector embeddings from that text. When you then use a _neural query_, the query text is passed through a language model and the resulting vector embeddings are compared with the document text vector embeddings to find the most relevant results. + +## OpenSearch components for semantic search + +For this tutorial, you'll implement semantic search using the following OpenSearch components: + +- [Model group]({{site.url}}{{site.baseurl}}/ml-commons-plugin/model-access-control#model-groups) +- [Pretrained language models provided by OpenSearch]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/) +- [Ingest pipeline]({{site.url}}{{site.baseurl}}/api-reference/ingest-apis/index/) +- [k-NN vector]({{site.url}}{{site.baseurl}}/field-types/supported-field-types/knn-vector/) +- [Neural search]({{site.url}}{{site.baseurl}}/search-plugins/neural-search/) +- [Search pipeline]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/) +- [Normalization processor]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/normalization-processor/) +- [Hybrid query]({{site.url}}{{site.baseurl}}/query-dsl/compound/hybrid/) + +You'll find the explanations of all these components as you follow the tutorial, so don't worry if you're not familiar with some of them. Each link in the preceding list will take you to a corresponding documentation section if you want to learn more. + +## Prerequisites + +Before you get started, make sure to update the default [cluster settings]({{site.url}}{{site.baseurl}}/api-reference/cluster-api/cluster-settings/) to the following: + +```json +PUT _cluster/settings +{ + "persistent": { + "plugins": { + "ml_commons": { + "only_run_on_ml_node": "false", + "model_access_control_enabled": "true", + "native_memory_threshold": "99", + "allow_registering_model_via_url": "true" + } + } + } +} +``` +{% include copy-curl.html %} + +For more information about machine learning-related cluster settings, see [ML Commons cluster settings]({{site.url}}{{site.baseurl}}/ml-commons-plugin/cluster-settings/). + +## Tutorial overview + +This tutorial consists of the following steps: + +1. [**Set up a machine learning (ML) language model**](#step-1-set-up-an-ml-language-model) + 1. [Choose a language model](#step-1a-choose-a-language-model) + 1. [Register a model group](#step-1b-register-a-model-group) + 1. [Register the model to the model group](#step-1c-register-the-model-to-the-model-group) + 1. [Deploy the model](#step-1d-deploy-the-model) +1. [**Ingest data with neural search**](#step-2-ingest-data-with-neural-search) + 1. [Create an ingest pipeline for neural search](#step-2a-create-an-ingest-pipeline-for-neural-search) + 1. [Create a k-NN index](#step-2b-create-a-k-nn-index) + 1. [Ingest documents into the index](#step-2c-ingest-documents-into-the-index) +1. [**Search the data**](#step-3-search-the-data) + - [Search with a keyword search](#search-with-a-keyword-search) + - [Search with a neural search](#search-with-a-neural-search) + - [Search with a combined keyword search and neural search](#search-with-a-combined-keyword-search-and-neural-search) + +Some steps in the tutorial contain optional `Test it` sections. You can ensure the step worked by running requests in these sections. + +After you're done, follow the steps in the [Clean up](#clean-up) section to delete all created components. + +## Tutorial + +You can follow this tutorial using your command line or the OpenSearch Dashboards [Dev Tools console]({{site.url}}{{site.baseurl}}/dashboards/dev-tools/run-queries/). + +## Step 1: Set up an ML language model + +Neural search requires a language model to generate vector embeddings from text fields both at ingestion time and query time. + +### Step 1(a): Choose a language model + +For this tutorial, you'll use the [DistilBERT](https://huggingface.co/docs/transformers/model_doc/distilbert) model from Hugging Face. It is one of the pretrained sentence transformer models available in OpenSearch. You'll need the name, version, and dimension of the model to register it. You can find this information in the [pretrained model table]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/#sentence-transformers) by selecting the `config_url` link for the TorchScript artifact of the model: + +- The model name is `huggingface/sentence-transformers/msmarco-distilbert-base-tas-b`. +- The model version is `1.0.1`. +- The number of dimensions for this model is `768`. + +#### Advanced: Using a different model + +You can choose to use another model: + +- One of the [pretrained language models provided by OpenSearch]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/). +- Your own model. For instructions how to set up a custom model, see [Model serving framework]({{site.url}}{{site.baseurl}}/ml-commons-plugin/ml-framework/). + +Take note of the dimensionality of the model because you'll need it when you set up a k-NN index. +{: .important} + +### Step 1(b): Register a model group + +For access control, models are organized into model groups—collections of versions of a particular model. Each model group name in the cluster must be globally unique. Registering a model group ensures the uniqueness of the model group name. + +If you are registering the first version of a model without first registering the model group, a new model group is created automatically. For more information, see [Model access control]({{site.url}}{{site.baseurl}}/ml-commons-plugin/model-access-control/). +{: .tip} + +To register a model group with a `public` access level, send the following request: + +```json +POST /_plugins/_ml/model_groups/_register +{ + "name": "NLP_model_group", + "description": "A model group for NLP models", + "access_mode": "public" +} +``` +{% include copy-curl.html %} + +OpenSearch sends back the model group ID: + +```json +{ + "model_group_id": "Z1eQf4oB5Vm0Tdw8EIP2", + "status": "CREATED" +} +``` + +You'll use this ID to register the chosen model to the model group. + +
+ + Test it + + {: .text-delta} + +Search for the newly created model group by providing its model group ID in the request: + +```json +POST /_plugins/_ml/model_groups/_search +{ + "query": { + "match": { + "_id": "Z1eQf4oB5Vm0Tdw8EIP2" + } + } +} +``` +{% include copy-curl.html %} + +The response contains the model group: + +```json +{ + "took": 0, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 1, + "hits": [ + { + "_index": ".plugins-ml-model-group", + "_id": "Z1eQf4oB5Vm0Tdw8EIP2", + "_version": 1, + "_seq_no": 14, + "_primary_term": 2, + "_score": 1, + "_source": { + "created_time": 1694357262582, + "access": "public", + "latest_version": 0, + "last_updated_time": 1694357262582, + "name": "NLP_model_group", + "description": "A model group for NLP models" + } + } + ] + } +} +``` +
+ + +### Step 1(c): Register the model to the model group + +The DistilBERT model you'll use To register the model to the model group, provide the model group ID in the register request: + +```json +POST /_plugins/_ml/models/_register +{ + "name": "huggingface/sentence-transformers/msmarco-distilbert-base-tas-b", + "version": "1.0.1", + "model_group_id": "Z1eQf4oB5Vm0Tdw8EIP2", + "model_format": "TORCH_SCRIPT" +} +``` +{% include copy-curl.html %} + +Registering a model is an asynchronous task. OpenSearch sends back a task ID for this task: + +```json +{ + "task_id": "aFeif4oB5Vm0Tdw8yoN7", + "status": "CREATED" +} +``` + +OpenSearch downloads the config file for the model and the model contents from the URL. Because the model is larger than 10 MB in size, OpenSearch splits it into chunks of up to 10 MB and saves those chunks in the model index. You can check the status of the task using the Tasks API: + +```json +GET /_plugins/_ml/tasks/aFeif4oB5Vm0Tdw8yoN7 +``` +{% include copy-curl.html %} + +Once the task is complete, the task status will be `COMPLETED` and the Tasks API response will contain a model ID for the registered model: + +```json +{ + "model_id": "aVeif4oB5Vm0Tdw8zYO2", + "task_type": "DEPLOY_MODEL", + "function_name": "TEXT_EMBEDDING", + "state": "COMPLETED", + "worker_node": [ + "4p6FVOmJRtu3wehDD74hzQ" + ], + "create_time": 1694358489722, + "last_update_time": 1694358499139, + "is_async": true +} +``` + +You'll need the model ID in order to use this model for several following steps. + +
+ + Test it + + {: .text-delta} + +Search for the newly created model by providing its ID in the request: + +```json +POST /_plugins/_ml/models/_search +{ + "query": { + "match": { + "_id": "aVeif4oB5Vm0Tdw8zYO2" + } + } +} +``` +{% include copy-curl.html %} + +The response contains the model: + +```json +{ + "took": 1, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 1, + "hits": [ + { + "_index": ".plugins-ml-model", + "_id": "aVeif4oB5Vm0Tdw8zYO2", + "_version": 2, + "_seq_no": 95, + "_primary_term": 2, + "_score": 1, + "_source": { + "model_version": "1", + "created_time": 1694358490550, + "model_format": "TORCH_SCRIPT", + "model_state": "REGISTERED", + "total_chunks": 27, + "model_content_hash_value": "acdc81b652b83121f914c5912ae27c0fca8fabf270e6f191ace6979a19830413", + "model_config": { + "all_config": """{"_name_or_path":"old_models/msmarco-distilbert-base-tas-b/0_Transformer","activation":"gelu","architectures":["DistilBertModel"],"attention_dropout":0.1,"dim":768,"dropout":0.1,"hidden_dim":3072,"initializer_range":0.02,"max_position_embeddings":512,"model_type":"distilbert","n_heads":12,"n_layers":6,"pad_token_id":0,"qa_dropout":0.1,"seq_classif_dropout":0.2,"sinusoidal_pos_embds":false,"tie_weights_":true,"transformers_version":"4.7.0","vocab_size":30522}""", + "model_type": "distilbert", + "embedding_dimension": 768, + "framework_type": "SENTENCE_TRANSFORMERS" + }, + "last_updated_time": 1694358499122, + "last_registered_time": 1694358499121, + "name": "huggingface/sentence-transformers/msmarco-distilbert-base-tas-b", + "model_group_id": "Z1eQf4oB5Vm0Tdw8EIP2", + "model_content_size_in_bytes": 266352827, + "algorithm": "TEXT_EMBEDDING" + } + } + ] + } +} +``` + +The response contains the model information, including the `model_state` (`REGISTERED`) and the number of chunks it was split into `total_chunks` (27). +
+ +#### Advanced: Registering a custom model + +To register a custom model, you must provide a model configuration in the register request. For example, the following is a register request with the full format for the model used in this tutorial: + +```json +POST /_plugins/_ml/models/_register +{ + "name": "huggingface/sentence-transformers/msmarco-distilbert-base-tas-b", + "version": "1.0.1", + "model_group_id": "Z1eQf4oB5Vm0Tdw8EIP2", + "description": "This is a port of the DistilBert TAS-B Model to sentence-transformers model: It maps sentences & paragraphs to a 768 dimensional dense vector space and is optimized for the task of semantic search.", + "model_task_type": "TEXT_EMBEDDING", + "model_format": "TORCH_SCRIPT", + "model_content_size_in_bytes": 266352827, + "model_content_hash_value": "acdc81b652b83121f914c5912ae27c0fca8fabf270e6f191ace6979a19830413", + "model_config": { + "model_type": "distilbert", + "embedding_dimension": 768, + "framework_type": "sentence_transformers", + "all_config": """{"_name_or_path":"old_models/msmarco-distilbert-base-tas-b/0_Transformer","activation":"gelu","architectures":["DistilBertModel"],"attention_dropout":0.1,"dim":768,"dropout":0.1,"hidden_dim":3072,"initializer_range":0.02,"max_position_embeddings":512,"model_type":"distilbert","n_heads":12,"n_layers":6,"pad_token_id":0,"qa_dropout":0.1,"seq_classif_dropout":0.2,"sinusoidal_pos_embds":false,"tie_weights_":true,"transformers_version":"4.7.0","vocab_size":30522}""" + }, + "created_time": 1676073973126 +} +``` + +For more information, see [Model serving framework]({{site.url}}{{site.baseurl}}/ml-commons-plugin/ml-framework/). + +### Step 1(d): Deploy the model + +Once the model is registered, it is saved in the model index. Next, you'll need to deploy the model: create a model instance, caching the model in memory. To deploy the model, provide its model ID to the `_deploy` endpoint: + +```json +POST /_plugins/_ml/models/aVeif4oB5Vm0Tdw8zYO2/_deploy +``` +{% include copy-curl.html %} + +Similarly to the register operation, the deploy operation is asynchronous so you'll get a task ID in the response: + +```json +{ + "task_id": "ale6f4oB5Vm0Tdw8NINO", + "status": "CREATED" +} +``` + +You can check the status of the task using the Tasks API: + +```json +GET /_plugins/_ml/tasks/ale6f4oB5Vm0Tdw8NINO +``` +{% include copy-curl.html %} + +Once the task is complete, the task status will be `COMPLETED`: + +```json +{ + "model_id": "aVeif4oB5Vm0Tdw8zYO2", + "task_type": "DEPLOY_MODEL", + "function_name": "TEXT_EMBEDDING", + "state": "COMPLETED", + "worker_node": [ + "4p6FVOmJRtu3wehDD74hzQ" + ], + "create_time": 1694360024141, + "last_update_time": 1694360027940, + "is_async": true +} +``` + +
+ + Test it + + {: .text-delta} + +Search for the deployed model by providing its ID in the request: + +```json +POST /_plugins/_ml/models/_search +{ + "query": { + "match": { + "_id": "aVeif4oB5Vm0Tdw8zYO2" + } + } +} +``` +{% include copy-curl.html %} + +The response shows the model status as `DEPLOYED`: + +```json +{ + "took": 0, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 1, + "hits": [ + { + "_index": ".plugins-ml-model", + "_id": "aVeif4oB5Vm0Tdw8zYO2", + "_version": 4, + "_seq_no": 97, + "_primary_term": 2, + "_score": 1, + "_source": { + "last_deployed_time": 1694360027940, + "model_version": "1", + "created_time": 1694358490550, + "deploy_to_all_nodes": true, + "model_format": "TORCH_SCRIPT", + "model_state": "DEPLOYED", + "planning_worker_node_count": 1, + "total_chunks": 27, + "model_content_hash_value": "acdc81b652b83121f914c5912ae27c0fca8fabf270e6f191ace6979a19830413", + "model_config": { + "all_config": """{"_name_or_path":"old_models/msmarco-distilbert-base-tas-b/0_Transformer","activation":"gelu","architectures":["DistilBertModel"],"attention_dropout":0.1,"dim":768,"dropout":0.1,"hidden_dim":3072,"initializer_range":0.02,"max_position_embeddings":512,"model_type":"distilbert","n_heads":12,"n_layers":6,"pad_token_id":0,"qa_dropout":0.1,"seq_classif_dropout":0.2,"sinusoidal_pos_embds":false,"tie_weights_":true,"transformers_version":"4.7.0","vocab_size":30522}""", + "model_type": "distilbert", + "embedding_dimension": 768, + "framework_type": "SENTENCE_TRANSFORMERS" + }, + "last_updated_time": 1694360027940, + "last_registered_time": 1694358499121, + "name": "huggingface/sentence-transformers/msmarco-distilbert-base-tas-b", + "current_worker_node_count": 1, + "model_group_id": "Z1eQf4oB5Vm0Tdw8EIP2", + "model_content_size_in_bytes": 266352827, + "planning_worker_nodes": [ + "4p6FVOmJRtu3wehDD74hzQ" + ], + "algorithm": "TEXT_EMBEDDING" + } + } + ] + } +} +``` + +You can also receive statistics for all deployed models in your cluster sending a Models Profile API request: + +```json +GET /_plugins/_ml/profile/models +``` +
+ +## Step 2: Ingest data with neural search + +Neural search uses a language model to transform text into vector embeddings. During ingestion, neural search creates vector embeddings for the text fields in the request. During search, you can use the same model on the query text to perform vector similarity search on the documents. + +### Step 2(a): Create an ingest pipeline for neural search + +The first step in setting up [neural search]({{site.url}}{{site.baseurl}}/search-plugins/neural-search/) is to create an [ingest pipeline]({{site.url}}{{site.baseurl}}/api-reference/ingest-apis/index/). The ingest pipeline will contain one processor: a task that transforms document fields. For neural search, you'll need to set up a `text_embedding` processor that takes in text and creates vector embeddings from that text. You'll need a `model_id` of the model you set up in the previous section and a `field_map`, which specifies the name of the field to take the text from (`text`) and the name of the field to record embeddings in (`passage_embedding`): + +```json +PUT /_ingest/pipeline/nlp-ingest-pipeline +{ + "description": "An NLP ingest pipeline", + "processors": [ + { + "text_embedding": { + "model_id": "aVeif4oB5Vm0Tdw8zYO2", + "field_map": { + "text": "passage_embedding" + } + } + } + ] +} +``` +{% include copy-curl.html %} + +
+ + Test it + + {: .text-delta} + +Search for the created ingest pipeline by using the Ingest API: + +```json +GET /_ingest/pipeline +``` +{% include copy-curl.html %} + +The response contains the ingest pipeline: + +```json +{ + "nlp-ingest-pipeline": { + "description": "An NLP ingest pipeline", + "processors": [ + { + "text_embedding": { + "model_id": "aVeif4oB5Vm0Tdw8zYO2", + "field_map": { + "text": "passage_embedding" + } + } + } + ] + } +} +``` +
+ +### Step 2(b): Create a k-NN index + +Now you'll create a k-NN index with a field called `text` that holds an image description and a [`knn_vector`]({{site.url}}{{site.baseurl}}/field-types/supported-field-types/knn-vector/) field called `passage_embedding` that holds the vector embedding of the text. Additionally, set the default ingest pipeline to the `nlp-ingest-pipeline` you created in the previous step: + + +```json +PUT /my-nlp-index +{ + "settings": { + "index.knn": true, + "default_pipeline": "nlp-ingest-pipeline" + }, + "mappings": { + "properties": { + "id": { + "type": "text" + }, + "passage_embedding": { + "type": "knn_vector", + "dimension": 768, + "method": { + "engine": "lucene", + "space_type": "l2", + "name": "hnsw", + "parameters": {} + } + }, + "text": { + "type": "text" + } + } + } +} +``` +{% include copy-curl.html %} + +Setting up a k-NN index allows to later perform a vector search on the `passage_embedding` field. + +
+ + Test it + + {: .text-delta} + +Use the following requests to get the settings and the mappings of the created index: + +```json +GET /my-nlp-index/_settings +``` +{% include copy-curl.html %} + +```json +GET /my-nlp-index/_mappings +``` +{% include copy-curl.html %} + +
+ +### Step 2(c): Ingest documents into the index + +In this step, you'll ingest several sample documents into the index. The sample data is taken from the [Flickr image dataset](https://www.kaggle.com/datasets/hsankesara/flickr-image-dataset). Each document contains a `text` field that corresponds to the image description and an `id` field that corresponds to the image ID: + +```json +PUT /my-nlp-index/_doc/1 +{ + "text": "A West Highland Terrier runs across the dirt .", + "id": "2714220101.jpg" +} +``` +{% include copy-curl.html %} + +```json +PUT /my-nlp-index/_doc/2 +{ + "text": "A West Virginia university women 's basketball team , officials , and a small gathering of fans are in a West Virginia arena .", + "id": "4319130149.jpg" +} +``` +{% include copy-curl.html %} + +```json +PUT /my-nlp-index/_doc/3 +{ + "text": "An older , seated woman with wild gray hair has makeup applied by a younger woman with equally wild , but blond-dyed hair .", + "id": "6813821371.jpg" +} +``` +{% include copy-curl.html %} + +```json +PUT /my-nlp-index/_doc/4 +{ + "text": "A man who is riding a wild horse in the rodeo is very near to falling off .", + "id": "4427058951.jpg" +} +``` +{% include copy-curl.html %} + +```json +PUT /my-nlp-index/_doc/5 +{ + "text": "A rodeo cowboy , wearing a cowboy hat , is being thrown off of a wild white horse .", + "id": "2691147709.jpg" +} +``` +{% include copy-curl.html %} + +## Step 3: Search the data + +Now you'll search the index using keyword search, neural search, and a combination of the two. + +### Search with a keyword search + +To search with keyword search, use a `match` query. You'll exclude embeddings from the results: + +```json +GET /my-nlp-index/_search +{ + "_source": { + "excludes": [ + "passage_embedding" + ] + }, + "query": { + "match": { + "text": { + "query": "wild west" + } + } + } +} +``` +{% include copy-curl.html %} + +Documents containing the words `rodeo` and `cowboy` are scored lower because semantic meaning is not considered: + +
+ + Results + + {: .text-delta} + +```json +{ + "took": 1, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 5, + "relation": "eq" + }, + "max_score": 1.7723373, + "hits": [ + { + "_index": "my-nlp-index", + "_id": "2", + "_score": 1.7723373, + "_source": { + "text": "A West Virginia university women 's basketball team , officials , and a small gathering of fans are in a West Virginia arena .", + "id": "4319130149.jpg" + } + }, + { + "_index": "my-nlp-index", + "_id": "1", + "_score": 1.7579391, + "_source": { + "text": "A West Highland Terrier runs across the dirt .", + "id": "2714220101.jpg" + } + }, + { + "_index": "my-nlp-index", + "_id": "3", + "_score": 0.54217947, + "_source": { + "text": "An older , seated woman with wild gray hair has makeup applied by a younger woman with equally wild , but blond-dyed hair .", + "id": "6813821371.jpg" + } + }, + { + "_index": "my-nlp-index", + "_id": "5", + "_score": 0.43677896, + "_source": { + "text": "A rodeo cowboy , wearing a cowboy hat , is being thrown off of a wild white horse .", + "id": "2691147709.jpg" + } + }, + { + "_index": "my-nlp-index", + "_id": "4", + "_score": 0.4261033, + "_source": { + "text": "A man who is riding a wild horse in the rodeo is very near to falling off .", + "id": "4427058951.jpg" + } + } + ] + } +} +``` +
+ +### Search with a neural search + +To search with neural search, use a `neural` query and provide the model ID of the model set up earlier so that vector embeddings for the query text are generated with the model used at ingestion time: + +```json +GET /my-nlp-index/_search +{ + "_source": { + "excludes": [ + "passage_embedding" + ] + }, + "query": { + "neural": { + "passage_embedding": { + "query_text": "wild west", + "model_id": "aVeif4oB5Vm0Tdw8zYO2", + "k": 5 + } + } + } +} +``` +{% include copy-curl.html %} + +Documents containing the words `rodeo` and `cowboy` are now scored higher: + +
+ + Results + + {: .text-delta} + +```json +{ + "took": 27, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 5, + "relation": "eq" + }, + "max_score": 0.016028818, + "hits": [ + { + "_index": "my-nlp-index", + "_id": "3", + "_score": 0.016028818, + "_source": { + "text": "An older , seated woman with wild gray hair has makeup applied by a younger woman with equally wild , but blond-dyed hair .", + "id": "6813821371.jpg" + } + }, + { + "_index": "my-nlp-index", + "_id": "4", + "_score": 0.01585195, + "_source": { + "text": "A man who is riding a wild horse in the rodeo is very near to falling off .", + "id": "4427058951.jpg" + } + }, + { + "_index": "my-nlp-index", + "_id": "1", + "_score": 0.015625207, + "_source": { + "text": "A West Highland Terrier runs across the dirt .", + "id": "2714220101.jpg" + } + }, + { + "_index": "my-nlp-index", + "_id": "5", + "_score": 0.015177963, + "_source": { + "text": "A rodeo cowboy , wearing a cowboy hat , is being thrown off of a wild white horse .", + "id": "2691147709.jpg" + } + }, + { + "_index": "my-nlp-index", + "_id": "2", + "_score": 0.013272902, + "_source": { + "text": "A West Virginia university women 's basketball team , officials , and a small gathering of fans are in a West Virginia arena .", + "id": "4319130149.jpg" + } + } + ] + } +} +``` +
+ +### Search with a combined keyword search and neural search + +To combine keyword search and neural search, you need to set up a [search pipeline]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/) that is similar to the ingest pipeline but runs at search time instead of ingestion time. The search pipeline you'll configure intercepts search results at an intermediate stage and applies the [`normalization_processor`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/normalization-processor/) to them. The `normalization_processor` normalizes and combines the document scores from multiple query clauses, rescoring the documents according to the chosen normalization and combination techniques. + +#### Step 1: Configure a search pipeline + +To configure a search pipeline with a `normalization_processor`, use the following request. The normalization technique in the processor is set to `min_max` and the combination technique is set to `arithmetic_mean`. The `weights` array specifies the weights to assign each query clause as decimal percentages: + +```json +PUT /_search/pipeline/nlp-search-pipeline +{ + "description": "Post processor for hybrid search", + "phase_results_processors": [ + { + "normalization-processor": { + "normalization": { + "technique": "min_max" + }, + "combination": { + "technique": "arithmetic_mean", + "weights" : [0.3, 0.7] + } + } + } + ] +} +``` +{% include copy-curl.html %} + +#### Step 2: Search with the hybrid query + +The query you'll use to combine the `match` and `neural` query clauses is the [`hybrid` query]({{site.url}}{{site.baseurl}}/query-dsl/compound/hybrid/). Make sure to apply the previously created `nlp-search-pipeline` to the request in the query parameter: + +```json +GET /my-nlp-index/_search?search_pipeline=nlp-search-pipeline +{ + "_source": { + "exclude": [ + "passage_embedding" + ] + }, + "query": { + "hybrid": { + "queries": [ + { + "match": { + "text": { + "query": "horse" + } + } + }, + { + "neural": { + "passage_embedding": { + "query_text": "wild west", + "model_id": "aVeif4oB5Vm0Tdw8zYO2", + "k": 5 + } + } + } + ] + } + } +} +``` +{% include copy-curl.html %} + +OpenSearch returns documents that match the semantic meaning of the phrase `wild west` and the documents that contain the word `horse` are scored higher: + +
+ + Results + + {: .text-delta} + +```json +{ + "took": 27, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 5, + "relation": "eq" + }, + "max_score": 0.84563124, + "hits": [ + { + "_index": "my-nlp-index", + "_id": "5", + "_score": 0.84563124, + "_source": { + "text": "A rodeo cowboy , wearing a cowboy hat , is being thrown off of a wild white horse .", + "id": "2691147709.jpg" + } + }, + { + "_index": "my-nlp-index", + "_id": "3", + "_score": 0.5, + "_source": { + "text": "An older , seated woman with wild gray hair has makeup applied by a younger woman with equally wild , but blond-dyed hair .", + "id": "6813821371.jpg" + } + }, + { + "_index": "my-nlp-index", + "_id": "4", + "_score": 0.4684113, + "_source": { + "text": "A man who is riding a wild horse in the rodeo is very near to falling off .", + "id": "4427058951.jpg" + } + }, + { + "_index": "my-nlp-index", + "_id": "1", + "_score": 0.4267737, + "_source": { + "text": "A West Highland Terrier runs across the dirt .", + "id": "2714220101.jpg" + } + }, + { + "_index": "my-nlp-index", + "_id": "2", + "_score": 0.0005, + "_source": { + "text": "A West Virginia university women 's basketball team , officials , and a small gathering of fans are in a West Virginia arena .", + "id": "4319130149.jpg" + } + } + ] + } +} +``` +
+ +Instead of specifying the search pipeline with every request, you can set it as a default search pipeline for the index as follows: + +```json +PUT /my-nlp-index/_settings +{ + "index.search.default_pipeline" : "nlp-search-pipeline" +} +``` +{% include copy-curl.html %} + +You can now experiment with different weights, normalization techniques, and combination techniques. For more information, see [`normalization_processor`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/normalization-processor/) and [`hybrid` query]({{site.url}}{{site.baseurl}}/query-dsl/compound/hybrid/) documentation. + +### Clean up + +After you're done, delete the components you've created from the cluster: + +```json +DELETE /my-nlp-index +``` +{% include copy-curl.html %} + +```json +DELETE /_search/pipeline/nlp-search-pipeline +``` +{% include copy-curl.html %} + +```json +DELETE /_ingest/pipeline/nlp-ingest-pipeline +``` +{% include copy-curl.html %} + +```json +POST /_plugins/_ml/models/aVeif4oB5Vm0Tdw8zYO2/_undeploy +``` +{% include copy-curl.html %} + +```json +DELETE /_plugins/_ml/models/aVeif4oB5Vm0Tdw8zYO2 +``` +{% include copy-curl.html %} + +```json +DELETE /_plugins/_ml/model_groups/Z1eQf4oB5Vm0Tdw8EIP2 +``` +{% include copy-curl.html %} + +## Further reading + +- Read about the basics of semantic search in OpenSearch in [Building a semantic search engine in OpenSearch](https://opensearch.org/blog/semantic-search-solutions/) +- Read about the benefits of combining keyword and neural search, the normalization and combination technique options, and the benchmarking tests in [The ABCs of semantic search in OpenSearch: Architectures, benchmarks, and combination strategies](https://opensearch.org/blog/semantic-science-benchmarks/) \ No newline at end of file diff --git a/_query-dsl/compound/hybrid.md b/_query-dsl/compound/hybrid.md index bfb9f7398e..f6392f1dd8 100644 --- a/_query-dsl/compound/hybrid.md +++ b/_query-dsl/compound/hybrid.md @@ -12,36 +12,9 @@ Use a hybrid query to combine relevance scores from multiple queries into one sc ## Example -The following example request combines a score from a regular `match` query clause with a score from a `neural` query clause. It uses a [search pipeline]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/) with a [normalization processor]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/script-processor/), which specifies the techniques to normalize and combine query clause relevance scores: - -```json -POST flicker-index/_search?search_pipeline=normalizationPipeline -{ - "query": { - "hybrid": { - "queries": [ - { - "neural": { - "passage_embedding": { - "query_text": "Girl with Brown Hair", - "model_id": "ABCBMODELID", - "k": 20 - } - } - }, - { - "match": { - "passage_text": "Girl Brown hair" - } - } - ] - } - } -} -``` -{% include copy-curl.html %} - -To learn more about the normalization processor, see [Normalization processor]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/normalization-processor/). +Before using a `hybrid` query, you must configure a search pipeline with a [`normalization_processor`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/normalization-processor/) (see [this example]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/normalization-processor#example)). + +To try out the example, follow the [Semantic search tutorial]({{site.url}}{{site.baseurl}}/ml-commons-plugin/semantic-search#tutorial). ## Parameters @@ -49,4 +22,8 @@ The following table lists all top-level parameters supported by `hybrid` queries Parameter | Description :--- | :--- -`queries` | An array of one or more query clauses that are used to match documents. A document must match at least one query clause to be returned in the results. The documents' relevance scores from all query clauses are combined into one score by applying a [search pipeline]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/). The maximum number of query clauses is 5. Required. \ No newline at end of file +`queries` | An array of one or more query clauses that are used to match documents. A document must match at least one query clause to be returned in the results. The documents' relevance scores from all query clauses are combined into one score by applying a [search pipeline]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/). The maximum number of query clauses is 5. Required. + +## Disabling hybrid queries + +By default, hybrid queries are enabled. To disable hybrid queries in your cluster, set the `plugins.neural_search.hybrid_search_disabled` setting to `true` in `opensearch.yml`. \ No newline at end of file diff --git a/_search-plugins/search-pipelines/normalization-processor.md b/_search-plugins/search-pipelines/normalization-processor.md index c055414aa0..03883325af 100644 --- a/_search-plugins/search-pipelines/normalization-processor.md +++ b/_search-plugins/search-pipelines/normalization-processor.md @@ -13,7 +13,7 @@ The `normalization_processor` is a search phase results processor that runs betw ## Score normalization and combination -Many applications require both keyword matching and semantic understanding. For example, BM25 accurately provides relevant search results for a query containing keywords, and neural networks perform well when a query requires natural language understanding. Thus, you might want to combine BM25 search results with the results of k-NN or neural search. However, BM25 and k-NN search use different scales to calculate relevance scores for the matching documents. Before combining the scores from multiple queries, it is necessary to normalize those scores so they are on the same scale. For further reading about score normalization and combination, including benchmarks and discussion of various techniques, see this [semantic search blog](https://opensearch.org/blog/semantic-science-benchmarks/). +Many applications require both keyword matching and semantic understanding. For example, BM25 accurately provides relevant search results for a query containing keywords, and neural networks perform well when a query requires natural language understanding. Thus, you might want to combine BM25 search results with the results of k-NN or neural search. However, BM25 and k-NN search use different scales to calculate relevance scores for the matching documents. Before combining the scores from multiple queries, it is beneficial to normalize those scores so they are on the same scale, as shown by experimental data. For further reading about score normalization and combination, including benchmarks and discussion of various techniques, see this [semantic search blog](https://opensearch.org/blog/semantic-science-benchmarks/). ## Query then fetch @@ -29,35 +29,34 @@ The following table lists all available request fields. Field | Data type | Description :--- | :--- | :--- -`normalization.technique` | String | The technique for normalizing scores. Valid values are `min_max`, `L2`. Optional. Default is `min_max`. -`combination.technique` | String | The technique for combining scores. Valid values are `harmonic_mean`, `arithmetic_mean`, `geometric_mean`. Optional. Default is `arithmetic_mean`. +`normalization.technique` | String | The technique for normalizing scores. Valid values are [`min_max`](https://en.wikipedia.org/wiki/Feature_scaling#Rescaling_(min-max_normalization)), [`l2`](https://en.wikipedia.org/wiki/Cosine_similarity#L2-normalized_Euclidean_distance). Optional. Default is `min_max`. +`combination.technique` | String | The technique for combining scores. Valid values are [`arithmetic_mean`](https://en.wikipedia.org/wiki/Arithmetic_mean), [`geometric_mean`](https://en.wikipedia.org/wiki/Geometric_mean), and [`harmonic_mean`](https://en.wikipedia.org/wiki/Harmonic_mean). Optional. Default is `arithmetic_mean`. `combination.parameters.weights` | Array of floating-point values | Specifies the weights to use for each query. Valid values are in the [0.0, 1.0] range and signify decimal percentages. The closer the weight is to 1.0, the more weight is given to a query. The number of values in the `weights` array must equal the number of queries. The sum of the values in the array must equal 1.0. Optional. If not provided, all queries are given equal weight. `tag` | String | The processor's identifier. Optional. `description` | String | A description of the processor. Optional. -`ignore_failure` | Boolean | If `true`, OpenSearch [ignores a failure]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/#ignoring-processor-failures) of this processor and continues to run the remaining processors in the search pipeline. Optional. Default is `false`. +`ignore_failure` | Boolean | For this processor, this value is ignored. If the processor fails, the pipeline always fails with an error. ## Example -The following example demonstrates using a search pipeline with a `normalization_processor`. +The following example demonstrates using a search pipeline with a `normalization_processor`. To try out this example, follow the [Semantic search tutorial]({{site.url}}{{site.baseurl}}/ml-commons-plugin/semantic-search#tutorial). ### Creating a search pipeline -The following request creates a search pipeline with a `normalization_processor` that uses the `min_max` normalization technique and the `harmonic_mean` combination technique: +The following request creates a search pipeline with a `normalization_processor` that uses the `min_max` normalization technique and the `arithmetic_mean` combination technique: ```json -PUT /_search/pipeline/my_pipeline +PUT /_search/pipeline/nlp-search-pipeline { - "phase_results_processors" : [ + "description": "Post processor for hybrid search", + "phase_results_processors": [ { - "normalization-processor" : { - "normalization": { - "technique": "min_max", + "normalization-processor": { + "normalization": { + "technique": "min_max" }, - "combination": { - "technique" : "arithmetic_mean", - "parameters" : { - "weights" : [0.4, 0.7] - } + "combination": { + "technique": "arithmetic_mean", + "weights" : [0.3, 0.7] } } } @@ -71,23 +70,30 @@ PUT /_search/pipeline/my_pipeline Provide the query clauses that you want to combine in a `hybrid` query and apply the search pipeline created in the previous section so the scores are combined using the chosen techniques: ```json -POST flicker-index/_search?search_pipeline=normalizationPipeline +GET /my-nlp-index/_search?search_pipeline=nlp-search-pipeline { + "_source": { + "exclude": [ + "passage_embedding" + ] + }, "query": { "hybrid": { "queries": [ { - "neural": { - "passage_embedding": { - "query_text": "Girl with Brown Hair", - "model_id": "ABCBMODELID", - "k": 20 + "match": { + "text": { + "query": "horse" } } }, { - "match": { - "passage_text": "Girl Brown hair" + "neural": { + "passage_embedding": { + "query_text": "wild west", + "model_id": "aVeif4oB5Vm0Tdw8zYO2", + "k": 5 + } } } ] From c353572689c0cc08f728a9066216bdf14768da4c Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Mon, 11 Sep 2023 13:20:37 -0400 Subject: [PATCH 11/35] Reworded prerequisites Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index 3d57a64041..4431886849 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -38,7 +38,7 @@ You'll find the explanations of all these components as you follow the tutorial, ## Prerequisites -Before you get started, make sure to update the default [cluster settings]({{site.url}}{{site.baseurl}}/api-reference/cluster-api/cluster-settings/) to the following: +You'll start this tutorial with uploading an OpenSearch-provided machine learning (ML) model that will be used to generate embeddings. For the basic local setup with no dedicated ML nodes, send the following request that ensures the simplest ML configuration: ```json PUT _cluster/settings @@ -49,7 +49,6 @@ PUT _cluster/settings "only_run_on_ml_node": "false", "model_access_control_enabled": "true", "native_memory_threshold": "99", - "allow_registering_model_via_url": "true" } } } @@ -57,13 +56,15 @@ PUT _cluster/settings ``` {% include copy-curl.html %} -For more information about machine learning-related cluster settings, see [ML Commons cluster settings]({{site.url}}{{site.baseurl}}/ml-commons-plugin/cluster-settings/). +#### Advanced + +To register a custom model, you need to specify an additional cluster setting `"allow_registering_model_via_url": "true"`. Additionally, you may want to specify `"only_run_on_ml_node": "false"` for improved performance. For more information about ML-related cluster settings, see [ML Commons cluster settings]({{site.url}}{{site.baseurl}}/ml-commons-plugin/cluster-settings/). ## Tutorial overview This tutorial consists of the following steps: -1. [**Set up a machine learning (ML) language model**](#step-1-set-up-an-ml-language-model) +1. [**Set up an ML language model**](#step-1-set-up-an-ml-language-model) 1. [Choose a language model](#step-1a-choose-a-language-model) 1. [Register a model group](#step-1b-register-a-model-group) 1. [Register the model to the model group](#step-1c-register-the-model-to-the-model-group) From 9cff096ae8e244b4de962a0eaf7dbb2509748dbd Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Mon, 11 Sep 2023 13:23:54 -0400 Subject: [PATCH 12/35] Removed comma Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index 4431886849..6c6eb31ac3 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -48,7 +48,7 @@ PUT _cluster/settings "ml_commons": { "only_run_on_ml_node": "false", "model_access_control_enabled": "true", - "native_memory_threshold": "99", + "native_memory_threshold": "99" } } } From 7ee90cd84edc2e7c43a24cc7e7a7d48781bdf61b Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Mon, 11 Sep 2023 13:28:46 -0400 Subject: [PATCH 13/35] Rewording advanced prerequisites Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index 6c6eb31ac3..7fa554fec7 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -58,7 +58,7 @@ PUT _cluster/settings #### Advanced -To register a custom model, you need to specify an additional cluster setting `"allow_registering_model_via_url": "true"`. Additionally, you may want to specify `"only_run_on_ml_node": "false"` for improved performance. For more information about ML-related cluster settings, see [ML Commons cluster settings]({{site.url}}{{site.baseurl}}/ml-commons-plugin/cluster-settings/). +To register a custom model, you need to specify an additional `"allow_registering_model_via_url": "true"` cluster setting. On clusters with dedicated ML nodes, you may want to specify `"only_run_on_ml_node": "true"` for improved performance. For more information about ML-related cluster settings, see [ML Commons cluster settings]({{site.url}}{{site.baseurl}}/ml-commons-plugin/cluster-settings/). ## Tutorial overview From 7f360ba6d98c06765b74ad96f086e4c5db82d0f3 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Mon, 11 Sep 2023 15:13:42 -0400 Subject: [PATCH 14/35] Changed searching for ML model to shorter request Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index 7fa554fec7..21e0b93907 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -259,14 +259,7 @@ You'll need the model ID in order to use this model for several following steps. Search for the newly created model by providing its ID in the request: ```json -POST /_plugins/_ml/models/_search -{ - "query": { - "match": { - "_id": "aVeif4oB5Vm0Tdw8zYO2" - } - } -} +POST /_plugins/_ml/models/_search/aVeif4oB5Vm0Tdw8zYO2 ``` {% include copy-curl.html %} From a8985851dcfac9ac8c8cdd52b10fa4826c152a51 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Mon, 11 Sep 2023 16:43:25 -0400 Subject: [PATCH 15/35] Update task type in register model response Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index 21e0b93907..664417d7ab 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -236,7 +236,7 @@ Once the task is complete, the task status will be `COMPLETED` and the Tasks API ```json { "model_id": "aVeif4oB5Vm0Tdw8zYO2", - "task_type": "DEPLOY_MODEL", + "task_type": "REGISTER_MODEL", "function_name": "TEXT_EMBEDDING", "state": "COMPLETED", "worker_node": [ From 6e1a73c46f3c98fadb7cc24144c42a709cf052b5 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Mon, 11 Sep 2023 21:14:41 -0400 Subject: [PATCH 16/35] Changing example Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index 664417d7ab..c0dbd4967a 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -202,7 +202,7 @@ The response contains the model group: ### Step 1(c): Register the model to the model group -The DistilBERT model you'll use To register the model to the model group, provide the model group ID in the register request: +To register the model to the model group, provide the model group ID in the register request: ```json POST /_plugins/_ml/models/_register @@ -598,8 +598,8 @@ In this step, you'll ingest several sample documents into the index. The sample ```json PUT /my-nlp-index/_doc/1 { - "text": "A West Highland Terrier runs across the dirt .", - "id": "2714220101.jpg" + "text": "A West Virginia university women 's basketball team , officials , and a small gathering of fans are in a West Virginia arena .", + "id": "4319130149.jpg" } ``` {% include copy-curl.html %} @@ -607,8 +607,8 @@ PUT /my-nlp-index/_doc/1 ```json PUT /my-nlp-index/_doc/2 { - "text": "A West Virginia university women 's basketball team , officials , and a small gathering of fans are in a West Virginia arena .", - "id": "4319130149.jpg" + "text": "A wild animal races across an uncut field with a minimal amount of trees .", + "id": "1775029934.jpg" } ``` {% include copy-curl.html %} @@ -616,8 +616,8 @@ PUT /my-nlp-index/_doc/2 ```json PUT /my-nlp-index/_doc/3 { - "text": "An older , seated woman with wild gray hair has makeup applied by a younger woman with equally wild , but blond-dyed hair .", - "id": "6813821371.jpg" + "text": "People line the stands which advertise Freemont 's orthopedics , a cowboy rides a light brown bucking bronco .", + "id": "2664027527.jpg" } ``` {% include copy-curl.html %} From b842fcfc39ce42ed906bd02fd24ea68082b368f2 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Mon, 11 Sep 2023 21:29:34 -0400 Subject: [PATCH 17/35] Added huggingface prefix to model names Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/pretrained-models.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/_ml-commons-plugin/pretrained-models.md b/_ml-commons-plugin/pretrained-models.md index e8bbcf8a4c..86d7e71bfa 100644 --- a/_ml-commons-plugin/pretrained-models.md +++ b/_ml-commons-plugin/pretrained-models.md @@ -41,14 +41,14 @@ Sentence transformer models map sentences and paragraphs across a dimensional de The following table provides a list of sentence transformer models and artifact links to download them. As of OpenSearch 2.6, all artifacts are set to version 1.0.1. -| **Model name** | **Vector dimensions** | **Auto-truncation** | **Torchscript artifact** | **ONNX artifact** | +| Model name | Vector dimensions | Auto-truncation | TorchScript artifact | ONNX artifact | |---|---|---|---| -| `sentence-transformers/all-distilroberta-v1` | 768-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-distilroberta-v1/1.0.1/torch_script/sentence-transformers_all-distilroberta-v1-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-distilroberta-v1/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-distilroberta-v1/1.0.1/onnx/sentence-transformers_all-distilroberta-v1-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-distilroberta-v1/1.0.1/onnx/config.json) | -| `sentence-transformers/all-MiniLM-L6-v2` | 384-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L6-v2/1.0.1/torch_script/sentence-transformers_all-MiniLM-L6-v2-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L6-v2/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L6-v2/1.0.1/onnx/sentence-transformers_all-MiniLM-L6-v2-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L6-v2/1.0.1/onnx/config.json) | -| `sentence-transformers/all-MiniLM-L12-v2` | 384-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L12-v2/1.0.1/torch_script/sentence-transformers_all-MiniLM-L12-v2-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L12-v2/1.0.1/onnx/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L12-v2/1.0.1/onnx/sentence-transformers_all-MiniLM-L12-v2-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L12-v2/1.0.1/onnx/config.json) | -| `sentence-transformers/all-mpnet-base-v2` | 768-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-mpnet-base-v2/1.0.1/torch_script/sentence-transformers_all-mpnet-base-v2-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-mpnet-base-v2/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-mpnet-base-v2/1.0.1/onnx/sentence-transformers_all-mpnet-base-v2-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-mpnet-base-v2/1.0.1/onnx/config.json) | -| `sentence-transformers/msmarco-distilbert-base-tas-b` | 768-dimensional dense vector space. Optimized for semantic search. | No | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/msmarco-distilbert-base-tas-b/1.0.1/torch_script/sentence-transformers_msmarco-distilbert-base-tas-b-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/msmarco-distilbert-base-tas-b/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/msmarco-distilbert-base-tas-b/1.0.1/onnx/sentence-transformers_msmarco-distilbert-base-tas-b-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/msmarco-distilbert-base-tas-b/1.0.1/onnx/config.json) | -| `sentence-transformers/multi-qa-MiniLM-L6-cos-v1` | 384 dimensional dense vector space. Designed for semantic search and trained on 215 million question/answer pairs. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/torch_script/sentence-transformers_multi-qa-MiniLM-L6-cos-v1-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/onnx/sentence-transformers_multi-qa-MiniLM-L6-cos-v1-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/onnx/config.json) | -| `sentence-transformers/multi-qa-mpnet-base-dot-v1` | 384 dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/torch_script/sentence-transformers_multi-qa-mpnet-base-dot-v1-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/onnx/sentence-transformers_multi-qa-mpnet-base-dot-v1-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/onnx/config.json) | -| `sentence-transformers/paraphrase-MiniLM-L3-v2` | 384-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-MiniLM-L3-v2/1.0.1/torch_script/sentence-transformers_paraphrase-MiniLM-L3-v2-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-MiniLM-L3-v2/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-MiniLM-L3-v2/1.0.1/onnx/sentence-transformers_paraphrase-MiniLM-L3-v2-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-MiniLM-L3-v2/1.0.1/onnx/config.json) | -| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | 384-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/1.0.1/torch_script/sentence-transformers_paraphrase-multilingual-MiniLM-L12-v2-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/1.0.1/onnx/sentence-transformers_paraphrase-multilingual-MiniLM-L12-v2-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/1.0.1/onnx/config.json) | \ No newline at end of file +| `huggingface/sentence-transformers/all-distilroberta-v1` | 768-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-distilroberta-v1/1.0.1/torch_script/sentence-transformers_all-distilroberta-v1-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-distilroberta-v1/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-distilroberta-v1/1.0.1/onnx/sentence-transformers_all-distilroberta-v1-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-distilroberta-v1/1.0.1/onnx/config.json) | +| `huggingface/sentence-transformers/all-MiniLM-L6-v2` | 384-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L6-v2/1.0.1/torch_script/sentence-transformers_all-MiniLM-L6-v2-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L6-v2/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L6-v2/1.0.1/onnx/sentence-transformers_all-MiniLM-L6-v2-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L6-v2/1.0.1/onnx/config.json) | +| `huggingface/sentence-transformers/all-MiniLM-L12-v2` | 384-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L12-v2/1.0.1/torch_script/sentence-transformers_all-MiniLM-L12-v2-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L12-v2/1.0.1/onnx/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L12-v2/1.0.1/onnx/sentence-transformers_all-MiniLM-L12-v2-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L12-v2/1.0.1/onnx/config.json) | +| `huggingface/sentence-transformers/all-mpnet-base-v2` | 768-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-mpnet-base-v2/1.0.1/torch_script/sentence-transformers_all-mpnet-base-v2-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-mpnet-base-v2/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-mpnet-base-v2/1.0.1/onnx/sentence-transformers_all-mpnet-base-v2-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-mpnet-base-v2/1.0.1/onnx/config.json) | +| `huggingface/sentence-transformers/msmarco-distilbert-base-tas-b` | 768-dimensional dense vector space. Optimized for semantic search. | No | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/msmarco-distilbert-base-tas-b/1.0.1/torch_script/sentence-transformers_msmarco-distilbert-base-tas-b-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/msmarco-distilbert-base-tas-b/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/msmarco-distilbert-base-tas-b/1.0.1/onnx/sentence-transformers_msmarco-distilbert-base-tas-b-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/msmarco-distilbert-base-tas-b/1.0.1/onnx/config.json) | +| `huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1` | 384 dimensional dense vector space. Designed for semantic search and trained on 215 million question/answer pairs. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/torch_script/sentence-transformers_multi-qa-MiniLM-L6-cos-v1-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/onnx/sentence-transformers_multi-qa-MiniLM-L6-cos-v1-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/onnx/config.json) | +| `huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1` | 384 dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/torch_script/sentence-transformers_multi-qa-mpnet-base-dot-v1-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/onnx/sentence-transformers_multi-qa-mpnet-base-dot-v1-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/onnx/config.json) | +| `huggingface/sentence-transformers/paraphrase-MiniLM-L3-v2` | 384-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-MiniLM-L3-v2/1.0.1/torch_script/sentence-transformers_paraphrase-MiniLM-L3-v2-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-MiniLM-L3-v2/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-MiniLM-L3-v2/1.0.1/onnx/sentence-transformers_paraphrase-MiniLM-L3-v2-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-MiniLM-L3-v2/1.0.1/onnx/config.json) | +| `huggingface/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | 384-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/1.0.1/torch_script/sentence-transformers_paraphrase-multilingual-MiniLM-L12-v2-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/1.0.1/onnx/sentence-transformers_paraphrase-multilingual-MiniLM-L12-v2-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/1.0.1/onnx/config.json) | \ No newline at end of file From d7971cbff421b2e1046705a28a5ba4b8d5c91a04 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Mon, 11 Sep 2023 21:58:50 -0400 Subject: [PATCH 18/35] Change example responses Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 97 ++++++++++++--------------- 1 file changed, 44 insertions(+), 53 deletions(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index c0dbd4967a..e395186e2a 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -667,7 +667,7 @@ GET /my-nlp-index/_search ``` {% include copy-curl.html %} -Documents containing the words `rodeo` and `cowboy` are scored lower because semantic meaning is not considered: +Document 3 is not returned because it does not contain the specified keywords. Documents containing the words `rodeo` and `cowboy` are scored lower because semantic meaning is not considered:
@@ -677,7 +677,7 @@ Documents containing the words `rodeo` and `cowboy` are scored lower because sem ```json { - "took": 1, + "took": 647, "timed_out": false, "_shards": { "total": 1, @@ -687,15 +687,15 @@ Documents containing the words `rodeo` and `cowboy` are scored lower because sem }, "hits": { "total": { - "value": 5, + "value": 4, "relation": "eq" }, - "max_score": 1.7723373, + "max_score": 1.7878418, "hits": [ { "_index": "my-nlp-index", - "_id": "2", - "_score": 1.7723373, + "_id": "1", + "_score": 1.7878418, "_source": { "text": "A West Virginia university women 's basketball team , officials , and a small gathering of fans are in a West Virginia arena .", "id": "4319130149.jpg" @@ -703,26 +703,17 @@ Documents containing the words `rodeo` and `cowboy` are scored lower because sem }, { "_index": "my-nlp-index", - "_id": "1", - "_score": 1.7579391, - "_source": { - "text": "A West Highland Terrier runs across the dirt .", - "id": "2714220101.jpg" - } - }, - { - "_index": "my-nlp-index", - "_id": "3", - "_score": 0.54217947, + "_id": "2", + "_score": 0.58093566, "_source": { - "text": "An older , seated woman with wild gray hair has makeup applied by a younger woman with equally wild , but blond-dyed hair .", - "id": "6813821371.jpg" + "text": "A wild animal races across an uncut field with a minimal amount of trees .", + "id": "1775029934.jpg" } }, { "_index": "my-nlp-index", "_id": "5", - "_score": 0.43677896, + "_score": 0.55228686, "_source": { "text": "A rodeo cowboy , wearing a cowboy hat , is being thrown off of a wild white horse .", "id": "2691147709.jpg" @@ -731,7 +722,7 @@ Documents containing the words `rodeo` and `cowboy` are scored lower because sem { "_index": "my-nlp-index", "_id": "4", - "_score": 0.4261033, + "_score": 0.53899646, "_source": { "text": "A man who is riding a wild horse in the rodeo is very near to falling off .", "id": "4427058951.jpg" @@ -768,7 +759,7 @@ GET /my-nlp-index/_search ``` {% include copy-curl.html %} -Documents containing the words `rodeo` and `cowboy` are now scored higher: +The results contain all five documents. The document order is now closer to the desired one because semantic meaning is considered:
@@ -778,7 +769,7 @@ Documents containing the words `rodeo` and `cowboy` are now scored higher: ```json { - "took": 27, + "took": 25, "timed_out": false, "_shards": { "total": 1, @@ -791,17 +782,8 @@ Documents containing the words `rodeo` and `cowboy` are now scored higher: "value": 5, "relation": "eq" }, - "max_score": 0.016028818, + "max_score": 0.01585195, "hits": [ - { - "_index": "my-nlp-index", - "_id": "3", - "_score": 0.016028818, - "_source": { - "text": "An older , seated woman with wild gray hair has makeup applied by a younger woman with equally wild , but blond-dyed hair .", - "id": "6813821371.jpg" - } - }, { "_index": "my-nlp-index", "_id": "4", @@ -813,11 +795,11 @@ Documents containing the words `rodeo` and `cowboy` are now scored higher: }, { "_index": "my-nlp-index", - "_id": "1", - "_score": 0.015625207, + "_id": "2", + "_score": 0.015748845, "_source": { - "text": "A West Highland Terrier runs across the dirt .", - "id": "2714220101.jpg" + "text": "A wild animal races across an uncut field with a minimal amount of trees.", + "id": "1775029934.jpg" } }, { @@ -831,12 +813,21 @@ Documents containing the words `rodeo` and `cowboy` are now scored higher: }, { "_index": "my-nlp-index", - "_id": "2", + "_id": "1", "_score": 0.013272902, "_source": { "text": "A West Virginia university women 's basketball team , officials , and a small gathering of fans are in a West Virginia arena .", "id": "4319130149.jpg" } + }, + { + "_index": "my-nlp-index", + "_id": "3", + "_score": 0.011347735, + "_source": { + "text": "People line the stands which advertise Freemont 's orthopedics , a cowboy rides a light brown bucking bronco .", + "id": "2664027527.jpg" + } } ] } @@ -891,7 +882,7 @@ GET /my-nlp-index/_search?search_pipeline=nlp-search-pipeline { "match": { "text": { - "query": "horse" + "query": "cowboy rodeo bronco" } } }, @@ -911,7 +902,7 @@ GET /my-nlp-index/_search?search_pipeline=nlp-search-pipeline ``` {% include copy-curl.html %} -OpenSearch returns documents that match the semantic meaning of the phrase `wild west` and the documents that contain the word `horse` are scored higher: +Not only does OpenSearch return documents that match the semantic meaning of `wild west`, but now the documents containing the words related to the wild west theme are scored higher relative to the others:
@@ -921,7 +912,7 @@ OpenSearch returns documents that match the semantic meaning of the phrase `wild ```json { - "took": 27, + "took": 26, "timed_out": false, "_shards": { "total": 1, @@ -934,12 +925,12 @@ OpenSearch returns documents that match the semantic meaning of the phrase `wild "value": 5, "relation": "eq" }, - "max_score": 0.84563124, + "max_score": 0.8744404, "hits": [ { "_index": "my-nlp-index", "_id": "5", - "_score": 0.84563124, + "_score": 0.8744404, "_source": { "text": "A rodeo cowboy , wearing a cowboy hat , is being thrown off of a wild white horse .", "id": "2691147709.jpg" @@ -948,16 +939,16 @@ OpenSearch returns documents that match the semantic meaning of the phrase `wild { "_index": "my-nlp-index", "_id": "3", - "_score": 0.5, + "_score": 0.5005, "_source": { - "text": "An older , seated woman with wild gray hair has makeup applied by a younger woman with equally wild , but blond-dyed hair .", - "id": "6813821371.jpg" + "text": "People line the stands which advertise Freemont 's orthopedics , a cowboy rides a light brown bucking bronco .", + "id": "2664027527.jpg" } }, { "_index": "my-nlp-index", "_id": "4", - "_score": 0.4684113, + "_score": 0.5005, "_source": { "text": "A man who is riding a wild horse in the rodeo is very near to falling off .", "id": "4427058951.jpg" @@ -965,17 +956,17 @@ OpenSearch returns documents that match the semantic meaning of the phrase `wild }, { "_index": "my-nlp-index", - "_id": "1", - "_score": 0.4267737, + "_id": "2", + "_score": 0.48855463, "_source": { - "text": "A West Highland Terrier runs across the dirt .", - "id": "2714220101.jpg" + "text": "A wild animal races across an uncut field with a minimal amount of trees.", + "id": "1775029934.jpg" } }, { "_index": "my-nlp-index", - "_id": "2", - "_score": 0.0005, + "_id": "1", + "_score": 0.21370724, "_source": { "text": "A West Virginia university women 's basketball team , officials , and a small gathering of fans are in a West Virginia arena .", "id": "4319130149.jpg" From 6ca775fef7ddcaea71f16524e8c5798c55c262d4 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Tue, 12 Sep 2023 13:14:38 -0400 Subject: [PATCH 19/35] Added note about huggingface prefix Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/pretrained-models.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_ml-commons-plugin/pretrained-models.md b/_ml-commons-plugin/pretrained-models.md index 86d7e71bfa..987297f00e 100644 --- a/_ml-commons-plugin/pretrained-models.md +++ b/_ml-commons-plugin/pretrained-models.md @@ -39,7 +39,7 @@ The ML Framework supports the following models, categorized by type. All models Sentence transformer models map sentences and paragraphs across a dimensional dense vector space. The number of vectors depends on the model. Use these models for use cases such as clustering and semantic search. -The following table provides a list of sentence transformer models and artifact links to download them. As of OpenSearch 2.6, all artifacts are set to version 1.0.1. +The following table provides a list of sentence transformer models and artifact links to download them. Note that you must prefix the model name with `huggingface/`, as shown in the Model name column. As of OpenSearch 2.6, all artifacts are set to version 1.0.1. | Model name | Vector dimensions | Auto-truncation | TorchScript artifact | ONNX artifact | |---|---|---|---| From b16de8d0b63be4be2da37951a8e361040ebef526 Mon Sep 17 00:00:00 2001 From: kolchfa-aws <105444904+kolchfa-aws@users.noreply.github.com> Date: Tue, 12 Sep 2023 14:24:43 -0400 Subject: [PATCH 20/35] Update _ml-commons-plugin/semantic-search.md Co-authored-by: Naarcha-AWS <97990722+Naarcha-AWS@users.noreply.github.com> Signed-off-by: kolchfa-aws <105444904+kolchfa-aws@users.noreply.github.com> --- _ml-commons-plugin/semantic-search.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index e395186e2a..ef7e9af739 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -354,7 +354,7 @@ POST /_plugins/_ml/models/aVeif4oB5Vm0Tdw8zYO2/_deploy ``` {% include copy-curl.html %} -Similarly to the register operation, the deploy operation is asynchronous so you'll get a task ID in the response: +Like the register operation, the deploy operation is asynchronous so you'll get a task ID in the response: ```json { From f7bc213119e20386b0e68bdf842daf1810ad41ed Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Tue, 12 Sep 2023 14:35:35 -0400 Subject: [PATCH 21/35] Implemented doc review comments Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index ef7e9af739..a6764b7559 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -38,7 +38,7 @@ You'll find the explanations of all these components as you follow the tutorial, ## Prerequisites -You'll start this tutorial with uploading an OpenSearch-provided machine learning (ML) model that will be used to generate embeddings. For the basic local setup with no dedicated ML nodes, send the following request that ensures the simplest ML configuration: +For this simple example, you'll use an OpenSearch-provided machine learning (ML) model and a cluster with no dedicated ML nodes. To ensure this basic local setup works, send the following request to update ML-related cluster settings: ```json PUT _cluster/settings @@ -58,7 +58,12 @@ PUT _cluster/settings #### Advanced -To register a custom model, you need to specify an additional `"allow_registering_model_via_url": "true"` cluster setting. On clusters with dedicated ML nodes, you may want to specify `"only_run_on_ml_node": "true"` for improved performance. For more information about ML-related cluster settings, see [ML Commons cluster settings]({{site.url}}{{site.baseurl}}/ml-commons-plugin/cluster-settings/). +For a more advanced setup, note the following requirements: + +- To register a custom model, you need to specify an additional `"allow_registering_model_via_url": "true"` cluster setting. +- On clusters with dedicated ML nodes, you may want to specify `"only_run_on_ml_node": "true"` for improved performance. + +For more information about ML-related cluster settings, see [ML Commons cluster settings]({{site.url}}{{site.baseurl}}/ml-commons-plugin/cluster-settings/). ## Tutorial overview @@ -837,7 +842,7 @@ The results contain all five documents. The document order is now closer to the ### Search with a combined keyword search and neural search -To combine keyword search and neural search, you need to set up a [search pipeline]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/) that is similar to the ingest pipeline but runs at search time instead of ingestion time. The search pipeline you'll configure intercepts search results at an intermediate stage and applies the [`normalization_processor`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/normalization-processor/) to them. The `normalization_processor` normalizes and combines the document scores from multiple query clauses, rescoring the documents according to the chosen normalization and combination techniques. +To combine keyword search and neural search, you need to set up a [search pipeline]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/) that runs at search time. The search pipeline you'll configure intercepts search results at an intermediate stage and applies the [`normalization_processor`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/normalization-processor/) to them. The `normalization_processor` normalizes and combines the document scores from multiple query clauses, rescoring the documents according to the chosen normalization and combination techniques. #### Step 1: Configure a search pipeline From c605b5a4a212686d47bdd67652da0e23773adae5 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Tue, 12 Sep 2023 15:57:00 -0400 Subject: [PATCH 22/35] List weights under parameters Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 37 +++++++++++-------- .../normalization-processor.md | 7 +++- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index a6764b7559..108a3963bf 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -860,7 +860,12 @@ PUT /_search/pipeline/nlp-search-pipeline }, "combination": { "technique": "arithmetic_mean", - "weights" : [0.3, 0.7] + "parameters": { + "weights": [ + 0.3, + 0.7 + ] + } } } } @@ -917,7 +922,7 @@ Not only does OpenSearch return documents that match the semantic meaning of `wi ```json { - "took": 26, + "took": 27, "timed_out": false, "_shards": { "total": 1, @@ -930,30 +935,21 @@ Not only does OpenSearch return documents that match the semantic meaning of `wi "value": 5, "relation": "eq" }, - "max_score": 0.8744404, + "max_score": 0.86481035, "hits": [ { "_index": "my-nlp-index", "_id": "5", - "_score": 0.8744404, + "_score": 0.86481035, "_source": { "text": "A rodeo cowboy , wearing a cowboy hat , is being thrown off of a wild white horse .", "id": "2691147709.jpg" } }, - { - "_index": "my-nlp-index", - "_id": "3", - "_score": 0.5005, - "_source": { - "text": "People line the stands which advertise Freemont 's orthopedics , a cowboy rides a light brown bucking bronco .", - "id": "2664027527.jpg" - } - }, { "_index": "my-nlp-index", "_id": "4", - "_score": 0.5005, + "_score": 0.7003, "_source": { "text": "A man who is riding a wild horse in the rodeo is very near to falling off .", "id": "4427058951.jpg" @@ -962,16 +958,25 @@ Not only does OpenSearch return documents that match the semantic meaning of `wi { "_index": "my-nlp-index", "_id": "2", - "_score": 0.48855463, + "_score": 0.6839765, "_source": { "text": "A wild animal races across an uncut field with a minimal amount of trees.", "id": "1775029934.jpg" } }, + { + "_index": "my-nlp-index", + "_id": "3", + "_score": 0.3007, + "_source": { + "text": "People line the stands which advertise Freemont 's orthopedics , a cowboy rides a light brown bucking bronco .", + "id": "2664027527.jpg" + } + }, { "_index": "my-nlp-index", "_id": "1", - "_score": 0.21370724, + "_score": 0.29919013, "_source": { "text": "A West Virginia university women 's basketball team , officials , and a small gathering of fans are in a West Virginia arena .", "id": "4319130149.jpg" diff --git a/_search-plugins/search-pipelines/normalization-processor.md b/_search-plugins/search-pipelines/normalization-processor.md index 03883325af..28d6af7ca8 100644 --- a/_search-plugins/search-pipelines/normalization-processor.md +++ b/_search-plugins/search-pipelines/normalization-processor.md @@ -56,7 +56,12 @@ PUT /_search/pipeline/nlp-search-pipeline }, "combination": { "technique": "arithmetic_mean", - "weights" : [0.3, 0.7] + "parameters": { + "weights": [ + 0.3, + 0.7 + ] + } } } } From 1f89522b75d5db6db752ffb69e7eec42d8bf09c8 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Tue, 12 Sep 2023 16:15:38 -0400 Subject: [PATCH 23/35] Remove one-shard warning for normalization processor Signed-off-by: Fanit Kolchina --- _search-plugins/search-pipelines/normalization-processor.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/_search-plugins/search-pipelines/normalization-processor.md b/_search-plugins/search-pipelines/normalization-processor.md index 28d6af7ca8..18c9e226dc 100644 --- a/_search-plugins/search-pipelines/normalization-processor.md +++ b/_search-plugins/search-pipelines/normalization-processor.md @@ -109,6 +109,3 @@ GET /my-nlp-index/_search?search_pipeline=nlp-search-pipeline {% include copy-curl.html %} For more information, see [Hybrid query]({{site.url}}{{site.baseurl}}/query-dsl/compound/hybrid/). - -The `normalization_processor` does not produce consistent results for a cluster with one node and one shard. -{: .warning} \ No newline at end of file From 1bbb9297611fd22ad7a0a9337efe48d776d87d74 Mon Sep 17 00:00:00 2001 From: kolchfa-aws <105444904+kolchfa-aws@users.noreply.github.com> Date: Wed, 13 Sep 2023 10:31:39 -0400 Subject: [PATCH 24/35] Apply suggestions from code review Co-authored-by: Nathan Bower Signed-off-by: kolchfa-aws <105444904+kolchfa-aws@users.noreply.github.com> --- _ml-commons-plugin/pretrained-models.md | 6 +- _ml-commons-plugin/semantic-search.md | 62 +++++++++---------- _query-dsl/compound/hybrid.md | 4 +- .../normalization-processor.md | 8 +-- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/_ml-commons-plugin/pretrained-models.md b/_ml-commons-plugin/pretrained-models.md index 987297f00e..c4bea64ae8 100644 --- a/_ml-commons-plugin/pretrained-models.md +++ b/_ml-commons-plugin/pretrained-models.md @@ -39,7 +39,7 @@ The ML Framework supports the following models, categorized by type. All models Sentence transformer models map sentences and paragraphs across a dimensional dense vector space. The number of vectors depends on the model. Use these models for use cases such as clustering and semantic search. -The following table provides a list of sentence transformer models and artifact links to download them. Note that you must prefix the model name with `huggingface/`, as shown in the Model name column. As of OpenSearch 2.6, all artifacts are set to version 1.0.1. +The following table provides a list of sentence transformer models and artifact links you can use to download them. Note that you must prefix the model name with `huggingface/`, as shown in the **Model name** column. As of OpenSearch 2.6, all artifacts are set to version 1.0.1. | Model name | Vector dimensions | Auto-truncation | TorchScript artifact | ONNX artifact | |---|---|---|---| @@ -48,7 +48,7 @@ The following table provides a list of sentence transformer models and artifact | `huggingface/sentence-transformers/all-MiniLM-L12-v2` | 384-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L12-v2/1.0.1/torch_script/sentence-transformers_all-MiniLM-L12-v2-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L12-v2/1.0.1/onnx/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L12-v2/1.0.1/onnx/sentence-transformers_all-MiniLM-L12-v2-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-MiniLM-L12-v2/1.0.1/onnx/config.json) | | `huggingface/sentence-transformers/all-mpnet-base-v2` | 768-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-mpnet-base-v2/1.0.1/torch_script/sentence-transformers_all-mpnet-base-v2-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-mpnet-base-v2/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-mpnet-base-v2/1.0.1/onnx/sentence-transformers_all-mpnet-base-v2-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/all-mpnet-base-v2/1.0.1/onnx/config.json) | | `huggingface/sentence-transformers/msmarco-distilbert-base-tas-b` | 768-dimensional dense vector space. Optimized for semantic search. | No | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/msmarco-distilbert-base-tas-b/1.0.1/torch_script/sentence-transformers_msmarco-distilbert-base-tas-b-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/msmarco-distilbert-base-tas-b/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/msmarco-distilbert-base-tas-b/1.0.1/onnx/sentence-transformers_msmarco-distilbert-base-tas-b-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/msmarco-distilbert-base-tas-b/1.0.1/onnx/config.json) | -| `huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1` | 384 dimensional dense vector space. Designed for semantic search and trained on 215 million question/answer pairs. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/torch_script/sentence-transformers_multi-qa-MiniLM-L6-cos-v1-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/onnx/sentence-transformers_multi-qa-MiniLM-L6-cos-v1-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/onnx/config.json) | -| `huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1` | 384 dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/torch_script/sentence-transformers_multi-qa-mpnet-base-dot-v1-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/onnx/sentence-transformers_multi-qa-mpnet-base-dot-v1-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/onnx/config.json) | +| `huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1` | 384-dimensional dense vector space. Designed for semantic search and trained on 215 million question/answer pairs. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/torch_script/sentence-transformers_multi-qa-MiniLM-L6-cos-v1-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/onnx/sentence-transformers_multi-qa-MiniLM-L6-cos-v1-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/1.0.1/onnx/config.json) | +| `huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1` | 384-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/torch_script/sentence-transformers_multi-qa-mpnet-base-dot-v1-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/onnx/sentence-transformers_multi-qa-mpnet-base-dot-v1-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/multi-qa-mpnet-base-dot-v1/1.0.1/onnx/config.json) | | `huggingface/sentence-transformers/paraphrase-MiniLM-L3-v2` | 384-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-MiniLM-L3-v2/1.0.1/torch_script/sentence-transformers_paraphrase-MiniLM-L3-v2-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-MiniLM-L3-v2/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-MiniLM-L3-v2/1.0.1/onnx/sentence-transformers_paraphrase-MiniLM-L3-v2-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-MiniLM-L3-v2/1.0.1/onnx/config.json) | | `huggingface/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | 384-dimensional dense vector space. | Yes | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/1.0.1/torch_script/sentence-transformers_paraphrase-multilingual-MiniLM-L12-v2-1.0.1-torch_script.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/1.0.1/torch_script/config.json) | - [model_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/1.0.1/onnx/sentence-transformers_paraphrase-multilingual-MiniLM-L12-v2-1.0.1-onnx.zip)
- [config_url](https://artifacts.opensearch.org/models/ml-models/huggingface/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/1.0.1/onnx/config.json) | \ No newline at end of file diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index 108a3963bf..9ae62a370d 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -7,7 +7,7 @@ nav_order: 140 # Semantic search -By default, OpenSearch calculates document scores using the [Okapi BM25](https://en.wikipedia.org/wiki/Okapi_BM25) algorithm. BM25 is a keyword-based algorithm that performs well on a query containing keywords but fails to capture the semantic meaning of the query terms. Semantic search, unlike keyword-based search, takes into account the meaning of the query in the search context. Thus, semantic search performs well when a query requires natural language understanding. +By default, OpenSearch calculates document scores using the [Okapi BM25](https://en.wikipedia.org/wiki/Okapi_BM25) algorithm. BM25 is a keyword-based algorithm that performs well on queries containing keywords but fails to capture the semantic meaning of the query terms. Semantic search, unlike keyword-based search, takes into account the meaning of the query in the search context. Thus, semantic search performs well when a query requires natural language understanding. In this tutorial, you'll learn how to: @@ -19,11 +19,11 @@ In this tutorial, you'll learn how to: It's helpful to understand the following terms before starting this tutorial: - _Semantic search_: Employs neural search in order to determine the intention of the user's query in the search context and improve search relevance. -- _Neural search_: When indexing documents containing text, neural search uses language models to generate vector embeddings from that text. When you then use a _neural query_, the query text is passed through a language model and the resulting vector embeddings are compared with the document text vector embeddings to find the most relevant results. +- _Neural search_: When indexing documents containing text, neural search uses language models to generate vector embeddings from that text. When you then use a _neural query_, the query text is passed through a language model, and the resulting vector embeddings are compared with the document text vector embeddings to find the most relevant results. ## OpenSearch components for semantic search -For this tutorial, you'll implement semantic search using the following OpenSearch components: +In this tutorial, you'll implement semantic search using the following OpenSearch components: - [Model group]({{site.url}}{{site.baseurl}}/ml-commons-plugin/model-access-control#model-groups) - [Pretrained language models provided by OpenSearch]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/) @@ -34,11 +34,11 @@ For this tutorial, you'll implement semantic search using the following OpenSear - [Normalization processor]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/normalization-processor/) - [Hybrid query]({{site.url}}{{site.baseurl}}/query-dsl/compound/hybrid/) -You'll find the explanations of all these components as you follow the tutorial, so don't worry if you're not familiar with some of them. Each link in the preceding list will take you to a corresponding documentation section if you want to learn more. +You'll find descriptions of all these components as you follow the tutorial, so don't worry if you're not familiar with some of them. Each link in the preceding list will take you to the documentation section for the corresponding component. ## Prerequisites -For this simple example, you'll use an OpenSearch-provided machine learning (ML) model and a cluster with no dedicated ML nodes. To ensure this basic local setup works, send the following request to update ML-related cluster settings: +For this simple example, you'll use an OpenSearch-provided machine learning (ML) model and a cluster with no dedicated ML nodes. To ensure that this basic local setup works, send the following request to update ML-related cluster settings: ```json PUT _cluster/settings @@ -83,7 +83,7 @@ This tutorial consists of the following steps: - [Search with a neural search](#search-with-a-neural-search) - [Search with a combined keyword search and neural search](#search-with-a-combined-keyword-search-and-neural-search) -Some steps in the tutorial contain optional `Test it` sections. You can ensure the step worked by running requests in these sections. +Some steps in the tutorial contain optional `Test it` sections. You can ensure that the step worked by running requests in these sections. After you're done, follow the steps in the [Clean up](#clean-up) section to delete all created components. @@ -93,7 +93,7 @@ You can follow this tutorial using your command line or the OpenSearch Dashboard ## Step 1: Set up an ML language model -Neural search requires a language model to generate vector embeddings from text fields both at ingestion time and query time. +Neural search requires a language model in order to generate vector embeddings from text fields, both at ingestion time and query time. ### Step 1(a): Choose a language model @@ -108,7 +108,7 @@ For this tutorial, you'll use the [DistilBERT](https://huggingface.co/docs/trans You can choose to use another model: - One of the [pretrained language models provided by OpenSearch]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/). -- Your own model. For instructions how to set up a custom model, see [Model serving framework]({{site.url}}{{site.baseurl}}/ml-commons-plugin/ml-framework/). +- Your own model. For instructions on how to set up a custom model, see [Model-serving framework]({{site.url}}{{site.baseurl}}/ml-commons-plugin/ml-framework/). Take note of the dimensionality of the model because you'll need it when you set up a k-NN index. {: .important} @@ -229,7 +229,7 @@ Registering a model is an asynchronous task. OpenSearch sends back a task ID for } ``` -OpenSearch downloads the config file for the model and the model contents from the URL. Because the model is larger than 10 MB in size, OpenSearch splits it into chunks of up to 10 MB and saves those chunks in the model index. You can check the status of the task using the Tasks API: +OpenSearch downloads the config file for the model and the model contents from the URL. Because the model is larger than 10 MB in size, OpenSearch splits it into chunks of up to 10 MB and saves those chunks in the model index. You can check the status of the task by using the Tasks API: ```json GET /_plugins/_ml/tasks/aFeif4oB5Vm0Tdw8yoN7 @@ -253,7 +253,7 @@ Once the task is complete, the task status will be `COMPLETED` and the Tasks API } ``` -You'll need the model ID in order to use this model for several following steps. +You'll need the model ID in order to use this model for several of the following steps.
@@ -320,12 +320,12 @@ The response contains the model: } ``` -The response contains the model information, including the `model_state` (`REGISTERED`) and the number of chunks it was split into `total_chunks` (27). +The response contains the model information, including the `model_state` (`REGISTERED`) and the number of chunks into which it was split `total_chunks` (27).
#### Advanced: Registering a custom model -To register a custom model, you must provide a model configuration in the register request. For example, the following is a register request with the full format for the model used in this tutorial: +To register a custom model, you must provide a model configuration in the register request. For example, the following is a register request containing the full format for the model used in this tutorial: ```json POST /_plugins/_ml/models/_register @@ -348,7 +348,7 @@ POST /_plugins/_ml/models/_register } ``` -For more information, see [Model serving framework]({{site.url}}{{site.baseurl}}/ml-commons-plugin/ml-framework/). +For more information, see [Model-serving framework]({{site.url}}{{site.baseurl}}/ml-commons-plugin/ml-framework/). ### Step 1(d): Deploy the model @@ -359,7 +359,7 @@ POST /_plugins/_ml/models/aVeif4oB5Vm0Tdw8zYO2/_deploy ``` {% include copy-curl.html %} -Like the register operation, the deploy operation is asynchronous so you'll get a task ID in the response: +Like the register operation, the deploy operation is asynchronous, so you'll get a task ID in the response: ```json { @@ -368,7 +368,7 @@ Like the register operation, the deploy operation is asynchronous so you'll get } ``` -You can check the status of the task using the Tasks API: +You can check the status of the task by using the Tasks API: ```json GET /_plugins/_ml/tasks/ale6f4oB5Vm0Tdw8NINO @@ -471,7 +471,7 @@ The response shows the model status as `DEPLOYED`: } ``` -You can also receive statistics for all deployed models in your cluster sending a Models Profile API request: +You can also receive statistics for all deployed models in your cluster by sending a Models Profile API request: ```json GET /_plugins/_ml/profile/models @@ -484,7 +484,7 @@ Neural search uses a language model to transform text into vector embeddings. Du ### Step 2(a): Create an ingest pipeline for neural search -The first step in setting up [neural search]({{site.url}}{{site.baseurl}}/search-plugins/neural-search/) is to create an [ingest pipeline]({{site.url}}{{site.baseurl}}/api-reference/ingest-apis/index/). The ingest pipeline will contain one processor: a task that transforms document fields. For neural search, you'll need to set up a `text_embedding` processor that takes in text and creates vector embeddings from that text. You'll need a `model_id` of the model you set up in the previous section and a `field_map`, which specifies the name of the field to take the text from (`text`) and the name of the field to record embeddings in (`passage_embedding`): +The first step in setting up [neural search]({{site.url}}{{site.baseurl}}/search-plugins/neural-search/) is to create an [ingest pipeline]({{site.url}}{{site.baseurl}}/api-reference/ingest-apis/index/). The ingest pipeline will contain one processor: a task that transforms document fields. For neural search, you'll need to set up a `text_embedding` processor that takes in text and creates vector embeddings from that text. You'll need the `model_id` of the model you set up in the previous section and a `field_map`, which specifies the name of the field from which to take the text (`text`) and the name of the field in which to record embeddings (`passage_embedding`): ```json PUT /_ingest/pipeline/nlp-ingest-pipeline @@ -540,7 +540,7 @@ The response contains the ingest pipeline: ### Step 2(b): Create a k-NN index -Now you'll create a k-NN index with a field called `text` that holds an image description and a [`knn_vector`]({{site.url}}{{site.baseurl}}/field-types/supported-field-types/knn-vector/) field called `passage_embedding` that holds the vector embedding of the text. Additionally, set the default ingest pipeline to the `nlp-ingest-pipeline` you created in the previous step: +Now you'll create a k-NN index with a field named `text`, which contains an image description, and a [`knn_vector`]({{site.url}}{{site.baseurl}}/field-types/supported-field-types/knn-vector/) field named `passage_embedding`, which contains the vector embedding of the text. Additionally, set the default ingest pipeline to the `nlp-ingest-pipeline` you created in the previous step: ```json @@ -574,7 +574,7 @@ PUT /my-nlp-index ``` {% include copy-curl.html %} -Setting up a k-NN index allows to later perform a vector search on the `passage_embedding` field. +Setting up a k-NN index allows you to later perform a vector search on the `passage_embedding` field.
@@ -598,7 +598,7 @@ GET /my-nlp-index/_mappings ### Step 2(c): Ingest documents into the index -In this step, you'll ingest several sample documents into the index. The sample data is taken from the [Flickr image dataset](https://www.kaggle.com/datasets/hsankesara/flickr-image-dataset). Each document contains a `text` field that corresponds to the image description and an `id` field that corresponds to the image ID: +In this step, you'll ingest several sample documents into the index. The sample data is taken from the [Flickr image dataset](https://www.kaggle.com/datasets/hsankesara/flickr-image-dataset). Each document contains a `text` field corresponding to the image description and an `id` field corresponding to the image ID: ```json PUT /my-nlp-index/_doc/1 @@ -649,9 +649,9 @@ PUT /my-nlp-index/_doc/5 Now you'll search the index using keyword search, neural search, and a combination of the two. -### Search with a keyword search +### Search using a keyword search -To search with keyword search, use a `match` query. You'll exclude embeddings from the results: +To search using a keyword search, use a `match` query. You'll exclude embeddings from the results: ```json GET /my-nlp-index/_search @@ -739,9 +739,9 @@ Document 3 is not returned because it does not contain the specified keywords. D ```
-### Search with a neural search +### Search using a neural search -To search with neural search, use a `neural` query and provide the model ID of the model set up earlier so that vector embeddings for the query text are generated with the model used at ingestion time: +To search using a neural search, use a `neural` query and provide the model ID of the model you set up earlier so that vector embeddings for the query text are generated with the model used at ingestion time: ```json GET /my-nlp-index/_search @@ -840,13 +840,13 @@ The results contain all five documents. The document order is now closer to the ```
-### Search with a combined keyword search and neural search +### Search using a combined keyword search and neural search To combine keyword search and neural search, you need to set up a [search pipeline]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/) that runs at search time. The search pipeline you'll configure intercepts search results at an intermediate stage and applies the [`normalization_processor`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/normalization-processor/) to them. The `normalization_processor` normalizes and combines the document scores from multiple query clauses, rescoring the documents according to the chosen normalization and combination techniques. #### Step 1: Configure a search pipeline -To configure a search pipeline with a `normalization_processor`, use the following request. The normalization technique in the processor is set to `min_max` and the combination technique is set to `arithmetic_mean`. The `weights` array specifies the weights to assign each query clause as decimal percentages: +To configure a search pipeline with a `normalization_processor`, use the following request. The normalization technique in the processor is set to `min_max`, and the combination technique is set to `arithmetic_mean`. The `weights` array specifies the weights assigned to each query clause as decimal percentages: ```json PUT /_search/pipeline/nlp-search-pipeline @@ -912,7 +912,7 @@ GET /my-nlp-index/_search?search_pipeline=nlp-search-pipeline ``` {% include copy-curl.html %} -Not only does OpenSearch return documents that match the semantic meaning of `wild west`, but now the documents containing the words related to the wild west theme are scored higher relative to the others: +Not only does OpenSearch return documents that match the semantic meaning of `wild west`, but now the documents containing words related to the wild west theme are also scored higher relative to the others:
@@ -988,7 +988,7 @@ Not only does OpenSearch return documents that match the semantic meaning of `wi ```
-Instead of specifying the search pipeline with every request, you can set it as a default search pipeline for the index as follows: +Instead of specifying the search pipeline in every request, you can set it as a default search pipeline for the index as follows: ```json PUT /my-nlp-index/_settings @@ -998,7 +998,7 @@ PUT /my-nlp-index/_settings ``` {% include copy-curl.html %} -You can now experiment with different weights, normalization techniques, and combination techniques. For more information, see [`normalization_processor`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/normalization-processor/) and [`hybrid` query]({{site.url}}{{site.baseurl}}/query-dsl/compound/hybrid/) documentation. +You can now experiment with different weights, normalization techniques, and combination techniques. For more information, see the [`normalization_processor`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/normalization-processor/) and [`hybrid` query]({{site.url}}{{site.baseurl}}/query-dsl/compound/hybrid/) documentation. ### Clean up @@ -1036,5 +1036,5 @@ DELETE /_plugins/_ml/model_groups/Z1eQf4oB5Vm0Tdw8EIP2 ## Further reading -- Read about the basics of semantic search in OpenSearch in [Building a semantic search engine in OpenSearch](https://opensearch.org/blog/semantic-search-solutions/) -- Read about the benefits of combining keyword and neural search, the normalization and combination technique options, and the benchmarking tests in [The ABCs of semantic search in OpenSearch: Architectures, benchmarks, and combination strategies](https://opensearch.org/blog/semantic-science-benchmarks/) \ No newline at end of file +- Read about the basics of OpenSearch semantic search in [Building a semantic search engine in OpenSearch](https://opensearch.org/blog/semantic-search-solutions/). +- Read about the benefits of combining keyword and neural search, the normalization and combination technique options, and benchmarking tests in [The ABCs of semantic search in OpenSearch: Architectures, benchmarks, and combination strategies](https://opensearch.org/blog/semantic-science-benchmarks/). \ No newline at end of file diff --git a/_query-dsl/compound/hybrid.md b/_query-dsl/compound/hybrid.md index f6392f1dd8..f317205ca2 100644 --- a/_query-dsl/compound/hybrid.md +++ b/_query-dsl/compound/hybrid.md @@ -8,7 +8,7 @@ nav_order: 70 # Hybrid query -Use a hybrid query to combine relevance scores from multiple queries into one score for a given document. A hybrid query contains a list of one or more queries and calculates document scores at the shard level independently for each subquery. The subquery rewriting is done at the coordinating node level to avoid duplicate computations. +You can use a hybrid query to combine relevance scores from multiple queries into one score for a given document. A hybrid query contains a list of one or more queries and independently calculates document scores at the shard level for each subquery. The subquery rewriting is performed at the coordinating node level in order to avoid duplicate computations. ## Example @@ -22,7 +22,7 @@ The following table lists all top-level parameters supported by `hybrid` queries Parameter | Description :--- | :--- -`queries` | An array of one or more query clauses that are used to match documents. A document must match at least one query clause to be returned in the results. The documents' relevance scores from all query clauses are combined into one score by applying a [search pipeline]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/). The maximum number of query clauses is 5. Required. +`queries` | An array of one or more query clauses that are used to match documents. A document must match at least one query clause in order to be returned in the results. The documents' relevance scores from all query clauses are combined into one score by applying a [search pipeline]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/index/). The maximum number of query clauses is 5. Required. ## Disabling hybrid queries diff --git a/_search-plugins/search-pipelines/normalization-processor.md b/_search-plugins/search-pipelines/normalization-processor.md index 18c9e226dc..5a08075094 100644 --- a/_search-plugins/search-pipelines/normalization-processor.md +++ b/_search-plugins/search-pipelines/normalization-processor.md @@ -13,11 +13,11 @@ The `normalization_processor` is a search phase results processor that runs betw ## Score normalization and combination -Many applications require both keyword matching and semantic understanding. For example, BM25 accurately provides relevant search results for a query containing keywords, and neural networks perform well when a query requires natural language understanding. Thus, you might want to combine BM25 search results with the results of k-NN or neural search. However, BM25 and k-NN search use different scales to calculate relevance scores for the matching documents. Before combining the scores from multiple queries, it is beneficial to normalize those scores so they are on the same scale, as shown by experimental data. For further reading about score normalization and combination, including benchmarks and discussion of various techniques, see this [semantic search blog](https://opensearch.org/blog/semantic-science-benchmarks/). +Many applications require both keyword matching and semantic understanding. For example, BM25 accurately provides relevant search results for a query containing keywords, and neural networks perform well when a query requires natural language understanding. Thus, you might want to combine BM25 search results with the results of a k-NN or neural search. However, BM25 and k-NN search use different scales to calculate relevance scores for the matching documents. Before combining the scores from multiple queries, it is beneficial to normalize them so that they are on the same scale, as shown by experimental data. For further reading about score normalization and combination, including benchmarks and various techniques, see [this semantic search blog post](https://opensearch.org/blog/semantic-science-benchmarks/). ## Query then fetch -OpenSearch supports two search types: `query_then_fetch` and `dfs_query_then_fetch`. The following diagram outlines the query then fetch process that includes a normalization processor. +OpenSearch supports two search types: `query_then_fetch` and `dfs_query_then_fetch`. The following diagram outlines the query-then-fetch process, which includes a normalization processor. ![Normalization processor flow diagram]({{site.url}}{{site.baseurl}}/images/normalization-processor.png) @@ -29,7 +29,7 @@ The following table lists all available request fields. Field | Data type | Description :--- | :--- | :--- -`normalization.technique` | String | The technique for normalizing scores. Valid values are [`min_max`](https://en.wikipedia.org/wiki/Feature_scaling#Rescaling_(min-max_normalization)), [`l2`](https://en.wikipedia.org/wiki/Cosine_similarity#L2-normalized_Euclidean_distance). Optional. Default is `min_max`. +`normalization.technique` | String | The technique for normalizing scores. Valid values are [`min_max`](https://en.wikipedia.org/wiki/Feature_scaling#Rescaling_(min-max_normalization)) and [`l2`](https://en.wikipedia.org/wiki/Cosine_similarity#L2-normalized_Euclidean_distance). Optional. Default is `min_max`. `combination.technique` | String | The technique for combining scores. Valid values are [`arithmetic_mean`](https://en.wikipedia.org/wiki/Arithmetic_mean), [`geometric_mean`](https://en.wikipedia.org/wiki/Geometric_mean), and [`harmonic_mean`](https://en.wikipedia.org/wiki/Harmonic_mean). Optional. Default is `arithmetic_mean`. `combination.parameters.weights` | Array of floating-point values | Specifies the weights to use for each query. Valid values are in the [0.0, 1.0] range and signify decimal percentages. The closer the weight is to 1.0, the more weight is given to a query. The number of values in the `weights` array must equal the number of queries. The sum of the values in the array must equal 1.0. Optional. If not provided, all queries are given equal weight. `tag` | String | The processor's identifier. Optional. @@ -72,7 +72,7 @@ PUT /_search/pipeline/nlp-search-pipeline ### Using a search pipeline -Provide the query clauses that you want to combine in a `hybrid` query and apply the search pipeline created in the previous section so the scores are combined using the chosen techniques: +Provide the query clauses that you want to combine in a `hybrid` query and apply the search pipeline created in the previous section so that the scores are combined using the chosen techniques: ```json GET /my-nlp-index/_search?search_pipeline=nlp-search-pipeline From e42f8ad103834d00338b0631b166fde4f2ba854b Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Wed, 13 Sep 2023 12:01:42 -0400 Subject: [PATCH 25/35] Implemented editorial comments Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 53 +++++++++---------- _search-plugins/search-pipelines/index.md | 6 +-- .../normalization-processor.md | 8 +-- .../search-pipelines/search-processors.md | 6 +-- 4 files changed, 35 insertions(+), 38 deletions(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index 108a3963bf..4a93a6e1a6 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -69,19 +69,19 @@ For more information about ML-related cluster settings, see [ML Commons cluster This tutorial consists of the following steps: -1. [**Set up an ML language model**](#step-1-set-up-an-ml-language-model) - 1. [Choose a language model](#step-1a-choose-a-language-model) - 1. [Register a model group](#step-1b-register-a-model-group) - 1. [Register the model to the model group](#step-1c-register-the-model-to-the-model-group) - 1. [Deploy the model](#step-1d-deploy-the-model) -1. [**Ingest data with neural search**](#step-2-ingest-data-with-neural-search) - 1. [Create an ingest pipeline for neural search](#step-2a-create-an-ingest-pipeline-for-neural-search) - 1. [Create a k-NN index](#step-2b-create-a-k-nn-index) - 1. [Ingest documents into the index](#step-2c-ingest-documents-into-the-index) -1. [**Search the data**](#step-3-search-the-data) - - [Search with a keyword search](#search-with-a-keyword-search) - - [Search with a neural search](#search-with-a-neural-search) - - [Search with a combined keyword search and neural search](#search-with-a-combined-keyword-search-and-neural-search) +1. [**Set up an ML language model**](#step-1-set-up-an-ml-language-model). + 1. [Choose a language model](#step-1a-choose-a-language-model). + 1. [Register a model group](#step-1b-register-a-model-group). + 1. [Register the model to the model group](#step-1c-register-the-model-to-the-model-group). + 1. [Deploy the model](#step-1d-deploy-the-model). +1. [**Ingest data with neural search**](#step-2-ingest-data-with-neural-search). + 1. [Create an ingest pipeline for neural search](#step-2a-create-an-ingest-pipeline-for-neural-search). + 1. [Create a k-NN index](#step-2b-create-a-k-nn-index). + 1. [Ingest documents into the index](#step-2c-ingest-documents-into-the-index). +1. [**Search the data**](#step-3-search-the-data). + - [Search with a keyword search](#search-with-a-keyword-search). + - [Search with a neural search](#search-with-a-neural-search). + - [Search with a combined keyword search and neural search](#search-with-a-combined-keyword-search-and-neural-search). Some steps in the tutorial contain optional `Test it` sections. You can ensure the step worked by running requests in these sections. @@ -97,7 +97,7 @@ Neural search requires a language model to generate vector embeddings from text ### Step 1(a): Choose a language model -For this tutorial, you'll use the [DistilBERT](https://huggingface.co/docs/transformers/model_doc/distilbert) model from Hugging Face. It is one of the pretrained sentence transformer models available in OpenSearch. You'll need the name, version, and dimension of the model to register it. You can find this information in the [pretrained model table]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/#sentence-transformers) by selecting the `config_url` link for the TorchScript artifact of the model: +For this tutorial, you'll use the [DistilBERT](https://huggingface.co/docs/transformers/model_doc/distilbert) model from Hugging Face. It is one of the pretrained sentence transformer models available in OpenSearch. You'll need the name, version, and dimension of the model to register it. You can find this information in the [pretrained model table]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/#sentence-transformers) by selecting the `config_url` link corresponding to the model's TorchScript artifact: - The model name is `huggingface/sentence-transformers/msmarco-distilbert-base-tas-b`. - The model version is `1.0.1`. @@ -105,22 +105,19 @@ For this tutorial, you'll use the [DistilBERT](https://huggingface.co/docs/trans #### Advanced: Using a different model -You can choose to use another model: - -- One of the [pretrained language models provided by OpenSearch]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/). -- Your own model. For instructions how to set up a custom model, see [Model serving framework]({{site.url}}{{site.baseurl}}/ml-commons-plugin/ml-framework/). +Alternatively, you can choose to use one of the [pretrained language models provided by OpenSearch]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/) or your own custom model. For instructions on how to set up a custom model, see [Model-serving framework]({{site.url}}{{site.baseurl}}/ml-commons-plugin/ml-framework/). Take note of the dimensionality of the model because you'll need it when you set up a k-NN index. {: .important} ### Step 1(b): Register a model group -For access control, models are organized into model groups—collections of versions of a particular model. Each model group name in the cluster must be globally unique. Registering a model group ensures the uniqueness of the model group name. +For access control, models are organized into model groups (collections of versions of a particular model). Each model group name in the cluster must be globally unique. Registering a model group ensures the uniqueness of the model group name. If you are registering the first version of a model without first registering the model group, a new model group is created automatically. For more information, see [Model access control]({{site.url}}{{site.baseurl}}/ml-commons-plugin/model-access-control/). {: .tip} -To register a model group with a `public` access level, send the following request: +To register a model group with the access mode set to `public`, send the following request: ```json POST /_plugins/_ml/model_groups/_register @@ -236,7 +233,7 @@ GET /_plugins/_ml/tasks/aFeif4oB5Vm0Tdw8yoN7 ``` {% include copy-curl.html %} -Once the task is complete, the task status will be `COMPLETED` and the Tasks API response will contain a model ID for the registered model: +Once the task is complete, the task state will be `COMPLETED` and the Tasks API response will contain a model ID for the registered model: ```json { @@ -352,7 +349,7 @@ For more information, see [Model serving framework]({{site.url}}{{site.baseurl}} ### Step 1(d): Deploy the model -Once the model is registered, it is saved in the model index. Next, you'll need to deploy the model: create a model instance, caching the model in memory. To deploy the model, provide its model ID to the `_deploy` endpoint: +Once the model is registered, it is saved in the model index. Next, you'll need to deploy the model. Deploying a model creates a model instance and caches the model in memory. To deploy the model, provide its model ID to the `_deploy` endpoint: ```json POST /_plugins/_ml/models/aVeif4oB5Vm0Tdw8zYO2/_deploy @@ -375,7 +372,7 @@ GET /_plugins/_ml/tasks/ale6f4oB5Vm0Tdw8NINO ``` {% include copy-curl.html %} -Once the task is complete, the task status will be `COMPLETED`: +Once the task is complete, the task state will be `COMPLETED`: ```json { @@ -412,7 +409,7 @@ POST /_plugins/_ml/models/_search ``` {% include copy-curl.html %} -The response shows the model status as `DEPLOYED`: +The response shows the model state as `DEPLOYED`: ```json { @@ -480,7 +477,7 @@ GET /_plugins/_ml/profile/models ## Step 2: Ingest data with neural search -Neural search uses a language model to transform text into vector embeddings. During ingestion, neural search creates vector embeddings for the text fields in the request. During search, you can use the same model on the query text to perform vector similarity search on the documents. +Neural search uses a language model to transform text into vector embeddings. During ingestion, neural search creates vector embeddings for the text fields in the request. During search, you can generate vector embeddings for the query text by applying the same model, allowing you perform vector similarity search on the documents. ### Step 2(a): Create an ingest pipeline for neural search @@ -764,7 +761,7 @@ GET /my-nlp-index/_search ``` {% include copy-curl.html %} -The results contain all five documents. The document order is now closer to the desired one because semantic meaning is considered: +This time, the response not only contains all five documents, but the document order is improved because neural search considers semantic meaning:
@@ -876,7 +873,7 @@ PUT /_search/pipeline/nlp-search-pipeline #### Step 2: Search with the hybrid query -The query you'll use to combine the `match` and `neural` query clauses is the [`hybrid` query]({{site.url}}{{site.baseurl}}/query-dsl/compound/hybrid/). Make sure to apply the previously created `nlp-search-pipeline` to the request in the query parameter: +You'll use the [`hybrid` query]({{site.url}}{{site.baseurl}}/query-dsl/compound/hybrid/) to combine the `match` and `neural` query clauses. Make sure to apply the previously created `nlp-search-pipeline` to the request in the query parameter: ```json GET /my-nlp-index/_search?search_pipeline=nlp-search-pipeline @@ -1002,7 +999,7 @@ You can now experiment with different weights, normalization techniques, and com ### Clean up -After you're done, delete the components you've created from the cluster: +After you're done, delete the components you've created in this tutorial from the cluster: ```json DELETE /my-nlp-index diff --git a/_search-plugins/search-pipelines/index.md b/_search-plugins/search-pipelines/index.md index b2a8a44e1e..a9ff3cd18e 100644 --- a/_search-plugins/search-pipelines/index.md +++ b/_search-plugins/search-pipelines/index.md @@ -14,9 +14,9 @@ You can use _search pipelines_ to build new or reuse existing result rerankers, The following is a list of search pipeline terminology: -* [_Search request processor_]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/search-processors#search-request-processors): A component that takes a search request (the query and the metadata passed in the request), performs an operation with or on the search request, and returns a search request. -* [_Search response processor_]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/search-processors#search-response-processors): A component that takes a search response and search request (the query, results, and metadata passed in the request), performs an operation with or on the search response, and returns a search response. -* [_Search phase results processor_]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/search-processors#search-phase-results-processors): A component that runs between search phases at the coordinating node level. A search phase results processor takes the results retrieved from one search phase and transforms them before passing them to the next search phase. +* [_Search request processor_]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/search-processors#search-request-processors): A component that intercepts a search request (the query and the metadata passed in the request), performs an operation with or on the search request, and returns the search request. +* [_Search response processor_]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/search-processors#search-response-processors): A component that intercepts a search response and search request (the query, results, and metadata passed in the request), performs an operation with or on the search response, and returns the search response. +* [_Search phase results processor_]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/search-processors#search-phase-results-processors): A component that runs between search phases at the coordinating node level. A search phase results processor intercepts the results retrieved from one search phase and transforms them before passing them to the next search phase. * [_Processor_]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/search-processors/): Either a search request processor or a search response processor. * _Search pipeline_: An ordered list of processors that is integrated into OpenSearch. The pipeline intercepts a query, performs processing on the query, sends it to OpenSearch, intercepts the results, performs processing on the results, and returns them to the calling application, as shown in the following diagram. diff --git a/_search-plugins/search-pipelines/normalization-processor.md b/_search-plugins/search-pipelines/normalization-processor.md index 18c9e226dc..fb4a6f40a9 100644 --- a/_search-plugins/search-pipelines/normalization-processor.md +++ b/_search-plugins/search-pipelines/normalization-processor.md @@ -9,7 +9,7 @@ grand_parent: Search pipelines # Normalization processor -The `normalization_processor` is a search phase results processor that runs between the query and fetch phases of search. It intercepts the query phase results and then normalizes and combines the document scores from different query clauses before passing the documents to the fetch phase. +The `normalization_processor` is a search phase results processor that runs between the query and fetch phases of search execution. It intercepts the query phase results and then normalizes and combines the document scores from different query clauses before passing the documents to the fetch phase. ## Score normalization and combination @@ -21,7 +21,7 @@ OpenSearch supports two search types: `query_then_fetch` and `dfs_query_then_fet ![Normalization processor flow diagram]({{site.url}}{{site.baseurl}}/images/normalization-processor.png) -When you send a search request to a node, this node becomes a _coordinating node_. During the first phase of search, the _query phase_, the coordinating node routes the search request to all shards in the index, including primary and replica shards. Each shard then runs the search query locally and returns metadata about the matching documents, which includes their doc IDs and relevance scores. The `normalization_processor` then normalizes and combines scores from different query clauses. The coordinating node merges and sorts the local result lists, compiling a global list of top documents that match the query. After that, search enters a _fetch phase_, in which the coordinating node requests the documents in the global list from the shards where they reside. Each shard returns the documents' `_source` to the coordinating node. Finally, the coordinating node sends a search response containing the results back to you. +When you send a search request to a node, the node becomes a _coordinating node_. During the first phase of search, the _query phase_, the coordinating node routes the search request to all shards in the index, including primary and replica shards. Each shard then runs the search query locally and returns metadata about the matching documents, which includes their document IDs and relevance scores. The `normalization_processor` then normalizes and combines scores from different query clauses. The coordinating node merges and sorts the local lists of results, compiling a global list of top documents that match the query. After that, search execution enters a _fetch phase_, in which the coordinating node requests the documents in the global list from the shards where they reside. Each shard returns the documents' `_source` to the coordinating node. Finally, the coordinating node sends a search response containing the results back to you. ## Request fields @@ -34,7 +34,7 @@ Field | Data type | Description `combination.parameters.weights` | Array of floating-point values | Specifies the weights to use for each query. Valid values are in the [0.0, 1.0] range and signify decimal percentages. The closer the weight is to 1.0, the more weight is given to a query. The number of values in the `weights` array must equal the number of queries. The sum of the values in the array must equal 1.0. Optional. If not provided, all queries are given equal weight. `tag` | String | The processor's identifier. Optional. `description` | String | A description of the processor. Optional. -`ignore_failure` | Boolean | For this processor, this value is ignored. If the processor fails, the pipeline always fails with an error. +`ignore_failure` | Boolean | For this processor, this value is ignored. If the processor fails, the pipeline always fails and returns an error. ## Example @@ -42,7 +42,7 @@ The following example demonstrates using a search pipeline with a `normalization ### Creating a search pipeline -The following request creates a search pipeline with a `normalization_processor` that uses the `min_max` normalization technique and the `arithmetic_mean` combination technique: +The following request creates a search pipeline containing a `normalization_processor` that uses the `min_max` normalization technique and the `arithmetic_mean` combination technique: ```json PUT /_search/pipeline/nlp-search-pipeline diff --git a/_search-plugins/search-pipelines/search-processors.md b/_search-plugins/search-pipelines/search-processors.md index dbee86e28f..3bf4061cd9 100644 --- a/_search-plugins/search-pipelines/search-processors.md +++ b/_search-plugins/search-pipelines/search-processors.md @@ -17,7 +17,7 @@ Search processors can be of the following types: ## Search request processors -A search request processor takes a search request (the query and the metadata passed in the request) and performs an operation on the search request before submitting the search request to the index. +A search request processor intercepts a search request (the query and the metadata passed in the request), performs an operation with or on the search request, and submits the search request to the index. The following table lists all supported search request processors. @@ -28,7 +28,7 @@ Processor | Description | Earliest available version ## Search response processors -A search response processor performs an operation on the search response and returns a search response. +A search response processor intercepts a search response and search request (the query, results, and metadata passed in the request), performs an operation with or on the search response, and returns the search response. The following table lists all supported search response processors. @@ -39,7 +39,7 @@ Processor | Description | Earliest available version ## Search phase results processors -A search phase results processor runs between search phases at the coordinating node level. It takes the results retrieved from one search phase and transforms them before passing them to the next search phase. +A search phase results processor runs between search phases at the coordinating node level. It intercepts the results retrieved from one search phase and transforms them before passing them to the next search phase. The following table lists all supported search request processors. From e1265080998f25ef71ce7ec1a056a71910254048 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Wed, 13 Sep 2023 12:11:33 -0400 Subject: [PATCH 26/35] Change links Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index 964d35e1fa..05eb9f7c32 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -79,11 +79,11 @@ This tutorial consists of the following steps: 1. [Create a k-NN index](#step-2b-create-a-k-nn-index). 1. [Ingest documents into the index](#step-2c-ingest-documents-into-the-index). 1. [**Search the data**](#step-3-search-the-data). - - [Search with a keyword search](#search-with-a-keyword-search). - - [Search with a neural search](#search-with-a-neural-search). - - [Search with a combined keyword search and neural search](#search-with-a-combined-keyword-search-and-neural-search). + - [Search using a keyword search](#search-using-a-keyword-search). + - [Search using a neural search](#search-using-a-neural-search). + - [Search using a combined keyword search and neural search](#search-using-a-combined-keyword-search-and-neural-search). -Some steps in the tutorial contain optional `Test it` sections. You can ensure that the step worked by running requests in these sections. +Some steps in the tutorial contain optional `Test it` sections. You can ensure that the step was successful by running requests in these sections. After you're done, follow the steps in the [Clean up](#clean-up) section to delete all created components. From 0c7b587451adc13f935012885b9d5830bdd94987 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Wed, 13 Sep 2023 12:40:57 -0400 Subject: [PATCH 27/35] More editorial feedback Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index 05eb9f7c32..4a1325402b 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -477,7 +477,7 @@ GET /_plugins/_ml/profile/models ## Step 2: Ingest data with neural search -Neural search uses a language model to transform text into vector embeddings. During ingestion, neural search creates vector embeddings for the text fields in the request. During search, you can generate vector embeddings for the query text by applying the same model, allowing you perform vector similarity search on the documents. +Neural search uses a language model to transform text into vector embeddings. During ingestion, neural search creates vector embeddings for the text fields in the request. During search, you can generate vector embeddings for the query text by applying the same model, allowing you to perform vector similarity search on the documents. ### Step 2(a): Create an ingest pipeline for neural search @@ -761,7 +761,7 @@ GET /my-nlp-index/_search ``` {% include copy-curl.html %} -This time, the response not only contains all five documents, but the document order is improved because neural search considers semantic meaning: +This time, the response not only contains all five documents, but the document order is also improved because neural search considers semantic meaning:
@@ -999,7 +999,7 @@ You can now experiment with different weights, normalization techniques, and com ### Clean up -After you're done, delete the components you've created in this tutorial from the cluster: +After you're done, delete the components you've created in tutorial from the cluster: ```json DELETE /my-nlp-index From 6d48cafa54b76db2aab36c67c3f1ad52e56e13d8 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Wed, 13 Sep 2023 13:03:32 -0400 Subject: [PATCH 28/35] Change model-serving framework to ML framework Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index 4a1325402b..604e69e494 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -345,7 +345,7 @@ POST /_plugins/_ml/models/_register } ``` -For more information, see [Model-serving framework]({{site.url}}{{site.baseurl}}/ml-commons-plugin/ml-framework/). +For more information, see [ML framework]({{site.url}}{{site.baseurl}}/ml-commons-plugin/ml-framework/). ### Step 1(d): Deploy the model From 838b42fcdbc41e555cefc201ad9c64953221d193 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Wed, 13 Sep 2023 14:18:22 -0400 Subject: [PATCH 29/35] Use get model API to check model status Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 154 +++++++++----------------- 1 file changed, 50 insertions(+), 104 deletions(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index 604e69e494..bca31384d4 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -261,7 +261,7 @@ You'll need the model ID in order to use this model for several of the following Search for the newly created model by providing its ID in the request: ```json -POST /_plugins/_ml/models/_search/aVeif4oB5Vm0Tdw8zYO2 +GET /_plugins/_ml/models/aVeif4oB5Vm0Tdw8zYO2 ``` {% include copy-curl.html %} @@ -269,51 +269,31 @@ The response contains the model: ```json { - "took": 1, - "timed_out": false, - "_shards": { - "total": 1, - "successful": 1, - "skipped": 0, - "failed": 0 + "name": "huggingface/sentence-transformers/msmarco-distilbert-base-tas-b", + "model_group_id": "Z1eQf4oB5Vm0Tdw8EIP2", + "algorithm": "TEXT_EMBEDDING", + "model_version": "1", + "model_format": "TORCH_SCRIPT", + "model_state": "REGISTERED", + "model_content_size_in_bytes": 266352827, + "model_content_hash_value": "acdc81b652b83121f914c5912ae27c0fca8fabf270e6f191ace6979a19830413", + "model_config": { + "model_type": "distilbert", + "embedding_dimension": 768, + "framework_type": "SENTENCE_TRANSFORMERS", + "all_config": """{"_name_or_path":"old_models/msmarco-distilbert-base-tas-b/0_Transformer","activation":"gelu","architectures":["DistilBertModel"],"attention_dropout":0.1,"dim":768,"dropout":0.1,"hidden_dim":3072,"initializer_range":0.02,"max_position_embeddings":512,"model_type":"distilbert","n_heads":12,"n_layers":6,"pad_token_id":0,"qa_dropout":0.1,"seq_classif_dropout":0.2,"sinusoidal_pos_embds":false,"tie_weights_":true,"transformers_version":"4.7.0","vocab_size":30522}""" }, - "hits": { - "total": { - "value": 1, - "relation": "eq" - }, - "max_score": 1, - "hits": [ - { - "_index": ".plugins-ml-model", - "_id": "aVeif4oB5Vm0Tdw8zYO2", - "_version": 2, - "_seq_no": 95, - "_primary_term": 2, - "_score": 1, - "_source": { - "model_version": "1", - "created_time": 1694358490550, - "model_format": "TORCH_SCRIPT", - "model_state": "REGISTERED", - "total_chunks": 27, - "model_content_hash_value": "acdc81b652b83121f914c5912ae27c0fca8fabf270e6f191ace6979a19830413", - "model_config": { - "all_config": """{"_name_or_path":"old_models/msmarco-distilbert-base-tas-b/0_Transformer","activation":"gelu","architectures":["DistilBertModel"],"attention_dropout":0.1,"dim":768,"dropout":0.1,"hidden_dim":3072,"initializer_range":0.02,"max_position_embeddings":512,"model_type":"distilbert","n_heads":12,"n_layers":6,"pad_token_id":0,"qa_dropout":0.1,"seq_classif_dropout":0.2,"sinusoidal_pos_embds":false,"tie_weights_":true,"transformers_version":"4.7.0","vocab_size":30522}""", - "model_type": "distilbert", - "embedding_dimension": 768, - "framework_type": "SENTENCE_TRANSFORMERS" - }, - "last_updated_time": 1694358499122, - "last_registered_time": 1694358499121, - "name": "huggingface/sentence-transformers/msmarco-distilbert-base-tas-b", - "model_group_id": "Z1eQf4oB5Vm0Tdw8EIP2", - "model_content_size_in_bytes": 266352827, - "algorithm": "TEXT_EMBEDDING" - } - } - ] - } + "created_time": 1694482261832, + "last_updated_time": 1694482324282, + "last_registered_time": 1694482270216, + "last_deployed_time": 1694482324282, + "total_chunks": 27, + "planning_worker_node_count": 1, + "current_worker_node_count": 1, + "planning_worker_nodes": [ + "4p6FVOmJRtu3wehDD74hzQ" + ], + "deploy_to_all_nodes": true } ``` @@ -398,14 +378,7 @@ Once the task is complete, the task state will be `COMPLETED`: Search for the deployed model by providing its ID in the request: ```json -POST /_plugins/_ml/models/_search -{ - "query": { - "match": { - "_id": "aVeif4oB5Vm0Tdw8zYO2" - } - } -} +GET /_plugins/_ml/models/aVeif4oB5Vm0Tdw8zYO2 ``` {% include copy-curl.html %} @@ -413,58 +386,31 @@ The response shows the model state as `DEPLOYED`: ```json { - "took": 0, - "timed_out": false, - "_shards": { - "total": 1, - "successful": 1, - "skipped": 0, - "failed": 0 + "name": "huggingface/sentence-transformers/msmarco-distilbert-base-tas-b", + "model_group_id": "Z1eQf4oB5Vm0Tdw8EIP2", + "algorithm": "TEXT_EMBEDDING", + "model_version": "1", + "model_format": "TORCH_SCRIPT", + "model_state": "DEPLOYED", + "model_content_size_in_bytes": 266352827, + "model_content_hash_value": "acdc81b652b83121f914c5912ae27c0fca8fabf270e6f191ace6979a19830413", + "model_config": { + "model_type": "distilbert", + "embedding_dimension": 768, + "framework_type": "SENTENCE_TRANSFORMERS", + "all_config": """{"_name_or_path":"old_models/msmarco-distilbert-base-tas-b/0_Transformer","activation":"gelu","architectures":["DistilBertModel"],"attention_dropout":0.1,"dim":768,"dropout":0.1,"hidden_dim":3072,"initializer_range":0.02,"max_position_embeddings":512,"model_type":"distilbert","n_heads":12,"n_layers":6,"pad_token_id":0,"qa_dropout":0.1,"seq_classif_dropout":0.2,"sinusoidal_pos_embds":false,"tie_weights_":true,"transformers_version":"4.7.0","vocab_size":30522}""" }, - "hits": { - "total": { - "value": 1, - "relation": "eq" - }, - "max_score": 1, - "hits": [ - { - "_index": ".plugins-ml-model", - "_id": "aVeif4oB5Vm0Tdw8zYO2", - "_version": 4, - "_seq_no": 97, - "_primary_term": 2, - "_score": 1, - "_source": { - "last_deployed_time": 1694360027940, - "model_version": "1", - "created_time": 1694358490550, - "deploy_to_all_nodes": true, - "model_format": "TORCH_SCRIPT", - "model_state": "DEPLOYED", - "planning_worker_node_count": 1, - "total_chunks": 27, - "model_content_hash_value": "acdc81b652b83121f914c5912ae27c0fca8fabf270e6f191ace6979a19830413", - "model_config": { - "all_config": """{"_name_or_path":"old_models/msmarco-distilbert-base-tas-b/0_Transformer","activation":"gelu","architectures":["DistilBertModel"],"attention_dropout":0.1,"dim":768,"dropout":0.1,"hidden_dim":3072,"initializer_range":0.02,"max_position_embeddings":512,"model_type":"distilbert","n_heads":12,"n_layers":6,"pad_token_id":0,"qa_dropout":0.1,"seq_classif_dropout":0.2,"sinusoidal_pos_embds":false,"tie_weights_":true,"transformers_version":"4.7.0","vocab_size":30522}""", - "model_type": "distilbert", - "embedding_dimension": 768, - "framework_type": "SENTENCE_TRANSFORMERS" - }, - "last_updated_time": 1694360027940, - "last_registered_time": 1694358499121, - "name": "huggingface/sentence-transformers/msmarco-distilbert-base-tas-b", - "current_worker_node_count": 1, - "model_group_id": "Z1eQf4oB5Vm0Tdw8EIP2", - "model_content_size_in_bytes": 266352827, - "planning_worker_nodes": [ - "4p6FVOmJRtu3wehDD74hzQ" - ], - "algorithm": "TEXT_EMBEDDING" - } - } - ] - } + "created_time": 1694482261832, + "last_updated_time": 1694482324282, + "last_registered_time": 1694482270216, + "last_deployed_time": 1694482324282, + "total_chunks": 27, + "planning_worker_node_count": 1, + "current_worker_node_count": 1, + "planning_worker_nodes": [ + "4p6FVOmJRtu3wehDD74hzQ" + ], + "deploy_to_all_nodes": true } ``` From 9ead90881f172a784a39ac7163880800bf6921f1 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Wed, 13 Sep 2023 17:59:40 -0400 Subject: [PATCH 30/35] Implemented tech review comments Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 43 ++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index bca31384d4..9de74b1d14 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -38,7 +38,7 @@ You'll find descriptions of all these components as you follow the tutorial, so ## Prerequisites -For this simple example, you'll use an OpenSearch-provided machine learning (ML) model and a cluster with no dedicated ML nodes. To ensure that this basic local setup works, send the following request to update ML-related cluster settings: +For this simple setup, you'll use an OpenSearch-provided machine learning (ML) model and a cluster with no dedicated ML nodes. To ensure that this basic local setup works, send the following request to update ML-related cluster settings: ```json PUT _cluster/settings @@ -61,7 +61,7 @@ PUT _cluster/settings For a more advanced setup, note the following requirements: - To register a custom model, you need to specify an additional `"allow_registering_model_via_url": "true"` cluster setting. -- On clusters with dedicated ML nodes, you may want to specify `"only_run_on_ml_node": "true"` for improved performance. +- In production, it's best practice to separate the workloads by having dedicated ML nodes. On clusters with dedicated ML nodes, specify `"only_run_on_ml_node": "true"` for improved performance. For more information about ML-related cluster settings, see [ML Commons cluster settings]({{site.url}}{{site.baseurl}}/ml-commons-plugin/cluster-settings/). @@ -297,7 +297,7 @@ The response contains the model: } ``` -The response contains the model information, including the `model_state` (`REGISTERED`) and the number of chunks into which it was split `total_chunks` (27). +The response contains the model information. You can see that the `model_state` is `REGISTERED`. Additionally, the model was split into 27 chunks, as shown in the `total_chunks` field.
#### Advanced: Registering a custom model @@ -427,7 +427,7 @@ Neural search uses a language model to transform text into vector embeddings. Du ### Step 2(a): Create an ingest pipeline for neural search -The first step in setting up [neural search]({{site.url}}{{site.baseurl}}/search-plugins/neural-search/) is to create an [ingest pipeline]({{site.url}}{{site.baseurl}}/api-reference/ingest-apis/index/). The ingest pipeline will contain one processor: a task that transforms document fields. For neural search, you'll need to set up a `text_embedding` processor that takes in text and creates vector embeddings from that text. You'll need the `model_id` of the model you set up in the previous section and a `field_map`, which specifies the name of the field from which to take the text (`text`) and the name of the field in which to record embeddings (`passage_embedding`): +Now that you have deployed a model, you can use this model to configure [neural search]({{site.url}}{{site.baseurl}}/search-plugins/neural-search/). First, you need to create an [ingest pipeline]({{site.url}}{{site.baseurl}}/api-reference/ingest-apis/index/) that contains one processor: a task that transforms document fields before documents are ingested into an index. For neural search, you'll set up a `text_embedding` processor that creates vector embeddings from text. You'll need the `model_id` of the model you set up in the previous section and a `field_map`, which specifies the name of the field from which to take the text (`text`) and the name of the field in which to record embeddings (`passage_embedding`): ```json PUT /_ingest/pipeline/nlp-ingest-pipeline @@ -588,6 +588,37 @@ PUT /my-nlp-index/_doc/5 ``` {% include copy-curl.html %} +When the documents are ingested into the index, the `text_embedding` processor creates an additional field that contains vector embeddings and adds that field to the document. To see an example document that is indexed, search for document 1: + +```json +GET /my-nlp-index/_search/1 +``` +{% include copy-curl.html %} + +The response shows the document `_source` containing the original `text` and `id` fields and the added `passage_embeddings` field: + +```json +{ + "_index": "my-nlp-index", + "_id": "1", + "_version": 1, + "_seq_no": 0, + "_primary_term": 1, + "found": true, + "_source": { + "passage_embedding": [ + 0.04491629, + -0.34105563, + 0.036822468, + -0.14139028, + ... + ], + "text": "A West Virginia university women 's basketball team , officials , and a small gathering of fans are in a West Virginia arena .", + "id": "4319130149.jpg" + } +} +``` + ## Step 3: Search the data Now you'll search the index using keyword search, neural search, and a combination of the two. @@ -943,6 +974,10 @@ PUT /my-nlp-index/_settings You can now experiment with different weights, normalization techniques, and combination techniques. For more information, see the [`normalization_processor`]({{site.url}}{{site.baseurl}}/search-plugins/search-pipelines/normalization-processor/) and [`hybrid` query]({{site.url}}{{site.baseurl}}/query-dsl/compound/hybrid/) documentation. +#### Advanced + +You can parametrize the search by using search templates, hiding implementation details and reducing the number of nested levels and thus the query complexity. For more information, see [search templates]({{site.url}}{{site.baseurl}}/search-plugins/search-template/). + ### Clean up After you're done, delete the components you've created in tutorial from the cluster: From 8f292f1300cbcc2358af5c92953a1fc7312c1bde Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Thu, 14 Sep 2023 18:25:31 -0400 Subject: [PATCH 31/35] Added neural search description and diagram Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 12 +++++++++--- images/neural-search-ingestion.png | Bin 0 -> 56324 bytes images/neural-search-query.png | Bin 0 -> 42753 bytes 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 images/neural-search-ingestion.png create mode 100644 images/neural-search-query.png diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index 9de74b1d14..2bc541e67c 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -19,7 +19,13 @@ In this tutorial, you'll learn how to: It's helpful to understand the following terms before starting this tutorial: - _Semantic search_: Employs neural search in order to determine the intention of the user's query in the search context and improve search relevance. -- _Neural search_: When indexing documents containing text, neural search uses language models to generate vector embeddings from that text. When you then use a _neural query_, the query text is passed through a language model, and the resulting vector embeddings are compared with the document text vector embeddings to find the most relevant results. +- _Neural search_: Facilitates vector search at ingestion time and at search time: + - At ingestion time, neural search uses language models to generate vector embeddings from the text fields in the document. The documents containing both the original text field and the vector embedding of the field are then indexed in a k-NN index, as shown in the following diagram. + + ![Neural search at ingestion time diagram]({{site.url}}{{site.baseurl}}/images/neural-search-ingestion.png) + - At search time, when you then use a _neural query_, the query text is passed through a language model, and the resulting vector embeddings are compared with the document text vector embeddings to find the most relevant results, as shown in the following diagram. + + ![Neural search at search time diagram]({{site.url}}{{site.baseurl}}/images/neural-search-query.png) ## OpenSearch components for semantic search @@ -97,7 +103,7 @@ Neural search requires a language model in order to generate vector embeddings f ### Step 1(a): Choose a language model -For this tutorial, you'll use the [DistilBERT](https://huggingface.co/docs/transformers/model_doc/distilbert) model from Hugging Face. It is one of the pretrained sentence transformer models available in OpenSearch. You'll need the name, version, and dimension of the model to register it. You can find this information in the [pretrained model table]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/#sentence-transformers) by selecting the `config_url` link corresponding to the model's TorchScript artifact: +For this tutorial, you'll use the [DistilBERT](https://huggingface.co/docs/transformers/model_doc/distilbert) model from Hugging Face. It is one of the pretrained sentence transformer models available in OpenSearch that has shown one of the best results in benchmarking tests (for details, see [this blog](https://opensearch.org/blog/semantic-science-benchmarks/)). You'll need the name, version, and dimension of the model to register it. You can find this information in the [pretrained model table]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/#sentence-transformers) by selecting the `config_url` link corresponding to the model's TorchScript artifact: - The model name is `huggingface/sentence-transformers/msmarco-distilbert-base-tas-b`. - The model version is `1.0.1`. @@ -105,7 +111,7 @@ For this tutorial, you'll use the [DistilBERT](https://huggingface.co/docs/trans #### Advanced: Using a different model -Alternatively, you can choose to use one of the [pretrained language models provided by OpenSearch]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/) or your own custom model. For instructions on how to set up a custom model, see [ML framework]({{site.url}}{{site.baseurl}}/ml-commons-plugin/ml-framework/). +Alternatively, you can choose to use one of the [pretrained language models provided by OpenSearch]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/) or your own custom model. For information about choosing a model, see [Further reading](#further-reading). For instructions on how to set up a custom model, see [ML framework]({{site.url}}{{site.baseurl}}/ml-commons-plugin/ml-framework/). Take note of the dimensionality of the model because you'll need it when you set up a k-NN index. {: .important} diff --git a/images/neural-search-ingestion.png b/images/neural-search-ingestion.png new file mode 100644 index 0000000000000000000000000000000000000000..9e69d80f548e8cbecbc7162bd36b6df977897acc GIT binary patch literal 56324 zcmeFYWmKHY(gunL4esvl?hrJ%yF0<%-C>YGun>Z~y9IZ5x8UyXcaptx&i>B6fA0Uw zTC?Vzx4NsltE)?%E+Q1L12G!W`%c*D44T6z+e$WUBJ2q6eG11K`hV`orOrU^tMmWnrqvnNot96Q|c zp2zvOw-QOG#sh1ROrc<7?t^2rV0oe=Y;B?u5D?XXg*r8JXtPE*Lrr2OqAoWxF5j4b zGcMN{!STmnh$c{f@#d}s4LU0xnYxjQU^(CB#td7ckf{HLl>!!A9sknu?&5B zB>c~1ygiTgH+$ab%}Ti_qR<(4oAk^e&&H%N$!g|aGx{&u$;ma@GDI*nb0M_G3OTE#^)J7c5W#2*sWag>GeuTejIhe8&9B)N$7-6HMeb+ zIOl^?4(e&z47LS$n89FPM#g{Wy@g;<#uKC^CiDKz>R~9o$IVZ@;MUnMYmFHh*mL9b zpjQs(!Qb0jko%UWPoVsRla#nedxh63%a-ejUSxgO>uBu;JxN41D7!0%#on!{OQl3`oiV2ZDWWVf%rKHbMt;mTm7H@}+#-CqOoywV5g9=Djr|d^3Rk~C0 zFf9NALXPV{XIn3s!fh@x{%l~@4q7dQTnI{^DX5)_U=PC30e>Ix)cClMAXV+VvgVm>i+m#|4V zEcJ)Oxc4RynnKP4Fqi(%nM715kpY{;0%U=6BFmY4g-A3(G+Cbe=KEyM7|rlE!gpE3 z7I>cEK7oo%Fi9b*hPn%A`Q7jfnC}DBJ1*>TzCoII^KPIt!V17QbeV6Ioa?$H`GU%4 zgGhmq$4&`h=j>B)s({eIqKBbmb7VUz_7>8ppcaGA1~o_Hh_;IQh#F6-up!t0T!p-H zL?*4BaXd+R2z5itMH;flbL?`$CQ$b)uvo+)jf1QDnV6AN+($miywAr{4zldj=~(Fn z7_1prX?T72^tig2@7QLNNxj_+8kx@ayq^jDpT4qsnk#CW-y0seBk;PHX(a_Oq zduT^)7-1-9+hr&9a_|!JvVZ$&N044gw@0H#x7Jv3I$Jv`AMFde4q78@6YMCg8@d4X z6dFHT0-76bJ+*+^2U=_zdS$ILXQ6|@DQQzp(|4xzj^&rkJMU;JBeSI9hIkS`C0fxW z6n`q#D+ZfYnsq8>oZX()nL(M2oZ*@6n=Lw&JLJ0|xnVsVw{&EKqEA)(z$>3Np)h$q zhh(Yp>H3q5)uCnRC)-);Iqy=jnQW_GOLR*YD_M&Yt0$|9dD|J^Op%GKBeMPD>4VAC zso80S`PK5}eBS(m%>J3-DXST;T=_Y-Ir-^|sdi(3_Dozd>|@SGOK4mzTnelt+%=}a zXlsDG_*3*FhB3x6{Vrp<7N3C(iy4+V4kr#`>Ok6ksx`YMtA}Z^4UTE^a&7;0Not*D z^&)Xi*e=Viz^?Wv@5s6amR2SG5dEn}JUu@>r)Ir&TRltlrWSr>v$<^DZNr-Fs?AGp z-<-I`v~w)6@Ogg@ZULj@*#{DqM zadn63&hU50)OTfwhcNk26|lC@zJ!}NN=TPXDuOC#OZeh2QSVC-(_q~Y8Q}>5OwkKS z4(v+Q@tBJQAyKeV#Sve1POH4nEUMfaqgD{cItWNCUiG+jbfqEQmlZ(!7A zjAJBF1E!%`I;df0Ep`mC9NilG61dexy$Nu_ZwgtN zb&Z7gc@>g&<8fl4*;XVzlWy5rG6f@-BZwv4YJ~cvt)cC^OIqD^ymK*T9%e7K?^|P9 z%e`9r9_f$5IG8w8Y=b%aY>(G+>!`h^2h1u)U9`is)bl(Q8=d|znthaPKN*u&gG<{p4^)U13^~ktdd{Fmgddu1lv*X=z zy~S8S!&?Yh?OmU3U-od*&2lzW+c+n9<1Z7d&0fiN^HM)))v;FfwkLGRuvj;U(EsRF zNc!9oJOlc(y=2nEj%0K@lNn%;ORmyPugkNydued_7c(CL_X}AKv6t0+8-%eY9J#$K70-7@tryo&*u+5|AMbaz5LD{s% z+i6YFUd8$PVVqg;j_`_q&gYx&kAvW`h)9f2(zdiS&GjzX^jS8Q{MD^zx#NSU@55q} z^ksaD`12f!pKDhR0_BX%UH2MTHO(nnAaohW`!euP^=4nrb$Xh3_mb1kT7PGZ-)%UY_YYo^B==A#LWqg-#*6thkVDPxHE0H466q`qYDX8e7bW_q< zF&e}XR1mfjR?d&$`)HrS|Ge37y)F=IB1kGoOQ1?vMp%Qz%YMr3d-ZU+N6HAnn6JP4 z%<$sXOZ%cu`D*{LwItfpGhnrGWM_A|PSaNAV}4Wa#bSRs`1Iv&TDgf3O>o91*YEfN z?bXuR^TvE-FP;%fpQr87*XNpkXPFaJdf+QQq{2Yp)+UI%G8_mu3do@>Ncixc*@0%j z?(Ul=*jvQj?uljJ&-Y&}K)|TOS3x_Hk~Y}|@5aXVS&im` zN0{45?IE9WQ`6Spl&@f97 z2;dVa@Ph;V0K?n(P>^@PZxrB1G!yI(72GTn{LeF}->-(k$|4^>0>71w9ZgMbohM^OO8; z!2^8$^_r1{=ywxmD}E9USp^~yJ4aI@4hAL$CK3TyA|fI_M-ww1B~kIey90mmlUO)A z+w(9oy1BVAxUn+WIhr#vb8~YuGO;kSu+Rfr&^vk9Ivcvv+d7f{#pDkkQBx;lM@xHW zOFLVlUwjRX>|C7rNl1PT^q;@K=4t9~`QMRjo&H`HaDj}!D2&VuOpO2G4eZMI>n)Fh zrMs!Mrl_S2P&~jf1X#IP`F^+m7v;Yr{-dYHe|vIqGX1mXKPdm(Q`O1TQN+#$IHa?{ ze|P5Z&i^F--I0&+*UJAPiND1B`z=t;05$$k z{iywZikmPnNx>j6oy3hT7?uuUo2OctAx89Qk<(y#LIIl!D^cif^FPUuz>veeUU}2r zdc*hP6GkqZR=j9yYS!IWT0D4pOHk=ixR^mHIv`R&Pz}Ij{9%}W|L`Az-Y+gSKTY}j z_dkhbpd2IjLI2wW1XY0Ea$uyG8IxlH7r;&Ur}X_J zVdw~#g${6lOLERVB(+z$Yd*|@x9a-8;;Z%VsHWemHbFZcN+H+Kr1~d|-~5P9fJ>C- zr7a))za4?24WIzmVhW>%G4wBzBri~hrp6+zSpFr+>R_OCJiO%kBk1K5!6?>G1Z z!N3j96d)cS7q1kr-H)Ks_#O;WXw`ApROAxR&+W0Wu&5-kq!oF=0bDlbvk3^1%ysoh$KThv;4=iwT$fv$bd({e{_bcmSoexv8 zPS`DDehuTM(kjhmX?ggdX0L!W#H;ypd$z!yYWrTy;}bSAGBTC!1hmAspnrl>#7wcD zqVH3msi?SkwqxCr{uc-FU1+{rt^jp`WF!n^2wqys?FJfuoyYe~ENtk4hq74}<|$3@ z8|#LkMMXnlws??;4L8ZlD;nBq3HSwQ$}6z(Y(8a)28h#4NAsHfWkgQMJIVn|UN*Ci zCg$?^-5mt_#y`(t8ecY930e_)>%q+(Zm*dE*emqX&TYOsd}gxPA(l=_n^|7ax369Z zCg<+)a&lP7ZudJ^xf;E+Lzf(La$)!`_ySJmC5W{8;S%w2GsPa(sPr2d zV6`ws^r8ZpIEO~XHK+F3D3Sr6nq!>#@k^s}K5TeeXtp8JU>1cH?jtTZ_>g^f|2~s) zQJMYMG0DUR9^PrdQ7*eyxem*__1d#gUy=UKo*pz$PtQV?It;V7W|V>$!k!vR#46hz z5;}b*xbDYViP37;6f(1Uu;&-Dpvp=ny88n!wsJkZldr1XS*^>az7~?uJZu#a>DqHy zru>+iZN^txnzy^NK%}VI8uEZevsp)sn|Hp_QaV-M&(&_-e=eG4&~?~1of?62=hFBb?~0WtxjFmZ9|X)ZW_*TDy*<6>RbxD) zE&8J5RV-iQON;S_x9mLKyk7O#@NoG&8N`dlT9|hHw0s+VvAKx)?B&i_|Mo(8d%A?C z&Di8H_Kgq4ui^!vm6xNJAd9wu5#}DMY3uw2`y9?Aram#Wt4Rn_BX#?wnq<#{9+Y<4 z+rGZce=ZR!YP?e!uT>B(BWT_7#NET-FL}nb0_8cZx5nMf$zo=!)9Noi+V&bKGW+$$ zWNmn_+?^htnq}nEtX*IT?Q7r7@LnFMD0nFCw7NZ=7M`E&QtZ!6 zp;A2pW46zkL0y07yTgS1L6AIM&7h%^jMf|f~Z*%x|t9rCwf4;OdoZGu4 z>`k}h#snJ49J%3%efNeu9|lBF={NBla(;^W>VmUbqIU9mic6o*3$Q2%5GIx|S1DB@ ztw$6HCqAD#Vqs|42x=y8L$0B`$to%7w{Teb%ps!?^{@gaXV{WI$? zKkHu&G}HNX@pP1`V3C@PtWLv>hcr4jfx*HeMYi2$`-^T^pJuH94$5X*J)eE1?z6G8 z6;TUv9yswQ%C8zMmWwE32$xW4?RKBh3#T)w^P#r@@h!`=Xbv=?B5$k0LHT9d0L)P; zRYqkZS2K&FP}TjJR>HOu!4J0*mx#w}G|QZplTht+x^T&`s*0temZ=goijiz5d*o9c zUiNoMI;@;0dwGRQISjt{mdvF#n`X-S*w`U80M+Uk&k%dM@acB(_p|(wYSd5&@;wyOZQhqEH!3>wCO-bPp&t>Xo;mKIJ`YPRPN+XZgtH~WM99Z);-#yr z3pifKHib(wl00*bv^82ERxhQ9!bY`gCbY0p*CIMc-F6UF++ihJt1 z+lwQbnC|Ya?AKAfN~w`>eBt!V_xsaL+}=N0(PoX-m2oIli#6)ZtaX7}BXl$<0NIlL zRasfQ3nHFeq1A%b{hdJQQCPy@+y>YmH&XO#`RpHb%Gtnpcz6owS5V~%R^LKozB03* z_o)oB?3il$ukS7Jl*bYdAl)T3m6@&;M|(N!g=Bau)^=4HiOYU=?)_TVW7(Ixk-}|i zV(rRocrBZPu~jU&aYbKN%52f&R>d;D>KWbs#!Z}q9gi%@YA{dW*W<^jCxveeIT?V3 znn%(V@)@2GmgYmm3clrN5(#-;ayDR1*{Y&Y`ZP0g`}Q$}kE}tGwq&KzejfixepU3q z5uM3&1Vka_^`5%Pf#z`HW&=&)XWl%dJEVYU9tpx0%R@K}nMh`}v55D1d)WM1a(q$e z;-5H22^jZk4tY1K3_?Ife+%wMJTuw=y1QHE9Q#Yk;H-L!F%?5R+2AHIK-$*E3-696 z4U7G)^*WUQXt&TuOHJoCd){XE{A47y^Ek-mvHf*18 z1U!jj^7Gw*URLb8z(BBE(fc%bV8I$OGplGx2$1rrZ- zM*fa2i9kWu`sqO53k-e&Lkr)$b7?tGweiHz|N@4w& zS%1HxLltmwl>{Chp)q^yw+GYo#f@`?aGk%YWMd2Yy^XV+i!1FE?c~jU zZ}dSsTE9fEl+Gj_BOWU%bWV;h@s9;V)@Egxar4q0iTDFCGQ z7wLdC=w$>XD#DEd9>17B^L4@&bHDTz6(&rt+qIk|K7HA7e$V#`K(%)$3XdeczjC|1w<@CSZ8aA_*1WlMdyM8ZCk>+Md0Lugb{t;=^5aSyANA2nFaEdzg)a%H)s&%6zrGuC|YZZ<8~sfA^w z9FuWtIkt{lW)lqT79^%RR(5qA_W27t@Q3=9GY2selmPY1{}Q^cCOyld(!l4{mo_xXsBgQ7+`37fNW@> z?;+Yg5L|&3@AJs|U{Gs8^h#s)S9=V{m!92p!fm~i6>6=%VXhn&;%mN>oNIA8!3e#> zL0;b7oz=!-H7MmU^)Fn4EdrRVes!+XPzP>gH9$xq1<~;!%_Q}C z1a@wEy1Q0C-{>h6ztL$+4$OfqL98!%eQ!LaVH5LdLCj`UnA2lv<8=pL%jGWt< z_m>K2lghONDG|T_%ou$eupy71rsSi;W2G1oz2X?HGMYX+f87tcp`Dk0K{=}N8n(68 zr~|D@9_i{(1V6vJbNcz(r#ts@)}x8$=-e^WB>3|nr(mJp(CZj$cBf;Y17e$GIO1;% zMo0_VUZt;AVUq{wd@T3_fTQ6g&oYW1ht$|T@5qKIOhen-8Ubtvc)LuDb<#h$QebF}9xZC$idJDMu zuyTuuQeh=oWE4D-tJc;ZiVY6B&utfORh=H;3lsbvKE?Cpp_W(qS(YUf=NFg4pR;2c zDV^1G?;q*DwnF=a-3q@@+?YBj7$B{+`!L4Rs+ELKDvJ+qJ-HG5?48DhMgDYUYi&R= zm+GU2>!X4E*AY~(1LHH!Uz-oBGX|3(N`nR_vE2(otq80in5~}`Hv0CW@%_#KlU zE1oGO8HeNUv*EKdvyD_RL@L%M5y^n}mHXrRr#|?bk4$}tI071h%o_-joUY6Y^TOLL zwg#sD+{J|LQtUGBn!1LN)PbDq^;T9U_Go5?j1~b*+}h=hfD~DhJ7KX)_oFPX=S`n2 zLfnhTUfgG|6Ru;tV$z( zW)Id^gNrps$>mYlZ2C>&vTAah<42ekiuvBymR~;eZaz%!A!DN_ijJm~x}BbFYjKR< zeJ8@~{j`bxb1x%Xz;6AA*yN^|Vzvy7_vK6QsHF z9HW(|M%+9!i_qdd?-31%B{&nZxH7@=qQ_KxIIfD(AnpHiVErjjCy*z9WV3sKlzL4; zh%*QTDXtz-=ngaiRmfY|V7bn4U%_Pt^Wp0kWHdC0ub+q1i3sV+=uivgHg>4TJ0^&P zeK!*-Czv(_?uDU{iA4Zl&KkS*5Mm)R46ROHVdv-PjDY;@>#XINWm-;dRtZ+`SEHoS zjt&sHf(7MT;}(e^HWX2efeU~bf(6P__Tk=d>1gH5)%D_JH~&G z8jhHdeon2a>F>E~eITOagJoRpI-jIO0Syo`kVFd%BsKVC1R`HyK#y&D`<}ERB+JCY zs1#Azm_G&UsAEa$-R%QH+;|_6SK`LnAJOC{06pIUMBnUhuGr*AC^#Vn6T)gY4qbf1`WvY(Bm{2A%-PpUao|>25C_mU1YOLrqCD7?DzsV?ppDq&h2*Hb z!~x(Dgcxy$xpaq;DuDF#TR+Tv%iJF z9SW2H_pZ{F5KsgIwqWn%9wJiHR)Ss-Uufa?$`qiqL!V^G;rrJ-gr>`Lx4{`K=0d*+(oD zYF$auemegeb&nD-*=E~P+FwUYC=L3Y@tsZcS3V4&cBzOHQE|aayk3bsiKDCJgS&m> z21A$TA&ONMK-Lo^a|&!^{|_<&3{dI7CmLaXy&z%^I0+NTk|?2xsCm%ohW^56izu4* zvvZmWVfx#w>0d~yEG6(lCtqK9E&~{TB%*S;P(!cJFoJLC;JNDVV2Z%NvK6c05w;@y zykORWnbLsxGg9p@5WwbJBgXHMK7x&}2a}MHU|L`GEl+X{emxle#$yQGhPz1V6V0Oe zRJLq|x7bdrtHkh~KjN#YrC%i=??`>Dzh1x10D~g>G?`gg%2qWi7<^10PN`Z2qyJB` z=>a_V@vO@XEtBe=^lg?KaisUgJ9>~?caEokKy!EhQ4U`JG$rChi?MQT>0Y4f zuy5ynfbPb{oNUr+s|)(-GRGGii`&upe(hbq*X^i`{FuatnbVnzAbYPX6hV31cMbE5 zOpZc(5P%=80+QTE>WsbWb<)H02Y!fyJa&5g{Tc0yas5YX3}B<42uIU;L-~spAbx_H zw-?=B0PGpr-Mf=1JjAnAsfl;#Is$&c@`e-f|3blo4(vdTn3tm>x-4U*(TJAnAXBb4 z8+iB>K2B-;(%)Y?9q{E>CdER?@LLPWq<`jVlO3ulyjp8+z87@&{COuPm{pm>`U_xt zaQI{d*b1_p-O{!9*`_2qVVoMHlMArb*b=uz_cM#rg5%FB_I z_l?Ut0PYg%#>U3^-4h^9opI=~BIL7psAk!tY^8ngdiJQtbB#^ww+Gy*5_|!_+r$S` zU78%5>EiRJXKGTw-nXc~ybciz*y&k!aA(##k?!LMoH8KdC=SS=VD}(0 zOB?X_cpK9lb3Wnm)Z8}=f3A0(!pf0)cV{gE+7g)ZIBf)sTskQ1eAz0Lq1c>uUU$)f zFdOhb2Qw7#J+PSlDB`wB;fIN4O3K%O71RW&XKnk~C2-^^iN)fU7 z_{yr@#>#>5U<|EY8(3t%lvl%eGOqfmN0x~qY2KiT@E<=MPXY`BJ}PeT{c>DFG!S~t z5*6Dq$T6e4(oqZ#PB=L-#Nf^wnGJ1nIx;9y z+|I0JG5rGGEA$1DJ)(OAVjTJbHgWqMYFJ5MMn*<=m4UZXfkRukJQzu8RO(qY947+Q%VMIz-8UCLh6CuB zHabKC!lp8tT&v9+TFi;UMb3y4BB4eOn)suw zcT?C;e=r* zPtw@8&K$^nBpt)C+8DRv;6}Suuz_w^B~%G?NjS z{#K8_fM*zKY$D8vtP?rI(A-sZ*A5d?2Vt#QscG7nM%De^!fT021?l+NdJ_XeErK6I z_inq|=E@1D#^rDH)HlR6No=QBNR4Ky5&U9Hc1#tu^X}U+>yty#r%}N4 z*F5S>4EO-Y#7xG0fa8O3uR&KZR`apoDqjd+ZKj0i8qsT?XOhp-rGrC5xuk&HFDTfG zXz(T%#iRGei+nO^Txj&#Zt=44!HOyc!phh9EExPty(7Vj3bA6@4A?rYP%$4W-pdLp zVo>gHHPbZe;VUaLorzGu>MB2JS|{$p)9pUfjRW04_!ZoC6<0Py3Vu4 z@|8e+w?M4L^ukBDrGhb5?gqlSM}T{=MKR{E{~IYAR?|JaxJyM^o^hTF?eoQ;QV$Kf zoM7O~^Yi)Dw%VdZm9Hu5o>+iN1==dVRO7FQ=BnhdUh>4dX|)!ltJtk# zWPOi`_!krWWUhTtU58#C9@+ND=G!chPQ5uyw`ejs3ZJu?n31dtcOt0UVTVjEQ7{4k?JFb(kGUHx&0FUV%6Zy(Kq#`x@xVV@tU0Rk`2lW01Q>*7 ze&CUd$5rPM&29Rm;Z8Pp+5`Yix0zZISG>ru8lI>-gzYU_YQ4H^T+&6UQ==+a#hiIG zs&u3wue2WK{$2OLllv^GcCcC{1T;2m(H)5E@)7CW(`>6XQtKT*EmClB%@Xg~hVxQO z#GDF@suviK36Xc~Mc>Bu7s~8P6~zOA<+CD@^;qjvIo_;Ja9_|)0T}hq{g(=IWmf2YejrjisBD5mw7>A;Bc&^JUwkRlTb;-2E$ISE8bgrT@N8svq2kp}vBhJbi%n+TKiF>C&@iJ4&p zjPhRi`J)4??Z+G8PN7e5m{K+}`F&8(u)70*C3hPS8En#yyAXiGmYNI^IWS% z1}x#^9h@5)XiO9HA*nbcDWAn_85bH}BnDoFmfQ}~4eIISwnp$L;g>Rz*Ly>E{rzq*-UvF;f1j%b?WvA>c(F00C`-F$clnH2f6= zSTO$*$VKl?mMJaJ@>HX9y7fWS27#;x*KP4!QvHs&hDbW@AWc7;aC4{riozcbuNG2; zD4@?}xlqp=sec6%LU1tWKEiN}G>FvNzha8A#wJ#ZOkJQ@OLgZHB)WeEaszb0kxY^d z=IUc(Vm>&Ff@FW-L&iUj+0RrDfA1Z$527ymAo%(0^4q1DhswY^&5XOU(WRs8xbh0F zpKiaslsNQ+Yzjx#t4*g&*?KS!sdpp7-;vwTr@&9;?#?DmNSgusm9avFOh3AOGEq+v zf+O5?@kJq9MVwWroUZ`}2J!k>Isp@t$WLsC`$KNvTR)%?U&HtJCBq{xb=B7)428S6 zP^(>>qcg3H7$ZgRX+^9=aTkR8JGAij!sN<-7@zPf=vIc+mEr&-WW+6dSL?#Z0M*JX}BG@GY zj)e0FrI>XJ{d$jr+*KIF^mr|kjzkZrvp_Z9PvH{oAK>Wr*{ux)77$IBH{t-?#=rA# z4g%zkhr7DKM$$HLkR`Eg@%*pl}i(>jS*-v(<-Zc zgTHZ4qtzjvti~|t&x;K|?wlV4wdWU(dVWF*XvU|_lp#h%nBuZ2zPxi^;@Y{^`s%K) zFmEM(8lrf1NBTIcB)W16o{rhxLH1K{AtkhPEDJ4CZ9gKuPQ1Ix2yuaTHn7YXUt?J{ zuu0YdD{UDTe3VaF9NU37XRKpDX^6CmEaS0?QA_$(yLt|*D6DlN?3x9Ji}d;%?e$62 zezLUDH6?hway9TANMj&inxKP*WKuOAk4ux1pe*vi;s( z@2?N0F)s%-a;k2Q7f~f*@Pysn?=f9!=|xa&yWH$_d+&_jWjhO#dJEbj+3L0j192FQ z9oG5!;m$2ZaIi-x&&JCMRP=PGk^qh&<$(77#D1k^a?36sVf+eVXBDn&Zw9baDd-N${7y?uND$2MIWq4)q{6o@nr_a`sn#d@4L=fl%ove2q-=fPDNdl@<*xT!g_g0 zWWCYuH&=*mr?)xku(j>)L)T4c>wW>^47%l_?`KH$+q^T?kLJp%{N8+L3KhYlFzDmS z2>3mNLlJNT&8CZ0OG4o=8G2Bb>Z~xOzEA}`KU^6D+d;#?boNGJ=HZEv7q3?tOL#o? z6Z#oE-(T#{mSE!H;cZuafEwg>-jB=1o~zK0z1$hjv6#$G5%A#wW^lRQQ-&u?byUjv z06Og&vuNw-ERlB)YuRFO-p^F|=Ue0~bMNuEkQgb=@rsXR?<;1d6$-qGuaA~cfK?IX z6*l$?nLnspa5;vtF~cN^P{7R0cDX!usEd=SGvi-ts+C^}#-*B zGse;Tqy?F`oN<034l-CAV!bjNp#9BEbu@IkD6@)p+s;$doL@XK?Mo@m=OR_{1Hm|w zm@Wj#0b>G2KiZ*5uaz7}om2#X_UX{ET{yKSndV*=Je{Ez1 zjDFd93?@0GzwiUt9EV5cTMxoanTAG)mWj{&GynG+0DIj#mmiFUaKluv3ma|1jn6ls zrUU2;OPCTxOItgDgplYOdtx>fOx48fcTJ*Sm(hYmiYj`!D8ny!y7{)_ohJdV>r^Dd zo>X~+vS?kMf@~MXmPKP692}crPDFAU0tmp`-5oijEkmjNi`$Y;#r-iM@DdQJnAP;Cr}@l|!))-QR`ezCMsxwk`#{ZAiUr(FM*^l~{bF^h@KVmEzrOd1#tX9&fjYpFHF+MZi8Itm3c0ztxfC|b_-}PNsk~q7au#{TrlwK7eC(w-L7QPpq-*U2DzTQbkgPq51#~*% zIqe&h9J*9Ep}u0Lyzha^%ve9xiFEkq$(5B`s)0_v{HRDVCkva#Q!z6pUpB34WCXs| z>kg_3Rw)|h?3l8vgThfb@2L72Th-c)9})Xl`)SQtlX)aYHsR+D_!PrhOTyCRmIi|N3` z{e|qLV%0&FH=x0w_U3M`;#tA0z5)~VX^RQhZZ$M6hwHD_hnwdwI6l@heqw20fGwzA z4?lkj*9^Kn$nT3Gr;h~Qwqr=?NEAV!)u@t7CqcAQRfZ)bB*0$_+lI15F}t1WSOUuc zFtClKljuN4JryN=B7-G`c`vsINxLeRhU<*BA0_*AUmkDr=(Ko~l$7=}3q|(OnyL&s z1Bn_WBJU-IMJ%E<9V3?F+G>5vtQV`ReU)6$eW=$GiW5*TRXk7yd@T^i(gi4dq5H9q z^YhIbZ;ny%V2kGb(gD?;10sJ!HW7(q7$dy$kDP_7=(&ikuiQ62Y6anV`4SrU` zm2FsIG4`lYuO!K=Y~MQudd-=Nz2*c081;noo3YK%=^{%dPlHWrQ&Y%RG84^)2kPSB zbM6m-zJ$nFuF93pS5qtIpdGx-A?L;zXdRN%3<~n_M^kpj%50-k1$AAh zh$QWU1;k}izL&dVmwBH|Y0{+%f~VbmlTz%dx9XnAIxIvxqJq84IG1oq-lR%lG$fM~ z^YC1II$MtHj6SM-GV;vKt0~X0Pt^0;MR~PeWlyKyDn1i%z)jS-HppP~fxCKZs)yGt zDGx{e(lgmxL;0&bQ=@%L10_#0t#^IP!MpCli_g}1hD;jemN3I@f|^5w8;q66^sfRCVJNNqm0mTp7Av{8%~gCy;?eo<_xP}tU0m_LDN39^P2`gExSWM;yzms)d@aRA9bf@lbkHAQ*-1dcbFnMbke-W~ zk0y?`kRvYFo&d1i7gW<>$mDqO8O7Hb2 zhJDBB?|W)}x5TqR{v>tg&p^lzhFFE{sg^5P@Zz67oMa+(ebgU2*~xs+Y8hj<7OWrk zUc69)P+W#juXRL4|LauB?Lx`?Ijs!Ns=Ts_8Gow35{${Lk8qJxKH2doVda={m`>kP z-6>xlaY^;a!U~33l2JtsXNhW2=+vWGw~Qr~CK537!Y0lN?vs|K(3)>xh6YF zV<%AB;;P)1Z?i(?z`*TG!ok?;-9WDuj(K;M*O00;%Za3o^Zdh#v-Ap6fwr|;Db-ci zGfOG)YYK;hg_>)3b*ldoioz|>L_^{1c)?ISc2?Gx#p&ug3I_T|`-ov3K@tt!MfP0* zqvI_`Vnks6DgbJ_8qC{J9Hj539orQ%h658q60H>l!f{<)i!2W!Hk8ijXT4U0zs#Jk zi3(q&jH->v5LfQIcYD(N<<{o>z<5MJx5ZaGMn4RpGymc0{BTmFG>skAZ7lsIFzo0= z`@T(t_qu#Pkla|jb9`Q6j=TQA?wNc^LGSI8>h?vi$UEjzU)_gh-%HU_I!<+U>KyH& zO$|0jE-+CtRjD>Y0ZDwxTL6JBDpnYomG^K3)5THJz1_HARrP?2!}4ZGHR30cN5x2Z z#C|hoj5ql+qX6&4m;}ChZj!G{0P5hZ(gKc z>7-5|f@hKaSjvhzzy_29CQF$l4}@sTOUQFbNnQ@5x+(_fCc(*Ta&=FUOibI5=WYxT z-B$1dtIg1HCD+CO*|#&D!uQDTtJ%!3y88aL9w9U)H4unJ=eCqlur)e7q9sKWMg`RP zeh{vjU#q^@$}A~}auo?REJ?v$=DIVlCE$Wi+a2w_meu}Tk;y-HwjLFV&)Zogvj`zV zMta&}r+-xKs zYVGU6v_hT(D)m`cC-}aAA<5U9{fKt6akRdlj9AZCIwJ?btNtVmtcGLGl~#L#EiG?y z4iDd8AwRznLKbNKyQA3b9i7V8y~Qd|U9L*PC$+Z#+bs&B4_L-*-*HB@{o(7w)GLVS z$RIki`Ddfh9&#`k;zL(LGyLK`*ZEvfywu8i!u2sXPp-@to{#uutd<$n+9(LTm;E5- z%5;MD&3q|udDAK)L=4jF;`}LAF{@GiX2NnR2b5tSp zv(g$;`?CLYV_cooaWGxr0L&yxPsl!BQ$*wb$|?>xMkr!WV=LNhV?$-N$>R(83nbpw z!YOoHJ{m`g&&pbdw|Vu#S_(z$R4R;ot?Ny{+MCRkR#!p8EmLM;VMZ6%Vb%z5ec7b^ zYFblO*X^HLvr~_xW>j;#fp9pO%22Fc{7IGAFVOk}lw6cvt73z!_5u&jM$fIylzny= zN+M0p)qYjAp1prNQ~oYAe#_QD`m%=41?(j^S4Wf=uuf3i(rvpPBL9kRZ$VVz=xv=D#fRA!<1!O$tIv)&dK%5L(IC~w^470tA$4x3%9?z%V z+iX|^umokN!@3`>m_TKl7)>}g_dCodqAtU}2ROL8jlT4fX4+YRrH9Lh!+L_^qZYW6 ze{~vQHT&vRrlAakz)+AiUY5Q04-T{5Dla0WYR_hLtQ*&S7wox3#B<>8pFfV+_my;91z{r>s6H3l&FMqeL`M&0Vzn?(O2EDIaukWZ ztV7~yrkQ4;Oz>La>3u!$x+ujaX{VY5W6u4(v!?lHqvOn@f#tyS#DL2TjLST}mDZ8?tWXi^|R){te>cST#P|14ke1u&ISp8l;QUm^m7 zGm$aM zQuDgY;XN zQ>o#VZ&T!2RAdnARS3c%W%+Xp&R;OT%5H!t{};M>W&T1*6;=A4q0dgmGE^o#6)2d9&EzqWx7b`55!h9BDguz-viI+9lNIduv4 zNzyo`6<7NSUH#4_+kpoXrtdO}$nn;*muSrjqoPQUFNRC26k1FQz5L5bxmJr9VU8@m zul+58CGv%kpkOo^VDP_mZG$5ny1-R$gpw`pg7Q|@7rmTIo@Zlh<;lFxzm<3soU}jS z=KfXlU!q?8Wz&hp|EsVN^UBOA|MK>swLmRhO$0#_^uF3WC{?Vhi9*7{wYHmF;9*3} z|MBc3vU=`{qnTX#kPGolMxRP_Dem_JNeR|~sb$Y%@{*cqz{sX}=CP8@EU8x-YxrPb!w7A{lgH?dlA+^rLGH0&~>(D-)2Ln@XE zlF`hK-QVRcws_p|pRYG+&<+m=7=on<3Gn4aMWI&hoOqh&1NP@xd0id?i$$CZhGg+U zHG_X>#o;(P0=2pY{}-=SBDHx=0G<61--+xPo$c1YiBAtgM9U}KaHExc$XLOrvS4*( z5$RK#PWY_W%T1+Nu%znwuKxwG5_U#Db=G*K3GvY6t-$W(X}D;V&l{gLDjy~QV?jY) zam!>k{X#N0{CK7S?b32J5a%(OA%)Ef6Mq10Yb=p=AB2V0i55U(@p?X5ClITpHrd?V zP4n+3>-of*T!|VpStj%Q`mkCp!8h++6tmcE@vH_PeB95=&E;LqA{E*2?H|hnB$S*l#!bSZkh-SG8Rs^Y^P5Xq_Lf*Y5rsV-K(E0AWVtzV~@1cl*2;lG1rl zatDm=oH-ZaJ{BRjrraX29m zs(Xm`y+aTh)KoMnEI81?XMwERb3-WndcE&m-`Du!R9>xtVF^wGR7YDPbReAS`^Q)yb?B1IN@@!sQelO(!CPLGqW~ zR>IJd)Xkx;2%V7KMoXhQSCNys!q{JSx*Xix<4uc$L*>Dm z*rL*iQC~m(;8CB;TrI5m-3e7m_lW-^>BdpkxSko@pU#L_gW3KkKqS`z;5lx;LGG88 zC(n5(Ht`e!k|6vg5pe^(*erW4Moo%iF`7&karu5f)o#*#|k7Gsc-Z5J}`Ky|{Qg)L^kf0`?})C0|YG8V=Qa zK=T`*$b-+vZyb7_eeYFAN$HVUz4GquHW&_#EI32WtKNk_yxxG~hn^Q3$D@(@)zhZ0 z(&5>W{ktTlXMOlhU#TVymTkmq8MMp#ezxM!F)va3=abI1F-yX#iEtI?5Z;rziE4@; zF>Iil6Qo72s3%6$*{nDHb`uYR_W`jKiKBXEwl03!4~Qt2;nv%!X~Pf|is)aRs;o4b zlN>!>0k<%8j8~qCZv?`Y24&ydC}eH7gJUD{b~eYc>^)!6mAKtRniU64QVoqP<9u&s z1mVl|)W)9ghD!|%JSUXgme-rFy+d4?eMeIiuJ0aVPr*ZXM6HVC;$O(W*1DN4rAH6r z`M8tl1wq-Zak@*rDKRTK$8TOGT*hPy@|uetjdxZTy36fW6fOJ1KAZ)H>WoRd$w&ly zFH<8gl~KgRaLw_3!=|G;#^uDyS_^9%Y`oLm`J|~^m6;)tBm`Xt9Orje9VoBmF8DS^ zp6KgPYH8Hr1gg$r~smU+5_z|k&km5IxBma6z|N9V)R zfThbxG@$7gbf#ay9EL~(WV_BJ;!kSb&fo>gA`$iFnn?Zm+{^aep~}idCw*BTQ^@OUEXv68qnUFazRU%OejmRJwH-_v!C1SF?8-FOFdq7zdb*t^MjByKd<(!4o_ms*C}mp!dtF_>omOHDgAdV zU{p#5s;$_=-Ch6dvtd$qM(l`vZ_7sC7LnJWV7B)1D;Z-Y6o +)=g#rwdsuA~EHU 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

V3!N-v6MJ@fVKadZrX&{0Yly ztne$s#XQ!ak>Jjhi7%|GP){2j6yJ20I%v5!|%FXqBFnGWNm0L4( zyZHbH=bSYHaiak$hks8+MTS&&S?afWTq2Kl-z&5AIKlH&ZY@s+c)x`yvfdJ9BG$kf zr4|@B603E;t#|mCpaeyc(dLKo=zwau;J-e@v;WPQm}87dm93t@pLXZ0YwW}u_16;W zd{`P7++kc1h%0$?^5@>&*^r_v$p@&ad&ntO4qnLp^10HUP&QiWtQW>&YC%McOk2NT z9~tNMLZP@&fLZ8eb#L(TI5EfR#+9t{t%_f6OiUHsw_?bx`R-k&ddHR~vIz8D?oL|o z#ayKYSL->CVT|Bf{P_lM5ness3#wZ5L zDU_A8%ML>I%g|3~OpxG#>X`ttyk{V|1 z5wx8zc@z9~-f1>_;eJW_wMt1H@}!~eFWYU{aqU7JYRfu=(m4(!nPRGk*A~Esf=LXj z9Y08tYPif;|0I-`@EGAwb@<5XV_@@3y60BUb-=y~pndw+;H{AilF`EUcfn;@Ec=lH zc)?fKt0{wwQXsMdQr2WPvD=XC!ymV!K4O|dzy+5iu2 z)gwlO67bI*P}9b%hE)%m!RHD$)|yT;z`Lkp^tgA8HPHV1cm88jyxb(&4CB64WI0>iNDgbAx1oA4v4T0eSfslrSVA`vE3M zP*VTj^?#fL)(Q!cPki!_DRYKtwU<2pGz~B<8~_y#4s1ruG0?eq&iqL=h_Y)uj>_DB zsW9e830cFr9D literal 0 HcmV?d00001 From 6fd7468b561be4fbcc981c0ea146d94deba46c3f Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Fri, 15 Sep 2023 09:59:22 -0400 Subject: [PATCH 32/35] More editorial comments Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index 2bc541e67c..bd6f63414d 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -24,7 +24,7 @@ It's helpful to understand the following terms before starting this tutorial: ![Neural search at ingestion time diagram]({{site.url}}{{site.baseurl}}/images/neural-search-ingestion.png) - At search time, when you then use a _neural query_, the query text is passed through a language model, and the resulting vector embeddings are compared with the document text vector embeddings to find the most relevant results, as shown in the following diagram. - + ![Neural search at search time diagram]({{site.url}}{{site.baseurl}}/images/neural-search-query.png) ## OpenSearch components for semantic search @@ -103,7 +103,7 @@ Neural search requires a language model in order to generate vector embeddings f ### Step 1(a): Choose a language model -For this tutorial, you'll use the [DistilBERT](https://huggingface.co/docs/transformers/model_doc/distilbert) model from Hugging Face. It is one of the pretrained sentence transformer models available in OpenSearch that has shown one of the best results in benchmarking tests (for details, see [this blog](https://opensearch.org/blog/semantic-science-benchmarks/)). You'll need the name, version, and dimension of the model to register it. You can find this information in the [pretrained model table]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/#sentence-transformers) by selecting the `config_url` link corresponding to the model's TorchScript artifact: +For this tutorial, you'll use the [DistilBERT](https://huggingface.co/docs/transformers/model_doc/distilbert) model from Hugging Face. It is one of the pretrained sentence transformer models available in OpenSearch that has shown some of the best results in benchmarking tests (for details, see [this blog post](https://opensearch.org/blog/semantic-science-benchmarks/)). You'll need the name, version, and dimension of the model to register it. You can find this information in the [pretrained model table]({{site.url}}{{site.baseurl}}/ml-commons-plugin/pretrained-models/#sentence-transformers) by selecting the `config_url` link corresponding to the model's TorchScript artifact: - The model name is `huggingface/sentence-transformers/msmarco-distilbert-base-tas-b`. - The model version is `1.0.1`. @@ -982,7 +982,7 @@ You can now experiment with different weights, normalization techniques, and com #### Advanced -You can parametrize the search by using search templates, hiding implementation details and reducing the number of nested levels and thus the query complexity. For more information, see [search templates]({{site.url}}{{site.baseurl}}/search-plugins/search-template/). +You can parameterize the search by using search templates. Search templates hide implementation details, reducing the number of nested levels and thus the query complexity. For more information, see [search templates]({{site.url}}{{site.baseurl}}/search-plugins/search-template/). ### Clean up From 20cb3dfcdf75bff625be0f5b64ae226bdb9ae909 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Fri, 15 Sep 2023 10:06:54 -0400 Subject: [PATCH 33/35] Add link to profile API Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/semantic-search.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_ml-commons-plugin/semantic-search.md b/_ml-commons-plugin/semantic-search.md index bd6f63414d..7acdb33e66 100644 --- a/_ml-commons-plugin/semantic-search.md +++ b/_ml-commons-plugin/semantic-search.md @@ -420,7 +420,7 @@ The response shows the model state as `DEPLOYED`: } ``` -You can also receive statistics for all deployed models in your cluster by sending a Models Profile API request: +You can also receive statistics for all deployed models in your cluster by sending a [Models Profile API]({{site.url}}{{site.baseurl}}/ml-commons-plugin/api/#profile) request: ```json GET /_plugins/_ml/profile/models From 0c3f58948eb058f3466de0ef69becf3c77049e65 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Mon, 18 Sep 2023 12:33:48 -0400 Subject: [PATCH 34/35] Addressed more tech review comments Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/api.md | 18 +++++++++++++++++- .../normalization-processor.md | 6 ++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/_ml-commons-plugin/api.md b/_ml-commons-plugin/api.md index 74faa98480..ab0516e71e 100644 --- a/_ml-commons-plugin/api.md +++ b/_ml-commons-plugin/api.md @@ -518,8 +518,24 @@ The API returns the following: ## Profile -The profile operation returns runtime information on ML tasks and models. The profile operation can help debug issues with models at runtime. +The profile operation returns runtime information on ML tasks and models. The profile operation can help debug issues with models at runtime. +### The number of requests returned + +By default, the Profile API monitors the last 100 requests. To change the number of monitoring requests, update the following cluster setting: + +```json +PUT _cluster/settings +{ + "persistent" : { + "plugins.ml_commons.monitoring_request_count" : 1000000 + } +} +``` + +To clear all monitoring requests, set `plugins.ml_commons.monitoring_request_count` to `0`. + +### Path and HTTP methods ```json GET /_plugins/_ml/profile diff --git a/_search-plugins/search-pipelines/normalization-processor.md b/_search-plugins/search-pipelines/normalization-processor.md index 6e0a6c7c8b..41b25907bc 100644 --- a/_search-plugins/search-pipelines/normalization-processor.md +++ b/_search-plugins/search-pipelines/normalization-processor.md @@ -109,3 +109,9 @@ GET /my-nlp-index/_search?search_pipeline=nlp-search-pipeline {% include copy-curl.html %} For more information, see [Hybrid query]({{site.url}}{{site.baseurl}}/query-dsl/compound/hybrid/). + +## Search tuning recommendations + +To improve search relevance, we recommend increasing the sample size. + +If you don't see some results you expect the hybrid query to return, it can be because the subqueries return too few documents. The `normalization_processor` only transforms the results returned by each subquery; it does not perform any additional sampling. During our experiments, we used [nDCG@10](https://en.wikipedia.org/wiki/Discounted_cumulative_gain) to measure quality of information retrieval depending on the number of documents returned (the size). We have found that size in the [100, 200] range works best for datasets of up to 10M documents. We do not recommend increasing the size beyond the recommended values because higher values of size do not improve search relevance but increase search latency. From 76036c48db87e454396444f087fb40c949fda3a8 Mon Sep 17 00:00:00 2001 From: Fanit Kolchina Date: Mon, 18 Sep 2023 16:29:59 -0400 Subject: [PATCH 35/35] Implemented editorial comments on changes Signed-off-by: Fanit Kolchina --- _ml-commons-plugin/api.md | 2 +- _search-plugins/search-pipelines/normalization-processor.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/_ml-commons-plugin/api.md b/_ml-commons-plugin/api.md index ab0516e71e..055b66d157 100644 --- a/_ml-commons-plugin/api.md +++ b/_ml-commons-plugin/api.md @@ -518,7 +518,7 @@ The API returns the following: ## Profile -The profile operation returns runtime information on ML tasks and models. The profile operation can help debug issues with models at runtime. +The profile operation returns runtime information about ML tasks and models. The profile operation can help debug model issues at runtime. ### The number of requests returned diff --git a/_search-plugins/search-pipelines/normalization-processor.md b/_search-plugins/search-pipelines/normalization-processor.md index 41b25907bc..be854d1319 100644 --- a/_search-plugins/search-pipelines/normalization-processor.md +++ b/_search-plugins/search-pipelines/normalization-processor.md @@ -114,4 +114,4 @@ For more information, see [Hybrid query]({{site.url}}{{site.baseurl}}/query-dsl/ To improve search relevance, we recommend increasing the sample size. -If you don't see some results you expect the hybrid query to return, it can be because the subqueries return too few documents. The `normalization_processor` only transforms the results returned by each subquery; it does not perform any additional sampling. During our experiments, we used [nDCG@10](https://en.wikipedia.org/wiki/Discounted_cumulative_gain) to measure quality of information retrieval depending on the number of documents returned (the size). We have found that size in the [100, 200] range works best for datasets of up to 10M documents. We do not recommend increasing the size beyond the recommended values because higher values of size do not improve search relevance but increase search latency. +If the hybrid query does not return some expected results, it may be because the subqueries return too few documents. The `normalization_processor` only transforms the results returned by each subquery; it does not perform any additional sampling. During our experiments, we used [nDCG@10](https://en.wikipedia.org/wiki/Discounted_cumulative_gain) to measure quality of information retrieval depending on the number of documents returned (the size). We have found that a size in the [100, 200] range works best for datasets of up to 10M documents. We do not recommend increasing the size beyond the recommended values because higher size values do not improve search relevance but increase search latency.