From 3fb1add7d8315a1cab860831c13260528ab1af99 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 17 Dec 2024 08:45:41 -0500 Subject: [PATCH 01/41] Working on standalone implementation. Signed-off-by: jzonthemtn --- .../.gitignore | 14 + .../Dockerfile | 6 + .../LICENSE.txt | 175 ++++++++ .../NOTICE.txt | 1 + .../README.md | 72 +++ .../aggs.sh | 20 + .../build.gradle | 52 +++ .../gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + .../settings.gradle | 1 + .../SearchQualityEvaluationFramework.java | 10 + .../SearchQualityEvaluationJobParameter.java | 248 ++++++++++ .../SearchQualityEvaluationJobRunner.java | 168 +++++++ .../eval/SearchQualityEvaluationPlugin.java | 213 +++++++++ .../SearchQualityEvaluationRestHandler.java | 417 +++++++++++++++++ .../eval/judgments/clickmodel/ClickModel.java | 23 + .../clickmodel/ClickModelParameters.java | 13 + .../clickmodel/coec/CoecClickModel.java | 422 ++++++++++++++++++ .../coec/CoecClickModelParameters.java | 55 +++ .../judgments/model/ClickthroughRate.java | 96 ++++ .../eval/judgments/model/Judgment.java | 97 ++++ .../eval/judgments/model/QuerySetQuery.java | 29 ++ .../model/ubi/event/EventAttributes.java | 82 ++++ .../model/ubi/event/EventObject.java | 58 +++ .../judgments/model/ubi/event/Position.java | 42 ++ .../judgments/model/ubi/event/UbiEvent.java | 82 ++++ .../model/ubi/query/QueryResponse.java | 58 +++ .../judgments/model/ubi/query/UbiQuery.java | 160 +++++++ .../opensearch/OpenSearchHelper.java | 342 ++++++++++++++ .../queryhash/IncrementalUserQueryHash.java | 51 +++ .../judgments/queryhash/UserQueryHash.java | 23 + .../eval/metrics/DcgSearchMetric.java | 64 +++ .../eval/metrics/NdcgSearchMetric.java | 62 +++ .../eval/metrics/PrecisionSearchMetric.java | 63 +++ .../opensearch/eval/metrics/SearchMetric.java | 70 +++ .../eval/runners/AbstractQuerySetRunner.java | 208 +++++++++ .../runners/OpenSearchQuerySetRunner.java | 290 ++++++++++++ .../opensearch/eval/runners/QueryResult.java | 72 +++ .../eval/runners/QuerySetRunResult.java | 108 +++++ .../eval/runners/RelevanceScores.java | 32 ++ .../eval/samplers/AbstractQuerySampler.java | 98 ++++ .../samplers/AbstractSamplerParameters.java | 41 ++ .../eval/samplers/AllQueriesQuerySampler.java | 79 ++++ .../AllQueriesQuerySamplerParameters.java | 17 + ...roportionalToSizeAbstractQuerySampler.java | 176 ++++++++ ...obabilityProportionalToSizeParameters.java | 17 + .../org/opensearch/eval/utils/MathUtils.java | 26 ++ .../org/opensearch/eval/utils/TimeUtils.java | 35 ++ .../eval/metrics/DcgSearchMetricTest.java | 41 ++ .../eval/metrics/NdcgSearchMetricTest.java | 41 ++ .../metrics/PrecisionSearchMetricTest.java | 30 ++ .../useful_queries.txt | 151 +++++++ settings.gradle | 2 +- 54 files changed, 4759 insertions(+), 1 deletion(-) create mode 100644 opensearch-search-quality-evaluation-framework/.gitignore create mode 100644 opensearch-search-quality-evaluation-framework/Dockerfile create mode 100644 opensearch-search-quality-evaluation-framework/LICENSE.txt create mode 100644 opensearch-search-quality-evaluation-framework/NOTICE.txt create mode 100644 opensearch-search-quality-evaluation-framework/README.md create mode 100755 opensearch-search-quality-evaluation-framework/aggs.sh create mode 100644 opensearch-search-quality-evaluation-framework/build.gradle create mode 100644 opensearch-search-quality-evaluation-framework/gradle.properties create mode 100644 opensearch-search-quality-evaluation-framework/gradle/wrapper/gradle-wrapper.jar create mode 100644 opensearch-search-quality-evaluation-framework/gradle/wrapper/gradle-wrapper.properties create mode 100644 opensearch-search-quality-evaluation-framework/settings.gradle create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationFramework.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/Judgment.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/SearchMetric.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/QueryResult.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/RelevanceScores.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/utils/MathUtils.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/utils/TimeUtils.java create mode 100644 opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java create mode 100644 opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java create mode 100644 opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java create mode 100644 opensearch-search-quality-evaluation-framework/useful_queries.txt diff --git a/opensearch-search-quality-evaluation-framework/.gitignore b/opensearch-search-quality-evaluation-framework/.gitignore new file mode 100644 index 0000000..6c884e1 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/.gitignore @@ -0,0 +1,14 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +# intellij files +.idea/ +*.iml +*.ipr +*.iws +build-idea/ +out/ + diff --git a/opensearch-search-quality-evaluation-framework/Dockerfile b/opensearch-search-quality-evaluation-framework/Dockerfile new file mode 100644 index 0000000..02f56c8 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/Dockerfile @@ -0,0 +1,6 @@ +FROM opensearchproject/opensearch:2.18.0 + +RUN /usr/share/opensearch/bin/opensearch-plugin install --batch https://github.com/opensearch-project/user-behavior-insights/releases/download/2.18.0.2/opensearch-ubi-2.18.0.2.zip + +ADD ./build/distributions/search-quality-evaluation-plugin-0.0.1.zip /tmp/search-quality-evaluation-plugin.zip +RUN /usr/share/opensearch/bin/opensearch-plugin install --batch file:/tmp/search-quality-evaluation-plugin.zip diff --git a/opensearch-search-quality-evaluation-framework/LICENSE.txt b/opensearch-search-quality-evaluation-framework/LICENSE.txt new file mode 100644 index 0000000..67db858 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/LICENSE.txt @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/opensearch-search-quality-evaluation-framework/NOTICE.txt b/opensearch-search-quality-evaluation-framework/NOTICE.txt new file mode 100644 index 0000000..be5c6b3 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/NOTICE.txt @@ -0,0 +1 @@ +Copyright Open Source Connections or its affiliates. All Rights Reserved. diff --git a/opensearch-search-quality-evaluation-framework/README.md b/opensearch-search-quality-evaluation-framework/README.md new file mode 100644 index 0000000..215ccce --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/README.md @@ -0,0 +1,72 @@ +# OpenSearch Evaluation Framework + +This is an OpenSearch plugin built on the OpenSearch job scheduler plugin. + +## API Endpoints + +| Method | Endpoint | Description | +|--------|-----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| +| `POST` | `/_plugins/search_quality_eval/queryset` | Create a query set by sampling from the `ubi_queries` index. The `name`, `description`, and `sampling` method parameters are required. | +| `POST` | `/_plugins/search_quality_eval/run` | Initiate a run of a query set. The `name` of the query set is a required parameter. | +| `POST` | `/_plugins/search_quality_eval/judgments` | Generate implicit judgments from UBI events and queries now. | +| `POST` | `/_plugins/search_quality_eval/schedule` | Create a scheduled job to generate implicit judgments. | + + +## Building + +Build the project from the top-level directory to build all projects. + +``` +cd .. +./gradlew build +``` + +## Running in Docker + +From this directory: + +``` +docker compose build && docker compose up +``` + +Verify the plugin is installed: + +``` +curl http://localhost:9200/_cat/plugins +``` + +In the list returned you should see: + +``` +opensearch search-quality-evaluation-plugin 2.17.1.0-SNAPSHOT +``` + +To create a schedule to generate implicit judgments: + +``` +curl -s -X POST "http://localhost:9200/_plugins/search_quality_eval/schedule?id=1&click_model=coec&job_name=test&interval=60" | jq +``` + +See the created job: + +``` +curl -s http://localhost:9200/search_quality_eval_scheduled_jobs/_search | jq +``` + +To run an on-demand job without scheduling: + +``` +curl -X POST "http://localhost:9200/_plugins/search_quality_eval/judgments?click_model=coec&max_rank=20" | jq +``` + +To see the job runs: + +``` +curl -X POST "http://localhost:9200/search_quality_eval_completed_jobs/_search" | jq +``` + +See the first 10 judgments: + +``` +curl -s http://localhost:9200/judgments/_search | jq +``` \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/aggs.sh b/opensearch-search-quality-evaluation-framework/aggs.sh new file mode 100755 index 0000000..5cf0e24 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/aggs.sh @@ -0,0 +1,20 @@ +#!/bin/bash -e + +curl -X GET http://localhost:9200/ubi_events/_search -H "Content-Type: application/json" -d' +{ + "size": 0, + "aggs": { + "By_Action": { + "terms": { + "field": "action_name" + }, + "aggs": { + "By_Position": { + "terms": { + "field": "event_attributes.position.ordinal" + } + } + } + } + } +}' | jq \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/build.gradle b/opensearch-search-quality-evaluation-framework/build.gradle new file mode 100644 index 0000000..bdd0586 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/build.gradle @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'java' +apply plugin: 'idea' + +ext { + projectSubstitutions = [:] + licenseFile = rootProject.file('LICENSE.txt') + noticeFile = rootProject.file('NOTICE.txt') +} + +test { + include "**/Test*.class" + include "**/*Test.class" + include "**/*Test.class" + include "**/*TestCase.class" +} + +group = 'org.opensearch' +version = "${evalVersion}" + +buildscript { + repositories { + mavenLocal() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } + } + + dependencies { + classpath "org.opensearch.gradle:build-tools:${opensearchVersion}" + } +} + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } +} + +dependencies { + implementation 'org.apache.logging.log4j:log4j-core:2.24.3' + implementation 'com.fasterxml.jackson.core:jackson-annotations:2.18.2' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2' + implementation 'org.apache.httpcomponents.core5:httpcore5:5.3.1' + implementation 'org.apache.httpcomponents.client5:httpclient5:5.4.1' + implementation 'commons-logging:commons-logging:1.3.4' + implementation 'com.google.code.gson:gson:2.11.0' +} diff --git a/opensearch-search-quality-evaluation-framework/gradle.properties b/opensearch-search-quality-evaluation-framework/gradle.properties new file mode 100644 index 0000000..2659a68 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/gradle.properties @@ -0,0 +1,2 @@ +opensearchVersion = 2.18.0 +evalVersion = 0.0.1 diff --git a/opensearch-search-quality-evaluation-framework/gradle/wrapper/gradle-wrapper.jar b/opensearch-search-quality-evaluation-framework/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/opensearch-search-quality-evaluation-framework/gradle/wrapper/gradle-wrapper.properties b/opensearch-search-quality-evaluation-framework/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2bbac7d --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/settings.gradle b/opensearch-search-quality-evaluation-framework/settings.gradle new file mode 100644 index 0000000..ef059e1 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'search-evaluation-framework' diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationFramework.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationFramework.java new file mode 100644 index 0000000..245ee64 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationFramework.java @@ -0,0 +1,10 @@ +package org.opensearch.eval; + +public class SearchQualityEvaluationFramework { + + public void main(String[] args) { + + + } + +} \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java new file mode 100644 index 0000000..2ea5379 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java @@ -0,0 +1,248 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval; + +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.jobscheduler.spi.ScheduledJobParameter; +import org.opensearch.jobscheduler.spi.schedule.Schedule; + +import java.io.IOException; +import java.time.Instant; + +public class SearchQualityEvaluationJobParameter implements ScheduledJobParameter { + + /** + * The name of the parameter for providing a name for the scheduled job. + */ + public static final String NAME_FIELD = "name"; + + /** + * The name of the parameter for creating a job as enabled or disabled. + */ + public static final String ENABLED_FILED = "enabled"; + + /** + * The name of the parameter for specifying when the job was last updated. + */ + public static final String LAST_UPDATE_TIME_FIELD = "last_update_time"; + + /** + * The name of the parameter for specifying a readable time for when the job was last updated. + */ + public static final String LAST_UPDATE_TIME_FIELD_READABLE = "last_update_time_field"; + public static final String SCHEDULE_FIELD = "schedule"; + public static final String ENABLED_TIME_FILED = "enabled_time"; + public static final String ENABLED_TIME_FILED_READABLE = "enabled_time_field"; + public static final String LOCK_DURATION_SECONDS = "lock_duration_seconds"; + public static final String JITTER = "jitter"; + + /** + * The name of the parameter that allows for specifying the type of click model to use. + */ + public static final String CLICK_MODEL = "click_model"; + + /** + * The name of the parameter that allows for setting a max rank value to use during judgment generation. + */ + public static final String MAX_RANK = "max_rank"; + + // Properties from ScheduledJobParameter. + private String jobName; + private Instant lastUpdateTime; + private Instant enabledTime; + private boolean enabled; + private Schedule schedule; + private Long lockDurationSeconds; + private Double jitter; + + // Custom properties. + private String clickModel; + private int maxRank; + + public SearchQualityEvaluationJobParameter() { + + } + + public SearchQualityEvaluationJobParameter(final String name, final Schedule schedule, + final Long lockDurationSeconds, final Double jitter, + final String clickModel, final int maxRank) { + this.jobName = name; + this.schedule = schedule; + this.enabled = true; + this.lockDurationSeconds = lockDurationSeconds; + this.jitter = jitter; + + final Instant now = Instant.now(); + this.enabledTime = now; + this.lastUpdateTime = now; + + // Custom properties. + this.clickModel = clickModel; + this.maxRank = maxRank; + + } + + @Override + public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + + builder.startObject(); + + builder + .field(NAME_FIELD, this.jobName) + .field(ENABLED_FILED, this.enabled) + .field(SCHEDULE_FIELD, this.schedule) + .field(CLICK_MODEL, this.clickModel) + .field(MAX_RANK, this.maxRank); + + if (this.enabledTime != null) { + builder.timeField(ENABLED_TIME_FILED, ENABLED_TIME_FILED_READABLE, this.enabledTime.toEpochMilli()); + } + + if (this.lastUpdateTime != null) { + builder.timeField(LAST_UPDATE_TIME_FIELD, LAST_UPDATE_TIME_FIELD_READABLE, this.lastUpdateTime.toEpochMilli()); + } + + if (this.lockDurationSeconds != null) { + builder.field(LOCK_DURATION_SECONDS, this.lockDurationSeconds); + } + + if (this.jitter != null) { + builder.field(JITTER, this.jitter); + } + + builder.endObject(); + + return builder; + + } + + @Override + public String getName() { + return this.jobName; + } + + @Override + public Instant getLastUpdateTime() { + return this.lastUpdateTime; + } + + @Override + public Instant getEnabledTime() { + return this.enabledTime; + } + + @Override + public Schedule getSchedule() { + return this.schedule; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } + + @Override + public Long getLockDurationSeconds() { + return this.lockDurationSeconds; + } + + @Override + public Double getJitter() { + return jitter; + } + + /** + * Sets the name of the job. + * @param jobName The name of the job. + */ + public void setJobName(String jobName) { + this.jobName = jobName; + } + + /** + * Sets when the job was last updated. + * @param lastUpdateTime An {@link Instant} of when the job was last updated. + */ + public void setLastUpdateTime(Instant lastUpdateTime) { + this.lastUpdateTime = lastUpdateTime; + } + + /** + * Sets when the job was enabled. + * @param enabledTime An {@link Instant} of when the job was enabled. + */ + public void setEnabledTime(Instant enabledTime) { + this.enabledTime = enabledTime; + } + + /** + * Sets whether the job is enabled. + * @param enabled A boolean representing whether the job is enabled. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * Sets the schedule for the job. + * @param schedule A {@link Schedule} for the job. + */ + public void setSchedule(Schedule schedule) { + this.schedule = schedule; + } + + /** + * Sets the lock duration for the cluster when running the job. + * @param lockDurationSeconds The lock duration in seconds. + */ + public void setLockDurationSeconds(Long lockDurationSeconds) { + this.lockDurationSeconds = lockDurationSeconds; + } + + /** + * Sets the jitter for the job. + * @param jitter The jitter for the job. + */ + public void setJitter(Double jitter) { + this.jitter = jitter; + } + + /** + * Gets the type of click model to use for implicit judgment generation. + * @return The type of click model to use for implicit judgment generation. + */ + public String getClickModel() { + return clickModel; + } + + /** + * Sets the click model type to use for implicit judgment generation. + * @param clickModel The click model type to use for implicit judgment generation. + */ + public void setClickModel(String clickModel) { + this.clickModel = clickModel; + } + + /** + * Gets the max rank to use when generating implicit judgments. + * @return The max rank to use when generating implicit judgments. + */ + public int getMaxRank() { + return maxRank; + } + + /** + * Sets the max rank to use when generating implicit judgments. + * @param maxRank The max rank to use when generating implicit judgments. + */ + public void setMaxRank(int maxRank) { + this.maxRank = maxRank; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java new file mode 100644 index 0000000..442ae4c --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java @@ -0,0 +1,168 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.index.IndexResponse; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.core.action.ActionListener; +import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel; +import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModelParameters; +import org.opensearch.jobscheduler.spi.JobExecutionContext; +import org.opensearch.jobscheduler.spi.ScheduledJobParameter; +import org.opensearch.jobscheduler.spi.ScheduledJobRunner; +import org.opensearch.jobscheduler.spi.utils.LockService; +import org.opensearch.threadpool.ThreadPool; + +import java.util.HashMap; +import java.util.Map; + +/** + * Job runner for scheduled implicit judgments jobs. + */ +public class SearchQualityEvaluationJobRunner implements ScheduledJobRunner { + + private static final Logger LOGGER = LogManager.getLogger(SearchQualityEvaluationJobRunner.class); + + private static SearchQualityEvaluationJobRunner INSTANCE; + + /** + * Gets a singleton instance of this class. + * @return A {@link SearchQualityEvaluationJobRunner}. + */ + public static SearchQualityEvaluationJobRunner getJobRunnerInstance() { + + LOGGER.info("Getting job runner instance"); + + if (INSTANCE != null) { + return INSTANCE; + } + + synchronized (SearchQualityEvaluationJobRunner.class) { + if (INSTANCE == null) { + INSTANCE = new SearchQualityEvaluationJobRunner(); + } + return INSTANCE; + } + + } + + private ClusterService clusterService; + private ThreadPool threadPool; + private Client client; + + private SearchQualityEvaluationJobRunner() { + + } + + public void setClusterService(ClusterService clusterService) { + this.clusterService = clusterService; + } + + public void setThreadPool(ThreadPool threadPool) { + this.threadPool = threadPool; + } + + public void setClient(Client client) { + this.client = client; + } + + @Override + public void runJob(final ScheduledJobParameter jobParameter, final JobExecutionContext context) { + + if(!(jobParameter instanceof SearchQualityEvaluationJobParameter)) { + throw new IllegalStateException( + "Job parameter is not instance of SampleJobParameter, type: " + jobParameter.getClass().getCanonicalName() + ); + } + + if(this.clusterService == null) { + throw new IllegalStateException("ClusterService is not initialized."); + } + + if(this.threadPool == null) { + throw new IllegalStateException("ThreadPool is not initialized."); + } + + final LockService lockService = context.getLockService(); + + final Runnable runnable = () -> { + + if (jobParameter.getLockDurationSeconds() != null) { + + lockService.acquireLock(jobParameter, context, ActionListener.wrap(lock -> { + + if (lock == null) { + return; + } + + final SearchQualityEvaluationJobParameter searchQualityEvaluationJobParameter = (SearchQualityEvaluationJobParameter) jobParameter; + + final long startTime = System.currentTimeMillis(); + final String judgmentsId; + + if("coec".equalsIgnoreCase(searchQualityEvaluationJobParameter.getClickModel())) { + + LOGGER.info("Beginning implicit judgment generation using clicks-over-expected-clicks."); + final CoecClickModelParameters coecClickModelParameters = new CoecClickModelParameters(searchQualityEvaluationJobParameter.getMaxRank()); + final CoecClickModel coecClickModel = new CoecClickModel(client, coecClickModelParameters); + + judgmentsId = coecClickModel.calculateJudgments(); + + } else { + + // Invalid click model. + throw new IllegalArgumentException("Invalid click model: " + searchQualityEvaluationJobParameter.getClickModel()); + + } + + final long elapsedTime = System.currentTimeMillis() - startTime; + LOGGER.info("Implicit judgment generation completed in {} ms", elapsedTime); + + final Map job = new HashMap<>(); + job.put("name", searchQualityEvaluationJobParameter.getName()); + job.put("click_model", searchQualityEvaluationJobParameter.getClickModel()); + job.put("started", startTime); + job.put("duration", elapsedTime); + job.put("judgments", judgmentsId); + job.put("invocation", "scheduled"); + job.put("max_rank", searchQualityEvaluationJobParameter.getMaxRank()); + + final IndexRequest indexRequest = new IndexRequest() + .index(SearchQualityEvaluationPlugin.COMPLETED_JOBS_INDEX_NAME) + .id(judgmentsId) + .source(job) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + + client.index(indexRequest, new ActionListener<>() { + @Override + public void onResponse(IndexResponse indexResponse) { + LOGGER.info("Successfully indexed implicit judgments {}", judgmentsId); + } + + @Override + public void onFailure(Exception ex) { + LOGGER.error("Unable to index implicit judgments", ex); + } + }); + + }, exception -> { throw new IllegalStateException("Failed to acquire lock."); })); + } + + }; + + threadPool.generic().submit(runnable); + + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java new file mode 100644 index 0000000..6a7b581 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java @@ -0,0 +1,213 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.client.Client; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.node.DiscoveryNodes; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.ClusterSettings; +import org.opensearch.common.settings.IndexScopedSettings; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.settings.SettingsFilter; +import org.opensearch.core.common.io.stream.NamedWriteableRegistry; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.core.xcontent.XContentParserUtils; +import org.opensearch.env.Environment; +import org.opensearch.env.NodeEnvironment; +import org.opensearch.jobscheduler.spi.JobSchedulerExtension; +import org.opensearch.jobscheduler.spi.ScheduledJobParser; +import org.opensearch.jobscheduler.spi.ScheduledJobRunner; +import org.opensearch.jobscheduler.spi.schedule.ScheduleParser; +import org.opensearch.plugins.ActionPlugin; +import org.opensearch.plugins.Plugin; +import org.opensearch.repositories.RepositoriesService; +import org.opensearch.rest.RestController; +import org.opensearch.rest.RestHandler; +import org.opensearch.script.ScriptService; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.watcher.ResourceWatcherService; + +import java.io.IOException; +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +/** + * Main class for the Search Quality Evaluation plugin. + */ +public class SearchQualityEvaluationPlugin extends Plugin implements ActionPlugin, JobSchedulerExtension { + + private static final Logger LOGGER = LogManager.getLogger(SearchQualityEvaluationPlugin.class); + + /** + * The name of the UBI index containing the queries. This should not be changed. + */ + public static final String UBI_QUERIES_INDEX_NAME = "ubi_queries"; + + /** + * The name of the UBI index containing the events. This should not be changed. + */ + public static final String UBI_EVENTS_INDEX_NAME = "ubi_events"; + + /** + * The name of the index to store the scheduled jobs to create implicit judgments. + */ + public static final String SCHEDULED_JOBS_INDEX_NAME = "search_quality_eval_scheduled_jobs"; + + /** + * The name of the index to store the completed jobs to create implicit judgments. + */ + public static final String COMPLETED_JOBS_INDEX_NAME = "search_quality_eval_completed_jobs"; + + /** + * The name of the index that stores the query sets. + */ + public static final String QUERY_SETS_INDEX_NAME = "search_quality_eval_query_sets"; + + /** + * The name of the index that stores the metrics for the dashboard. + */ + public static final String DASHBOARD_METRICS_INDEX_NAME = "sqe_metrics_sample_data"; + + /** + * The name of the index that stores the implicit judgments. + */ + public static final String JUDGMENTS_INDEX_NAME = "judgments"; + + @Override + public Collection createComponents( + final Client client, + final ClusterService clusterService, + final ThreadPool threadPool, + final ResourceWatcherService resourceWatcherService, + final ScriptService scriptService, + final NamedXContentRegistry xContentRegistry, + final Environment environment, + final NodeEnvironment nodeEnvironment, + final NamedWriteableRegistry namedWriteableRegistry, + final IndexNameExpressionResolver indexNameExpressionResolver, + final Supplier repositoriesServiceSupplier + ) { + + LOGGER.info("Creating search evaluation framework components"); + final SearchQualityEvaluationJobRunner jobRunner = SearchQualityEvaluationJobRunner.getJobRunnerInstance(); + jobRunner.setClusterService(clusterService); + jobRunner.setThreadPool(threadPool); + jobRunner.setClient(client); + + return Collections.emptyList(); + + } + + @Override + public String getJobType() { + return "scheduler_search_quality_eval"; + } + + @Override + public String getJobIndex() { + LOGGER.info("Getting job index name"); + return SCHEDULED_JOBS_INDEX_NAME; + } + + @Override + public ScheduledJobRunner getJobRunner() { + LOGGER.info("Creating job runner"); + return SearchQualityEvaluationJobRunner.getJobRunnerInstance(); + } + + @Override + public ScheduledJobParser getJobParser() { + + return (parser, id, jobDocVersion) -> { + + final SearchQualityEvaluationJobParameter jobParameter = new SearchQualityEvaluationJobParameter(); + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + + while (!parser.nextToken().equals(XContentParser.Token.END_OBJECT)) { + + final String fieldName = parser.currentName(); + + parser.nextToken(); + + switch (fieldName) { + case SearchQualityEvaluationJobParameter.NAME_FIELD: + jobParameter.setJobName(parser.text()); + break; + case SearchQualityEvaluationJobParameter.ENABLED_FILED: + jobParameter.setEnabled(parser.booleanValue()); + break; + case SearchQualityEvaluationJobParameter.ENABLED_TIME_FILED: + jobParameter.setEnabledTime(parseInstantValue(parser)); + break; + case SearchQualityEvaluationJobParameter.LAST_UPDATE_TIME_FIELD: + jobParameter.setLastUpdateTime(parseInstantValue(parser)); + break; + case SearchQualityEvaluationJobParameter.SCHEDULE_FIELD: + jobParameter.setSchedule(ScheduleParser.parse(parser)); + break; + case SearchQualityEvaluationJobParameter.LOCK_DURATION_SECONDS: + jobParameter.setLockDurationSeconds(parser.longValue()); + break; + case SearchQualityEvaluationJobParameter.JITTER: + jobParameter.setJitter(parser.doubleValue()); + break; + case SearchQualityEvaluationJobParameter.CLICK_MODEL: + jobParameter.setClickModel(parser.text()); + break; + case SearchQualityEvaluationJobParameter.MAX_RANK: + jobParameter.setMaxRank(parser.intValue()); + break; + default: + XContentParserUtils.throwUnknownToken(parser.currentToken(), parser.getTokenLocation()); + } + + } + + return jobParameter; + + }; + + } + + private Instant parseInstantValue(final XContentParser parser) throws IOException { + + if (XContentParser.Token.VALUE_NULL.equals(parser.currentToken())) { + return null; + } + + if (parser.currentToken().isValue()) { + return Instant.ofEpochMilli(parser.longValue()); + } + + XContentParserUtils.throwUnknownToken(parser.currentToken(), parser.getTokenLocation()); + return null; + + } + + @Override + public List getRestHandlers( + final Settings settings, + final RestController restController, + final ClusterSettings clusterSettings, + final IndexScopedSettings indexScopedSettings, + final SettingsFilter settingsFilter, + final IndexNameExpressionResolver indexNameExpressionResolver, + final Supplier nodesInCluster + ) { + return Collections.singletonList(new SearchQualityEvaluationRestHandler()); + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java new file mode 100644 index 0000000..ba56f04 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java @@ -0,0 +1,417 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.action.admin.indices.exists.indices.IndicesExistsRequest; +import org.opensearch.action.admin.indices.exists.indices.IndicesExistsResponse; +import org.opensearch.action.delete.DeleteRequest; +import org.opensearch.action.delete.DeleteResponse; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.index.IndexResponse; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.node.NodeClient; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel; +import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModelParameters; +import org.opensearch.eval.runners.OpenSearchQuerySetRunner; +import org.opensearch.eval.runners.QuerySetRunResult; +import org.opensearch.eval.samplers.AllQueriesQuerySampler; +import org.opensearch.eval.samplers.AllQueriesQuerySamplerParameters; +import org.opensearch.eval.samplers.ProbabilityProportionalToSizeAbstractQuerySampler; +import org.opensearch.eval.samplers.ProbabilityProportionalToSizeParameters; +import org.opensearch.jobscheduler.spi.schedule.IntervalSchedule; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestResponse; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +import static org.opensearch.eval.SearchQualityEvaluationPlugin.JUDGMENTS_INDEX_NAME; + +public class SearchQualityEvaluationRestHandler extends BaseRestHandler { + + private static final Logger LOGGER = LogManager.getLogger(SearchQualityEvaluationRestHandler.class); + + /** + * URL for the implicit judgment scheduling. + */ + public static final String SCHEDULING_URL = "/_plugins/search_quality_eval/schedule"; + + /** + * URL for on-demand implicit judgment generation. + */ + public static final String IMPLICIT_JUDGMENTS_URL = "/_plugins/search_quality_eval/judgments"; + + /** + * URL for managing query sets. + */ + public static final String QUERYSET_MANAGEMENT_URL = "/_plugins/search_quality_eval/queryset"; + + /** + * URL for initiating query sets to run on-demand. + */ + public static final String QUERYSET_RUN_URL = "/_plugins/search_quality_eval/run"; + + /** + * The placeholder in the query that gets replaced by the query term when running a query set. + */ + public static final String QUERY_PLACEHOLDER = "#$query##"; + + @Override + public String getName() { + return "Search Quality Evaluation Framework"; + } + + @Override + public List routes() { + return List.of( + new Route(RestRequest.Method.POST, IMPLICIT_JUDGMENTS_URL), + new Route(RestRequest.Method.POST, SCHEDULING_URL), + new Route(RestRequest.Method.DELETE, SCHEDULING_URL), + new Route(RestRequest.Method.POST, QUERYSET_MANAGEMENT_URL), + new Route(RestRequest.Method.POST, QUERYSET_RUN_URL)); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + + // Handle managing query sets. + if(QUERYSET_MANAGEMENT_URL.equalsIgnoreCase(request.path())) { + + // Creating a new query set by sampling the UBI queries. + if (request.method().equals(RestRequest.Method.POST)) { + + final String name = request.param("name"); + final String description = request.param("description"); + final String sampling = request.param("sampling", "pptss"); + final int querySetSize = Integer.parseInt(request.param("query_set_size", "1000")); + + // Create a query set by finding all the unique user_query terms. + if (AllQueriesQuerySampler.NAME.equalsIgnoreCase(sampling)) { + + // If we are not sampling queries, the query sets should just be directly + // indexed into OpenSearch using the `ubi_queries` index directly. + + try { + + final AllQueriesQuerySamplerParameters parameters = new AllQueriesQuerySamplerParameters(name, description, sampling, querySetSize); + final AllQueriesQuerySampler sampler = new AllQueriesQuerySampler(client, parameters); + + // Sample and index the queries. + final String querySetId = sampler.sample(); + + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"query_set\": \"" + querySetId + "\"}")); + + } catch(Exception ex) { + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "{\"error\": \"" + ex.getMessage() + "\"}")); + } + + + // Create a query set by using PPTSS sampling. + } else if (ProbabilityProportionalToSizeAbstractQuerySampler.NAME.equalsIgnoreCase(sampling)) { + + LOGGER.info("Creating query set using PPTSS"); + + final ProbabilityProportionalToSizeParameters parameters = new ProbabilityProportionalToSizeParameters(name, description, sampling, querySetSize); + final ProbabilityProportionalToSizeAbstractQuerySampler sampler = new ProbabilityProportionalToSizeAbstractQuerySampler(client, parameters); + + try { + + // Sample and index the queries. + final String querySetId = sampler.sample(); + + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"query_set\": \"" + querySetId + "\"}")); + + } catch(Exception ex) { + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "{\"error\": \"" + ex.getMessage() + "\"}")); + } + + } else { + // An Invalid sampling method was provided in the request. + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Invalid sampling method: " + sampling + "\"}")); + } + + } else { + // Invalid HTTP method for this endpoint. + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "{\"error\": \"" + request.method() + " is not allowed.\"}")); + } + + // Handle running query sets. + } else if(QUERYSET_RUN_URL.equalsIgnoreCase(request.path())) { + + final String querySetId = request.param("id"); + final String judgmentsId = request.param("judgments_id"); + final String index = request.param("index"); + final String searchPipeline = request.param("search_pipeline", null); + final String idField = request.param("id_field", "_id"); + final int k = Integer.parseInt(request.param("k", "10")); + final double threshold = Double.parseDouble(request.param("threshold", "1.0")); + + if(querySetId == null || querySetId.isEmpty() || judgmentsId == null || judgmentsId.isEmpty() || index == null || index.isEmpty()) { + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing required parameters.\"}")); + } + + if(k < 1) { + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"k must be a positive integer.\"}")); + } + + if(!request.hasContent()) { + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing query in body.\"}")); + } + + // Get the query JSON from the content. + final String query = new String(BytesReference.toBytes(request.content()), Charset.defaultCharset()); + + // Validate the query has a QUERY_PLACEHOLDER. + if(!query.contains(QUERY_PLACEHOLDER)) { + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing query placeholder in query.\"}")); + } + + try { + + final OpenSearchQuerySetRunner openSearchQuerySetRunner = new OpenSearchQuerySetRunner(client); + final QuerySetRunResult querySetRunResult = openSearchQuerySetRunner.run(querySetId, judgmentsId, index, searchPipeline, idField, query, k, threshold); + openSearchQuerySetRunner.save(querySetRunResult); + + } catch (Exception ex) { + LOGGER.error("Unable to run query set. Verify query set and judgments exist.", ex); + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, ex.getMessage())); + } + + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"message\": \"Run initiated for query set " + querySetId + "\"}")); + + // Handle the on-demand creation of implicit judgments. + } else if(IMPLICIT_JUDGMENTS_URL.equalsIgnoreCase(request.path())) { + + if (request.method().equals(RestRequest.Method.POST)) { + + //final long startTime = System.currentTimeMillis(); + final String clickModel = request.param("click_model", "coec"); + final int maxRank = Integer.parseInt(request.param("max_rank", "20")); + + if (CoecClickModel.CLICK_MODEL_NAME.equalsIgnoreCase(clickModel)) { + + final CoecClickModelParameters coecClickModelParameters = new CoecClickModelParameters(maxRank); + final CoecClickModel coecClickModel = new CoecClickModel(client, coecClickModelParameters); + + final String judgmentsId; + + // TODO: Run this in a separate thread. + try { + + // Create the judgments index. + createJudgmentsIndex(client); + + judgmentsId = coecClickModel.calculateJudgments(); + + // judgmentsId will be null if no judgments were created (and indexed). + if(judgmentsId == null) { + // TODO: Is Bad Request the appropriate error? Perhaps Conflict is more appropriate? + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"No judgments were created. Check the queries and events data.\"}")); + } + +// final long elapsedTime = System.currentTimeMillis() - startTime; +// +// final Map job = new HashMap<>(); +// job.put("name", "manual_generation"); +// job.put("click_model", clickModel); +// job.put("started", startTime); +// job.put("duration", elapsedTime); +// job.put("invocation", "on_demand"); +// job.put("judgments_id", judgmentsId); +// job.put("max_rank", maxRank); +// +// final String jobId = UUID.randomUUID().toString(); +// +// final IndexRequest indexRequest = new IndexRequest() +// .index(SearchQualityEvaluationPlugin.COMPLETED_JOBS_INDEX_NAME) +// .id(jobId) +// .source(job) +// .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); +// +// client.index(indexRequest, new ActionListener<>() { +// @Override +// public void onResponse(final IndexResponse indexResponse) { +// LOGGER.debug("Click model job completed successfully: {}", jobId); +// } +// +// @Override +// public void onFailure(final Exception ex) { +// LOGGER.error("Unable to run job with ID {}", jobId, ex); +// throw new RuntimeException("Unable to run job", ex); +// } +// }); + + } catch (Exception ex) { + throw new RuntimeException("Unable to generate judgments.", ex); + } + + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"judgments_id\": \"" + judgmentsId + "\"}")); + + } else { + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Invalid click model.\"}")); + } + + } else { + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "{\"error\": \"" + request.method() + " is not allowed.\"}")); + } + + // Handle the scheduling of creating implicit judgments. + } else if(SCHEDULING_URL.equalsIgnoreCase(request.path())) { + + if (request.method().equals(RestRequest.Method.POST)) { + + // Get the job parameters from the request. + final String id = request.param("id"); + final String jobName = request.param("job_name", UUID.randomUUID().toString()); + final String lockDurationSecondsString = request.param("lock_duration_seconds", "600"); + final Long lockDurationSeconds = lockDurationSecondsString != null ? Long.parseLong(lockDurationSecondsString) : null; + final String jitterString = request.param("jitter"); + final Double jitter = jitterString != null ? Double.parseDouble(jitterString) : null; + final String clickModel = request.param("click_model"); + final int maxRank = Integer.parseInt(request.param("max_rank", "20")); + + // Validate the request parameters. + if (id == null || clickModel == null) { + throw new IllegalArgumentException("The id and click_model parameters must be provided."); + } + + // Read the start_time. + final Instant startTime; + if (request.param("start_time") == null) { + startTime = Instant.now(); + } else { + startTime = Instant.ofEpochMilli(Long.parseLong(request.param("start_time"))); + } + + // Read the interval. + final int interval; + if (request.param("interval") == null) { + // Default to every 24 hours. + interval = 1440; + } else { + interval = Integer.parseInt(request.param("interval")); + } + + final SearchQualityEvaluationJobParameter jobParameter = new SearchQualityEvaluationJobParameter( + jobName, new IntervalSchedule(startTime, interval, ChronoUnit.MINUTES), lockDurationSeconds, + jitter, clickModel, maxRank + ); + + final IndexRequest indexRequest = new IndexRequest().index(SearchQualityEvaluationPlugin.SCHEDULED_JOBS_INDEX_NAME) + .id(id) + .source(jobParameter.toXContent(JsonXContent.contentBuilder(), null)) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + + return restChannel -> { + + // index the job parameter + client.index(indexRequest, new ActionListener<>() { + + @Override + public void onResponse(final IndexResponse indexResponse) { + + try { + + final RestResponse restResponse = new BytesRestResponse( + RestStatus.OK, + indexResponse.toXContent(JsonXContent.contentBuilder(), null) + ); + LOGGER.info("Created implicit judgments schedule for click-model {}: Job name {}, running every {} minutes starting {}", clickModel, jobName, interval, startTime); + + restChannel.sendResponse(restResponse); + + } catch (IOException e) { + restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, e.getMessage())); + } + + } + + @Override + public void onFailure(Exception e) { + restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, e.getMessage())); + } + }); + + }; + + // Delete a scheduled job to make implicit judgments. + } else if (request.method().equals(RestRequest.Method.DELETE)) { + + final String id = request.param("id"); + final DeleteRequest deleteRequest = new DeleteRequest().index(SearchQualityEvaluationPlugin.SCHEDULED_JOBS_INDEX_NAME).id(id); + + return restChannel -> client.delete(deleteRequest, new ActionListener<>() { + @Override + public void onResponse(final DeleteResponse deleteResponse) { + restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"message\": \"Scheduled job deleted.\"}")); + } + + @Override + public void onFailure(Exception e) { + restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, e.getMessage())); + } + }); + + } else { + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "{\"error\": \"" + request.method() + " is not allowed.\"}")); + } + + } else { + return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.NOT_FOUND, "{\"error\": \"" + request.path() + " was not found.\"}")); + } + + } + + private void createJudgmentsIndex(final NodeClient client) throws Exception { + + // If the judgments index does not exist we need to create it. + final IndicesExistsRequest indicesExistsRequest = new IndicesExistsRequest(JUDGMENTS_INDEX_NAME); + + final IndicesExistsResponse indicesExistsResponse = client.admin().indices().exists(indicesExistsRequest).get(); + + if(!indicesExistsResponse.isExists()) { + + // TODO: Read this mapping from a resource file instead. + final String mapping = "{\n" + + " \"properties\": {\n" + + " \"judgments_id\": { \"type\": \"keyword\" },\n" + + " \"query_id\": { \"type\": \"keyword\" },\n" + + " \"query\": { \"type\": \"keyword\" },\n" + + " \"document_id\": { \"type\": \"keyword\" },\n" + + " \"judgment\": { \"type\": \"double\" },\n" + + " \"timestamp\": { \"type\": \"date\", \"format\": \"strict_date_time\" }\n" + + " }\n" + + " }"; + + // Create the judgments index. + final CreateIndexRequest createIndexRequest = new CreateIndexRequest(JUDGMENTS_INDEX_NAME).mapping(mapping); + + // TODO: Don't use .get() + client.admin().indices().create(createIndexRequest).get(); + + } + + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java new file mode 100644 index 0000000..ea83a87 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.clickmodel; + +/** + * Base class for creating click models. + */ +public abstract class ClickModel { + + /** + * Calculate implicit judgments. + * @return The judgments ID. + * @throws Exception Thrown if the judgments cannot be created. + */ + public abstract String calculateJudgments() throws Exception; + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java new file mode 100644 index 0000000..8c42550 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java @@ -0,0 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.clickmodel; + +public abstract class ClickModelParameters { + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java new file mode 100644 index 0000000..f2e8aa8 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java @@ -0,0 +1,422 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.clickmodel.coec; + +import com.google.gson.Gson; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.search.SearchScrollRequest; +import org.opensearch.client.Client; +import org.opensearch.client.Requests; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.eval.SearchQualityEvaluationPlugin; +import org.opensearch.eval.judgments.clickmodel.ClickModel; +import org.opensearch.eval.judgments.model.ClickthroughRate; +import org.opensearch.eval.judgments.model.Judgment; +import org.opensearch.eval.judgments.model.ubi.event.UbiEvent; +import org.opensearch.eval.judgments.opensearch.OpenSearchHelper; +import org.opensearch.eval.judgments.queryhash.IncrementalUserQueryHash; +import org.opensearch.eval.utils.MathUtils; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.index.query.WrapperQueryBuilder; +import org.opensearch.search.Scroll; +import org.opensearch.search.SearchHit; +import org.opensearch.search.aggregations.AggregationBuilders; +import org.opensearch.search.aggregations.BucketOrder; +import org.opensearch.search.aggregations.bucket.terms.Terms; +import org.opensearch.search.aggregations.bucket.terms.TermsAggregationBuilder; +import org.opensearch.search.builder.SearchSourceBuilder; + +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; + +public class CoecClickModel extends ClickModel { + + public static final String CLICK_MODEL_NAME = "coec"; + + // OpenSearch indexes for COEC data. + public static final String INDEX_RANK_AGGREGATED_CTR = "rank_aggregated_ctr"; + public static final String INDEX_QUERY_DOC_CTR = "click_through_rates"; + + // UBI event names. + public static final String EVENT_CLICK = "click"; + public static final String EVENT_IMPRESSION = "impression"; + + private final CoecClickModelParameters parameters; + + private final OpenSearchHelper openSearchHelper; + + private final IncrementalUserQueryHash incrementalUserQueryHash = new IncrementalUserQueryHash(); + private final Gson gson = new Gson(); + private final Client client; + + private static final Logger LOGGER = LogManager.getLogger(CoecClickModel.class.getName()); + + public CoecClickModel(final Client client, final CoecClickModelParameters parameters) { + + this.parameters = parameters; + this.openSearchHelper = new OpenSearchHelper(client); + this.client = client; + + } + + @Override + public String calculateJudgments() throws Exception { + + final int maxRank = parameters.getMaxRank(); + + // Calculate and index the rank-aggregated click-through. + LOGGER.info("Beginning calculation of rank-aggregated click-through."); + final Map rankAggregatedClickThrough = getRankAggregatedClickThrough(); + LOGGER.info("Rank-aggregated clickthrough positions: {}", rankAggregatedClickThrough.size()); + showRankAggregatedClickThrough(rankAggregatedClickThrough); + + // Calculate and index the click-through rate for query/doc pairs. + LOGGER.info("Beginning calculation of clickthrough rates."); + final Map> clickthroughRates = getClickthroughRate(); + LOGGER.info("Clickthrough rates for number of queries: {}", clickthroughRates.size()); + showClickthroughRates(clickthroughRates); + + // Generate and index the implicit judgments. + LOGGER.info("Beginning calculation of implicit judgments."); + return calculateCoec(rankAggregatedClickThrough, clickthroughRates); + + } + + public String calculateCoec(final Map rankAggregatedClickThrough, + final Map> clickthroughRates) throws Exception { + + // Calculate the COEC. + // Numerator is the total number of clicks received by a query/result pair. + // Denominator is the expected clicks (EC) that an average result would receive after being impressed i times at rank r, + // and CTR is the average CTR for each position in the results page (up to R) computed over all queries and results. + + // Format: query_id, query, document, judgment + final Collection judgments = new LinkedList<>(); + + LOGGER.info("Count of queries: {}", clickthroughRates.size()); + + for(final String userQuery : clickthroughRates.keySet()) { + + // The clickthrough rates for this one query. + // A ClickthroughRate is a document with counts of impressions and clicks. + final Collection ctrs = clickthroughRates.get(userQuery); + + // Go through each clickthrough rate for this query. + for(final ClickthroughRate ctr : ctrs) { + + double denominatorSum = 0; + + for(int rank = 0; rank < parameters.getMaxRank(); rank++) { + + // The document's mean CTR at the rank. + final double meanCtrAtRank = rankAggregatedClickThrough.getOrDefault(rank, 0.0); + + // The number of times this document was shown as this rank. + final long countOfTimesShownAtRank = openSearchHelper.getCountOfQueriesForUserQueryHavingResultInRankR(userQuery, ctr.getObjectId(), rank); + + denominatorSum += (meanCtrAtRank * countOfTimesShownAtRank); + + } + + // Numerator is sum of clicks at all ranks up to the maxRank. + final int totalNumberClicksForQueryResult = ctr.getClicks(); + + // Divide the numerator by the denominator (value). + final double judgmentValue; + + if(denominatorSum == 0) { + judgmentValue = 0.0; + } else { + judgmentValue = totalNumberClicksForQueryResult / denominatorSum; + } + + // Hash the user query to get a query ID. + final int queryId = incrementalUserQueryHash.getHash(userQuery); + + // Add the judgment to the list. + // TODO: What to do for query ID when the values are per user_query instead? + final Judgment judgment = new Judgment(String.valueOf(queryId), userQuery, ctr.getObjectId(), judgmentValue); + judgments.add(judgment); + + } + + } + + LOGGER.info("Count of user queries: {}", clickthroughRates.size()); + LOGGER.info("Count of judgments: {}", judgments.size()); + + showJudgments(judgments); + + if(!(judgments.isEmpty())) { + return openSearchHelper.indexJudgments(judgments); + } else { + return null; + } + + } + + /** + * Gets the clickthrough rates for each query and its results. + * @return A map of user_query to the clickthrough rate for each query result. + * @throws IOException Thrown when a problem accessing OpenSearch. + */ + private Map> getClickthroughRate() throws Exception { + + // For each query: + // - Get each document returned in that query (in the QueryResponse object). + // - Calculate the click-through rate for the document. (clicks/impressions) + + // TODO: Allow for a time period and for a specific application. + + final String query = "{\n" + + " \"bool\": {\n" + + " \"should\": [\n" + + " {\n" + + " \"term\": {\n" + + " \"action_name\": \"click\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"term\": {\n" + + " \"action_name\": \"impression\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"must\": [\n" + + " {\n" + + " \"range\": {\n" + + " \"event_attributes.position.ordinal\": {\n" + + " \"lte\": " + parameters.getMaxRank() + "\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " }"; + + final BoolQueryBuilder queryBuilder = new BoolQueryBuilder().must(new WrapperQueryBuilder(query)); + final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(queryBuilder).size(1000); + final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(10L)); + + final SearchRequest searchRequest = Requests + .searchRequest(SearchQualityEvaluationPlugin.UBI_EVENTS_INDEX_NAME) + .source(searchSourceBuilder) + .scroll(scroll); + + // TODO Don't use .get() + SearchResponse searchResponse = client.search(searchRequest).get(); + + String scrollId = searchResponse.getScrollId(); + SearchHit[] searchHits = searchResponse.getHits().getHits(); + + final Map> queriesToClickthroughRates = new HashMap<>(); + + while (searchHits != null && searchHits.length > 0) { + + for (final SearchHit hit : searchHits) { + + final UbiEvent ubiEvent = AccessController.doPrivileged((PrivilegedAction) () -> gson.fromJson(hit.getSourceAsString(), UbiEvent.class)); + + // We need to the hash of the query_id because two users can both search + // for "computer" and those searches will have different query IDs, but they are the same search. + final String userQuery = openSearchHelper.getUserQuery(ubiEvent.getQueryId()); + + // userQuery will be null if there is not a query for this event in ubi_queries. + if(userQuery != null) { + + // Get the clicks for this queryId from the map, or an empty list if this is a new query. + final Set clickthroughRates = queriesToClickthroughRates.getOrDefault(userQuery, new LinkedHashSet<>()); + + // Get the ClickthroughRate object for the object that was interacted with. + final ClickthroughRate clickthroughRate = clickthroughRates.stream().filter(p -> p.getObjectId().equals(ubiEvent.getEventAttributes().getObject().getObjectId())).findFirst().orElse(new ClickthroughRate(ubiEvent.getEventAttributes().getObject().getObjectId())); + + if (EVENT_CLICK.equalsIgnoreCase(ubiEvent.getActionName())) { + //LOGGER.info("Logging a CLICK on " + ubiEvent.getEventAttributes().getObject().getObjectId()); + clickthroughRate.logClick(); + } else if (EVENT_IMPRESSION.equalsIgnoreCase(ubiEvent.getActionName())) { + //LOGGER.info("Logging an IMPRESSION on " + ubiEvent.getEventAttributes().getObject().getObjectId()); + clickthroughRate.logImpression(); + } else { + LOGGER.warn("Invalid event action name: {}", ubiEvent.getActionName()); + } + + clickthroughRates.add(clickthroughRate); + queriesToClickthroughRates.put(userQuery, clickthroughRates); + // LOGGER.debug("clickthroughRate = {}", queriesToClickthroughRates.size()); + + } + + } + + final SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); + scrollRequest.scroll(scroll); + + //LOGGER.info("Doing scroll to next results"); + // TODO: Getting a warning in the log that "QueryGroup _id can't be null, It should be set before accessing it. This is abnormal behaviour" + // I don't remember seeing this prior to 2.18.0 but it's possible I just didn't see it. + // https://github.com/opensearch-project/OpenSearch/blob/f105e4eb2ede1556b5dd3c743bea1ab9686ebccf/server/src/main/java/org/opensearch/wlm/QueryGroupTask.java#L73 + searchResponse = client.searchScroll(scrollRequest).get(); + //LOGGER.info("Scroll complete."); + + scrollId = searchResponse.getScrollId(); + + searchHits = searchResponse.getHits().getHits(); + + } + + openSearchHelper.indexClickthroughRates(queriesToClickthroughRates); + + return queriesToClickthroughRates; + + } + + /** + * Calculate the rank-aggregated click through from the UBI events. + * @return A map of positions to clickthrough rates. + * @throws IOException Thrown when a problem accessing OpenSearch. + */ + public Map getRankAggregatedClickThrough() throws Exception { + + final Map rankAggregatedClickThrough = new HashMap<>(); + + // TODO: Allow for a time period and for a specific application. + + final QueryBuilder findRangeNumber = QueryBuilders.rangeQuery("event_attributes.position.ordinal").lte(parameters.getMaxRank()); + final QueryBuilder queryBuilder = new BoolQueryBuilder().must(findRangeNumber); + + // Order the aggregations by key and not by value. + final BucketOrder bucketOrder = BucketOrder.key(true); + + final TermsAggregationBuilder positionsAggregator = AggregationBuilders.terms("By_Position").field("event_attributes.position.ordinal").order(bucketOrder).size(parameters.getMaxRank()); + final TermsAggregationBuilder actionNameAggregation = AggregationBuilders.terms("By_Action").field("action_name").subAggregation(positionsAggregator).order(bucketOrder).size(parameters.getMaxRank()); + + final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder() + .query(queryBuilder) + .aggregation(actionNameAggregation) + .from(0) + .size(0); + + final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.UBI_EVENTS_INDEX_NAME).source(searchSourceBuilder); + final SearchResponse searchResponse = client.search(searchRequest).get(); + + final Map clickCounts = new HashMap<>(); + final Map impressionCounts = new HashMap<>(); + + final Terms actionTerms = searchResponse.getAggregations().get("By_Action"); + final Collection actionBuckets = actionTerms.getBuckets(); + + LOGGER.debug("Aggregation query: {}", searchSourceBuilder.toString()); + + for(final Terms.Bucket actionBucket : actionBuckets) { + + // Handle the "impression" bucket. + if(EVENT_IMPRESSION.equalsIgnoreCase(actionBucket.getKey().toString())) { + + final Terms positionTerms = actionBucket.getAggregations().get("By_Position"); + final Collection positionBuckets = positionTerms.getBuckets(); + + for(final Terms.Bucket positionBucket : positionBuckets) { + LOGGER.debug("Inserting impression event from position {} with click count {}", positionBucket.getKey(), (double) positionBucket.getDocCount()); + impressionCounts.put(Integer.valueOf(positionBucket.getKey().toString()), (double) positionBucket.getDocCount()); + } + + } + + // Handle the "click" bucket. + if(EVENT_CLICK.equalsIgnoreCase(actionBucket.getKey().toString())) { + + final Terms positionTerms = actionBucket.getAggregations().get("By_Position"); + final Collection positionBuckets = positionTerms.getBuckets(); + + for(final Terms.Bucket positionBucket : positionBuckets) { + LOGGER.debug("Inserting client event from position {} with click count {}", positionBucket.getKey(), (double) positionBucket.getDocCount()); + clickCounts.put(Integer.valueOf(positionBucket.getKey().toString()), (double) positionBucket.getDocCount()); + } + + } + + } + + for(int rank = 0; rank < parameters.getMaxRank(); rank++) { + + if(impressionCounts.containsKey(rank)) { + + if(clickCounts.containsKey(rank)) { + + // Calculate the CTR by dividing the number of clicks by the number of impressions. + LOGGER.info("Position = {}, Impression Counts = {}, Click Count = {}", rank, impressionCounts.get(rank), clickCounts.get(rank)); + rankAggregatedClickThrough.put(rank, clickCounts.get(rank) / impressionCounts.get(rank)); + + } else { + + // This document has impressions but no clicks, so it's CTR is zero. + LOGGER.info("Position = {}, Impression Counts = {}, Impressions but no clicks so CTR is 0", rank, clickCounts.get(rank)); + rankAggregatedClickThrough.put(rank, 0.0); + + } + + } else { + + // No impressions so the clickthrough rate is 0. + LOGGER.info("No impressions for rank {}, so using CTR of 0", rank); + rankAggregatedClickThrough.put(rank, (double) 0); + + } + + } + + openSearchHelper.indexRankAggregatedClickthrough(rankAggregatedClickThrough); + + return rankAggregatedClickThrough; + + } + + private void showJudgments(final Collection judgments) { + + for(final Judgment judgment : judgments) { + LOGGER.info(judgment.toJudgmentString()); + } + + } + + private void showClickthroughRates(final Map> clickthroughRates) { + + for(final String userQuery : clickthroughRates.keySet()) { + + LOGGER.debug("user_query: {}", userQuery); + + for(final ClickthroughRate clickthroughRate : clickthroughRates.get(userQuery)) { + LOGGER.debug("\t - {}", clickthroughRate.toString()); + } + + } + + } + + private void showRankAggregatedClickThrough(final Map rankAggregatedClickThrough) { + + for(final int position : rankAggregatedClickThrough.keySet()) { + LOGGER.info("Position: {}, # ctr: {}", position, MathUtils.round(rankAggregatedClickThrough.get(position), parameters.getRoundingDigits())); + } + + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java new file mode 100644 index 0000000..36df03e --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.clickmodel.coec; + +import org.opensearch.eval.judgments.clickmodel.ClickModelParameters; + +/** + * The parameters for the {@link CoecClickModel}. + */ +public class CoecClickModelParameters extends ClickModelParameters { + + private final int maxRank; + private int roundingDigits = 3; + + /** + * Creates new parameters. + * @param maxRank The max rank to use when calculating the judgments. + */ + public CoecClickModelParameters(final int maxRank) { + this.maxRank = maxRank; + } + + /** + * Creates new parameters. + * @param maxRank The max rank to use when calculating the judgments. + * @param roundingDigits The number of decimal places to round calculated values to. + */ + public CoecClickModelParameters(final int maxRank, final int roundingDigits) { + this.maxRank = maxRank; + this.roundingDigits = roundingDigits; + } + + /** + * Gets the max rank for the implicit judgments calculation. + * @return The max rank for the implicit judgments calculation. + */ + public int getMaxRank() { + return maxRank; + } + + /** + * Gets the number of rounding digits to use for judgments. + * @return The number of rounding digits to use for judgments. + */ + public int getRoundingDigits() { + return roundingDigits; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java new file mode 100644 index 0000000..cef1f1f --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java @@ -0,0 +1,96 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.model; + +import org.opensearch.eval.utils.MathUtils; + +/** + * A query result and its number of clicks and total events. + */ +public class ClickthroughRate { + + private final String objectId; + private int clicks; + private int impressions; + + /** + * Creates a new clickthrough rate for an object. + * @param objectId The ID of the object. + */ + public ClickthroughRate(final String objectId) { + this.objectId = objectId; + this.clicks = 0; + this.impressions = 0; + } + + /** + * Creates a new clickthrough rate for an object given counts of clicks and events. + * @param objectId The object ID. + * @param clicks The count of clicks. + * @param impressions The count of events. + */ + public ClickthroughRate(final String objectId, final int clicks, final int impressions) { + this.objectId = objectId; + this.clicks = clicks; + this.impressions = impressions; + } + + @Override + public String toString() { + return "object_id: " + objectId + ", clicks: " + clicks + ", events: " + impressions + ", ctr: " + MathUtils.round(getClickthroughRate()); + } + + /** + * Log a click to this object. + * This increments clicks and events. + */ + public void logClick() { + clicks++; + } + + /** + * Log an impression to this object. + */ + public void logImpression() { + impressions++; + } + + /** + * Calculate the clickthrough rate. + * @return The clickthrough rate as clicks divided by events. + */ + public double getClickthroughRate() { + return (double) clicks / impressions; + } + + /** + * Gets the count of clicks. + * @return The count of clicks. + */ + public int getClicks() { + return clicks; + } + + /** + * Gets the count of events. + * @return The count of events. + */ + public int getImpressions() { + return impressions; + } + + /** + * Gets the object ID. + * @return The object ID. + */ + public String getObjectId() { + return objectId; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/Judgment.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/Judgment.java new file mode 100644 index 0000000..bc9955f --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/Judgment.java @@ -0,0 +1,97 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.model; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.eval.utils.MathUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * A judgment of a search result's quality for a given query. + */ +public class Judgment { + + private static final Logger LOGGER = LogManager.getLogger(Judgment.class.getName()); + + private final String queryId; + private final String query; + private final String document; + private final double judgment; + + /** + * Creates a new judgment. + * @param queryId The query ID for the judgment. + * @param query The query for the judgment. + * @param document The document in the jdugment. + * @param judgment The judgment value. + */ + public Judgment(final String queryId, final String query, final String document, final double judgment) { + this.queryId = queryId; + this.query = query; + this.document = document; + this.judgment = judgment; + } + + public String toJudgmentString() { + return queryId + ", " + query + ", " + document + ", " + MathUtils.round(judgment); + } + + public Map getJudgmentAsMap() { + + final Map judgmentMap = new HashMap<>(); + judgmentMap.put("query_id", queryId); + judgmentMap.put("query", query); + judgmentMap.put("document_id", document); + judgmentMap.put("judgment", judgment); + + return judgmentMap; + + } + + @Override + public String toString() { + return "query_id: " + queryId + ", query: " + query + ", document: " + document + ", judgment: " + MathUtils.round(judgment); + } + + /** + * Gets the judgment's query ID. + * @return The judgment's query ID. + */ + public String getQueryId() { + return queryId; + } + + /** + * Gets the judgment's query. + * @return The judgment's query. + */ + public String getQuery() { + return query; + } + + /** + * Gets the judgment's document. + * @return The judgment's document. + */ + public String getDocument() { + return document; + } + + /** + * Gets the judgment's value. + * @return The judgment's value. + */ + public double getJudgment() { + return judgment; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java new file mode 100644 index 0000000..2244df4 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java @@ -0,0 +1,29 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.model; + +public class QuerySetQuery { + + private final String query; + private final long frequency; + + public QuerySetQuery(final String query, final long frequency) { + this.query = query; + this.frequency = frequency; + } + + public String getQuery() { + return query; + } + + public long getFrequency() { + return frequency; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java new file mode 100644 index 0000000..cf09444 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java @@ -0,0 +1,82 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.model.ubi.event; + +import com.google.gson.annotations.SerializedName; + +/** + * Attributes on an UBI event. + */ +public class EventAttributes { + + @SerializedName("object") + private EventObject object; + + @SerializedName("session_id") + private String sessionId; + + @SerializedName("position") + private Position position; + + /** + * Creates a new object. + */ + public EventAttributes() { + + } + + /** + * Gets the {@link EventObject} for the event. + * @return A {@link EventObject}. + */ + public EventObject getObject() { + return object; + } + + /** + * Sets the {@link EventObject} for the event. + * @param object A {@link EventObject}. + */ + public void setObject(EventObject object) { + this.object = object; + } + + /** + * Gets the session ID for the event. + * @return The session ID for the event. + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the session ID for the event. + * @param sessionId The session ID for the evnet. + */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** + * Gets the {@link Position} associated with the event. + * @return The {@link Position} associated with the event. + */ + public Position getPosition() { + return position; + } + + /** + * Sets the {@link Position} associated with the event. + * @param position The {@link Position} associated with the event. + */ + public void setPosition(Position position) { + this.position = position; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java new file mode 100644 index 0000000..55595ba --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java @@ -0,0 +1,58 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.model.ubi.event; + +import com.google.gson.annotations.SerializedName; + +public class EventObject { + + @SerializedName("object_id_field") + private String objectIdField; + + @SerializedName("object_id") + private String objectId; + + @Override + public String toString() { + return "[" + objectIdField + ", " + objectId + "]"; + } + + /** + * Gets the object ID. + * @return The object ID. + */ + public String getObjectId() { + return objectId; + } + + /** + * Sets the object ID. + * @param objectId The object ID. + */ + public void setObjectId(String objectId) { + this.objectId = objectId; + } + + /** + * Gets the object ID field. + * @return The object ID field. + */ + public String getObjectIdField() { + return objectIdField; + } + + /** + * Sets the object ID field. + * @param objectIdField The object ID field. + */ + public void setObjectIdField(String objectIdField) { + this.objectIdField = objectIdField; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java new file mode 100644 index 0000000..e3ebaad --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.model.ubi.event; + +import com.google.gson.annotations.SerializedName; + +/** + * A position represents the location of a search result in an event. + */ +public class Position { + + @SerializedName("ordinal") + private int ordinal; + + @Override + public String toString() { + return String.valueOf(ordinal); + } + + /** + * Gets the ordinal of the position. + * @return The ordinal of the position. + */ + public int getOrdinal() { + return ordinal; + } + + /** + * Sets the ordinal of the position. + * @param ordinal The ordinal of the position. + */ + public void setOrdinal(int ordinal) { + this.ordinal = ordinal; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java new file mode 100644 index 0000000..61c0f8b --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java @@ -0,0 +1,82 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.model.ubi.event; + +import com.google.gson.annotations.SerializedName; + +/** + * Creates a representation of a UBI event. + */ +public class UbiEvent { + + @SerializedName("action_name") + private String actionName; + + @SerializedName("client_id") + private String clientId; + + @SerializedName("query_id") + private String queryId; + + @SerializedName("event_attributes") + private EventAttributes eventAttributes; + + /** + * Creates a new representation of an UBI event. + */ + public UbiEvent() { + + } + + @Override + public String toString() { + return actionName + ", " + clientId + ", " + queryId + ", " + eventAttributes.getObject().toString() + ", " + eventAttributes.getPosition().getOrdinal(); + } + + /** + * Gets the name of the action. + * @return The name of the action. + */ + public String getActionName() { + return actionName; + } + + /** + * Gets the client ID. + * @return The client ID. + */ + public String getClientId() { + return clientId; + } + + /** + * Gets the query ID. + * @return The query ID. + */ + public String getQueryId() { + return queryId; + } + + /** + * Gets the event attributes. + * @return The {@link EventAttributes}. + */ + public EventAttributes getEventAttributes() { + return eventAttributes; + } + + /** + * Sets the event attributes. + * @param eventAttributes The {@link EventAttributes}. + */ + public void setEventAttributes(EventAttributes eventAttributes) { + this.eventAttributes = eventAttributes; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java new file mode 100644 index 0000000..5d45ee0 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java @@ -0,0 +1,58 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.model.ubi.query; + +import java.util.List; + +/** + * A query response for a {@link UbiQuery query}. + */ +public class QueryResponse { + + private final String queryId; + private final String queryResponseId; + private final List queryResponseHitIds; + + /** + * Creates a query response. + * @param queryId The ID of the query. + * @param queryResponseId The ID of the query response. + * @param queryResponseHitIds A list of IDs for the hits in the query. + */ + public QueryResponse(final String queryId, final String queryResponseId, final List queryResponseHitIds) { + this.queryId = queryId; + this.queryResponseId = queryResponseId; + this.queryResponseHitIds = queryResponseHitIds; + } + + /** + * Gets the query ID. + * @return The query ID. + */ + public String getQueryId() { + return queryId; + } + + /** + * Gets the query response ID. + * @return The query response ID. + */ + public String getQueryResponseId() { + return queryResponseId; + } + + /** + * Gets the list of query response hit IDs. + * @return A list of query response hit IDs. + */ + public List getQueryResponseHitIds() { + return queryResponseHitIds; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java new file mode 100644 index 0000000..0b7ca0b --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java @@ -0,0 +1,160 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.model.ubi.query; + +import com.google.gson.annotations.SerializedName; + +import java.util.Map; + +/** + * Represents a UBI query. + */ +public class UbiQuery { + + @SerializedName("timestamp") + private String timestamp; + + @SerializedName("query_id") + private String queryId; + + @SerializedName("client_id") + private String clientId; + + @SerializedName("user_query") + private String userQuery; + + @SerializedName("query") + private String query; + + @SerializedName("query_attributes") + private Map queryAttributes; + + @SerializedName("query_response") + private QueryResponse queryResponse; + + /** + * Creates a new UBI query object. + */ + public UbiQuery() { + + } + + /** + * Gets the timestamp for the query. + * @return The timestamp for the query. + */ + public String getTimestamp() { + return timestamp; + } + + /** + * Sets the timestamp for the query. + * @param timestamp The timestamp for the query. + */ + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + /** + * Gets the query ID. + * @return The query ID. + */ + public String getQueryId() { + return queryId; + } + + /** + * Sets the query ID. + * @param queryId The query ID. + */ + public void setQueryId(String queryId) { + this.queryId = queryId; + } + + /** + * Sets the client ID. + * @param clientId The client ID. + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * Gets the client ID. + * @return The client ID. + */ + public String getClientId() { + return clientId; + } + + /** + * Gets the user query. + * @return The user query. + */ + public String getUserQuery() { + return userQuery; + } + + /** + * Sets the user query. + * @param userQuery The user query. + */ + public void setUserQuery(String userQuery) { + this.userQuery = userQuery; + } + + /** + * Gets the query. + * @return The query. + */ + public String getQuery() { + return query; + } + + /** + * Sets the query. + * @param query The query. + */ + public void setQuery(String query) { + this.query = query; + } + + /** + * Sets the query attributes. + * @return The query attributes. + */ + public Map getQueryAttributes() { + return queryAttributes; + } + + /** + * Sets the query attributes. + * @param queryAttributes The query attributes. + */ + public void setQueryAttributes(Map queryAttributes) { + this.queryAttributes = queryAttributes; + } + + /** + * Gets the query responses. + * @return The query responses. + */ + public QueryResponse getQueryResponse() { + return queryResponse; + } + + /** + * Sets the query responses. + * @param queryResponse The query responses. + */ + public void setQueryResponse(QueryResponse queryResponse) { + this.queryResponse = queryResponse; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java new file mode 100644 index 0000000..3c391b3 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java @@ -0,0 +1,342 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.opensearch; + +import com.google.gson.Gson; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.bulk.BulkResponse; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.Client; +import org.opensearch.core.action.ActionListener; +import org.opensearch.eval.judgments.model.ClickthroughRate; +import org.opensearch.eval.judgments.model.Judgment; +import org.opensearch.eval.judgments.model.ubi.query.UbiQuery; +import org.opensearch.eval.utils.TimeUtils; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.index.query.WrapperQueryBuilder; +import org.opensearch.search.SearchHit; +import org.opensearch.search.builder.SearchSourceBuilder; + +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.opensearch.eval.SearchQualityEvaluationPlugin.JUDGMENTS_INDEX_NAME; +import static org.opensearch.eval.SearchQualityEvaluationPlugin.UBI_EVENTS_INDEX_NAME; +import static org.opensearch.eval.SearchQualityEvaluationPlugin.UBI_QUERIES_INDEX_NAME; +import static org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel.INDEX_QUERY_DOC_CTR; +import static org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel.INDEX_RANK_AGGREGATED_CTR; + +/** + * Functionality for interacting with OpenSearch. + * TODO: Move these functions out of this class. + */ +public class OpenSearchHelper { + + private static final Logger LOGGER = LogManager.getLogger(OpenSearchHelper.class.getName()); + + private final Client client; + private final Gson gson = new Gson(); + + // Used to cache the query ID->user_query to avoid unnecessary lookups to OpenSearch. + private static final Map userQueryCache = new HashMap<>(); + + public OpenSearchHelper(final Client client) { + this.client = client; + } + + /** + * Gets the user query for a given query ID. + * @param queryId The query ID. + * @return The user query. + * @throws IOException Thrown when there is a problem accessing OpenSearch. + */ + public String getUserQuery(final String queryId) throws Exception { + + // If it's in the cache just get it and return it. + if(userQueryCache.containsKey(queryId)) { + return userQueryCache.get(queryId); + } + + // Cache it and return it. + final UbiQuery ubiQuery = getQueryFromQueryId(queryId); + + // ubiQuery will be null if the query does not exist. + if(ubiQuery != null) { + + userQueryCache.put(queryId, ubiQuery.getUserQuery()); + return ubiQuery.getUserQuery(); + + } else { + + return null; + + } + + } + + /** + * Gets the query object for a given query ID. + * @param queryId The query ID. + * @return A {@link UbiQuery} object for the given query ID. + * @throws Exception Thrown if the query cannot be retrieved. + */ + public UbiQuery getQueryFromQueryId(final String queryId) throws Exception { + + LOGGER.debug("Getting query from query ID {}", queryId); + + final String query = "{\"match\": {\"query_id\": \"" + queryId + "\" }}"; + final WrapperQueryBuilder qb = QueryBuilders.wrapperQuery(query); + + // The query_id should be unique anyway, but we are limiting it to a single result anyway. + final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.query(qb); + searchSourceBuilder.from(0); + searchSourceBuilder.size(1); + + final String[] indexes = {UBI_QUERIES_INDEX_NAME}; + + final SearchRequest searchRequest = new SearchRequest(indexes, searchSourceBuilder); + final SearchResponse response = client.search(searchRequest).get(); + + // If this does not return a query then we cannot calculate the judgments. Each even should have a query associated with it. + if(response.getHits().getHits() != null & response.getHits().getHits().length > 0) { + + final SearchHit hit = response.getHits().getHits()[0]; + return AccessController.doPrivileged((PrivilegedAction) () -> gson.fromJson(hit.getSourceAsString(), UbiQuery.class)); + + } else { + + LOGGER.warn("No query exists for query ID {} to calculate judgments.", queryId); + return null; + + } + + } + + private Collection getQueryIdsHavingUserQuery(final String userQuery) throws Exception { + + final String query = "{\"match\": {\"user_query\": \"" + userQuery + "\" }}"; + final WrapperQueryBuilder qb = QueryBuilders.wrapperQuery(query); + + final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.query(qb); + + final String[] indexes = {UBI_QUERIES_INDEX_NAME}; + + final SearchRequest searchRequest = new SearchRequest(indexes, searchSourceBuilder); + final SearchResponse response = client.search(searchRequest).get(); + + final Collection queryIds = new ArrayList<>(); + + for(final SearchHit hit : response.getHits().getHits()) { + final String queryId = hit.getSourceAsMap().get("query_id").toString(); + queryIds.add(queryId); + } + + return queryIds; + + } + + public long getCountOfQueriesForUserQueryHavingResultInRankR(final String userQuery, final String objectId, final int rank) throws Exception { + + long countOfTimesShownAtRank = 0; + + // Get all query IDs matching this user query. + final Collection queryIds = getQueryIdsHavingUserQuery(userQuery); + + // For each query ID, get the events with action_name = "impression" having a match on objectId and rank (position). + for(final String queryId : queryIds) { + + final String query = "{\n" + + " \"bool\": {\n" + + " \"must\": [\n" + + " {\n" + + " \"term\": {\n" + + " \"query_id\": \"" + queryId + "\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"term\": {\n" + + " \"action_name\": \"impression\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"term\": {\n" + + " \"event_attributes.position.ordinal\": \"" + rank + "\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"term\": {\n" + + " \"event_attributes.object.object_id\": \"" + objectId + "\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " }"; + + final WrapperQueryBuilder qb = QueryBuilders.wrapperQuery(query); + + final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.query(qb); + searchSourceBuilder.trackTotalHits(true); + searchSourceBuilder.size(0); + + final String[] indexes = {UBI_EVENTS_INDEX_NAME}; + + final SearchRequest searchRequest = new SearchRequest(indexes, searchSourceBuilder); + final SearchResponse response = client.search(searchRequest).get(); + + // Won't be null as long as trackTotalHits is true. + if(response.getHits().getTotalHits() != null) { + countOfTimesShownAtRank += response.getHits().getTotalHits().value; + } + + } + + LOGGER.debug("Count of {} having {} at rank {} = {}", userQuery, objectId, rank, countOfTimesShownAtRank); + + if(countOfTimesShownAtRank > 0) { + LOGGER.debug("Count of {} having {} at rank {} = {}", userQuery, objectId, rank, countOfTimesShownAtRank); + } + + return countOfTimesShownAtRank; + + } + + /** + * Index the rank-aggregated clickthrough values. + * @param rankAggregatedClickThrough A map of position to clickthrough values. + * @throws IOException Thrown when there is a problem accessing OpenSearch. + */ + public void indexRankAggregatedClickthrough(final Map rankAggregatedClickThrough) throws Exception { + + if(!rankAggregatedClickThrough.isEmpty()) { + + // TODO: Split this into multiple bulk insert requests. + + final BulkRequest request = new BulkRequest(); + + for (final int position : rankAggregatedClickThrough.keySet()) { + + final Map jsonMap = new HashMap<>(); + jsonMap.put("position", position); + jsonMap.put("ctr", rankAggregatedClickThrough.get(position)); + + final IndexRequest indexRequest = new IndexRequest(INDEX_RANK_AGGREGATED_CTR).id(UUID.randomUUID().toString()).source(jsonMap); + + request.add(indexRequest); + + } + + client.bulk(request).get(); + + } + + } + + /** + * Index the clickthrough rates. + * @param clickthroughRates A map of query IDs to a collection of {@link ClickthroughRate} objects. + * @throws IOException Thrown when there is a problem accessing OpenSearch. + */ + public void indexClickthroughRates(final Map> clickthroughRates) throws Exception { + + if(!clickthroughRates.isEmpty()) { + + final BulkRequest request = new BulkRequest(); + + for(final String userQuery : clickthroughRates.keySet()) { + + for(final ClickthroughRate clickthroughRate : clickthroughRates.get(userQuery)) { + + final Map jsonMap = new HashMap<>(); + jsonMap.put("user_query", userQuery); + jsonMap.put("clicks", clickthroughRate.getClicks()); + jsonMap.put("events", clickthroughRate.getImpressions()); + jsonMap.put("ctr", clickthroughRate.getClickthroughRate()); + jsonMap.put("object_id", clickthroughRate.getObjectId()); + + final IndexRequest indexRequest = new IndexRequest(INDEX_QUERY_DOC_CTR) + .id(UUID.randomUUID().toString()) + .source(jsonMap); + + request.add(indexRequest); + + } + + } + + client.bulk(request, new ActionListener<>() { + + @Override + public void onResponse(BulkResponse bulkItemResponses) { + if(bulkItemResponses.hasFailures()) { + LOGGER.error("Clickthrough rates were not all successfully indexed: {}", bulkItemResponses.buildFailureMessage()); + } else { + LOGGER.debug("Clickthrough rates has been successfully indexed."); + } + } + + @Override + public void onFailure(Exception ex) { + LOGGER.error("Indexing the clickthrough rates failed.", ex); + } + + }); + + } + + } + + /** + * Index the judgments. + * @param judgments A collection of {@link Judgment judgments}. + * @throws IOException Thrown when there is a problem accessing OpenSearch. + * @return The ID of the indexed judgments. + */ + public String indexJudgments(final Collection judgments) throws Exception { + + final String judgmentsId = UUID.randomUUID().toString(); + final String timestamp = TimeUtils.getTimestamp(); + + final BulkRequest bulkRequest = new BulkRequest(); + + for(final Judgment judgment : judgments) { + + final Map j = judgment.getJudgmentAsMap(); + j.put("judgments_id", judgmentsId); + j.put("timestamp", timestamp); + + final IndexRequest indexRequest = new IndexRequest(JUDGMENTS_INDEX_NAME) + .id(UUID.randomUUID().toString()) + .source(j); + + bulkRequest.add(indexRequest); + + } + + // TODO: Don't use .get() + client.bulk(bulkRequest).get(); + + return judgmentsId; + + } + +} \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java new file mode 100644 index 0000000..b893f43 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.queryhash; + +import java.util.HashMap; +import java.util.Map; + +/** + * Facilitates the hashing of user queries. + */ +public class IncrementalUserQueryHash implements UserQueryHash { + + private final Map userQueries; + private int count = 1; + + /** + * Creates a new instance of this class. + */ + public IncrementalUserQueryHash() { + this.userQueries = new HashMap<>(); + } + + @Override + public int getHash(String userQuery) { + + final int hash; + + if(userQueries.containsKey(userQuery)) { + + return userQueries.get(userQuery); + + } else { + + userQueries.put(userQuery, count); + hash = count; + count++; + + + } + + return hash; + + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java new file mode 100644 index 0000000..714f85a --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.judgments.queryhash; + +/** + * In interface for creating hashes of user queries. + */ +public interface UserQueryHash { + + /** + * Creates a unique integer given a user query. + * @param userQuery The user query. + * @return A unique integer representing the user query. + */ + int getHash(String userQuery); + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java new file mode 100644 index 0000000..446696f --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java @@ -0,0 +1,64 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.metrics; + +import java.util.List; + +/** + * Subclass of {@link SearchMetric} that calculates Discounted Cumulative Gain @ k. + */ +public class DcgSearchMetric extends SearchMetric { + + protected final List relevanceScores; + + /** + * Creates new DCG metrics. + * @param k The k value. + * @param relevanceScores A list of relevance scores. + */ + public DcgSearchMetric(final int k, final List relevanceScores) { + super(k); + this.relevanceScores = relevanceScores; + } + + @Override + public String getName() { + return "dcg_at_" + k; + } + + @Override + public double calculate() { + return calculateDcg(relevanceScores); + } + + protected double calculateDcg(final List relevanceScores) { + + // k should equal the size of relevanceScores. + + double dcg = 0.0; + + for (int i = 0; i < relevanceScores.size(); i++) { + + double d = log2(i + 2); + double n = Math.pow(2, relevanceScores.get(i)) - 1; + + if(d != 0) { + dcg += (n / d); + } + + } + return dcg; + + } + + private double log2(int N) { + return Math.log(N) / Math.log(2); + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java new file mode 100644 index 0000000..a392732 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.metrics; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Subclass of {@link SearchMetric} that calculates Normalized Discounted Cumulative Gain @ k. + */ +public class NdcgSearchMetric extends DcgSearchMetric { + + /** + * Creates new NDCG metrics. + * @param k The k value. + * @param relevanceScores A list of relevancy scores. + */ + public NdcgSearchMetric(final int k, final List relevanceScores) { + super(k, relevanceScores); + } + + @Override + public String getName() { + return "ndcg_at_" + k; + } + + @Override + public double calculate() { + + double dcg = super.calculate(); + + if(dcg == 0) { + + // The ndcg is 0. No need to continue. + return 0; + + } else { + + final List idealRelevanceScores = new ArrayList<>(relevanceScores); + idealRelevanceScores.sort(Collections.reverseOrder()); + + double idcg = super.calculateDcg(idealRelevanceScores); + + if(idcg == 0) { + return 0; + } else { + return dcg / idcg; + } + + } + + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java new file mode 100644 index 0000000..a2ac50b --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java @@ -0,0 +1,63 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.metrics; + +import java.util.List; + +/** + * Subclass of {@link SearchMetric} that calculates Precision @ k. + */ +public class PrecisionSearchMetric extends SearchMetric { + + private final double threshold; + private final List relevanceScores; + + /** + * Creates new precision metrics. + * @param k The k value. + * @param threshold The threshold for assigning binary relevancy scores to non-binary scores. + * Scores greater than or equal to this value will be assigned a relevancy score of 1 (relevant). + * Scores less than this value will be assigned a relevancy score of 0 (not relevant). + * @param relevanceScores A list of relevance scores. + */ + public PrecisionSearchMetric(final int k, final double threshold, final List relevanceScores) { + super(k); + this.threshold = threshold; + this.relevanceScores = relevanceScores; + } + + @Override + public String getName() { + return "precision_at_" + k; + } + + @Override + public double calculate() { + + double numberOfRelevantItems = 0; + + for(final double relevanceScore : relevanceScores) { + if(relevanceScore >= threshold) { + numberOfRelevantItems++; + } + } + + return numberOfRelevantItems / (double) k; + + } + + /** + * Gets the threshold value. + * @return The threshold value. + */ + public double threshold() { + return threshold; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/SearchMetric.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/SearchMetric.java new file mode 100644 index 0000000..acd580a --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/SearchMetric.java @@ -0,0 +1,70 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.metrics; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Base class for search metrics. + */ +public abstract class SearchMetric { + + protected static final Logger LOGGER = LogManager.getLogger(SearchMetric.class); + + protected int k; + + /** + * Gets the name of the metric, i.e. ndcg. + * @return The name of the metric. + */ + public abstract String getName(); + + /** + * Calculates the metric. + * @return The value of the metric. + */ + public abstract double calculate(); + + private Double value = Double.NaN; + + /** + * Creates the metric. + * @param k The k value. + */ + public SearchMetric(final int k) { + this.k = k; + } + + /** + * Gets the k value. + * @return The k value. + */ + public int getK() { + return k; + } + + /** + * Gets the value of the metric. If the metric has not yet been calculated, + * the metric will first be calculated by calling calculate. This + * function should be used in cases where repeated access to the metrics value is + * needed without recalculating the metrics value. + * @return The value of the metric. + */ + public double getValue() { + + if(Double.isNaN(value)) { + this.value = calculate(); + } + + return value; + + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java new file mode 100644 index 0000000..7ca0ad6 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java @@ -0,0 +1,208 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.runners; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.Client; +import org.opensearch.eval.SearchQualityEvaluationPlugin; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.search.builder.SearchSourceBuilder; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Base class for query set runners. Classes that extend this class + * should be specific to a search engine. See the {@link OpenSearchQuerySetRunner} for an example. + */ +public abstract class AbstractQuerySetRunner { + + private static final Logger LOGGER = LogManager.getLogger(AbstractQuerySetRunner.class); + + protected final Client client; + + public AbstractQuerySetRunner(final Client client) { + this.client = client; + } + + /** + * Runs the query set. + * @param querySetId The ID of the query set to run. + * @param judgmentsId The ID of the judgments set to use for search metric calculation. + * @param index The name of the index to run the query sets against. + * @param searchPipeline The name of the search pipeline to use, or null to not use a search pipeline. + * @param idField The field in the index that is used to uniquely identify a document. + * @param query The query that will be used to run the query set. + * @param k The k used for metrics calculation, i.e. DCG@k. + * @param threshold The cutoff for binary judgments. A judgment score greater than or equal + * to this value will be assigned a binary judgment value of 1. A judgment score + * less than this value will be assigned a binary judgment value of 0. + * @return The query set {@link QuerySetRunResult results} and calculated metrics. + */ + abstract QuerySetRunResult run(String querySetId, final String judgmentsId, final String index, final String searchPipeline, + final String idField, final String query, final int k, + final double threshold) throws Exception; + + /** + * Saves the query set results to a persistent store, which may be the search engine itself. + * @param result The {@link QuerySetRunResult results}. + */ + abstract void save(QuerySetRunResult result) throws Exception; + + /** + * Gets a query set from the index. + * @param querySetId The ID of the query set to get. + * @return The query set as a collection of maps of query to frequency + * @throws Exception Thrown if the query set cannot be retrieved. + */ + public final Collection> getQuerySet(final String querySetId) throws Exception { + + // Get the query set. + final SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); + sourceBuilder.query(QueryBuilders.matchQuery("_id", querySetId)); + + // Will be at most one match. + sourceBuilder.from(0); + sourceBuilder.size(1); + + final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.QUERY_SETS_INDEX_NAME).source(sourceBuilder); + + // TODO: Don't use .get() + final SearchResponse searchResponse = client.search(searchRequest).get(); + + if(searchResponse.getHits().getHits().length > 0) { + + // The queries from the query set that will be run. + return (Collection>) searchResponse.getHits().getAt(0).getSourceAsMap().get("queries"); + + } else { + + LOGGER.error("Unable to get query set with ID {}", querySetId); + + // The query set was not found. + throw new RuntimeException("The query set with ID " + querySetId + " was not found."); + + } + + } + + /** + * Get a judgment from the index. + * @param judgmentsId The ID of the judgments to find. + * @param query The user query. + * @param documentId The document ID. + * @return The value of the judgment, or NaN if the judgment cannot be found. + */ + public Double getJudgmentValue(final String judgmentsId, final String query, final String documentId) throws Exception { + + // Find a judgment that matches the judgments_id, query_id, and document_id fields in the index. + + final BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); + boolQueryBuilder.must(QueryBuilders.termQuery("judgments_id", judgmentsId)); + boolQueryBuilder.must(QueryBuilders.termQuery("query", query)); + boolQueryBuilder.must(QueryBuilders.termQuery("document_id", documentId)); + + final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.query(boolQueryBuilder); + + // Will be a max of 1 result since we are getting the judgments by ID. + searchSourceBuilder.from(0); + searchSourceBuilder.size(1); + + // Only include the judgment field in the response. + final String[] includeFields = new String[] {"judgment"}; + final String[] excludeFields = new String[] {}; + searchSourceBuilder.fetchSource(includeFields, excludeFields); + + final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.JUDGMENTS_INDEX_NAME).source(searchSourceBuilder); + + Double judgment = Double.NaN; + + final SearchResponse searchResponse = client.search(searchRequest).get(); + + if (searchResponse.getHits().getHits().length > 0) { + + final Map j = searchResponse.getHits().getAt(0).getSourceAsMap(); + + // LOGGER.debug("Judgment contains a value: {}", j.get("judgment")); + + // TODO: Why does this not exist in some cases? + if(j.containsKey("judgment")) { + judgment = (Double) j.get("judgment"); + } + + } else { + + // No judgment for this query/doc pair exists. + judgment = Double.NaN; + + } + + return judgment; + + } + + /** + * Gets the judgments for a query / document pairs. + * @param judgmentsId The judgments collection for which the judgment to retrieve belongs. + * @param query The user query. + * @param orderedDocumentIds A list of document IDs returned for the user query. + * @param k The k used for metrics calculation, i.e. DCG@k. + * @return An ordered list of relevance scores for the query / document pairs. + * @throws Exception Thrown if a judgment cannot be retrieved. + */ + protected RelevanceScores getRelevanceScores(final String judgmentsId, final String query, final List orderedDocumentIds, final int k) throws Exception { + + // Ordered list of scores. + final List scores = new ArrayList<>(); + + // Count the number of documents without judgments. + int documentsWithoutJudgmentsCount = 0; + + // For each document (up to k), get the judgment for the document. + for (int i = 0; i < k && i < orderedDocumentIds.size(); i++) { + + final String documentId = orderedDocumentIds.get(i); + + // Find the judgment value for this combination of query and documentId from the index. + final Double judgmentValue = getJudgmentValue(judgmentsId, query, documentId); + + // If a judgment for this query/doc pair is not found, Double.NaN will be returned. + if(!Double.isNaN(judgmentValue)) { + LOGGER.info("Score found for document ID {} with judgments {} and query {} = {}", documentId, judgmentsId, query, judgmentValue); + scores.add(judgmentValue); + } else { + //LOGGER.info("No score found for document ID {} with judgments {} and query {}", documentId, judgmentsId, query); + documentsWithoutJudgmentsCount++; + } + + } + + double frogs = ((double) documentsWithoutJudgmentsCount) / orderedDocumentIds.size(); + + if(Double.isNaN(frogs)) { + frogs = 1.0; + } + + // Multiply by 100 to be a percentage. + frogs *= 100; + + LOGGER.info("frogs for query {} = {} ------- {} / {}", query, frogs, documentsWithoutJudgmentsCount, orderedDocumentIds.size()); + + return new RelevanceScores(scores, frogs); + + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java new file mode 100644 index 0000000..a1f0c4f --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java @@ -0,0 +1,290 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.runners; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.action.admin.indices.exists.indices.IndicesExistsRequest; +import org.opensearch.action.admin.indices.exists.indices.IndicesExistsResponse; +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.bulk.BulkResponse; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.Client; +import org.opensearch.core.action.ActionListener; +import org.opensearch.eval.SearchQualityEvaluationPlugin; +import org.opensearch.eval.metrics.DcgSearchMetric; +import org.opensearch.eval.metrics.NdcgSearchMetric; +import org.opensearch.eval.metrics.PrecisionSearchMetric; +import org.opensearch.eval.metrics.SearchMetric; +import org.opensearch.eval.utils.TimeUtils; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.search.SearchHit; +import org.opensearch.search.builder.SearchSourceBuilder; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.opensearch.eval.SearchQualityEvaluationRestHandler.QUERY_PLACEHOLDER; + +/** + * A {@link AbstractQuerySetRunner} for Amazon OpenSearch. + */ +public class OpenSearchQuerySetRunner extends AbstractQuerySetRunner { + + private static final Logger LOGGER = LogManager.getLogger(OpenSearchQuerySetRunner.class); + + /** + * Creates a new query set runner + * + * @param client An OpenSearch {@link Client}. + */ + public OpenSearchQuerySetRunner(final Client client) { + super(client); + } + + @Override + public QuerySetRunResult run(final String querySetId, final String judgmentsId, final String index, + final String searchPipeline, final String idField, final String query, + final int k, final double threshold) throws Exception { + + final Collection> querySet = getQuerySet(querySetId); + LOGGER.info("Found {} queries in query set {}", querySet.size(), querySetId); + + try { + + // The results of each query. + final List queryResults = new ArrayList<>(); + + for (Map queryMap : querySet) { + + // Loop over each query in the map and run each one. + for (final String userQuery : queryMap.keySet()) { + + // Replace the query placeholder with the user query. + final String parsedQuery = query.replace(QUERY_PLACEHOLDER, userQuery); + + // Build the query from the one that was passed in. + final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + + searchSourceBuilder.query(QueryBuilders.wrapperQuery(parsedQuery)); + searchSourceBuilder.from(0); + searchSourceBuilder.size(k); + + final String[] includeFields = new String[]{idField}; + final String[] excludeFields = new String[]{}; + searchSourceBuilder.fetchSource(includeFields, excludeFields); + + // LOGGER.info(searchSourceBuilder.toString()); + + final SearchRequest searchRequest = new SearchRequest(index); + searchRequest.source(searchSourceBuilder); + + if(searchPipeline != null) { + searchSourceBuilder.pipeline(searchPipeline); + searchRequest.pipeline(searchPipeline); + } + + // This is to keep OpenSearch from rejecting queries. + // TODO: Look at using the Workload Management in 2.18.0. + Thread.sleep(50); + + client.search(searchRequest, new ActionListener<>() { + + @Override + public void onResponse(final SearchResponse searchResponse) { + + final List orderedDocumentIds = new ArrayList<>(); + + for (final SearchHit hit : searchResponse.getHits().getHits()) { + + final String documentId; + + if("_id".equals(idField)) { + documentId = hit.getId(); + } else { + // TODO: Need to check this field actually exists. + documentId = hit.getSourceAsMap().get(idField).toString(); + } + + orderedDocumentIds.add(documentId); + + } + + try { + + final RelevanceScores relevanceScores = getRelevanceScores(judgmentsId, userQuery, orderedDocumentIds, k); + + // Calculate the metrics for this query. + final SearchMetric dcgSearchMetric = new DcgSearchMetric(k, relevanceScores.getRelevanceScores()); + final SearchMetric ndcgSearchmetric = new NdcgSearchMetric(k, relevanceScores.getRelevanceScores()); + final SearchMetric precisionSearchMetric = new PrecisionSearchMetric(k, threshold, relevanceScores.getRelevanceScores()); + + final Collection searchMetrics = List.of(dcgSearchMetric, ndcgSearchmetric, precisionSearchMetric); + + queryResults.add(new QueryResult(userQuery, orderedDocumentIds, k, searchMetrics, relevanceScores.getFrogs())); + + } catch (Exception ex) { + LOGGER.error("Unable to get relevance scores for judgments {} and user query {}.", judgmentsId, userQuery, ex); + } + + } + + @Override + public void onFailure(Exception ex) { + LOGGER.error("Unable to search using query: {}", searchSourceBuilder.toString(), ex); + } + }); + + } + + } + + // Calculate the search metrics for the entire query set given the individual query set metrics. + // Sum up the metrics for each query per metric type. + final int querySetSize = queryResults.size(); + final Map sumOfMetrics = new HashMap<>(); + for(final QueryResult queryResult : queryResults) { + for(final SearchMetric searchMetric : queryResult.getSearchMetrics()) { + //LOGGER.info("Summing: {} - {}", searchMetric.getName(), searchMetric.getValue()); + sumOfMetrics.merge(searchMetric.getName(), searchMetric.getValue(), Double::sum); + } + } + + // Now divide by the number of queries. + final Map querySetMetrics = new HashMap<>(); + for(final String metric : sumOfMetrics.keySet()) { + //LOGGER.info("Dividing by the query set size: {} / {}", sumOfMetrics.get(metric), querySetSize); + querySetMetrics.put(metric, sumOfMetrics.get(metric) / querySetSize); + } + + final String querySetRunId = UUID.randomUUID().toString(); + final QuerySetRunResult querySetRunResult = new QuerySetRunResult(querySetRunId, querySetId, queryResults, querySetMetrics); + + LOGGER.info("Query set run complete: {}", querySetRunId); + + return querySetRunResult; + + } catch (Exception ex) { + throw new RuntimeException("Unable to run query set.", ex); + } + + } + + @Override + public void save(final QuerySetRunResult result) throws Exception { + + // Now, index the metrics as expected by the dashboards. + + // See https://github.com/o19s/opensearch-search-quality-evaluation/blob/main/opensearch-dashboard-prototyping/METRICS_SCHEMA.md + // See https://github.com/o19s/opensearch-search-quality-evaluation/blob/main/opensearch-dashboard-prototyping/sample_data.ndjson + + final IndicesExistsRequest indicesExistsRequest = new IndicesExistsRequest(SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME); + + client.admin().indices().exists(indicesExistsRequest, new ActionListener<>() { + + @Override + public void onResponse(IndicesExistsResponse indicesExistsResponse) { + + if(!indicesExistsResponse.isExists()) { + + // Create the index. + // TODO: Read this mapping from a resource file instead. + final String mapping = "{\n" + + " \"properties\": {\n" + + " \"datetime\": { \"type\": \"date\", \"format\": \"strict_date_time\" },\n" + + " \"search_config\": { \"type\": \"keyword\" },\n" + + " \"query_set_id\": { \"type\": \"keyword\" },\n" + + " \"query\": { \"type\": \"keyword\" },\n" + + " \"metric\": { \"type\": \"keyword\" },\n" + + " \"value\": { \"type\": \"double\" },\n" + + " \"application\": { \"type\": \"keyword\" },\n" + + " \"evaluation_id\": { \"type\": \"keyword\" },\n" + + " \"frogs_percent\": { \"type\": \"double\" }\n" + + " }\n" + + " }"; + + // Create the judgments index. + final CreateIndexRequest createIndexRequest = new CreateIndexRequest(SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME).mapping(mapping); + + client.admin().indices().create(createIndexRequest, new ActionListener<>() { + + @Override + public void onResponse(CreateIndexResponse createIndexResponse) { + LOGGER.info("{} index created.", SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME); + } + + @Override + public void onFailure(Exception ex) { + LOGGER.error("Unable to create the {} index.", SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME, ex); + } + + }); + + } + + } + + @Override + public void onFailure(Exception ex) { + LOGGER.error("Unable to determine if {} index exists.", SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME, ex); + } + + }); + + final BulkRequest bulkRequest = new BulkRequest(); + final String timestamp = TimeUtils.getTimestamp(); + + for(final QueryResult queryResult : result.getQueryResults()) { + + for(final SearchMetric searchMetric : queryResult.getSearchMetrics()) { + + // TODO: Make sure all of these items have values. + final Map metrics = new HashMap<>(); + metrics.put("datetime", timestamp); + metrics.put("search_config", "research_1"); + metrics.put("query_set_id", result.getQuerySetId()); + metrics.put("query", queryResult.getQuery()); + metrics.put("metric", searchMetric.getName()); + metrics.put("value", searchMetric.getValue()); + metrics.put("application", "sample_data"); + metrics.put("evaluation_id", result.getRunId()); + metrics.put("frogs_percent", queryResult.getFrogs()); + + // TODO: This is using the index name from the sample data. + bulkRequest.add(new IndexRequest("sqe_metrics_sample_data").source(metrics)); + + } + + } + + client.bulk(bulkRequest, new ActionListener<>() { + + @Override + public void onResponse(BulkResponse bulkItemResponses) { + LOGGER.info("Successfully indexed {} metrics.", bulkItemResponses.getItems().length); + } + + @Override + public void onFailure(Exception ex) { + LOGGER.error("Unable to bulk index metrics.", ex); + } + + }); + + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/QueryResult.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/QueryResult.java new file mode 100644 index 0000000..cc2b118 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/QueryResult.java @@ -0,0 +1,72 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.runners; + +import org.opensearch.eval.metrics.SearchMetric; + +import java.util.Collection; +import java.util.List; + +/** + * Contains the search results for a single query. + */ +public class QueryResult { + + private final String query; + private final List orderedDocumentIds; + private final int k; + private final Collection searchMetrics; + private final double frogs; + + /** + * Creates the search results. + * @param query The query used to generate this result. + * @param orderedDocumentIds A list of ordered document IDs in the same order as they appeared + * in the query. + * @param k The k used for metrics calculation, i.e. DCG@k. + * @param searchMetrics A collection of {@link SearchMetric} for this query. + * @param frogs The percentage of documents not having a judgment. + */ + public QueryResult(final String query, final List orderedDocumentIds, final int k, final Collection searchMetrics, final double frogs) { + this.query = query; + this.orderedDocumentIds = orderedDocumentIds; + this.k = k; + this.searchMetrics = searchMetrics; + this.frogs = frogs; + } + + /** + * Gets the query used to generate this result. + * @return The query used to generate this result. + */ + public String getQuery() { + return query; + } + + /** + * Gets the list of ordered document IDs. + * @return A list of ordered documented IDs. + */ + public List getOrderedDocumentIds() { + return orderedDocumentIds; + } + + public int getK() { + return k; + } + + public Collection getSearchMetrics() { + return searchMetrics; + } + + public double getFrogs() { + return frogs; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java new file mode 100644 index 0000000..280ba9c --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java @@ -0,0 +1,108 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.runners; + +import org.opensearch.eval.metrics.SearchMetric; +import org.opensearch.eval.utils.TimeUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The results of a query set run. + */ +public class QuerySetRunResult { + + private final String runId; + private final String querySetId; + private final List queryResults; + private final Map metrics; + private final String timestamp; + + /** + * Creates a new query set run result. A random UUID is generated as the run ID. + * @param runId A unique identifier for this query set run. + * @param querySetId A unique identifier for the query set. + * @param queryResults A collection of {@link QueryResult} that contains the queries and search results. + * @param metrics A map of metric name to value. + */ + public QuerySetRunResult(final String runId, final String querySetId, final List queryResults, final Map metrics) { + this.runId = runId; + this.querySetId = querySetId; + this.queryResults = queryResults; + this.metrics = metrics; + this.timestamp = TimeUtils.getTimestamp(); + } + + /** + * Get the run's ID. + * @return The run's ID. + */ + public String getRunId() { + return runId; + } + + /** + * Gets the query set ID. + * @return The query set ID. + */ + public String getQuerySetId() { + return querySetId; + } + + /** + * Gets the search metrics. + * @return The search metrics. + */ + public Map getSearchMetrics() { + return metrics; + } + + /** + * Gets the results of the query set run. + * @return A collection of {@link QueryResult results}. + */ + public Collection getQueryResults() { + return queryResults; + } + + public String getTimestamp() { + return timestamp; + } + + public Collection> getQueryResultsAsMap() { + + final Collection> qs = new ArrayList<>(); + + for(final QueryResult queryResult : queryResults) { + + final Map q = new HashMap<>(); + + q.put("query", queryResult.getQuery()); + q.put("document_ids", queryResult.getOrderedDocumentIds()); + q.put("frogs", queryResult.getFrogs()); + + // Calculate and add each metric to the map. + for(final SearchMetric searchMetric : queryResult.getSearchMetrics()) { + q.put(searchMetric.getName(), searchMetric.calculate()); + } + + qs.add(q); + + } + + return qs; + + } + + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/RelevanceScores.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/RelevanceScores.java new file mode 100644 index 0000000..d57de40 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/RelevanceScores.java @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.runners; + +import java.util.List; + +public class RelevanceScores { + + private List relevanceScores; + private double frogs; + + public RelevanceScores(final List relevanceScores, final double frogs) { + this.relevanceScores = relevanceScores; + this.frogs = frogs; + } + + public List getRelevanceScores() { + return relevanceScores; + } + + + public double getFrogs() { + return frogs; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java new file mode 100644 index 0000000..3c70f0a --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java @@ -0,0 +1,98 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.samplers; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.index.IndexResponse; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.action.ActionListener; +import org.opensearch.eval.SearchQualityEvaluationPlugin; +import org.opensearch.eval.utils.TimeUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * An interface for sampling UBI queries. + */ +public abstract class AbstractQuerySampler { + + private static final Logger LOGGER = LogManager.getLogger(AbstractQuerySampler.class); + + /** + * Gets the name of the sampler. + * @return The name of the sampler. + */ + public abstract String getName(); + + /** + * Samples the queries and inserts the query set into an index. + * @return A query set ID. + */ + public abstract String sample() throws Exception; + + /** + * Index the query set. + */ + protected String indexQuerySet(final NodeClient client, final String name, final String description, final String sampling, Map queries) throws Exception { + + LOGGER.info("Indexing {} queries for query set {}", queries.size(), name); + + final Collection> querySetQueries = new ArrayList<>(); + + // Convert the queries map to an object. + for(final String query : queries.keySet()) { + + // Map of the query itself to the frequency of the query. + final Map querySetQuery = new HashMap<>(); + querySetQuery.put(query, queries.get(query)); + + querySetQueries.add(querySetQuery); + + } + + final Map querySet = new HashMap<>(); + querySet.put("name", name); + querySet.put("description", description); + querySet.put("sampling", sampling); + querySet.put("queries", querySetQueries); + querySet.put("timestamp", TimeUtils.getTimestamp()); + + final String querySetId = UUID.randomUUID().toString(); + + // TODO: Create a mapping for the query set index. + final IndexRequest indexRequest = new IndexRequest().index(SearchQualityEvaluationPlugin.QUERY_SETS_INDEX_NAME) + .id(querySetId) + .source(querySet) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + + client.index(indexRequest, new ActionListener<>() { + + @Override + public void onResponse(IndexResponse indexResponse) { + LOGGER.info("Indexed query set {} having name {}", querySetId, name); + } + + @Override + public void onFailure(Exception ex) { + LOGGER.error("Unable to index query set {}", querySetId, ex); + } + }); + + return querySetId; + + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java new file mode 100644 index 0000000..c8d731a --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.samplers; + +public class AbstractSamplerParameters { + + private final String name; + private final String description; + private final String sampling; + private final int querySetSize; + + public AbstractSamplerParameters(final String name, final String description, final String sampling, final int querySetSize) { + this.name = name; + this.description = description; + this.sampling = sampling; + this.querySetSize = querySetSize; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getSampling() { + return sampling; + } + + public int getQuerySetSize() { + return querySetSize; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java new file mode 100644 index 0000000..263d70a --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java @@ -0,0 +1,79 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.samplers; + +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.node.NodeClient; +import org.opensearch.eval.SearchQualityEvaluationPlugin; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.search.SearchHit; +import org.opensearch.search.builder.SearchSourceBuilder; + +import java.util.HashMap; +import java.util.Map; + +/** + * An implementation of {@link AbstractQuerySampler} that uses all UBI queries without any sampling. + */ +public class AllQueriesQuerySampler extends AbstractQuerySampler { + + public static final String NAME = "none"; + + private final NodeClient client; + private final AllQueriesQuerySamplerParameters parameters; + + /** + * Creates a new sampler. + * @param client The OpenSearch {@link NodeClient client}. + */ + public AllQueriesQuerySampler(final NodeClient client, final AllQueriesQuerySamplerParameters parameters) { + this.client = client; + this.parameters = parameters; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String sample() throws Exception { + + // Get queries from the UBI queries index. + // TODO: This needs to use scroll or something else. + final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.query(QueryBuilders.matchAllQuery()); + searchSourceBuilder.from(0); + searchSourceBuilder.size(parameters.getQuerySetSize()); + + final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.UBI_QUERIES_INDEX_NAME).source(searchSourceBuilder); + + // TODO: Don't use .get() + final SearchResponse searchResponse = client.search(searchRequest).get(); + + final Map queries = new HashMap<>(); + + for(final SearchHit hit : searchResponse.getHits().getHits()) { + + final Map fields = hit.getSourceAsMap(); + queries.merge(fields.get("user_query").toString(), 1L, Long::sum); + + // Will be useful for paging once implemented. + if(queries.size() > parameters.getQuerySetSize()) { + break; + } + + } + + return indexQuerySet(client, parameters.getName(), parameters.getDescription(), parameters.getSampling(), queries); + + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java new file mode 100644 index 0000000..3149668 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.samplers; + +public class AllQueriesQuerySamplerParameters extends AbstractSamplerParameters { + + public AllQueriesQuerySamplerParameters(final String name, final String description, final String sampling, final int querySetSize) { + super(name, description, sampling, querySetSize); + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java new file mode 100644 index 0000000..79f2c7c --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java @@ -0,0 +1,176 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.samplers; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.search.SearchScrollRequest; +import org.opensearch.client.node.NodeClient; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.eval.SearchQualityEvaluationPlugin; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.search.Scroll; +import org.opensearch.search.SearchHit; +import org.opensearch.search.builder.SearchSourceBuilder; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * An implementation of {@link AbstractQuerySampler} that uses PPTSS sampling. + * See https://opensourceconnections.com/blog/2022/10/13/how-to-succeed-with-explicit-relevance-evaluation-using-probability-proportional-to-size-sampling/ + * for more information on PPTSS. + */ +public class ProbabilityProportionalToSizeAbstractQuerySampler extends AbstractQuerySampler { + + public static final String NAME = "pptss"; + + private static final Logger LOGGER = LogManager.getLogger(ProbabilityProportionalToSizeAbstractQuerySampler.class); + + private final NodeClient client; + private final ProbabilityProportionalToSizeParameters parameters; + + /** + * Creates a new PPTSS sampler. + * @param client The OpenSearch {@link NodeClient client}. + * @param parameters The {@link ProbabilityProportionalToSizeParameters parameters} for the sampling. + */ + public ProbabilityProportionalToSizeAbstractQuerySampler(final NodeClient client, final ProbabilityProportionalToSizeParameters parameters) { + this.client = client; + this.parameters = parameters; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String sample() throws Exception { + + // TODO: Can this be changed to an aggregation? + // An aggregation is limited (?) to 10,000 which could miss some queries. + + // Get queries from the UBI queries index. + final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.query(QueryBuilders.matchAllQuery()); + searchSourceBuilder.size(10000); + final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(10L)); + + final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.UBI_QUERIES_INDEX_NAME); + searchRequest.scroll(scroll); + searchRequest.source(searchSourceBuilder); + + // TODO: Don't use .get() + SearchResponse searchResponse = client.search(searchRequest).get(); + + String scrollId = searchResponse.getScrollId(); + SearchHit[] searchHits = searchResponse.getHits().getHits(); + + final Collection userQueries = new ArrayList<>(); + + while (searchHits != null && searchHits.length > 0) { + + for(final SearchHit hit : searchHits) { + final Map fields = hit.getSourceAsMap(); + userQueries.add(fields.get("user_query").toString()); + // LOGGER.info("user queries count: {} user query: {}", userQueries.size(), fields.get("user_query").toString()); + } + + final SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); + scrollRequest.scroll(scroll); + + // TODO: Don't use .get() + searchResponse = client.searchScroll(scrollRequest).get(); + + scrollId = searchResponse.getScrollId(); + searchHits = searchResponse.getHits().getHits(); + + } + + // LOGGER.info("User queries found: {}", userQueries); + + final Map weights = new HashMap<>(); + + // Increment the weight for each user query. + for(final String userQuery : userQueries) { + weights.merge(userQuery, 1L, Long::sum); + } + + // The total number of queries will be used to normalize the weights. + final long countOfQueries = userQueries.size(); + + // Calculate the normalized weights by dividing by the total number of queries. + final Map normalizedWeights = new HashMap<>(); + for(final String userQuery : weights.keySet()) { + normalizedWeights.put(userQuery, weights.get(userQuery) / (double) countOfQueries); + //LOGGER.info("{}: {}/{} = {}", userQuery, weights.get(userQuery), countOfQueries, normalizedWeights.get(userQuery)); + } + + // Ensure all normalized weights sum to 1. + final double sumOfNormalizedWeights = normalizedWeights.values().stream().reduce(0.0, Double::sum); + if(!compare(1.0, sumOfNormalizedWeights)) { + throw new RuntimeException("Summed normalized weights do not equal 1.0: Actual value: " + sumOfNormalizedWeights); + } else { + LOGGER.info("Summed normalized weights sum to {}", sumOfNormalizedWeights); + } + + final Map querySet = new HashMap<>(); + final Set randomNumbers = new HashSet<>(); + + // Generate random numbers between 0 and 1 for the size of the query set. + // Do this until our query set has reached the requested maximum size. + // This may require generating more random numbers than what was requested + // because removing duplicate user queries will require randomly picking more queries. + int count = 1; + + // TODO: How to short-circuit this such that if the same query gets picked over and over, the loop will never end. + final int max = 5000; + while(querySet.size() < parameters.getQuerySetSize() && count < max) { + + // Make a random number not yet used. + double random; + do { + random = Math.random(); + } while (randomNumbers.contains(random)); + randomNumbers.add(random); + + // Find the weight closest to the random weight in the map of deltas. + double smallestDelta = Integer.MAX_VALUE; + String closestQuery = null; + for(final String query : normalizedWeights.keySet()) { + final double delta = Math.abs(normalizedWeights.get(query) - random); + if(delta < smallestDelta) { + smallestDelta = delta; + closestQuery = query; + } + } + + querySet.put(closestQuery, weights.get(closestQuery)); + count++; + + //LOGGER.info("Generated random value: {}; Smallest delta = {}; Closest query = {}", random, smallestDelta, closestQuery); + + } + + return indexQuerySet(client, parameters.getName(), parameters.getDescription(), parameters.getSampling(), querySet); + + } + + public static boolean compare(double a, double b) { + return Math.abs(a - b) < 0.00001; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java new file mode 100644 index 0000000..d5e4311 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.samplers; + +public class ProbabilityProportionalToSizeParameters extends AbstractSamplerParameters { + + public ProbabilityProportionalToSizeParameters(final String name, final String description, final String sampling, final int querySetSize) { + super(name, description, sampling, querySetSize); + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/utils/MathUtils.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/utils/MathUtils.java new file mode 100644 index 0000000..d83adcd --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/utils/MathUtils.java @@ -0,0 +1,26 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.utils; + +public class MathUtils { + + private MathUtils() { + + } + + public static String round(final double value, final int decimalPlaces) { + double factor = Math.pow(10, decimalPlaces); + return String.valueOf(Math.round(value * factor) / factor); + } + + public static String round(final double value) { + return round(value, 3); + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/utils/TimeUtils.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/utils/TimeUtils.java new file mode 100644 index 0000000..1948b60 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/utils/TimeUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.utils; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + * This is a utility class. + */ +public class TimeUtils { + + /** + * Generate a timestamp in the yyyy-MM-ddTHH:mm:ss.SSSZ format. + * @return A timestamp in the yyyy-MM-ddTHH:mm:ss.SSSZ format. + */ + public static String getTimestamp() { + + final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + + final Date date = new Date(); + return formatter.format(date); + + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java b/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java new file mode 100644 index 0000000..f3755f3 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.metrics; + +import org.opensearch.test.OpenSearchTestCase; + +import java.util.List; + +public class DcgSearchMetricTest extends OpenSearchTestCase { + + public void testCalculate() { + + final int k = 10; + final List relevanceScores = List.of(1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 0.0); + + final DcgSearchMetric dcgSearchMetric = new DcgSearchMetric(k, relevanceScores); + final double dcg = dcgSearchMetric.calculate(); + + assertEquals(13.864412483585935, dcg, 0.0); + + } + + public void testCalculateAllZeros() { + + final int k = 10; + final List relevanceScores = List.of(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + + final DcgSearchMetric dcgSearchMetric = new DcgSearchMetric(k, relevanceScores); + final double dcg = dcgSearchMetric.calculate(); + + assertEquals(0.0, dcg, 0.0); + + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java b/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java new file mode 100644 index 0000000..08795f8 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.metrics; + +import org.opensearch.test.OpenSearchTestCase; + +import java.util.List; + +public class NdcgSearchMetricTest extends OpenSearchTestCase { + + public void testCalculate() { + + final int k = 10; + final List relevanceScores = List.of(1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 0.0); + + final NdcgSearchMetric ndcgSearchMetric = new NdcgSearchMetric(k, relevanceScores); + final double ndcg = ndcgSearchMetric.calculate(); + + assertEquals(0.7151195094457645, ndcg, 0.0); + + } + + public void testCalculateAllZeros() { + + final int k = 10; + final List relevanceScores = List.of(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + + final NdcgSearchMetric ndcgSearchMetric = new NdcgSearchMetric(k, relevanceScores); + final double ndcg = ndcgSearchMetric.calculate(); + + assertEquals(0.0, ndcg, 0.0); + + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java b/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java new file mode 100644 index 0000000..b6c260f --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval.metrics; + +import org.opensearch.test.OpenSearchTestCase; + +import java.util.List; + +public class PrecisionSearchMetricTest extends OpenSearchTestCase { + + public void testCalculate() { + + final int k = 10; + final double threshold = 1.0; + final List relevanceScores = List.of(1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 0.0); + + final PrecisionSearchMetric precisionSearchMetric = new PrecisionSearchMetric(k, threshold, relevanceScores); + final double precision = precisionSearchMetric.calculate(); + + assertEquals(0.9, precision, 0.0); + + } + +} diff --git a/opensearch-search-quality-evaluation-framework/useful_queries.txt b/opensearch-search-quality-evaluation-framework/useful_queries.txt new file mode 100644 index 0000000..35c8335 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/useful_queries.txt @@ -0,0 +1,151 @@ +DELETE ubi_events +DELETE ubi_queries + +GET ubi_events/_mapping +GET ubi_events/_search + +GET ubi_queries/_mapping +GET ubi_queries/_search + +DELETE judgments +GET judgments/_search + + +PUT ubi_queries +{ + "mappings": { + "properties": { + "timestamp": { "type": "date", "format": "strict_date_time" }, + "query_id": { "type": "keyword", "ignore_above": 100 }, + "query": { "type": "text" }, + "query_response_id": { "type": "keyword", "ignore_above": 100 }, + "query_response_hit_ids": { "type": "keyword" }, + "user_query": { "type": "keyword", "ignore_above": 256 }, + "query_attributes": { "type": "flat_object" }, + "client_id": { "type": "keyword", "ignore_above": 100 }, + "application": { "type": "keyword", "ignore_above": 100 } + } + } +} + +PUT ubi_events +{ +"mappings": { + "properties": { + "application": { "type": "keyword", "ignore_above": 256 }, + "action_name": { "type": "keyword", "ignore_above": 100 }, + "client_id": { "type": "keyword", "ignore_above": 100 }, + "query_id": { "type": "keyword", "ignore_above": 100 }, + "message": { "type": "keyword", "ignore_above": 1024 }, + "message_type": { "type": "keyword", "ignore_above": 100 }, + "timestamp": { + "type": "date", + "format":"strict_date_time", + "ignore_malformed": true, + "doc_values": true + }, + "event_attributes": { + "dynamic": true, + "properties": { + "position": { + "properties": { + "ordinal": { "type": "integer" }, + "x": { "type": "integer" }, + "y": { "type": "integer" }, + "page_depth": { "type": "integer" }, + "scroll_depth": { "type": "integer" }, + "trail": { "type": "text", + "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } + } + } + } + }, + "object": { + "properties": { + "internal_id": { "type": "keyword" }, + "object_id": { "type": "keyword", "ignore_above": 256 }, + "object_id_field": { "type": "keyword", "ignore_above": 100 }, + "name": { "type": "keyword", "ignore_above": 256 }, + "description": { "type": "text", + "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } + }, + "object_detail": { "type": "object" } + } + } + } + } + } + } +} + +GET ubi_events/_search +{ + "query": { + "range": { + "event_attributes.position.ordinal": { + "lte": 20 + } + } + } +} + +GET ubi_queries/_search +{ + "query": { + "term": { + "user_query": "batteries" + } + } +} + +GET ubi_events/_search +{ + "query": { + "bool": { + "must": [ + { + "term": { + "query_id": "cdc01f67-0b24-4c96-bb56-a89234f4fb0c" + } + }, + { + "term": { + "action_name": "click" + } + }, + { + "term": { + "event_attributes.position.ordinal": "0" + } + }, + { + "term": { + "event_attributes.object.object_id": "B0797J3DWK" + } + } + ] + } + } + } +} + +GET ubi_events/_search +{ + "size": 0, + "aggs": { + "By_Action": { + "terms": { + "field": "action_name", + "size": 20 + }, + "aggs": { + "By_Position": { + "terms": { + "field": "event_attributes.position.ordinal", + "size": 20 + } + } + } + } + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 7f2d692..b6e6b20 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,3 @@ rootProject.name = 'search-evaluation-framework' include 'opensearch-search-quality-evaluation-plugin' -include 'opensearch-search-quality-implicit-judgments' \ No newline at end of file +include 'opensearch-search-quality-evaluation-framework' \ No newline at end of file From 17dcece0f48ad8e0a0da663e1fb839d34accd96b Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 17 Dec 2024 08:49:27 -0500 Subject: [PATCH 02/41] Working on standalone implementation. Signed-off-by: jzonthemtn --- .../build.gradle | 1 + .../java/org/opensearch/eval/Constants.java | 45 ++++ .../eval/SearchQualityEvaluationPlugin.java | 213 ------------------ .../clickmodel/coec/CoecClickModel.java | 1 - .../eval/runners/AbstractQuerySetRunner.java | 6 - .../runners/OpenSearchQuerySetRunner.java | 1 - .../eval/samplers/AbstractQuerySampler.java | 4 +- .../eval/samplers/AllQueriesQuerySampler.java | 4 +- ...roportionalToSizeAbstractQuerySampler.java | 4 +- 9 files changed, 52 insertions(+), 227 deletions(-) create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/Constants.java delete mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java diff --git a/opensearch-search-quality-evaluation-framework/build.gradle b/opensearch-search-quality-evaluation-framework/build.gradle index bdd0586..069bf34 100644 --- a/opensearch-search-quality-evaluation-framework/build.gradle +++ b/opensearch-search-quality-evaluation-framework/build.gradle @@ -42,6 +42,7 @@ repositories { } dependencies { + implementation 'org.opensearch.client:opensearch-java:2.19.0' implementation 'org.apache.logging.log4j:log4j-core:2.24.3' implementation 'com.fasterxml.jackson.core:jackson-annotations:2.18.2' implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2' diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/Constants.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/Constants.java new file mode 100644 index 0000000..6d0eccb --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/Constants.java @@ -0,0 +1,45 @@ +package org.opensearch.eval; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class Constants { + + private static final Logger LOGGER = LogManager.getLogger(Constants.class); + + /** + * The name of the UBI index containing the queries. This should not be changed. + */ + public static final String UBI_QUERIES_INDEX_NAME = "ubi_queries"; + + /** + * The name of the UBI index containing the events. This should not be changed. + */ + public static final String UBI_EVENTS_INDEX_NAME = "ubi_events"; + + /** + * The name of the index to store the scheduled jobs to create implicit judgments. + */ + public static final String SCHEDULED_JOBS_INDEX_NAME = "search_quality_eval_scheduled_jobs"; + + /** + * The name of the index to store the completed jobs to create implicit judgments. + */ + public static final String COMPLETED_JOBS_INDEX_NAME = "search_quality_eval_completed_jobs"; + + /** + * The name of the index that stores the query sets. + */ + public static final String QUERY_SETS_INDEX_NAME = "search_quality_eval_query_sets"; + + /** + * The name of the index that stores the metrics for the dashboard. + */ + public static final String DASHBOARD_METRICS_INDEX_NAME = "sqe_metrics_sample_data"; + + /** + * The name of the index that stores the implicit judgments. + */ + public static final String JUDGMENTS_INDEX_NAME = "judgments"; + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java deleted file mode 100644 index 6a7b581..0000000 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.client.Client; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.node.DiscoveryNodes; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.settings.ClusterSettings; -import org.opensearch.common.settings.IndexScopedSettings; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.settings.SettingsFilter; -import org.opensearch.core.common.io.stream.NamedWriteableRegistry; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.core.xcontent.XContentParserUtils; -import org.opensearch.env.Environment; -import org.opensearch.env.NodeEnvironment; -import org.opensearch.jobscheduler.spi.JobSchedulerExtension; -import org.opensearch.jobscheduler.spi.ScheduledJobParser; -import org.opensearch.jobscheduler.spi.ScheduledJobRunner; -import org.opensearch.jobscheduler.spi.schedule.ScheduleParser; -import org.opensearch.plugins.ActionPlugin; -import org.opensearch.plugins.Plugin; -import org.opensearch.repositories.RepositoriesService; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestHandler; -import org.opensearch.script.ScriptService; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.watcher.ResourceWatcherService; - -import java.io.IOException; -import java.time.Instant; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.function.Supplier; - -/** - * Main class for the Search Quality Evaluation plugin. - */ -public class SearchQualityEvaluationPlugin extends Plugin implements ActionPlugin, JobSchedulerExtension { - - private static final Logger LOGGER = LogManager.getLogger(SearchQualityEvaluationPlugin.class); - - /** - * The name of the UBI index containing the queries. This should not be changed. - */ - public static final String UBI_QUERIES_INDEX_NAME = "ubi_queries"; - - /** - * The name of the UBI index containing the events. This should not be changed. - */ - public static final String UBI_EVENTS_INDEX_NAME = "ubi_events"; - - /** - * The name of the index to store the scheduled jobs to create implicit judgments. - */ - public static final String SCHEDULED_JOBS_INDEX_NAME = "search_quality_eval_scheduled_jobs"; - - /** - * The name of the index to store the completed jobs to create implicit judgments. - */ - public static final String COMPLETED_JOBS_INDEX_NAME = "search_quality_eval_completed_jobs"; - - /** - * The name of the index that stores the query sets. - */ - public static final String QUERY_SETS_INDEX_NAME = "search_quality_eval_query_sets"; - - /** - * The name of the index that stores the metrics for the dashboard. - */ - public static final String DASHBOARD_METRICS_INDEX_NAME = "sqe_metrics_sample_data"; - - /** - * The name of the index that stores the implicit judgments. - */ - public static final String JUDGMENTS_INDEX_NAME = "judgments"; - - @Override - public Collection createComponents( - final Client client, - final ClusterService clusterService, - final ThreadPool threadPool, - final ResourceWatcherService resourceWatcherService, - final ScriptService scriptService, - final NamedXContentRegistry xContentRegistry, - final Environment environment, - final NodeEnvironment nodeEnvironment, - final NamedWriteableRegistry namedWriteableRegistry, - final IndexNameExpressionResolver indexNameExpressionResolver, - final Supplier repositoriesServiceSupplier - ) { - - LOGGER.info("Creating search evaluation framework components"); - final SearchQualityEvaluationJobRunner jobRunner = SearchQualityEvaluationJobRunner.getJobRunnerInstance(); - jobRunner.setClusterService(clusterService); - jobRunner.setThreadPool(threadPool); - jobRunner.setClient(client); - - return Collections.emptyList(); - - } - - @Override - public String getJobType() { - return "scheduler_search_quality_eval"; - } - - @Override - public String getJobIndex() { - LOGGER.info("Getting job index name"); - return SCHEDULED_JOBS_INDEX_NAME; - } - - @Override - public ScheduledJobRunner getJobRunner() { - LOGGER.info("Creating job runner"); - return SearchQualityEvaluationJobRunner.getJobRunnerInstance(); - } - - @Override - public ScheduledJobParser getJobParser() { - - return (parser, id, jobDocVersion) -> { - - final SearchQualityEvaluationJobParameter jobParameter = new SearchQualityEvaluationJobParameter(); - XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); - - while (!parser.nextToken().equals(XContentParser.Token.END_OBJECT)) { - - final String fieldName = parser.currentName(); - - parser.nextToken(); - - switch (fieldName) { - case SearchQualityEvaluationJobParameter.NAME_FIELD: - jobParameter.setJobName(parser.text()); - break; - case SearchQualityEvaluationJobParameter.ENABLED_FILED: - jobParameter.setEnabled(parser.booleanValue()); - break; - case SearchQualityEvaluationJobParameter.ENABLED_TIME_FILED: - jobParameter.setEnabledTime(parseInstantValue(parser)); - break; - case SearchQualityEvaluationJobParameter.LAST_UPDATE_TIME_FIELD: - jobParameter.setLastUpdateTime(parseInstantValue(parser)); - break; - case SearchQualityEvaluationJobParameter.SCHEDULE_FIELD: - jobParameter.setSchedule(ScheduleParser.parse(parser)); - break; - case SearchQualityEvaluationJobParameter.LOCK_DURATION_SECONDS: - jobParameter.setLockDurationSeconds(parser.longValue()); - break; - case SearchQualityEvaluationJobParameter.JITTER: - jobParameter.setJitter(parser.doubleValue()); - break; - case SearchQualityEvaluationJobParameter.CLICK_MODEL: - jobParameter.setClickModel(parser.text()); - break; - case SearchQualityEvaluationJobParameter.MAX_RANK: - jobParameter.setMaxRank(parser.intValue()); - break; - default: - XContentParserUtils.throwUnknownToken(parser.currentToken(), parser.getTokenLocation()); - } - - } - - return jobParameter; - - }; - - } - - private Instant parseInstantValue(final XContentParser parser) throws IOException { - - if (XContentParser.Token.VALUE_NULL.equals(parser.currentToken())) { - return null; - } - - if (parser.currentToken().isValue()) { - return Instant.ofEpochMilli(parser.longValue()); - } - - XContentParserUtils.throwUnknownToken(parser.currentToken(), parser.getTokenLocation()); - return null; - - } - - @Override - public List getRestHandlers( - final Settings settings, - final RestController restController, - final ClusterSettings clusterSettings, - final IndexScopedSettings indexScopedSettings, - final SettingsFilter settingsFilter, - final IndexNameExpressionResolver indexNameExpressionResolver, - final Supplier nodesInCluster - ) { - return Collections.singletonList(new SearchQualityEvaluationRestHandler()); - } - -} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java index f2e8aa8..f5192a7 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java @@ -17,7 +17,6 @@ import org.opensearch.client.Client; import org.opensearch.client.Requests; import org.opensearch.common.unit.TimeValue; -import org.opensearch.eval.SearchQualityEvaluationPlugin; import org.opensearch.eval.judgments.clickmodel.ClickModel; import org.opensearch.eval.judgments.model.ClickthroughRate; import org.opensearch.eval.judgments.model.Judgment; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java index 7ca0ad6..52c3322 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java @@ -10,13 +10,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; import org.opensearch.client.Client; -import org.opensearch.eval.SearchQualityEvaluationPlugin; -import org.opensearch.index.query.BoolQueryBuilder; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.search.builder.SearchSourceBuilder; import java.util.ArrayList; import java.util.Collection; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java index a1f0c4f..52f162b 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java @@ -21,7 +21,6 @@ import org.opensearch.action.search.SearchResponse; import org.opensearch.client.Client; import org.opensearch.core.action.ActionListener; -import org.opensearch.eval.SearchQualityEvaluationPlugin; import org.opensearch.eval.metrics.DcgSearchMetric; import org.opensearch.eval.metrics.NdcgSearchMetric; import org.opensearch.eval.metrics.PrecisionSearchMetric; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java index 3c70f0a..4246780 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java @@ -15,7 +15,7 @@ import org.opensearch.action.support.WriteRequest; import org.opensearch.client.node.NodeClient; import org.opensearch.core.action.ActionListener; -import org.opensearch.eval.SearchQualityEvaluationPlugin; +import org.opensearch.eval.Constants; import org.opensearch.eval.utils.TimeUtils; import java.util.ArrayList; @@ -73,7 +73,7 @@ protected String indexQuerySet(final NodeClient client, final String name, final final String querySetId = UUID.randomUUID().toString(); // TODO: Create a mapping for the query set index. - final IndexRequest indexRequest = new IndexRequest().index(SearchQualityEvaluationPlugin.QUERY_SETS_INDEX_NAME) + final IndexRequest indexRequest = new IndexRequest().index(Constants.QUERY_SETS_INDEX_NAME) .id(querySetId) .source(querySet) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java index 263d70a..e9d4401 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java @@ -11,7 +11,7 @@ import org.opensearch.action.search.SearchRequest; import org.opensearch.action.search.SearchResponse; import org.opensearch.client.node.NodeClient; -import org.opensearch.eval.SearchQualityEvaluationPlugin; +import org.opensearch.eval.Constants; import org.opensearch.index.query.QueryBuilders; import org.opensearch.search.SearchHit; import org.opensearch.search.builder.SearchSourceBuilder; @@ -53,7 +53,7 @@ public String sample() throws Exception { searchSourceBuilder.from(0); searchSourceBuilder.size(parameters.getQuerySetSize()); - final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.UBI_QUERIES_INDEX_NAME).source(searchSourceBuilder); + final SearchRequest searchRequest = new SearchRequest(Constants.UBI_QUERIES_INDEX_NAME).source(searchSourceBuilder); // TODO: Don't use .get() final SearchResponse searchResponse = client.search(searchRequest).get(); diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java index 79f2c7c..112494c 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java @@ -15,7 +15,7 @@ import org.opensearch.action.search.SearchScrollRequest; import org.opensearch.client.node.NodeClient; import org.opensearch.common.unit.TimeValue; -import org.opensearch.eval.SearchQualityEvaluationPlugin; +import org.opensearch.eval.Constants; import org.opensearch.index.query.QueryBuilders; import org.opensearch.search.Scroll; import org.opensearch.search.SearchHit; @@ -69,7 +69,7 @@ public String sample() throws Exception { searchSourceBuilder.size(10000); final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(10L)); - final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.UBI_QUERIES_INDEX_NAME); + final SearchRequest searchRequest = new SearchRequest(Constants.UBI_QUERIES_INDEX_NAME); searchRequest.scroll(scroll); searchRequest.source(searchSourceBuilder); From f1048fb0c8c0d49e9aaea6b10bad4b2bfc91ab5c Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 17 Dec 2024 08:57:20 -0500 Subject: [PATCH 03/41] Working on standalone implementation. Signed-off-by: jzonthemtn --- .../build.gradle | 11 - .../SearchQualityEvaluationJobParameter.java | 248 ------------------ .../SearchQualityEvaluationJobRunner.java | 168 ------------ .../clickmodel/coec/CoecClickModel.java | 6 +- .../opensearch/OpenSearchHelper.java | 16 +- .../eval/runners/AbstractQuerySetRunner.java | 15 +- 6 files changed, 19 insertions(+), 445 deletions(-) delete mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java delete mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java diff --git a/opensearch-search-quality-evaluation-framework/build.gradle b/opensearch-search-quality-evaluation-framework/build.gradle index 069bf34..08bdcbc 100644 --- a/opensearch-search-quality-evaluation-framework/build.gradle +++ b/opensearch-search-quality-evaluation-framework/build.gradle @@ -6,12 +6,6 @@ apply plugin: 'java' apply plugin: 'idea' -ext { - projectSubstitutions = [:] - licenseFile = rootProject.file('LICENSE.txt') - noticeFile = rootProject.file('NOTICE.txt') -} - test { include "**/Test*.class" include "**/*Test.class" @@ -29,16 +23,11 @@ buildscript { mavenCentral() maven { url "https://plugins.gradle.org/m2/" } } - - dependencies { - classpath "org.opensearch.gradle:build-tools:${opensearchVersion}" - } } repositories { mavenLocal() mavenCentral() - maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } } dependencies { diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java deleted file mode 100644 index 2ea5379..0000000 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval; - -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.jobscheduler.spi.ScheduledJobParameter; -import org.opensearch.jobscheduler.spi.schedule.Schedule; - -import java.io.IOException; -import java.time.Instant; - -public class SearchQualityEvaluationJobParameter implements ScheduledJobParameter { - - /** - * The name of the parameter for providing a name for the scheduled job. - */ - public static final String NAME_FIELD = "name"; - - /** - * The name of the parameter for creating a job as enabled or disabled. - */ - public static final String ENABLED_FILED = "enabled"; - - /** - * The name of the parameter for specifying when the job was last updated. - */ - public static final String LAST_UPDATE_TIME_FIELD = "last_update_time"; - - /** - * The name of the parameter for specifying a readable time for when the job was last updated. - */ - public static final String LAST_UPDATE_TIME_FIELD_READABLE = "last_update_time_field"; - public static final String SCHEDULE_FIELD = "schedule"; - public static final String ENABLED_TIME_FILED = "enabled_time"; - public static final String ENABLED_TIME_FILED_READABLE = "enabled_time_field"; - public static final String LOCK_DURATION_SECONDS = "lock_duration_seconds"; - public static final String JITTER = "jitter"; - - /** - * The name of the parameter that allows for specifying the type of click model to use. - */ - public static final String CLICK_MODEL = "click_model"; - - /** - * The name of the parameter that allows for setting a max rank value to use during judgment generation. - */ - public static final String MAX_RANK = "max_rank"; - - // Properties from ScheduledJobParameter. - private String jobName; - private Instant lastUpdateTime; - private Instant enabledTime; - private boolean enabled; - private Schedule schedule; - private Long lockDurationSeconds; - private Double jitter; - - // Custom properties. - private String clickModel; - private int maxRank; - - public SearchQualityEvaluationJobParameter() { - - } - - public SearchQualityEvaluationJobParameter(final String name, final Schedule schedule, - final Long lockDurationSeconds, final Double jitter, - final String clickModel, final int maxRank) { - this.jobName = name; - this.schedule = schedule; - this.enabled = true; - this.lockDurationSeconds = lockDurationSeconds; - this.jitter = jitter; - - final Instant now = Instant.now(); - this.enabledTime = now; - this.lastUpdateTime = now; - - // Custom properties. - this.clickModel = clickModel; - this.maxRank = maxRank; - - } - - @Override - public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { - - builder.startObject(); - - builder - .field(NAME_FIELD, this.jobName) - .field(ENABLED_FILED, this.enabled) - .field(SCHEDULE_FIELD, this.schedule) - .field(CLICK_MODEL, this.clickModel) - .field(MAX_RANK, this.maxRank); - - if (this.enabledTime != null) { - builder.timeField(ENABLED_TIME_FILED, ENABLED_TIME_FILED_READABLE, this.enabledTime.toEpochMilli()); - } - - if (this.lastUpdateTime != null) { - builder.timeField(LAST_UPDATE_TIME_FIELD, LAST_UPDATE_TIME_FIELD_READABLE, this.lastUpdateTime.toEpochMilli()); - } - - if (this.lockDurationSeconds != null) { - builder.field(LOCK_DURATION_SECONDS, this.lockDurationSeconds); - } - - if (this.jitter != null) { - builder.field(JITTER, this.jitter); - } - - builder.endObject(); - - return builder; - - } - - @Override - public String getName() { - return this.jobName; - } - - @Override - public Instant getLastUpdateTime() { - return this.lastUpdateTime; - } - - @Override - public Instant getEnabledTime() { - return this.enabledTime; - } - - @Override - public Schedule getSchedule() { - return this.schedule; - } - - @Override - public boolean isEnabled() { - return this.enabled; - } - - @Override - public Long getLockDurationSeconds() { - return this.lockDurationSeconds; - } - - @Override - public Double getJitter() { - return jitter; - } - - /** - * Sets the name of the job. - * @param jobName The name of the job. - */ - public void setJobName(String jobName) { - this.jobName = jobName; - } - - /** - * Sets when the job was last updated. - * @param lastUpdateTime An {@link Instant} of when the job was last updated. - */ - public void setLastUpdateTime(Instant lastUpdateTime) { - this.lastUpdateTime = lastUpdateTime; - } - - /** - * Sets when the job was enabled. - * @param enabledTime An {@link Instant} of when the job was enabled. - */ - public void setEnabledTime(Instant enabledTime) { - this.enabledTime = enabledTime; - } - - /** - * Sets whether the job is enabled. - * @param enabled A boolean representing whether the job is enabled. - */ - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - /** - * Sets the schedule for the job. - * @param schedule A {@link Schedule} for the job. - */ - public void setSchedule(Schedule schedule) { - this.schedule = schedule; - } - - /** - * Sets the lock duration for the cluster when running the job. - * @param lockDurationSeconds The lock duration in seconds. - */ - public void setLockDurationSeconds(Long lockDurationSeconds) { - this.lockDurationSeconds = lockDurationSeconds; - } - - /** - * Sets the jitter for the job. - * @param jitter The jitter for the job. - */ - public void setJitter(Double jitter) { - this.jitter = jitter; - } - - /** - * Gets the type of click model to use for implicit judgment generation. - * @return The type of click model to use for implicit judgment generation. - */ - public String getClickModel() { - return clickModel; - } - - /** - * Sets the click model type to use for implicit judgment generation. - * @param clickModel The click model type to use for implicit judgment generation. - */ - public void setClickModel(String clickModel) { - this.clickModel = clickModel; - } - - /** - * Gets the max rank to use when generating implicit judgments. - * @return The max rank to use when generating implicit judgments. - */ - public int getMaxRank() { - return maxRank; - } - - /** - * Sets the max rank to use when generating implicit judgments. - * @param maxRank The max rank to use when generating implicit judgments. - */ - public void setMaxRank(int maxRank) { - this.maxRank = maxRank; - } - -} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java deleted file mode 100644 index 442ae4c..0000000 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.index.IndexResponse; -import org.opensearch.action.support.WriteRequest; -import org.opensearch.client.Client; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.action.ActionListener; -import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel; -import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModelParameters; -import org.opensearch.jobscheduler.spi.JobExecutionContext; -import org.opensearch.jobscheduler.spi.ScheduledJobParameter; -import org.opensearch.jobscheduler.spi.ScheduledJobRunner; -import org.opensearch.jobscheduler.spi.utils.LockService; -import org.opensearch.threadpool.ThreadPool; - -import java.util.HashMap; -import java.util.Map; - -/** - * Job runner for scheduled implicit judgments jobs. - */ -public class SearchQualityEvaluationJobRunner implements ScheduledJobRunner { - - private static final Logger LOGGER = LogManager.getLogger(SearchQualityEvaluationJobRunner.class); - - private static SearchQualityEvaluationJobRunner INSTANCE; - - /** - * Gets a singleton instance of this class. - * @return A {@link SearchQualityEvaluationJobRunner}. - */ - public static SearchQualityEvaluationJobRunner getJobRunnerInstance() { - - LOGGER.info("Getting job runner instance"); - - if (INSTANCE != null) { - return INSTANCE; - } - - synchronized (SearchQualityEvaluationJobRunner.class) { - if (INSTANCE == null) { - INSTANCE = new SearchQualityEvaluationJobRunner(); - } - return INSTANCE; - } - - } - - private ClusterService clusterService; - private ThreadPool threadPool; - private Client client; - - private SearchQualityEvaluationJobRunner() { - - } - - public void setClusterService(ClusterService clusterService) { - this.clusterService = clusterService; - } - - public void setThreadPool(ThreadPool threadPool) { - this.threadPool = threadPool; - } - - public void setClient(Client client) { - this.client = client; - } - - @Override - public void runJob(final ScheduledJobParameter jobParameter, final JobExecutionContext context) { - - if(!(jobParameter instanceof SearchQualityEvaluationJobParameter)) { - throw new IllegalStateException( - "Job parameter is not instance of SampleJobParameter, type: " + jobParameter.getClass().getCanonicalName() - ); - } - - if(this.clusterService == null) { - throw new IllegalStateException("ClusterService is not initialized."); - } - - if(this.threadPool == null) { - throw new IllegalStateException("ThreadPool is not initialized."); - } - - final LockService lockService = context.getLockService(); - - final Runnable runnable = () -> { - - if (jobParameter.getLockDurationSeconds() != null) { - - lockService.acquireLock(jobParameter, context, ActionListener.wrap(lock -> { - - if (lock == null) { - return; - } - - final SearchQualityEvaluationJobParameter searchQualityEvaluationJobParameter = (SearchQualityEvaluationJobParameter) jobParameter; - - final long startTime = System.currentTimeMillis(); - final String judgmentsId; - - if("coec".equalsIgnoreCase(searchQualityEvaluationJobParameter.getClickModel())) { - - LOGGER.info("Beginning implicit judgment generation using clicks-over-expected-clicks."); - final CoecClickModelParameters coecClickModelParameters = new CoecClickModelParameters(searchQualityEvaluationJobParameter.getMaxRank()); - final CoecClickModel coecClickModel = new CoecClickModel(client, coecClickModelParameters); - - judgmentsId = coecClickModel.calculateJudgments(); - - } else { - - // Invalid click model. - throw new IllegalArgumentException("Invalid click model: " + searchQualityEvaluationJobParameter.getClickModel()); - - } - - final long elapsedTime = System.currentTimeMillis() - startTime; - LOGGER.info("Implicit judgment generation completed in {} ms", elapsedTime); - - final Map job = new HashMap<>(); - job.put("name", searchQualityEvaluationJobParameter.getName()); - job.put("click_model", searchQualityEvaluationJobParameter.getClickModel()); - job.put("started", startTime); - job.put("duration", elapsedTime); - job.put("judgments", judgmentsId); - job.put("invocation", "scheduled"); - job.put("max_rank", searchQualityEvaluationJobParameter.getMaxRank()); - - final IndexRequest indexRequest = new IndexRequest() - .index(SearchQualityEvaluationPlugin.COMPLETED_JOBS_INDEX_NAME) - .id(judgmentsId) - .source(job) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); - - client.index(indexRequest, new ActionListener<>() { - @Override - public void onResponse(IndexResponse indexResponse) { - LOGGER.info("Successfully indexed implicit judgments {}", judgmentsId); - } - - @Override - public void onFailure(Exception ex) { - LOGGER.error("Unable to index implicit judgments", ex); - } - }); - - }, exception -> { throw new IllegalStateException("Failed to acquire lock."); })); - } - - }; - - threadPool.generic().submit(runnable); - - } - -} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java index f5192a7..d6c3a6a 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java @@ -16,7 +16,7 @@ import org.opensearch.action.search.SearchScrollRequest; import org.opensearch.client.Client; import org.opensearch.client.Requests; -import org.opensearch.common.unit.TimeValue; +import org.opensearch.eval.Constants; import org.opensearch.eval.judgments.clickmodel.ClickModel; import org.opensearch.eval.judgments.model.ClickthroughRate; import org.opensearch.eval.judgments.model.Judgment; @@ -216,7 +216,7 @@ private Map> getClickthroughRate() throws Exceptio final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(10L)); final SearchRequest searchRequest = Requests - .searchRequest(SearchQualityEvaluationPlugin.UBI_EVENTS_INDEX_NAME) + .searchRequest(Constants.UBI_EVENTS_INDEX_NAME) .source(searchSourceBuilder) .scroll(scroll); @@ -313,7 +313,7 @@ public Map getRankAggregatedClickThrough() throws Exception { .from(0) .size(0); - final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.UBI_EVENTS_INDEX_NAME).source(searchSourceBuilder); + final SearchRequest searchRequest = new SearchRequest(Constants.UBI_EVENTS_INDEX_NAME).source(searchSourceBuilder); final SearchResponse searchResponse = client.search(searchRequest).get(); final Map clickCounts = new HashMap<>(); diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java index 3c391b3..c6ea7d7 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java @@ -17,7 +17,7 @@ import org.opensearch.action.search.SearchRequest; import org.opensearch.action.search.SearchResponse; import org.opensearch.client.Client; -import org.opensearch.core.action.ActionListener; +import org.opensearch.eval.Constants; import org.opensearch.eval.judgments.model.ClickthroughRate; import org.opensearch.eval.judgments.model.Judgment; import org.opensearch.eval.judgments.model.ubi.query.UbiQuery; @@ -28,7 +28,6 @@ import org.opensearch.search.builder.SearchSourceBuilder; import java.io.IOException; -import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collection; @@ -37,9 +36,6 @@ import java.util.Set; import java.util.UUID; -import static org.opensearch.eval.SearchQualityEvaluationPlugin.JUDGMENTS_INDEX_NAME; -import static org.opensearch.eval.SearchQualityEvaluationPlugin.UBI_EVENTS_INDEX_NAME; -import static org.opensearch.eval.SearchQualityEvaluationPlugin.UBI_QUERIES_INDEX_NAME; import static org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel.INDEX_QUERY_DOC_CTR; import static org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel.INDEX_RANK_AGGREGATED_CTR; @@ -110,7 +106,7 @@ public UbiQuery getQueryFromQueryId(final String queryId) throws Exception { searchSourceBuilder.from(0); searchSourceBuilder.size(1); - final String[] indexes = {UBI_QUERIES_INDEX_NAME}; + final String[] indexes = {Constants.UBI_QUERIES_INDEX_NAME}; final SearchRequest searchRequest = new SearchRequest(indexes, searchSourceBuilder); final SearchResponse response = client.search(searchRequest).get(); @@ -119,7 +115,7 @@ public UbiQuery getQueryFromQueryId(final String queryId) throws Exception { if(response.getHits().getHits() != null & response.getHits().getHits().length > 0) { final SearchHit hit = response.getHits().getHits()[0]; - return AccessController.doPrivileged((PrivilegedAction) () -> gson.fromJson(hit.getSourceAsString(), UbiQuery.class)); + return gson.fromJson(hit.getSourceAsString(), UbiQuery.class); } else { @@ -138,7 +134,7 @@ private Collection getQueryIdsHavingUserQuery(final String userQuery) th final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(qb); - final String[] indexes = {UBI_QUERIES_INDEX_NAME}; + final String[] indexes = {Constants.UBI_QUERIES_INDEX_NAME}; final SearchRequest searchRequest = new SearchRequest(indexes, searchSourceBuilder); final SearchResponse response = client.search(searchRequest).get(); @@ -198,7 +194,7 @@ public long getCountOfQueriesForUserQueryHavingResultInRankR(final String userQu searchSourceBuilder.trackTotalHits(true); searchSourceBuilder.size(0); - final String[] indexes = {UBI_EVENTS_INDEX_NAME}; + final String[] indexes = {Constants.UBI_EVENTS_INDEX_NAME}; final SearchRequest searchRequest = new SearchRequest(indexes, searchSourceBuilder); final SearchResponse response = client.search(searchRequest).get(); @@ -324,7 +320,7 @@ public String indexJudgments(final Collection judgments) throws Except j.put("judgments_id", judgmentsId); j.put("timestamp", timestamp); - final IndexRequest indexRequest = new IndexRequest(JUDGMENTS_INDEX_NAME) + final IndexRequest indexRequest = new IndexRequest(Constants.JUDGMENTS_INDEX_NAME) .id(UUID.randomUUID().toString()) .source(j); diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java index 52c3322..356e72b 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java @@ -10,7 +10,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.client.Client; +import org.opensearch.client.OpenSearchClient; +import org.opensearch.client.opensearch.core.SearchRequest; +import org.opensearch.eval.Constants; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.search.builder.SearchSourceBuilder; import java.util.ArrayList; import java.util.Collection; @@ -25,9 +30,9 @@ public abstract class AbstractQuerySetRunner { private static final Logger LOGGER = LogManager.getLogger(AbstractQuerySetRunner.class); - protected final Client client; + protected final OpenSearchClient client; - public AbstractQuerySetRunner(final Client client) { + public AbstractQuerySetRunner(final OpenSearchClient client) { this.client = client; } @@ -71,7 +76,7 @@ public final Collection> getQuerySet(final String querySetId) sourceBuilder.from(0); sourceBuilder.size(1); - final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.QUERY_SETS_INDEX_NAME).source(sourceBuilder); + final SearchRequest searchRequest = new SearchRequest(Constants.QUERY_SETS_INDEX_NAME).source(sourceBuilder); // TODO: Don't use .get() final SearchResponse searchResponse = client.search(searchRequest).get(); @@ -120,7 +125,7 @@ public Double getJudgmentValue(final String judgmentsId, final String query, fin final String[] excludeFields = new String[] {}; searchSourceBuilder.fetchSource(includeFields, excludeFields); - final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.JUDGMENTS_INDEX_NAME).source(searchSourceBuilder); + final SearchRequest searchRequest = new SearchRequest(Constants.JUDGMENTS_INDEX_NAME).source(searchSourceBuilder); Double judgment = Double.NaN; From e937df136772811927eb75c44c1dad0844dd6ed4 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 17 Dec 2024 09:02:20 -0500 Subject: [PATCH 04/41] Working on standalone implementation. Signed-off-by: jzonthemtn --- .../Dockerfile | 6 - .../aggs.sh | 20 --- .../build.gradle | 3 +- .../gradle.properties | 2 - .../clickmodel/coec/CoecClickModel.java | 21 +-- .../opensearch/OpenSearchHelper.java | 37 ++--- .../eval/runners/AbstractQuerySetRunner.java | 7 +- .../useful_queries.txt | 151 ------------------ 8 files changed, 24 insertions(+), 223 deletions(-) delete mode 100644 opensearch-search-quality-evaluation-framework/Dockerfile delete mode 100755 opensearch-search-quality-evaluation-framework/aggs.sh delete mode 100644 opensearch-search-quality-evaluation-framework/gradle.properties delete mode 100644 opensearch-search-quality-evaluation-framework/useful_queries.txt diff --git a/opensearch-search-quality-evaluation-framework/Dockerfile b/opensearch-search-quality-evaluation-framework/Dockerfile deleted file mode 100644 index 02f56c8..0000000 --- a/opensearch-search-quality-evaluation-framework/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM opensearchproject/opensearch:2.18.0 - -RUN /usr/share/opensearch/bin/opensearch-plugin install --batch https://github.com/opensearch-project/user-behavior-insights/releases/download/2.18.0.2/opensearch-ubi-2.18.0.2.zip - -ADD ./build/distributions/search-quality-evaluation-plugin-0.0.1.zip /tmp/search-quality-evaluation-plugin.zip -RUN /usr/share/opensearch/bin/opensearch-plugin install --batch file:/tmp/search-quality-evaluation-plugin.zip diff --git a/opensearch-search-quality-evaluation-framework/aggs.sh b/opensearch-search-quality-evaluation-framework/aggs.sh deleted file mode 100755 index 5cf0e24..0000000 --- a/opensearch-search-quality-evaluation-framework/aggs.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -e - -curl -X GET http://localhost:9200/ubi_events/_search -H "Content-Type: application/json" -d' -{ - "size": 0, - "aggs": { - "By_Action": { - "terms": { - "field": "action_name" - }, - "aggs": { - "By_Position": { - "terms": { - "field": "event_attributes.position.ordinal" - } - } - } - } - } -}' | jq \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/build.gradle b/opensearch-search-quality-evaluation-framework/build.gradle index 08bdcbc..92b698e 100644 --- a/opensearch-search-quality-evaluation-framework/build.gradle +++ b/opensearch-search-quality-evaluation-framework/build.gradle @@ -14,7 +14,7 @@ test { } group = 'org.opensearch' -version = "${evalVersion}" +version = "1.0.0-SNAPSHOT" buildscript { repositories { @@ -32,6 +32,7 @@ repositories { dependencies { implementation 'org.opensearch.client:opensearch-java:2.19.0' + implementation 'commons-cli:commons-cli:1.9.0' implementation 'org.apache.logging.log4j:log4j-core:2.24.3' implementation 'com.fasterxml.jackson.core:jackson-annotations:2.18.2' implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2' diff --git a/opensearch-search-quality-evaluation-framework/gradle.properties b/opensearch-search-quality-evaluation-framework/gradle.properties deleted file mode 100644 index 2659a68..0000000 --- a/opensearch-search-quality-evaluation-framework/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -opensearchVersion = 2.18.0 -evalVersion = 0.0.1 diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java index d6c3a6a..31c0140 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java @@ -11,11 +11,7 @@ import com.google.gson.Gson; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.action.search.SearchScrollRequest; -import org.opensearch.client.Client; -import org.opensearch.client.Requests; + import org.opensearch.eval.Constants; import org.opensearch.eval.judgments.clickmodel.ClickModel; import org.opensearch.eval.judgments.model.ClickthroughRate; @@ -24,21 +20,8 @@ import org.opensearch.eval.judgments.opensearch.OpenSearchHelper; import org.opensearch.eval.judgments.queryhash.IncrementalUserQueryHash; import org.opensearch.eval.utils.MathUtils; -import org.opensearch.index.query.BoolQueryBuilder; -import org.opensearch.index.query.QueryBuilder; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.index.query.WrapperQueryBuilder; -import org.opensearch.search.Scroll; -import org.opensearch.search.SearchHit; -import org.opensearch.search.aggregations.AggregationBuilders; -import org.opensearch.search.aggregations.BucketOrder; -import org.opensearch.search.aggregations.bucket.terms.Terms; -import org.opensearch.search.aggregations.bucket.terms.TermsAggregationBuilder; -import org.opensearch.search.builder.SearchSourceBuilder; import java.io.IOException; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashSet; @@ -232,7 +215,7 @@ private Map> getClickthroughRate() throws Exceptio for (final SearchHit hit : searchHits) { - final UbiEvent ubiEvent = AccessController.doPrivileged((PrivilegedAction) () -> gson.fromJson(hit.getSourceAsString(), UbiEvent.class)); + final UbiEvent ubiEvent = gson.fromJson(hit.getSourceAsString(), UbiEvent.class); // We need to the hash of the query_id because two users can both search // for "computer" and those searches will have different query IDs, but they are the same search. diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java index c6ea7d7..ad288e5 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java @@ -9,26 +9,16 @@ package org.opensearch.eval.judgments.opensearch; import com.google.gson.Gson; +import org.apache.hc.core5.http.HttpHost; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.action.bulk.BulkRequest; -import org.opensearch.action.bulk.BulkResponse; -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.client.Client; -import org.opensearch.eval.Constants; -import org.opensearch.eval.judgments.model.ClickthroughRate; -import org.opensearch.eval.judgments.model.Judgment; +import org.opensearch.client.json.jackson.JacksonJsonpMapper; +import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.transport.OpenSearchTransport; +import org.opensearch.client.transport.httpclient5.ApacheHttpClient5TransportBuilder; import org.opensearch.eval.judgments.model.ubi.query.UbiQuery; -import org.opensearch.eval.utils.TimeUtils; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.index.query.WrapperQueryBuilder; -import org.opensearch.search.SearchHit; -import org.opensearch.search.builder.SearchSourceBuilder; import java.io.IOException; -import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -47,14 +37,25 @@ public class OpenSearchHelper { private static final Logger LOGGER = LogManager.getLogger(OpenSearchHelper.class.getName()); - private final Client client; + private final OpenSearchClient client; private final Gson gson = new Gson(); // Used to cache the query ID->user_query to avoid unnecessary lookups to OpenSearch. private static final Map userQueryCache = new HashMap<>(); - public OpenSearchHelper(final Client client) { - this.client = client; + public OpenSearchHelper() { + + final HttpHost[] hosts = new HttpHost[] { + new HttpHost("http", "localhost", 9200) + }; + + final OpenSearchTransport transport = ApacheHttpClient5TransportBuilder + .builder(hosts) + .setMapper(new JacksonJsonpMapper()) + .build(); + + this.client = new OpenSearchClient(transport); + } /** diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java index 356e72b..894be46 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java @@ -10,12 +10,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.client.OpenSearchClient; -import org.opensearch.client.opensearch.core.SearchRequest; -import org.opensearch.eval.Constants; -import org.opensearch.index.query.BoolQueryBuilder; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.client.opensearch.OpenSearchClient; import java.util.ArrayList; import java.util.Collection; diff --git a/opensearch-search-quality-evaluation-framework/useful_queries.txt b/opensearch-search-quality-evaluation-framework/useful_queries.txt deleted file mode 100644 index 35c8335..0000000 --- a/opensearch-search-quality-evaluation-framework/useful_queries.txt +++ /dev/null @@ -1,151 +0,0 @@ -DELETE ubi_events -DELETE ubi_queries - -GET ubi_events/_mapping -GET ubi_events/_search - -GET ubi_queries/_mapping -GET ubi_queries/_search - -DELETE judgments -GET judgments/_search - - -PUT ubi_queries -{ - "mappings": { - "properties": { - "timestamp": { "type": "date", "format": "strict_date_time" }, - "query_id": { "type": "keyword", "ignore_above": 100 }, - "query": { "type": "text" }, - "query_response_id": { "type": "keyword", "ignore_above": 100 }, - "query_response_hit_ids": { "type": "keyword" }, - "user_query": { "type": "keyword", "ignore_above": 256 }, - "query_attributes": { "type": "flat_object" }, - "client_id": { "type": "keyword", "ignore_above": 100 }, - "application": { "type": "keyword", "ignore_above": 100 } - } - } -} - -PUT ubi_events -{ -"mappings": { - "properties": { - "application": { "type": "keyword", "ignore_above": 256 }, - "action_name": { "type": "keyword", "ignore_above": 100 }, - "client_id": { "type": "keyword", "ignore_above": 100 }, - "query_id": { "type": "keyword", "ignore_above": 100 }, - "message": { "type": "keyword", "ignore_above": 1024 }, - "message_type": { "type": "keyword", "ignore_above": 100 }, - "timestamp": { - "type": "date", - "format":"strict_date_time", - "ignore_malformed": true, - "doc_values": true - }, - "event_attributes": { - "dynamic": true, - "properties": { - "position": { - "properties": { - "ordinal": { "type": "integer" }, - "x": { "type": "integer" }, - "y": { "type": "integer" }, - "page_depth": { "type": "integer" }, - "scroll_depth": { "type": "integer" }, - "trail": { "type": "text", - "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } - } - } - } - }, - "object": { - "properties": { - "internal_id": { "type": "keyword" }, - "object_id": { "type": "keyword", "ignore_above": 256 }, - "object_id_field": { "type": "keyword", "ignore_above": 100 }, - "name": { "type": "keyword", "ignore_above": 256 }, - "description": { "type": "text", - "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } - }, - "object_detail": { "type": "object" } - } - } - } - } - } - } -} - -GET ubi_events/_search -{ - "query": { - "range": { - "event_attributes.position.ordinal": { - "lte": 20 - } - } - } -} - -GET ubi_queries/_search -{ - "query": { - "term": { - "user_query": "batteries" - } - } -} - -GET ubi_events/_search -{ - "query": { - "bool": { - "must": [ - { - "term": { - "query_id": "cdc01f67-0b24-4c96-bb56-a89234f4fb0c" - } - }, - { - "term": { - "action_name": "click" - } - }, - { - "term": { - "event_attributes.position.ordinal": "0" - } - }, - { - "term": { - "event_attributes.object.object_id": "B0797J3DWK" - } - } - ] - } - } - } -} - -GET ubi_events/_search -{ - "size": 0, - "aggs": { - "By_Action": { - "terms": { - "field": "action_name", - "size": 20 - }, - "aggs": { - "By_Position": { - "terms": { - "field": "event_attributes.position.ordinal", - "size": 20 - } - } - } - } - } -} \ No newline at end of file From 871a9bf0990242643093fb2daf5fa55414047565 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 17 Dec 2024 09:08:12 -0500 Subject: [PATCH 05/41] Working on standalone implementation. Signed-off-by: jzonthemtn --- .../SearchQualityEvaluationRestHandler.java | 148 +----------------- .../eval/samplers/AbstractQuerySampler.java | 51 +++--- 2 files changed, 33 insertions(+), 166 deletions(-) diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java index ba56f04..fe61017 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java @@ -10,20 +10,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.action.admin.indices.create.CreateIndexRequest; -import org.opensearch.action.admin.indices.create.CreateIndexResponse; -import org.opensearch.action.admin.indices.exists.indices.IndicesExistsRequest; -import org.opensearch.action.admin.indices.exists.indices.IndicesExistsResponse; -import org.opensearch.action.delete.DeleteRequest; -import org.opensearch.action.delete.DeleteResponse; -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.index.IndexResponse; -import org.opensearch.action.support.WriteRequest; -import org.opensearch.client.node.NodeClient; -import org.opensearch.common.xcontent.json.JsonXContent; -import org.opensearch.core.action.ActionListener; -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.core.rest.RestStatus; import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel; import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModelParameters; import org.opensearch.eval.runners.OpenSearchQuerySetRunner; @@ -32,23 +18,11 @@ import org.opensearch.eval.samplers.AllQueriesQuerySamplerParameters; import org.opensearch.eval.samplers.ProbabilityProportionalToSizeAbstractQuerySampler; import org.opensearch.eval.samplers.ProbabilityProportionalToSizeParameters; -import org.opensearch.jobscheduler.spi.schedule.IntervalSchedule; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.RestResponse; import java.io.IOException; import java.nio.charset.Charset; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.ExecutionException; -import static org.opensearch.eval.SearchQualityEvaluationPlugin.JUDGMENTS_INDEX_NAME; - -public class SearchQualityEvaluationRestHandler extends BaseRestHandler { +public class SearchQualityEvaluationRestHandler { private static final Logger LOGGER = LogManager.getLogger(SearchQualityEvaluationRestHandler.class); @@ -77,21 +51,6 @@ public class SearchQualityEvaluationRestHandler extends BaseRestHandler { */ public static final String QUERY_PLACEHOLDER = "#$query##"; - @Override - public String getName() { - return "Search Quality Evaluation Framework"; - } - - @Override - public List routes() { - return List.of( - new Route(RestRequest.Method.POST, IMPLICIT_JUDGMENTS_URL), - new Route(RestRequest.Method.POST, SCHEDULING_URL), - new Route(RestRequest.Method.DELETE, SCHEDULING_URL), - new Route(RestRequest.Method.POST, QUERYSET_MANAGEMENT_URL), - new Route(RestRequest.Method.POST, QUERYSET_RUN_URL)); - } - @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { @@ -276,107 +235,6 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "{\"error\": \"" + request.method() + " is not allowed.\"}")); } - // Handle the scheduling of creating implicit judgments. - } else if(SCHEDULING_URL.equalsIgnoreCase(request.path())) { - - if (request.method().equals(RestRequest.Method.POST)) { - - // Get the job parameters from the request. - final String id = request.param("id"); - final String jobName = request.param("job_name", UUID.randomUUID().toString()); - final String lockDurationSecondsString = request.param("lock_duration_seconds", "600"); - final Long lockDurationSeconds = lockDurationSecondsString != null ? Long.parseLong(lockDurationSecondsString) : null; - final String jitterString = request.param("jitter"); - final Double jitter = jitterString != null ? Double.parseDouble(jitterString) : null; - final String clickModel = request.param("click_model"); - final int maxRank = Integer.parseInt(request.param("max_rank", "20")); - - // Validate the request parameters. - if (id == null || clickModel == null) { - throw new IllegalArgumentException("The id and click_model parameters must be provided."); - } - - // Read the start_time. - final Instant startTime; - if (request.param("start_time") == null) { - startTime = Instant.now(); - } else { - startTime = Instant.ofEpochMilli(Long.parseLong(request.param("start_time"))); - } - - // Read the interval. - final int interval; - if (request.param("interval") == null) { - // Default to every 24 hours. - interval = 1440; - } else { - interval = Integer.parseInt(request.param("interval")); - } - - final SearchQualityEvaluationJobParameter jobParameter = new SearchQualityEvaluationJobParameter( - jobName, new IntervalSchedule(startTime, interval, ChronoUnit.MINUTES), lockDurationSeconds, - jitter, clickModel, maxRank - ); - - final IndexRequest indexRequest = new IndexRequest().index(SearchQualityEvaluationPlugin.SCHEDULED_JOBS_INDEX_NAME) - .id(id) - .source(jobParameter.toXContent(JsonXContent.contentBuilder(), null)) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); - - return restChannel -> { - - // index the job parameter - client.index(indexRequest, new ActionListener<>() { - - @Override - public void onResponse(final IndexResponse indexResponse) { - - try { - - final RestResponse restResponse = new BytesRestResponse( - RestStatus.OK, - indexResponse.toXContent(JsonXContent.contentBuilder(), null) - ); - LOGGER.info("Created implicit judgments schedule for click-model {}: Job name {}, running every {} minutes starting {}", clickModel, jobName, interval, startTime); - - restChannel.sendResponse(restResponse); - - } catch (IOException e) { - restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, e.getMessage())); - } - - } - - @Override - public void onFailure(Exception e) { - restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, e.getMessage())); - } - }); - - }; - - // Delete a scheduled job to make implicit judgments. - } else if (request.method().equals(RestRequest.Method.DELETE)) { - - final String id = request.param("id"); - final DeleteRequest deleteRequest = new DeleteRequest().index(SearchQualityEvaluationPlugin.SCHEDULED_JOBS_INDEX_NAME).id(id); - - return restChannel -> client.delete(deleteRequest, new ActionListener<>() { - @Override - public void onResponse(final DeleteResponse deleteResponse) { - restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"message\": \"Scheduled job deleted.\"}")); - } - - @Override - public void onFailure(Exception e) { - restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, e.getMessage())); - } - }); - - } else { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "{\"error\": \"" + request.method() + " is not allowed.\"}")); - } - } else { return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.NOT_FOUND, "{\"error\": \"" + request.path() + " was not found.\"}")); } @@ -386,7 +244,7 @@ public void onFailure(Exception e) { private void createJudgmentsIndex(final NodeClient client) throws Exception { // If the judgments index does not exist we need to create it. - final IndicesExistsRequest indicesExistsRequest = new IndicesExistsRequest(JUDGMENTS_INDEX_NAME); + final IndicesExistsRequest indicesExistsRequest = new IndicesExistsRequest(Constants.JUDGMENTS_INDEX_NAME); final IndicesExistsResponse indicesExistsResponse = client.admin().indices().exists(indicesExistsRequest).get(); @@ -405,7 +263,7 @@ private void createJudgmentsIndex(final NodeClient client) throws Exception { " }"; // Create the judgments index. - final CreateIndexRequest createIndexRequest = new CreateIndexRequest(JUDGMENTS_INDEX_NAME).mapping(mapping); + final CreateIndexRequest createIndexRequest = new CreateIndexRequest(Constants.JUDGMENTS_INDEX_NAME).mapping(mapping); // TODO: Don't use .get() client.admin().indices().create(createIndexRequest).get(); diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java index 4246780..4209add 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java @@ -10,11 +10,10 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.index.IndexResponse; -import org.opensearch.action.support.WriteRequest; -import org.opensearch.client.node.NodeClient; -import org.opensearch.core.action.ActionListener; + +import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.opensearch.core.IndexRequest; +import org.opensearch.client.opensearch.indices.CreateIndexRequest; import org.opensearch.eval.Constants; import org.opensearch.eval.utils.TimeUtils; @@ -46,7 +45,7 @@ public abstract class AbstractQuerySampler { /** * Index the query set. */ - protected String indexQuerySet(final NodeClient client, final String name, final String description, final String sampling, Map queries) throws Exception { + protected String indexQuerySet(final OpenSearchClient client, final String name, final String description, final String sampling, Map queries) throws Exception { LOGGER.info("Indexing {} queries for query set {}", queries.size(), name); @@ -73,23 +72,33 @@ protected String indexQuerySet(final NodeClient client, final String name, final final String querySetId = UUID.randomUUID().toString(); // TODO: Create a mapping for the query set index. - final IndexRequest indexRequest = new IndexRequest().index(Constants.QUERY_SETS_INDEX_NAME) - .id(querySetId) - .source(querySet) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); - client.index(indexRequest, new ActionListener<>() { + final IndexData indexData = new IndexData("Document 1", "Text for document 1"); - @Override - public void onResponse(IndexResponse indexResponse) { - LOGGER.info("Indexed query set {} having name {}", querySetId, name); - } - - @Override - public void onFailure(Exception ex) { - LOGGER.error("Unable to index query set {}", querySetId, ex); - } - }); + final IndexRequest indexRequest = new IndexRequest.Builder().index(Constants.QUERY_SETS_INDEX_NAME) + .id(querySetId) + .document(indexData) + .source(querySet); + + client.index(indexRequest); +// +// final IndexRequest indexRequest = new IndexRequest().index(Constants.QUERY_SETS_INDEX_NAME) +// .id(querySetId) +// .source(querySet) +// .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); +// +// client.index(indexRequest, new ActionListener<>() { +// +// @Override +// public void onResponse(IndexResponse indexResponse) { +// LOGGER.info("Indexed query set {} having name {}", querySetId, name); +// } +// +// @Override +// public void onFailure(Exception ex) { +// LOGGER.error("Unable to index query set {}", querySetId, ex); +// } +// }); return querySetId; From bdcf6c582f827b0f3b042f2633bfbd54c63c68a3 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Mon, 30 Dec 2024 14:03:00 -0500 Subject: [PATCH 06/41] Working on converting to a standalone app. --- .../build.gradle | 43 --- .../gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - .../pom.xml | 51 ++++ .../settings.gradle | 1 - .../eval/SearchQualityEvaluationApp.java | 268 +++++++++++++++++ .../SearchQualityEvaluationRestHandler.java | 275 ------------------ .../OpenSearchEngine.java} | 92 +++++- .../opensearch/eval/engine/SearchEngine.java | 27 ++ .../clickmodel/coec/CoecClickModel.java | 28 +- .../model/ClickthroughRate.java | 2 +- .../eval/{judgments => }/model/Judgment.java | 2 +- .../{judgments => }/model/QuerySetQuery.java | 2 +- .../model/ubi/event/EventAttributes.java | 2 +- .../model/ubi/event/EventObject.java | 2 +- .../model/ubi/event/Position.java | 2 +- .../model/ubi/event/UbiEvent.java | 2 +- .../model/ubi/query/QueryResponse.java | 2 +- .../model/ubi/query/UbiQuery.java | 2 +- .../eval/runners/AbstractQuerySetRunner.java | 8 +- .../runners/OpenSearchQuerySetRunner.java | 118 +++----- 21 files changed, 503 insertions(+), 431 deletions(-) delete mode 100644 opensearch-search-quality-evaluation-framework/build.gradle delete mode 100644 opensearch-search-quality-evaluation-framework/gradle/wrapper/gradle-wrapper.jar delete mode 100644 opensearch-search-quality-evaluation-framework/gradle/wrapper/gradle-wrapper.properties create mode 100644 opensearch-search-quality-evaluation-framework/pom.xml delete mode 100644 opensearch-search-quality-evaluation-framework/settings.gradle create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationApp.java delete mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java rename opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/{judgments/opensearch/OpenSearchHelper.java => engine/OpenSearchEngine.java} (79%) create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java rename opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/{judgments => }/model/ClickthroughRate.java (98%) rename opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/{judgments => }/model/Judgment.java (98%) rename opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/{judgments => }/model/QuerySetQuery.java (93%) rename opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/{judgments => }/model/ubi/event/EventAttributes.java (97%) rename opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/{judgments => }/model/ubi/event/EventObject.java (95%) rename opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/{judgments => }/model/ubi/event/Position.java (94%) rename opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/{judgments => }/model/ubi/event/UbiEvent.java (97%) rename opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/{judgments => }/model/ubi/query/QueryResponse.java (96%) rename opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/{judgments => }/model/ubi/query/UbiQuery.java (98%) diff --git a/opensearch-search-quality-evaluation-framework/build.gradle b/opensearch-search-quality-evaluation-framework/build.gradle deleted file mode 100644 index 92b698e..0000000 --- a/opensearch-search-quality-evaluation-framework/build.gradle +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -apply plugin: 'java' -apply plugin: 'idea' - -test { - include "**/Test*.class" - include "**/*Test.class" - include "**/*Test.class" - include "**/*TestCase.class" -} - -group = 'org.opensearch' -version = "1.0.0-SNAPSHOT" - -buildscript { - repositories { - mavenLocal() - maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } - mavenCentral() - maven { url "https://plugins.gradle.org/m2/" } - } -} - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - implementation 'org.opensearch.client:opensearch-java:2.19.0' - implementation 'commons-cli:commons-cli:1.9.0' - implementation 'org.apache.logging.log4j:log4j-core:2.24.3' - implementation 'com.fasterxml.jackson.core:jackson-annotations:2.18.2' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2' - implementation 'org.apache.httpcomponents.core5:httpcore5:5.3.1' - implementation 'org.apache.httpcomponents.client5:httpclient5:5.4.1' - implementation 'commons-logging:commons-logging:1.3.4' - implementation 'com.google.code.gson:gson:2.11.0' -} diff --git a/opensearch-search-quality-evaluation-framework/gradle/wrapper/gradle-wrapper.jar b/opensearch-search-quality-evaluation-framework/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM diff --git a/opensearch-search-quality-evaluation-framework/gradle/wrapper/gradle-wrapper.properties b/opensearch-search-quality-evaluation-framework/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 2bbac7d..0000000 --- a/opensearch-search-quality-evaluation-framework/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/pom.xml b/opensearch-search-quality-evaluation-framework/pom.xml new file mode 100644 index 0000000..568973a --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + org.opensearch + search-evaluation-framework + 1.0.0-SNAPSHOT + search-evaluation-framework + https://www.ubisearch.dev + + UTF-8 + 17 + + + + org.opensearch.client + opensearch-java + 2.19.0 + + + commons-cli + commons-cli + 1.9.0 + + + org.apache.logging.log4j + log4j-core + 2.24.3 + + + org.apache.httpcomponents.core5 + httpcore5 + 5.3.1 + + + org.apache.httpcomponents.client5 + httpclient5 + 5.4.1 + + + commons-logging + commons-logging + 1.3.4 + + + com.google.code.gson + gson + 2.11.0 + + + \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/settings.gradle b/opensearch-search-quality-evaluation-framework/settings.gradle deleted file mode 100644 index ef059e1..0000000 --- a/opensearch-search-quality-evaluation-framework/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'search-evaluation-framework' diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationApp.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationApp.java new file mode 100644 index 0000000..2f71b38 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationApp.java @@ -0,0 +1,268 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.eval; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class SearchQualityEvaluationApp { + + private static final Logger LOGGER = LogManager.getLogger(SearchQualityEvaluationApp.class); + + public static void main(String args[]) throws ParseException { + + final Options options = new Options(); + options.addOption("c", true, "create a click model"); + options.addOption("q", true, "run a query set"); + + final CommandLineParser parser = new DefaultParser(); + final CommandLine cmd = parser.parse(options, args); + + if(cmd.hasOption("c")) { + final String clickModel = cmd.getOptionValue("c"); + // TODO: Create the click model. + } else { + + } + + } + +// +// /** +// * The placeholder in the query that gets replaced by the query term when running a query set. +// */ +// public static final String QUERY_PLACEHOLDER = "#$query##"; +// +// @Override +// protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { +// +// // Handle managing query sets. +// if(QUERYSET_MANAGEMENT_URL.equalsIgnoreCase(request.path())) { +// +// // Creating a new query set by sampling the UBI queries. +// if (request.method().equals(RestRequest.Method.POST)) { +// +// final String name = request.param("name"); +// final String description = request.param("description"); +// final String sampling = request.param("sampling", "pptss"); +// final int querySetSize = Integer.parseInt(request.param("query_set_size", "1000")); +// +// // Create a query set by finding all the unique user_query terms. +// if (AllQueriesQuerySampler.NAME.equalsIgnoreCase(sampling)) { +// +// // If we are not sampling queries, the query sets should just be directly +// // indexed into OpenSearch using the `ubi_queries` index directly. +// +// try { +// +// final AllQueriesQuerySamplerParameters parameters = new AllQueriesQuerySamplerParameters(name, description, sampling, querySetSize); +// final AllQueriesQuerySampler sampler = new AllQueriesQuerySampler(client, parameters); +// +// // Sample and index the queries. +// final String querySetId = sampler.sample(); +// +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"query_set\": \"" + querySetId + "\"}")); +// +// } catch(Exception ex) { +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "{\"error\": \"" + ex.getMessage() + "\"}")); +// } +// +// +// // Create a query set by using PPTSS sampling. +// } else if (ProbabilityProportionalToSizeAbstractQuerySampler.NAME.equalsIgnoreCase(sampling)) { +// +// LOGGER.info("Creating query set using PPTSS"); +// +// final ProbabilityProportionalToSizeParameters parameters = new ProbabilityProportionalToSizeParameters(name, description, sampling, querySetSize); +// final ProbabilityProportionalToSizeAbstractQuerySampler sampler = new ProbabilityProportionalToSizeAbstractQuerySampler(client, parameters); +// +// try { +// +// // Sample and index the queries. +// final String querySetId = sampler.sample(); +// +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"query_set\": \"" + querySetId + "\"}")); +// +// } catch(Exception ex) { +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "{\"error\": \"" + ex.getMessage() + "\"}")); +// } +// +// } else { +// // An Invalid sampling method was provided in the request. +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Invalid sampling method: " + sampling + "\"}")); +// } +// +// } else { +// // Invalid HTTP method for this endpoint. +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "{\"error\": \"" + request.method() + " is not allowed.\"}")); +// } +// +// // Handle running query sets. +// } else if(QUERYSET_RUN_URL.equalsIgnoreCase(request.path())) { +// +// final String querySetId = request.param("id"); +// final String judgmentsId = request.param("judgments_id"); +// final String index = request.param("index"); +// final String searchPipeline = request.param("search_pipeline", null); +// final String idField = request.param("id_field", "_id"); +// final int k = Integer.parseInt(request.param("k", "10")); +// final double threshold = Double.parseDouble(request.param("threshold", "1.0")); +// +// if(querySetId == null || querySetId.isEmpty() || judgmentsId == null || judgmentsId.isEmpty() || index == null || index.isEmpty()) { +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing required parameters.\"}")); +// } +// +// if(k < 1) { +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"k must be a positive integer.\"}")); +// } +// +// if(!request.hasContent()) { +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing query in body.\"}")); +// } +// +// // Get the query JSON from the content. +// final String query = new String(BytesReference.toBytes(request.content()), Charset.defaultCharset()); +// +// // Validate the query has a QUERY_PLACEHOLDER. +// if(!query.contains(QUERY_PLACEHOLDER)) { +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing query placeholder in query.\"}")); +// } +// +// try { +// +// final OpenSearchQuerySetRunner openSearchQuerySetRunner = new OpenSearchQuerySetRunner(client); +// final QuerySetRunResult querySetRunResult = openSearchQuerySetRunner.run(querySetId, judgmentsId, index, searchPipeline, idField, query, k, threshold); +// openSearchQuerySetRunner.save(querySetRunResult); +// +// } catch (Exception ex) { +// LOGGER.error("Unable to run query set. Verify query set and judgments exist.", ex); +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, ex.getMessage())); +// } +// +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"message\": \"Run initiated for query set " + querySetId + "\"}")); +// +// // Handle the on-demand creation of implicit judgments. +// } else if(IMPLICIT_JUDGMENTS_URL.equalsIgnoreCase(request.path())) { +// +// if (request.method().equals(RestRequest.Method.POST)) { +// +// //final long startTime = System.currentTimeMillis(); +// final String clickModel = request.param("click_model", "coec"); +// final int maxRank = Integer.parseInt(request.param("max_rank", "20")); +// +// if (CoecClickModel.CLICK_MODEL_NAME.equalsIgnoreCase(clickModel)) { +// +// final CoecClickModelParameters coecClickModelParameters = new CoecClickModelParameters(maxRank); +// final CoecClickModel coecClickModel = new CoecClickModel(client, coecClickModelParameters); +// +// final String judgmentsId; +// +// // TODO: Run this in a separate thread. +// try { +// +// // Create the judgments index. +// createJudgmentsIndex(client); +// +// judgmentsId = coecClickModel.calculateJudgments(); +// +// // judgmentsId will be null if no judgments were created (and indexed). +// if(judgmentsId == null) { +// // TODO: Is Bad Request the appropriate error? Perhaps Conflict is more appropriate? +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"No judgments were created. Check the queries and events data.\"}")); +// } +// +//// final long elapsedTime = System.currentTimeMillis() - startTime; +//// +//// final Map job = new HashMap<>(); +//// job.put("name", "manual_generation"); +//// job.put("click_model", clickModel); +//// job.put("started", startTime); +//// job.put("duration", elapsedTime); +//// job.put("invocation", "on_demand"); +//// job.put("judgments_id", judgmentsId); +//// job.put("max_rank", maxRank); +//// +//// final String jobId = UUID.randomUUID().toString(); +//// +//// final IndexRequest indexRequest = new IndexRequest() +//// .index(SearchQualityEvaluationPlugin.COMPLETED_JOBS_INDEX_NAME) +//// .id(jobId) +//// .source(job) +//// .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); +//// +//// client.index(indexRequest, new ActionListener<>() { +//// @Override +//// public void onResponse(final IndexResponse indexResponse) { +//// LOGGER.debug("Click model job completed successfully: {}", jobId); +//// } +//// +//// @Override +//// public void onFailure(final Exception ex) { +//// LOGGER.error("Unable to run job with ID {}", jobId, ex); +//// throw new RuntimeException("Unable to run job", ex); +//// } +//// }); +// +// } catch (Exception ex) { +// throw new RuntimeException("Unable to generate judgments.", ex); +// } +// +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"judgments_id\": \"" + judgmentsId + "\"}")); +// +// } else { +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Invalid click model.\"}")); +// } +// +// } else { +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "{\"error\": \"" + request.method() + " is not allowed.\"}")); +// } +// +// } else { +// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.NOT_FOUND, "{\"error\": \"" + request.path() + " was not found.\"}")); +// } +// +// } +// +// private void createJudgmentsIndex(final NodeClient client) throws Exception { +// +// // If the judgments index does not exist we need to create it. +// final IndicesExistsRequest indicesExistsRequest = new IndicesExistsRequest(Constants.JUDGMENTS_INDEX_NAME); +// +// final IndicesExistsResponse indicesExistsResponse = client.admin().indices().exists(indicesExistsRequest).get(); +// +// if(!indicesExistsResponse.isExists()) { +// +// // TODO: Read this mapping from a resource file instead. +// final String mapping = "{\n" + +// " \"properties\": {\n" + +// " \"judgments_id\": { \"type\": \"keyword\" },\n" + +// " \"query_id\": { \"type\": \"keyword\" },\n" + +// " \"query\": { \"type\": \"keyword\" },\n" + +// " \"document_id\": { \"type\": \"keyword\" },\n" + +// " \"judgment\": { \"type\": \"double\" },\n" + +// " \"timestamp\": { \"type\": \"date\", \"format\": \"strict_date_time\" }\n" + +// " }\n" + +// " }"; +// +// // Create the judgments index. +// final CreateIndexRequest createIndexRequest = new CreateIndexRequest(Constants.JUDGMENTS_INDEX_NAME).mapping(mapping); +// +// // TODO: Don't use .get() +// client.admin().indices().create(createIndexRequest).get(); +// +// } +// +// } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java deleted file mode 100644 index fe61017..0000000 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel; -import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModelParameters; -import org.opensearch.eval.runners.OpenSearchQuerySetRunner; -import org.opensearch.eval.runners.QuerySetRunResult; -import org.opensearch.eval.samplers.AllQueriesQuerySampler; -import org.opensearch.eval.samplers.AllQueriesQuerySamplerParameters; -import org.opensearch.eval.samplers.ProbabilityProportionalToSizeAbstractQuerySampler; -import org.opensearch.eval.samplers.ProbabilityProportionalToSizeParameters; - -import java.io.IOException; -import java.nio.charset.Charset; - -public class SearchQualityEvaluationRestHandler { - - private static final Logger LOGGER = LogManager.getLogger(SearchQualityEvaluationRestHandler.class); - - /** - * URL for the implicit judgment scheduling. - */ - public static final String SCHEDULING_URL = "/_plugins/search_quality_eval/schedule"; - - /** - * URL for on-demand implicit judgment generation. - */ - public static final String IMPLICIT_JUDGMENTS_URL = "/_plugins/search_quality_eval/judgments"; - - /** - * URL for managing query sets. - */ - public static final String QUERYSET_MANAGEMENT_URL = "/_plugins/search_quality_eval/queryset"; - - /** - * URL for initiating query sets to run on-demand. - */ - public static final String QUERYSET_RUN_URL = "/_plugins/search_quality_eval/run"; - - /** - * The placeholder in the query that gets replaced by the query term when running a query set. - */ - public static final String QUERY_PLACEHOLDER = "#$query##"; - - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - - // Handle managing query sets. - if(QUERYSET_MANAGEMENT_URL.equalsIgnoreCase(request.path())) { - - // Creating a new query set by sampling the UBI queries. - if (request.method().equals(RestRequest.Method.POST)) { - - final String name = request.param("name"); - final String description = request.param("description"); - final String sampling = request.param("sampling", "pptss"); - final int querySetSize = Integer.parseInt(request.param("query_set_size", "1000")); - - // Create a query set by finding all the unique user_query terms. - if (AllQueriesQuerySampler.NAME.equalsIgnoreCase(sampling)) { - - // If we are not sampling queries, the query sets should just be directly - // indexed into OpenSearch using the `ubi_queries` index directly. - - try { - - final AllQueriesQuerySamplerParameters parameters = new AllQueriesQuerySamplerParameters(name, description, sampling, querySetSize); - final AllQueriesQuerySampler sampler = new AllQueriesQuerySampler(client, parameters); - - // Sample and index the queries. - final String querySetId = sampler.sample(); - - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"query_set\": \"" + querySetId + "\"}")); - - } catch(Exception ex) { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "{\"error\": \"" + ex.getMessage() + "\"}")); - } - - - // Create a query set by using PPTSS sampling. - } else if (ProbabilityProportionalToSizeAbstractQuerySampler.NAME.equalsIgnoreCase(sampling)) { - - LOGGER.info("Creating query set using PPTSS"); - - final ProbabilityProportionalToSizeParameters parameters = new ProbabilityProportionalToSizeParameters(name, description, sampling, querySetSize); - final ProbabilityProportionalToSizeAbstractQuerySampler sampler = new ProbabilityProportionalToSizeAbstractQuerySampler(client, parameters); - - try { - - // Sample and index the queries. - final String querySetId = sampler.sample(); - - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"query_set\": \"" + querySetId + "\"}")); - - } catch(Exception ex) { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "{\"error\": \"" + ex.getMessage() + "\"}")); - } - - } else { - // An Invalid sampling method was provided in the request. - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Invalid sampling method: " + sampling + "\"}")); - } - - } else { - // Invalid HTTP method for this endpoint. - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "{\"error\": \"" + request.method() + " is not allowed.\"}")); - } - - // Handle running query sets. - } else if(QUERYSET_RUN_URL.equalsIgnoreCase(request.path())) { - - final String querySetId = request.param("id"); - final String judgmentsId = request.param("judgments_id"); - final String index = request.param("index"); - final String searchPipeline = request.param("search_pipeline", null); - final String idField = request.param("id_field", "_id"); - final int k = Integer.parseInt(request.param("k", "10")); - final double threshold = Double.parseDouble(request.param("threshold", "1.0")); - - if(querySetId == null || querySetId.isEmpty() || judgmentsId == null || judgmentsId.isEmpty() || index == null || index.isEmpty()) { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing required parameters.\"}")); - } - - if(k < 1) { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"k must be a positive integer.\"}")); - } - - if(!request.hasContent()) { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing query in body.\"}")); - } - - // Get the query JSON from the content. - final String query = new String(BytesReference.toBytes(request.content()), Charset.defaultCharset()); - - // Validate the query has a QUERY_PLACEHOLDER. - if(!query.contains(QUERY_PLACEHOLDER)) { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing query placeholder in query.\"}")); - } - - try { - - final OpenSearchQuerySetRunner openSearchQuerySetRunner = new OpenSearchQuerySetRunner(client); - final QuerySetRunResult querySetRunResult = openSearchQuerySetRunner.run(querySetId, judgmentsId, index, searchPipeline, idField, query, k, threshold); - openSearchQuerySetRunner.save(querySetRunResult); - - } catch (Exception ex) { - LOGGER.error("Unable to run query set. Verify query set and judgments exist.", ex); - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, ex.getMessage())); - } - - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"message\": \"Run initiated for query set " + querySetId + "\"}")); - - // Handle the on-demand creation of implicit judgments. - } else if(IMPLICIT_JUDGMENTS_URL.equalsIgnoreCase(request.path())) { - - if (request.method().equals(RestRequest.Method.POST)) { - - //final long startTime = System.currentTimeMillis(); - final String clickModel = request.param("click_model", "coec"); - final int maxRank = Integer.parseInt(request.param("max_rank", "20")); - - if (CoecClickModel.CLICK_MODEL_NAME.equalsIgnoreCase(clickModel)) { - - final CoecClickModelParameters coecClickModelParameters = new CoecClickModelParameters(maxRank); - final CoecClickModel coecClickModel = new CoecClickModel(client, coecClickModelParameters); - - final String judgmentsId; - - // TODO: Run this in a separate thread. - try { - - // Create the judgments index. - createJudgmentsIndex(client); - - judgmentsId = coecClickModel.calculateJudgments(); - - // judgmentsId will be null if no judgments were created (and indexed). - if(judgmentsId == null) { - // TODO: Is Bad Request the appropriate error? Perhaps Conflict is more appropriate? - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"No judgments were created. Check the queries and events data.\"}")); - } - -// final long elapsedTime = System.currentTimeMillis() - startTime; -// -// final Map job = new HashMap<>(); -// job.put("name", "manual_generation"); -// job.put("click_model", clickModel); -// job.put("started", startTime); -// job.put("duration", elapsedTime); -// job.put("invocation", "on_demand"); -// job.put("judgments_id", judgmentsId); -// job.put("max_rank", maxRank); -// -// final String jobId = UUID.randomUUID().toString(); -// -// final IndexRequest indexRequest = new IndexRequest() -// .index(SearchQualityEvaluationPlugin.COMPLETED_JOBS_INDEX_NAME) -// .id(jobId) -// .source(job) -// .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); -// -// client.index(indexRequest, new ActionListener<>() { -// @Override -// public void onResponse(final IndexResponse indexResponse) { -// LOGGER.debug("Click model job completed successfully: {}", jobId); -// } -// -// @Override -// public void onFailure(final Exception ex) { -// LOGGER.error("Unable to run job with ID {}", jobId, ex); -// throw new RuntimeException("Unable to run job", ex); -// } -// }); - - } catch (Exception ex) { - throw new RuntimeException("Unable to generate judgments.", ex); - } - - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"judgments_id\": \"" + judgmentsId + "\"}")); - - } else { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Invalid click model.\"}")); - } - - } else { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "{\"error\": \"" + request.method() + " is not allowed.\"}")); - } - - } else { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.NOT_FOUND, "{\"error\": \"" + request.path() + " was not found.\"}")); - } - - } - - private void createJudgmentsIndex(final NodeClient client) throws Exception { - - // If the judgments index does not exist we need to create it. - final IndicesExistsRequest indicesExistsRequest = new IndicesExistsRequest(Constants.JUDGMENTS_INDEX_NAME); - - final IndicesExistsResponse indicesExistsResponse = client.admin().indices().exists(indicesExistsRequest).get(); - - if(!indicesExistsResponse.isExists()) { - - // TODO: Read this mapping from a resource file instead. - final String mapping = "{\n" + - " \"properties\": {\n" + - " \"judgments_id\": { \"type\": \"keyword\" },\n" + - " \"query_id\": { \"type\": \"keyword\" },\n" + - " \"query\": { \"type\": \"keyword\" },\n" + - " \"document_id\": { \"type\": \"keyword\" },\n" + - " \"judgment\": { \"type\": \"double\" },\n" + - " \"timestamp\": { \"type\": \"date\", \"format\": \"strict_date_time\" }\n" + - " }\n" + - " }"; - - // Create the judgments index. - final CreateIndexRequest createIndexRequest = new CreateIndexRequest(Constants.JUDGMENTS_INDEX_NAME).mapping(mapping); - - // TODO: Don't use .get() - client.admin().indices().create(createIndexRequest).get(); - - } - - } - -} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java similarity index 79% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java rename to opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java index ad288e5..32cb2b1 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java @@ -6,7 +6,7 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.eval.judgments.opensearch; +package org.opensearch.eval.engine; import com.google.gson.Gson; import org.apache.hc.core5.http.HttpHost; @@ -14,9 +14,23 @@ import org.apache.logging.log4j.Logger; import org.opensearch.client.json.jackson.JacksonJsonpMapper; import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.opensearch._types.Refresh; +import org.opensearch.client.opensearch._types.mapping.IntegerNumberProperty; +import org.opensearch.client.opensearch._types.mapping.Property; +import org.opensearch.client.opensearch._types.mapping.TypeMapping; +import org.opensearch.client.opensearch.core.BulkRequest; +import org.opensearch.client.opensearch.core.BulkResponse; +import org.opensearch.client.opensearch.core.IndexRequest; +import org.opensearch.client.opensearch.core.bulk.BulkOperation; +import org.opensearch.client.opensearch.core.bulk.IndexOperation; +import org.opensearch.client.opensearch.indices.CreateIndexRequest; +import org.opensearch.client.opensearch.indices.ExistsRequest; import org.opensearch.client.transport.OpenSearchTransport; import org.opensearch.client.transport.httpclient5.ApacheHttpClient5TransportBuilder; -import org.opensearch.eval.judgments.model.ubi.query.UbiQuery; +import org.opensearch.eval.model.ClickthroughRate; +import org.opensearch.eval.model.Judgment; +import org.opensearch.eval.model.ubi.query.UbiQuery; +import org.opensearch.eval.utils.TimeUtils; import java.io.IOException; import java.util.ArrayList; @@ -31,11 +45,10 @@ /** * Functionality for interacting with OpenSearch. - * TODO: Move these functions out of this class. */ -public class OpenSearchHelper { +public class OpenSearchEngine extends SearchEngine { - private static final Logger LOGGER = LogManager.getLogger(OpenSearchHelper.class.getName()); + private static final Logger LOGGER = LogManager.getLogger(OpenSearchEngine.class.getName()); private final OpenSearchClient client; private final Gson gson = new Gson(); @@ -43,7 +56,7 @@ public class OpenSearchHelper { // Used to cache the query ID->user_query to avoid unnecessary lookups to OpenSearch. private static final Map userQueryCache = new HashMap<>(); - public OpenSearchHelper() { + public OpenSearchEngine() { final HttpHost[] hosts = new HttpHost[] { new HttpHost("http", "localhost", 9200) @@ -58,12 +71,74 @@ public OpenSearchHelper() { } + @Override + public boolean doesIndexExist(final String index) throws IOException { + + return client.indices().exists(ExistsRequest.of(s -> s.index(index))).value(); + + } + + @Override + public boolean createIndex(String index, Map mapping) throws IOException { + + // TODO: Build the mapping. + final TypeMapping mapping2 = new TypeMapping.Builder() + .properties("age", new Property.Builder().integer(new IntegerNumberProperty.Builder().build()).build()) + .build(); + + final CreateIndexRequest createIndexRequest = new CreateIndexRequest.Builder().index(index).mappings(mapping2).build(); + + return Boolean.TRUE.equals(client.indices().create(createIndexRequest).acknowledged()); + + } + + @Override + public boolean deleteIndex(String index) throws IOException { + + return client.indices().delete(s -> s.index(index)).acknowledged(); + + } + + @Override + public String indexJudgment(String index, String id, Judgment judgment) throws IOException { + + if(id == null) { + id = UUID.randomUUID().toString(); + } + + final IndexRequest indexRequest = new IndexRequest.Builder().index(index).id(id).document(judgment).build(); + return client.index(indexRequest).id(); + + } + + @Override + public boolean bulkIndex(String index, Map documents) throws IOException { + + final ArrayList bulkOperations = new ArrayList<>(); + + for(final String id : documents.keySet()) { + final Object document = documents.get(id); + bulkOperations.add(new BulkOperation.Builder().index(IndexOperation.of(io -> io.index(index).id(id).document(document))).build()); + } + + final BulkRequest.Builder bulkReq = new BulkRequest.Builder() + .index(index) + .operations(bulkOperations) + .refresh(Refresh.WaitFor); + + final BulkResponse bulkResponse = client.bulk(bulkReq.build()); + + return !bulkResponse.errors(); + + } + /** * Gets the user query for a given query ID. * @param queryId The query ID. * @return The user query. * @throws IOException Thrown when there is a problem accessing OpenSearch. */ + @Override public String getUserQuery(final String queryId) throws Exception { // If it's in the cache just get it and return it. @@ -94,6 +169,7 @@ public String getUserQuery(final String queryId) throws Exception { * @return A {@link UbiQuery} object for the given query ID. * @throws Exception Thrown if the query cannot be retrieved. */ + @Override public UbiQuery getQueryFromQueryId(final String queryId) throws Exception { LOGGER.debug("Getting query from query ID {}", queryId); @@ -151,6 +227,7 @@ private Collection getQueryIdsHavingUserQuery(final String userQuery) th } + @Override public long getCountOfQueriesForUserQueryHavingResultInRankR(final String userQuery, final String objectId, final int rank) throws Exception { long countOfTimesShownAtRank = 0; @@ -222,6 +299,7 @@ public long getCountOfQueriesForUserQueryHavingResultInRankR(final String userQu * @param rankAggregatedClickThrough A map of position to clickthrough values. * @throws IOException Thrown when there is a problem accessing OpenSearch. */ + @Override public void indexRankAggregatedClickthrough(final Map rankAggregatedClickThrough) throws Exception { if(!rankAggregatedClickThrough.isEmpty()) { @@ -253,6 +331,7 @@ public void indexRankAggregatedClickthrough(final Map rankAggre * @param clickthroughRates A map of query IDs to a collection of {@link ClickthroughRate} objects. * @throws IOException Thrown when there is a problem accessing OpenSearch. */ + @Override public void indexClickthroughRates(final Map> clickthroughRates) throws Exception { if(!clickthroughRates.isEmpty()) { @@ -308,6 +387,7 @@ public void onFailure(Exception ex) { * @throws IOException Thrown when there is a problem accessing OpenSearch. * @return The ID of the indexed judgments. */ + @Override public String indexJudgments(final Collection judgments) throws Exception { final String judgmentsId = UUID.randomUUID().toString(); diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java new file mode 100644 index 0000000..66559ec --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java @@ -0,0 +1,27 @@ +package org.opensearch.eval.engine; + +import org.opensearch.eval.model.ClickthroughRate; +import org.opensearch.eval.model.Judgment; +import org.opensearch.eval.model.ubi.query.UbiQuery; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +public abstract class SearchEngine { + + public abstract boolean doesIndexExist(String index) throws IOException; + public abstract boolean createIndex(String index, Map mapping) throws IOException; + public abstract boolean deleteIndex(String index) throws IOException; + + public abstract String getUserQuery(final String queryId) throws Exception; + public abstract UbiQuery getQueryFromQueryId(final String queryId) throws Exception; + public abstract long getCountOfQueriesForUserQueryHavingResultInRankR(final String userQuery, final String objectId, final int rank) throws Exception; + public abstract void indexRankAggregatedClickthrough(final Map rankAggregatedClickThrough) throws Exception; + public abstract void indexClickthroughRates(final Map> clickthroughRates) throws Exception; + public abstract String indexJudgment(String index, String id, Judgment judgment) throws IOException; + + public abstract boolean bulkIndex(String index, Map documents) throws IOException; + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java index 31c0140..24a1492 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java @@ -11,13 +11,12 @@ import com.google.gson.Gson; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - import org.opensearch.eval.Constants; +import org.opensearch.eval.engine.SearchEngine; import org.opensearch.eval.judgments.clickmodel.ClickModel; -import org.opensearch.eval.judgments.model.ClickthroughRate; -import org.opensearch.eval.judgments.model.Judgment; -import org.opensearch.eval.judgments.model.ubi.event.UbiEvent; -import org.opensearch.eval.judgments.opensearch.OpenSearchHelper; +import org.opensearch.eval.model.ClickthroughRate; +import org.opensearch.eval.model.Judgment; +import org.opensearch.eval.model.ubi.event.UbiEvent; import org.opensearch.eval.judgments.queryhash.IncrementalUserQueryHash; import org.opensearch.eval.utils.MathUtils; @@ -43,19 +42,16 @@ public class CoecClickModel extends ClickModel { private final CoecClickModelParameters parameters; - private final OpenSearchHelper openSearchHelper; - private final IncrementalUserQueryHash incrementalUserQueryHash = new IncrementalUserQueryHash(); private final Gson gson = new Gson(); - private final Client client; + private final SearchEngine searchEngine; private static final Logger LOGGER = LogManager.getLogger(CoecClickModel.class.getName()); - public CoecClickModel(final Client client, final CoecClickModelParameters parameters) { + public CoecClickModel(final SearchEngine searchEngine, final CoecClickModelParameters parameters) { this.parameters = parameters; - this.openSearchHelper = new OpenSearchHelper(client); - this.client = client; + this.searchEngine = searchEngine; } @@ -112,7 +108,7 @@ public String calculateCoec(final Map rankAggregatedClickThroug final double meanCtrAtRank = rankAggregatedClickThrough.getOrDefault(rank, 0.0); // The number of times this document was shown as this rank. - final long countOfTimesShownAtRank = openSearchHelper.getCountOfQueriesForUserQueryHavingResultInRankR(userQuery, ctr.getObjectId(), rank); + final long countOfTimesShownAtRank = searchEngine.getCountOfQueriesForUserQueryHavingResultInRankR(userQuery, ctr.getObjectId(), rank); denominatorSum += (meanCtrAtRank * countOfTimesShownAtRank); @@ -148,7 +144,7 @@ public String calculateCoec(final Map rankAggregatedClickThroug showJudgments(judgments); if(!(judgments.isEmpty())) { - return openSearchHelper.indexJudgments(judgments); + return searchEngine.indexJudgments(judgments); } else { return null; } @@ -219,7 +215,7 @@ private Map> getClickthroughRate() throws Exceptio // We need to the hash of the query_id because two users can both search // for "computer" and those searches will have different query IDs, but they are the same search. - final String userQuery = openSearchHelper.getUserQuery(ubiEvent.getQueryId()); + final String userQuery = searchEngine.getUserQuery(ubiEvent.getQueryId()); // userQuery will be null if there is not a query for this event in ubi_queries. if(userQuery != null) { @@ -264,7 +260,7 @@ private Map> getClickthroughRate() throws Exceptio } - openSearchHelper.indexClickthroughRates(queriesToClickthroughRates); + searchEngine.indexClickthroughRates(queriesToClickthroughRates); return queriesToClickthroughRates; @@ -365,7 +361,7 @@ public Map getRankAggregatedClickThrough() throws Exception { } - openSearchHelper.indexRankAggregatedClickthrough(rankAggregatedClickThrough); + searchEngine.indexRankAggregatedClickthrough(rankAggregatedClickThrough); return rankAggregatedClickThrough; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ClickthroughRate.java similarity index 98% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java rename to opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ClickthroughRate.java index cef1f1f..2736ffe 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ClickthroughRate.java @@ -6,7 +6,7 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.eval.judgments.model; +package org.opensearch.eval.model; import org.opensearch.eval.utils.MathUtils; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/Judgment.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/Judgment.java similarity index 98% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/Judgment.java rename to opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/Judgment.java index bc9955f..89ec7b0 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/Judgment.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/Judgment.java @@ -6,7 +6,7 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.eval.judgments.model; +package org.opensearch.eval.model; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/QuerySetQuery.java similarity index 93% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java rename to opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/QuerySetQuery.java index 2244df4..eaa825b 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/QuerySetQuery.java @@ -6,7 +6,7 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.eval.judgments.model; +package org.opensearch.eval.model; public class QuerySetQuery { diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventAttributes.java similarity index 97% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java rename to opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventAttributes.java index cf09444..1be71c0 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventAttributes.java @@ -6,7 +6,7 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.eval.judgments.model.ubi.event; +package org.opensearch.eval.model.ubi.event; import com.google.gson.annotations.SerializedName; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java similarity index 95% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java rename to opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java index 55595ba..27c9982 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java @@ -6,7 +6,7 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.eval.judgments.model.ubi.event; +package org.opensearch.eval.model.ubi.event; import com.google.gson.annotations.SerializedName; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/Position.java similarity index 94% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java rename to opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/Position.java index e3ebaad..5671ed8 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/Position.java @@ -6,7 +6,7 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.eval.judgments.model.ubi.event; +package org.opensearch.eval.model.ubi.event; import com.google.gson.annotations.SerializedName; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java similarity index 97% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java rename to opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java index 61c0f8b..c29a053 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java @@ -6,7 +6,7 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.eval.judgments.model.ubi.event; +package org.opensearch.eval.model.ubi.event; import com.google.gson.annotations.SerializedName; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/query/QueryResponse.java similarity index 96% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java rename to opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/query/QueryResponse.java index 5d45ee0..e257b3a 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/query/QueryResponse.java @@ -6,7 +6,7 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.eval.judgments.model.ubi.query; +package org.opensearch.eval.model.ubi.query; import java.util.List; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java similarity index 98% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java rename to opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java index 0b7ca0b..0e87ab0 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java @@ -6,7 +6,7 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.eval.judgments.model.ubi.query; +package org.opensearch.eval.model.ubi.query; import com.google.gson.annotations.SerializedName; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java index 894be46..306eaf6 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java @@ -10,7 +10,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.eval.engine.SearchEngine; import java.util.ArrayList; import java.util.Collection; @@ -25,10 +25,10 @@ public abstract class AbstractQuerySetRunner { private static final Logger LOGGER = LogManager.getLogger(AbstractQuerySetRunner.class); - protected final OpenSearchClient client; + protected final SearchEngine searchEngine; - public AbstractQuerySetRunner(final OpenSearchClient client) { - this.client = client; + public AbstractQuerySetRunner(final SearchEngine searchEngine) { + this.searchEngine = searchEngine; } /** diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java index 52f162b..c4f2380 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java @@ -10,25 +10,13 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.action.admin.indices.create.CreateIndexRequest; -import org.opensearch.action.admin.indices.create.CreateIndexResponse; -import org.opensearch.action.admin.indices.exists.indices.IndicesExistsRequest; -import org.opensearch.action.admin.indices.exists.indices.IndicesExistsResponse; -import org.opensearch.action.bulk.BulkRequest; -import org.opensearch.action.bulk.BulkResponse; -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.client.Client; -import org.opensearch.core.action.ActionListener; +import org.opensearch.eval.Constants; +import org.opensearch.eval.engine.SearchEngine; import org.opensearch.eval.metrics.DcgSearchMetric; import org.opensearch.eval.metrics.NdcgSearchMetric; import org.opensearch.eval.metrics.PrecisionSearchMetric; import org.opensearch.eval.metrics.SearchMetric; import org.opensearch.eval.utils.TimeUtils; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.search.SearchHit; -import org.opensearch.search.builder.SearchSourceBuilder; import java.util.ArrayList; import java.util.Collection; @@ -37,8 +25,6 @@ import java.util.Map; import java.util.UUID; -import static org.opensearch.eval.SearchQualityEvaluationRestHandler.QUERY_PLACEHOLDER; - /** * A {@link AbstractQuerySetRunner} for Amazon OpenSearch. */ @@ -46,13 +32,15 @@ public class OpenSearchQuerySetRunner extends AbstractQuerySetRunner { private static final Logger LOGGER = LogManager.getLogger(OpenSearchQuerySetRunner.class); + public static final String QUERY_PLACEHOLDER = "#?query##"; + /** * Creates a new query set runner * - * @param client An OpenSearch {@link Client}. + * @param searchEngine An OpenSearch engine {@link SearchEngine}. */ - public OpenSearchQuerySetRunner(final Client client) { - super(client); + public OpenSearchQuerySetRunner(final SearchEngine searchEngine) { + super(searchEngine); } @Override @@ -92,7 +80,7 @@ public QuerySetRunResult run(final String querySetId, final String judgmentsId, final SearchRequest searchRequest = new SearchRequest(index); searchRequest.source(searchSourceBuilder); - if(searchPipeline != null) { + if (searchPipeline != null) { searchSourceBuilder.pipeline(searchPipeline); searchRequest.pipeline(searchPipeline); } @@ -112,7 +100,7 @@ public void onResponse(final SearchResponse searchResponse) { final String documentId; - if("_id".equals(idField)) { + if ("_id".equals(idField)) { documentId = hit.getId(); } else { // TODO: Need to check this field actually exists. @@ -156,8 +144,8 @@ public void onFailure(Exception ex) { // Sum up the metrics for each query per metric type. final int querySetSize = queryResults.size(); final Map sumOfMetrics = new HashMap<>(); - for(final QueryResult queryResult : queryResults) { - for(final SearchMetric searchMetric : queryResult.getSearchMetrics()) { + for (final QueryResult queryResult : queryResults) { + for (final SearchMetric searchMetric : queryResult.getSearchMetrics()) { //LOGGER.info("Summing: {} - {}", searchMetric.getName(), searchMetric.getValue()); sumOfMetrics.merge(searchMetric.getName(), searchMetric.getValue(), Double::sum); } @@ -165,7 +153,7 @@ public void onFailure(Exception ex) { // Now divide by the number of queries. final Map querySetMetrics = new HashMap<>(); - for(final String metric : sumOfMetrics.keySet()) { + for (final String metric : sumOfMetrics.keySet()) { //LOGGER.info("Dividing by the query set size: {} / {}", sumOfMetrics.get(metric), querySetSize); querySetMetrics.put(metric, sumOfMetrics.get(metric) / querySetSize); } @@ -191,58 +179,44 @@ public void save(final QuerySetRunResult result) throws Exception { // See https://github.com/o19s/opensearch-search-quality-evaluation/blob/main/opensearch-dashboard-prototyping/METRICS_SCHEMA.md // See https://github.com/o19s/opensearch-search-quality-evaluation/blob/main/opensearch-dashboard-prototyping/sample_data.ndjson - final IndicesExistsRequest indicesExistsRequest = new IndicesExistsRequest(SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME); - - client.admin().indices().exists(indicesExistsRequest, new ActionListener<>() { - - @Override - public void onResponse(IndicesExistsResponse indicesExistsResponse) { - - if(!indicesExistsResponse.isExists()) { - - // Create the index. - // TODO: Read this mapping from a resource file instead. - final String mapping = "{\n" + - " \"properties\": {\n" + - " \"datetime\": { \"type\": \"date\", \"format\": \"strict_date_time\" },\n" + - " \"search_config\": { \"type\": \"keyword\" },\n" + - " \"query_set_id\": { \"type\": \"keyword\" },\n" + - " \"query\": { \"type\": \"keyword\" },\n" + - " \"metric\": { \"type\": \"keyword\" },\n" + - " \"value\": { \"type\": \"double\" },\n" + - " \"application\": { \"type\": \"keyword\" },\n" + - " \"evaluation_id\": { \"type\": \"keyword\" },\n" + - " \"frogs_percent\": { \"type\": \"double\" }\n" + - " }\n" + - " }"; - - // Create the judgments index. - final CreateIndexRequest createIndexRequest = new CreateIndexRequest(SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME).mapping(mapping); - - client.admin().indices().create(createIndexRequest, new ActionListener<>() { - - @Override - public void onResponse(CreateIndexResponse createIndexResponse) { - LOGGER.info("{} index created.", SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME); - } - - @Override - public void onFailure(Exception ex) { - LOGGER.error("Unable to create the {} index.", SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME, ex); - } - - }); + final boolean dashboardMetricsIndexExists = searchEngine.doesIndexExist(Constants.DASHBOARD_METRICS_INDEX_NAME); + + if (!dashboardMetricsIndexExists) { + + // Create the index. + // TODO: Read this mapping from a resource file instead. + final String mapping = "{\n" + + " \"properties\": {\n" + + " \"datetime\": { \"type\": \"date\", \"format\": \"strict_date_time\" },\n" + + " \"search_config\": { \"type\": \"keyword\" },\n" + + " \"query_set_id\": { \"type\": \"keyword\" },\n" + + " \"query\": { \"type\": \"keyword\" },\n" + + " \"metric\": { \"type\": \"keyword\" },\n" + + " \"value\": { \"type\": \"double\" },\n" + + " \"application\": { \"type\": \"keyword\" },\n" + + " \"evaluation_id\": { \"type\": \"keyword\" },\n" + + " \"frogs_percent\": { \"type\": \"double\" }\n" + + " }\n" + + " }"; + + // Create the judgments index. + final CreateIndexRequest createIndexRequest = new CreateIndexRequest(SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME).mapping(mapping); + + client.admin().indices().create(createIndexRequest, new ActionListener<>() { + + @Override + public void onResponse(CreateIndexResponse createIndexResponse) { + LOGGER.info("{} index created.", SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME); + } + @Override + public void onFailure(Exception ex) { + LOGGER.error("Unable to create the {} index.", SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME, ex); } - } + }); - @Override - public void onFailure(Exception ex) { - LOGGER.error("Unable to determine if {} index exists.", SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME, ex); - } - - }); + } final BulkRequest bulkRequest = new BulkRequest(); final String timestamp = TimeUtils.getTimestamp(); From 504e3f95f10cfa272a382ed784f6102fe571e895 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Mon, 30 Dec 2024 14:24:29 -0500 Subject: [PATCH 07/41] Working on converting to a standalone app. --- .../eval/engine/OpenSearchEngine.java | 36 +++++++++++++++---- .../opensearch/eval/engine/SearchEngine.java | 6 ++-- .../clickmodel/coec/CoecClickModel.java | 2 +- .../eval/model/data/AbstractData.java | 5 +++ .../eval/model/{ => data}/Judgment.java | 4 +-- .../eval/samplers/AbstractQuerySampler.java | 12 ++----- .../eval/samplers/AllQueriesQuerySampler.java | 12 +++---- ...roportionalToSizeAbstractQuerySampler.java | 12 +++---- 8 files changed, 55 insertions(+), 34 deletions(-) create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java rename opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/{ => data}/Judgment.java (96%) diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java index 32cb2b1..bfe0bec 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java @@ -15,20 +15,26 @@ import org.opensearch.client.json.jackson.JacksonJsonpMapper; import org.opensearch.client.opensearch.OpenSearchClient; import org.opensearch.client.opensearch._types.Refresh; +import org.opensearch.client.opensearch._types.Time; import org.opensearch.client.opensearch._types.mapping.IntegerNumberProperty; import org.opensearch.client.opensearch._types.mapping.Property; import org.opensearch.client.opensearch._types.mapping.TypeMapping; import org.opensearch.client.opensearch.core.BulkRequest; import org.opensearch.client.opensearch.core.BulkResponse; import org.opensearch.client.opensearch.core.IndexRequest; +import org.opensearch.client.opensearch.core.ScrollRequest; +import org.opensearch.client.opensearch.core.ScrollResponse; +import org.opensearch.client.opensearch.core.SearchResponse; import org.opensearch.client.opensearch.core.bulk.BulkOperation; import org.opensearch.client.opensearch.core.bulk.IndexOperation; +import org.opensearch.client.opensearch.core.search.Hit; import org.opensearch.client.opensearch.indices.CreateIndexRequest; import org.opensearch.client.opensearch.indices.ExistsRequest; import org.opensearch.client.transport.OpenSearchTransport; import org.opensearch.client.transport.httpclient5.ApacheHttpClient5TransportBuilder; +import org.opensearch.eval.Constants; import org.opensearch.eval.model.ClickthroughRate; -import org.opensearch.eval.model.Judgment; +import org.opensearch.eval.model.data.Judgment; import org.opensearch.eval.model.ubi.query.UbiQuery; import org.opensearch.eval.utils.TimeUtils; @@ -36,6 +42,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -99,15 +106,30 @@ public boolean deleteIndex(String index) throws IOException { } - @Override - public String indexJudgment(String index, String id, Judgment judgment) throws IOException { + public Collection getJudgments(final String index) throws IOException { + + final Collection judgments = new ArrayList<>(); + + final SearchResponse searchResponse = client.search(s -> s.index(index).size(1000).scroll(Time.of(t -> t.offset(1000))), Judgment.class); + + String scrollId = searchResponse.scrollId(); + List> searchHits = searchResponse.hits().hits(); + + while (searchHits != null && !searchHits.isEmpty()) { + + for (int i = 0; i < searchResponse.hits().hits().size(); i++) { + judgments.add(searchResponse.hits().hits().get(i).source()); + } + + final ScrollRequest scrollRequest = new ScrollRequest.Builder().scrollId(scrollId).build(); + final ScrollResponse scrollResponse = client.scroll(scrollRequest, Judgment.class); + + scrollId = scrollResponse.scrollId(); + searchHits = scrollResponse.hits().hits(); - if(id == null) { - id = UUID.randomUUID().toString(); } - final IndexRequest indexRequest = new IndexRequest.Builder().index(index).id(id).document(judgment).build(); - return client.index(indexRequest).id(); + return judgments; } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java index 66559ec..1996f11 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java @@ -1,7 +1,7 @@ package org.opensearch.eval.engine; import org.opensearch.eval.model.ClickthroughRate; -import org.opensearch.eval.model.Judgment; +import org.opensearch.eval.model.data.Judgment; import org.opensearch.eval.model.ubi.query.UbiQuery; import java.io.IOException; @@ -20,8 +20,10 @@ public abstract class SearchEngine { public abstract long getCountOfQueriesForUserQueryHavingResultInRankR(final String userQuery, final String objectId, final int rank) throws Exception; public abstract void indexRankAggregatedClickthrough(final Map rankAggregatedClickThrough) throws Exception; public abstract void indexClickthroughRates(final Map> clickthroughRates) throws Exception; - public abstract String indexJudgment(String index, String id, Judgment judgment) throws IOException; + public abstract String indexJudgments(final Collection judgments) throws Exception; public abstract boolean bulkIndex(String index, Map documents) throws IOException; + public abstract Collection getJudgments(final String index) throws IOException; + } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java index 24a1492..d879947 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java @@ -15,7 +15,7 @@ import org.opensearch.eval.engine.SearchEngine; import org.opensearch.eval.judgments.clickmodel.ClickModel; import org.opensearch.eval.model.ClickthroughRate; -import org.opensearch.eval.model.Judgment; +import org.opensearch.eval.model.data.Judgment; import org.opensearch.eval.model.ubi.event.UbiEvent; import org.opensearch.eval.judgments.queryhash.IncrementalUserQueryHash; import org.opensearch.eval.utils.MathUtils; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java new file mode 100644 index 0000000..f095ec3 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java @@ -0,0 +1,5 @@ +package org.opensearch.eval.model.data; + +public abstract class AbstractData { + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/Judgment.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/Judgment.java similarity index 96% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/Judgment.java rename to opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/Judgment.java index 89ec7b0..d8b853b 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/Judgment.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/Judgment.java @@ -6,7 +6,7 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.eval.model; +package org.opensearch.eval.model.data; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -18,7 +18,7 @@ /** * A judgment of a search result's quality for a given query. */ -public class Judgment { +public class Judgment extends AbstractData { private static final Logger LOGGER = LogManager.getLogger(Judgment.class.getName()); diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java index 4209add..6945b59 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java @@ -15,6 +15,7 @@ import org.opensearch.client.opensearch.core.IndexRequest; import org.opensearch.client.opensearch.indices.CreateIndexRequest; import org.opensearch.eval.Constants; +import org.opensearch.eval.engine.SearchEngine; import org.opensearch.eval.utils.TimeUtils; import java.util.ArrayList; @@ -45,7 +46,7 @@ public abstract class AbstractQuerySampler { /** * Index the query set. */ - protected String indexQuerySet(final OpenSearchClient client, final String name, final String description, final String sampling, Map queries) throws Exception { + protected String indexQuerySet(final SearchEngine searchEngine, final String name, final String description, final String sampling, Map queries) throws Exception { LOGGER.info("Indexing {} queries for query set {}", queries.size(), name); @@ -73,15 +74,6 @@ protected String indexQuerySet(final OpenSearchClient client, final String name, // TODO: Create a mapping for the query set index. - final IndexData indexData = new IndexData("Document 1", "Text for document 1"); - - final IndexRequest indexRequest = new IndexRequest.Builder().index(Constants.QUERY_SETS_INDEX_NAME) - .id(querySetId) - .document(indexData) - .source(querySet); - - client.index(indexRequest); -// // final IndexRequest indexRequest = new IndexRequest().index(Constants.QUERY_SETS_INDEX_NAME) // .id(querySetId) // .source(querySet) diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java index e9d4401..7e5e615 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java @@ -10,8 +10,8 @@ import org.opensearch.action.search.SearchRequest; import org.opensearch.action.search.SearchResponse; -import org.opensearch.client.node.NodeClient; import org.opensearch.eval.Constants; +import org.opensearch.eval.engine.SearchEngine; import org.opensearch.index.query.QueryBuilders; import org.opensearch.search.SearchHit; import org.opensearch.search.builder.SearchSourceBuilder; @@ -26,15 +26,15 @@ public class AllQueriesQuerySampler extends AbstractQuerySampler { public static final String NAME = "none"; - private final NodeClient client; + private final SearchEngine searchEngine; private final AllQueriesQuerySamplerParameters parameters; /** * Creates a new sampler. - * @param client The OpenSearch {@link NodeClient client}. + * @param searchEngine The OpenSearch {@link SearchEngine engine}. */ - public AllQueriesQuerySampler(final NodeClient client, final AllQueriesQuerySamplerParameters parameters) { - this.client = client; + public AllQueriesQuerySampler(final SearchEngine searchEngine, final AllQueriesQuerySamplerParameters parameters) { + this.searchEngine = searchEngine; this.parameters = parameters; } @@ -72,7 +72,7 @@ public String sample() throws Exception { } - return indexQuerySet(client, parameters.getName(), parameters.getDescription(), parameters.getSampling(), queries); + return indexQuerySet(searchEngine, parameters.getName(), parameters.getDescription(), parameters.getSampling(), queries); } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java index 112494c..82939b7 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java @@ -13,9 +13,9 @@ import org.opensearch.action.search.SearchRequest; import org.opensearch.action.search.SearchResponse; import org.opensearch.action.search.SearchScrollRequest; -import org.opensearch.client.node.NodeClient; import org.opensearch.common.unit.TimeValue; import org.opensearch.eval.Constants; +import org.opensearch.eval.engine.SearchEngine; import org.opensearch.index.query.QueryBuilders; import org.opensearch.search.Scroll; import org.opensearch.search.SearchHit; @@ -39,16 +39,16 @@ public class ProbabilityProportionalToSizeAbstractQuerySampler extends AbstractQ private static final Logger LOGGER = LogManager.getLogger(ProbabilityProportionalToSizeAbstractQuerySampler.class); - private final NodeClient client; + private final SearchEngine searchEngine; private final ProbabilityProportionalToSizeParameters parameters; /** * Creates a new PPTSS sampler. - * @param client The OpenSearch {@link NodeClient client}. + * @param searchEngine The OpenSearch {@link SearchEngine engine}. * @param parameters The {@link ProbabilityProportionalToSizeParameters parameters} for the sampling. */ - public ProbabilityProportionalToSizeAbstractQuerySampler(final NodeClient client, final ProbabilityProportionalToSizeParameters parameters) { - this.client = client; + public ProbabilityProportionalToSizeAbstractQuerySampler(final SearchEngine searchEngine, final ProbabilityProportionalToSizeParameters parameters) { + this.searchEngine = searchEngine; this.parameters = parameters; } @@ -165,7 +165,7 @@ public String sample() throws Exception { } - return indexQuerySet(client, parameters.getName(), parameters.getDescription(), parameters.getSampling(), querySet); + return indexQuerySet(searchEngine, parameters.getName(), parameters.getDescription(), parameters.getSampling(), querySet); } From 75a7641b18f369684e96fe8f800fbdf5a9bb4a98 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Mon, 30 Dec 2024 15:17:20 -0500 Subject: [PATCH 08/41] Working on converting to a standalone app. --- .../eval/engine/OpenSearchEngine.java | 119 ++++++++++-------- .../opensearch/eval/engine/SearchEngine.java | 4 + .../eval/model/data/AbstractData.java | 14 +++ .../eval/model/data/ClickThroughRate.java | 61 +++++++++ .../opensearch/eval/model/data/Judgment.java | 24 +++- .../opensearch/eval/model/data/QuerySet.java | 63 ++++++++++ .../data/RankAggregatedClickThrough.java | 35 ++++++ .../eval/samplers/AbstractQuerySampler.java | 21 ++-- .../eval/samplers/AllQueriesQuerySampler.java | 5 - ...roportionalToSizeAbstractQuerySampler.java | 54 +------- 10 files changed, 279 insertions(+), 121 deletions(-) create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QuerySet.java create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java index bfe0bec..77da718 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java @@ -34,7 +34,10 @@ import org.opensearch.client.transport.httpclient5.ApacheHttpClient5TransportBuilder; import org.opensearch.eval.Constants; import org.opensearch.eval.model.ClickthroughRate; +import org.opensearch.eval.model.data.ClickThroughRate; import org.opensearch.eval.model.data.Judgment; +import org.opensearch.eval.model.data.QuerySet; +import org.opensearch.eval.model.data.RankAggregatedClickThrough; import org.opensearch.eval.model.ubi.query.UbiQuery; import org.opensearch.eval.utils.TimeUtils; @@ -106,6 +109,45 @@ public boolean deleteIndex(String index) throws IOException { } + @Override + public String indexQuerySet(final QuerySet querySet) throws IOException { + + final String index = Constants.QUERY_SETS_INDEX_NAME; + final String id = querySet.getId(); + + final IndexRequest indexRequest = new IndexRequest.Builder().index(index).id(id).document(querySet).build(); + return client.index(indexRequest).id(); + + } + + @Override + public Collection getUbiQueries() throws IOException { + + final Collection ubiQueries = new ArrayList<>(); + + final SearchResponse searchResponse = client.search(s -> s.index(Constants.UBI_QUERIES_INDEX_NAME).size(1000).scroll(Time.of(t -> t.offset(1000))), UbiQuery.class); + + String scrollId = searchResponse.scrollId(); + List> searchHits = searchResponse.hits().hits(); + + while (searchHits != null && !searchHits.isEmpty()) { + + for (int i = 0; i < searchResponse.hits().hits().size(); i++) { + ubiQueries.add(searchResponse.hits().hits().get(i).source()); + } + + final ScrollRequest scrollRequest = new ScrollRequest.Builder().scrollId(scrollId).build(); + final ScrollResponse scrollResponse = client.scroll(scrollRequest, UbiQuery.class); + + scrollId = scrollResponse.scrollId(); + searchHits = scrollResponse.hits().hits(); + + } + + return ubiQueries; + + } + public Collection getJudgments(final String index) throws IOException { final Collection judgments = new ArrayList<>(); @@ -326,24 +368,21 @@ public void indexRankAggregatedClickthrough(final Map rankAggre if(!rankAggregatedClickThrough.isEmpty()) { - // TODO: Split this into multiple bulk insert requests. - - final BulkRequest request = new BulkRequest(); + // TODO: Use bulk indexing. for (final int position : rankAggregatedClickThrough.keySet()) { - final Map jsonMap = new HashMap<>(); - jsonMap.put("position", position); - jsonMap.put("ctr", rankAggregatedClickThrough.get(position)); + final String id = UUID.randomUUID().toString(); - final IndexRequest indexRequest = new IndexRequest(INDEX_RANK_AGGREGATED_CTR).id(UUID.randomUUID().toString()).source(jsonMap); + final RankAggregatedClickThrough r = new RankAggregatedClickThrough(id); + r.setPosition(position); + r.setCtr(rankAggregatedClickThrough.get(position)); - request.add(indexRequest); + final IndexRequest indexRequest = new IndexRequest.Builder().index(INDEX_RANK_AGGREGATED_CTR).id(id).document(r).build(); + client.index(indexRequest); } - client.bulk(request).get(); - } } @@ -358,47 +397,28 @@ public void indexClickthroughRates(final Map> clic if(!clickthroughRates.isEmpty()) { - final BulkRequest request = new BulkRequest(); + // TODO: Use bulk inserts. - for(final String userQuery : clickthroughRates.keySet()) { + for (final String userQuery : clickthroughRates.keySet()) { - for(final ClickthroughRate clickthroughRate : clickthroughRates.get(userQuery)) { + for (final ClickthroughRate clickthroughRate : clickthroughRates.get(userQuery)) { - final Map jsonMap = new HashMap<>(); - jsonMap.put("user_query", userQuery); - jsonMap.put("clicks", clickthroughRate.getClicks()); - jsonMap.put("events", clickthroughRate.getImpressions()); - jsonMap.put("ctr", clickthroughRate.getClickthroughRate()); - jsonMap.put("object_id", clickthroughRate.getObjectId()); + final String id = UUID.randomUUID().toString(); - final IndexRequest indexRequest = new IndexRequest(INDEX_QUERY_DOC_CTR) - .id(UUID.randomUUID().toString()) - .source(jsonMap); + final ClickThroughRate clickThroughRate = new ClickThroughRate(); + clickThroughRate.setUserQuery(userQuery); + clickThroughRate.setClicks(clickthroughRate.getClicks()); + clickThroughRate.setEvents(clickthroughRate.getImpressions()); + clickThroughRate.setCtr(clickthroughRate.getClickthroughRate()); + clickThroughRate.setObjectId(clickthroughRate.getObjectId()); - request.add(indexRequest); + final IndexRequest indexRequest = new IndexRequest.Builder().index(INDEX_QUERY_DOC_CTR).id(id).document(clickThroughRate).build(); + client.index(indexRequest); } } - client.bulk(request, new ActionListener<>() { - - @Override - public void onResponse(BulkResponse bulkItemResponses) { - if(bulkItemResponses.hasFailures()) { - LOGGER.error("Clickthrough rates were not all successfully indexed: {}", bulkItemResponses.buildFailureMessage()); - } else { - LOGGER.debug("Clickthrough rates has been successfully indexed."); - } - } - - @Override - public void onFailure(Exception ex) { - LOGGER.error("Indexing the clickthrough rates failed.", ex); - } - - }); - } } @@ -415,25 +435,18 @@ public String indexJudgments(final Collection judgments) throws Except final String judgmentsId = UUID.randomUUID().toString(); final String timestamp = TimeUtils.getTimestamp(); - final BulkRequest bulkRequest = new BulkRequest(); + // TODO: Use bulk imports. for(final Judgment judgment : judgments) { - final Map j = judgment.getJudgmentAsMap(); - j.put("judgments_id", judgmentsId); - j.put("timestamp", timestamp); + judgment.setJudgmentsId(judgmentsId); + judgment.setTimestamp(timestamp); - final IndexRequest indexRequest = new IndexRequest(Constants.JUDGMENTS_INDEX_NAME) - .id(UUID.randomUUID().toString()) - .source(j); - - bulkRequest.add(indexRequest); + final IndexRequest indexRequest = new IndexRequest.Builder().index(Constants.JUDGMENTS_INDEX_NAME).id(judgment.getId()).document(judgment).build(); + client.index(indexRequest); } - // TODO: Don't use .get() - client.bulk(bulkRequest).get(); - return judgmentsId; } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java index 1996f11..611308b 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java @@ -2,6 +2,7 @@ import org.opensearch.eval.model.ClickthroughRate; import org.opensearch.eval.model.data.Judgment; +import org.opensearch.eval.model.data.QuerySet; import org.opensearch.eval.model.ubi.query.UbiQuery; import java.io.IOException; @@ -26,4 +27,7 @@ public abstract class SearchEngine { public abstract Collection getJudgments(final String index) throws IOException; + public abstract String indexQuerySet(QuerySet querySet) throws IOException; + public abstract Collection getUbiQueries() throws IOException; + } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java index f095ec3..f815b3a 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java @@ -2,4 +2,18 @@ public abstract class AbstractData { + private String id; + + public AbstractData(final String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java new file mode 100644 index 0000000..6edcf40 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java @@ -0,0 +1,61 @@ +package org.opensearch.eval.model.data; + +import java.util.UUID; + +public class ClickThroughRate extends AbstractData { + + private String userQuery; + private long clicks; + private long events; + private double ctr; + private String objectId; + + public ClickThroughRate() { + super(UUID.randomUUID().toString()); + } + + public ClickThroughRate(String id) { + super(id); + } + + public String getUserQuery() { + return userQuery; + } + + public void setUserQuery(String userQuery) { + this.userQuery = userQuery; + } + + public long getClicks() { + return clicks; + } + + public void setClicks(long clicks) { + this.clicks = clicks; + } + + public long getEvents() { + return events; + } + + public void setEvents(long events) { + this.events = events; + } + + public double getCtr() { + return ctr; + } + + public void setCtr(double ctr) { + this.ctr = ctr; + } + + public String getObjectId() { + return objectId; + } + + public void setObjectId(String objectId) { + this.objectId = objectId; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/Judgment.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/Judgment.java index d8b853b..3285598 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/Judgment.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/Judgment.java @@ -26,15 +26,19 @@ public class Judgment extends AbstractData { private final String query; private final String document; private final double judgment; + private String judgmentsId; + private String timestamp; /** * Creates a new judgment. + * @param id The judgment ID. * @param queryId The query ID for the judgment. * @param query The query for the judgment. - * @param document The document in the jdugment. + * @param document The document in the judgment. * @param judgment The judgment value. */ - public Judgment(final String queryId, final String query, final String document, final double judgment) { + public Judgment(final String id, final String queryId, final String query, final String document, final double judgment) { + super(id); this.queryId = queryId; this.query = query; this.document = document; @@ -94,4 +98,20 @@ public double getJudgment() { return judgment; } + public String getJudgmentsId() { + return judgmentsId; + } + + public void setJudgmentsId(String judgmentsId) { + this.judgmentsId = judgmentsId; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QuerySet.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QuerySet.java new file mode 100644 index 0000000..9f5327c --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QuerySet.java @@ -0,0 +1,63 @@ +package org.opensearch.eval.model.data; + +import java.util.Collection; +import java.util.Map; +import java.util.UUID; + +public class QuerySet extends AbstractData { + + private String name; + private String description; + private String sampling; + private Collection> querySetQueries; + private String timestamp; + + public QuerySet() { + super(UUID.randomUUID().toString()); + } + + public QuerySet(String id) { + super(id); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getSampling() { + return sampling; + } + + public void setSampling(String sampling) { + this.sampling = sampling; + } + + public Collection> getQuerySetQueries() { + return querySetQueries; + } + + public void setQuerySetQueries(Collection> querySetQueries) { + this.querySetQueries = querySetQueries; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java new file mode 100644 index 0000000..90c74be --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java @@ -0,0 +1,35 @@ +package org.opensearch.eval.model.data; + +import java.util.UUID; + +public class RankAggregatedClickThrough extends AbstractData { + + private int position; + private double ctr; + + public RankAggregatedClickThrough(String id) { + super(id); + } + + public RankAggregatedClickThrough() { + super(UUID.randomUUID().toString()); + } + + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } + + public double getCtr() { + return ctr; + } + + public void setCtr(double ctr) { + this.ctr = ctr; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java index 6945b59..e43b37b 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java @@ -10,19 +10,14 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - -import org.opensearch.client.opensearch.OpenSearchClient; -import org.opensearch.client.opensearch.core.IndexRequest; -import org.opensearch.client.opensearch.indices.CreateIndexRequest; -import org.opensearch.eval.Constants; import org.opensearch.eval.engine.SearchEngine; +import org.opensearch.eval.model.data.QuerySet; import org.opensearch.eval.utils.TimeUtils; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; -import java.util.UUID; /** * An interface for sampling UBI queries. @@ -63,14 +58,14 @@ protected String indexQuerySet(final SearchEngine searchEngine, final String nam } - final Map querySet = new HashMap<>(); - querySet.put("name", name); - querySet.put("description", description); - querySet.put("sampling", sampling); - querySet.put("queries", querySetQueries); - querySet.put("timestamp", TimeUtils.getTimestamp()); + final QuerySet querySet = new QuerySet(); + querySet.setName(name); + querySet.setDescription(description); + querySet.setSampling(sampling); + querySet.setQuerySetQueries(querySetQueries); + querySet.setTimestamp(TimeUtils.getTimestamp()); - final String querySetId = UUID.randomUUID().toString(); + final String querySetId = searchEngine.indexQuerySet(querySet); // TODO: Create a mapping for the query set index. diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java index 7e5e615..be78e64 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java @@ -8,13 +8,8 @@ */ package org.opensearch.eval.samplers; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; import org.opensearch.eval.Constants; import org.opensearch.eval.engine.SearchEngine; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.search.SearchHit; -import org.opensearch.search.builder.SearchSourceBuilder; import java.util.HashMap; import java.util.Map; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java index 82939b7..ebef273 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java @@ -10,21 +10,13 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.action.search.SearchScrollRequest; -import org.opensearch.common.unit.TimeValue; -import org.opensearch.eval.Constants; import org.opensearch.eval.engine.SearchEngine; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.search.Scroll; -import org.opensearch.search.SearchHit; -import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.eval.model.ubi.query.UbiQuery; -import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -60,45 +52,11 @@ public String getName() { @Override public String sample() throws Exception { - // TODO: Can this be changed to an aggregation? - // An aggregation is limited (?) to 10,000 which could miss some queries. + final Collection ubiQueries = searchEngine.getUbiQueries(); - // Get queries from the UBI queries index. - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(QueryBuilders.matchAllQuery()); - searchSourceBuilder.size(10000); - final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(10L)); - - final SearchRequest searchRequest = new SearchRequest(Constants.UBI_QUERIES_INDEX_NAME); - searchRequest.scroll(scroll); - searchRequest.source(searchSourceBuilder); - - // TODO: Don't use .get() - SearchResponse searchResponse = client.search(searchRequest).get(); - - String scrollId = searchResponse.getScrollId(); - SearchHit[] searchHits = searchResponse.getHits().getHits(); - - final Collection userQueries = new ArrayList<>(); - - while (searchHits != null && searchHits.length > 0) { - - for(final SearchHit hit : searchHits) { - final Map fields = hit.getSourceAsMap(); - userQueries.add(fields.get("user_query").toString()); - // LOGGER.info("user queries count: {} user query: {}", userQueries.size(), fields.get("user_query").toString()); - } - - final SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); - scrollRequest.scroll(scroll); - - // TODO: Don't use .get() - searchResponse = client.searchScroll(scrollRequest).get(); - - scrollId = searchResponse.getScrollId(); - searchHits = searchResponse.getHits().getHits(); - - } + final List userQueries = ubiQueries.stream() + .map(UbiQuery::getUserQuery) + .toList(); // LOGGER.info("User queries found: {}", userQueries); From aa26e3bed55d687dd9023648b57314a571cf7cfd Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Mon, 30 Dec 2024 15:20:42 -0500 Subject: [PATCH 09/41] Working on converting to a standalone app. --- .../java/org/opensearch/eval/model/data/AbstractData.java | 8 ++++++++ .../org/opensearch/eval/model/data/ClickThroughRate.java | 8 ++++++++ .../java/org/opensearch/eval/model/data/QuerySet.java | 8 ++++++++ .../eval/model/data/RankAggregatedClickThrough.java | 8 ++++++++ .../opensearch/eval/runners/AbstractQuerySetRunner.java | 3 +++ 5 files changed, 35 insertions(+) diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java index f815b3a..1aaab07 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java @@ -1,3 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ package org.opensearch.eval.model.data; public abstract class AbstractData { diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java index 6edcf40..b389130 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java @@ -1,3 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ package org.opensearch.eval.model.data; import java.util.UUID; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QuerySet.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QuerySet.java index 9f5327c..712005a 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QuerySet.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QuerySet.java @@ -1,3 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ package org.opensearch.eval.model.data; import java.util.Collection; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java index 90c74be..770b9cf 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java @@ -1,3 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ package org.opensearch.eval.model.data; import java.util.UUID; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java index 306eaf6..3260ae0 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java @@ -10,6 +10,9 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.client.opensearch.core.SearchRequest; +import org.opensearch.client.opensearch.core.SearchResponse; +import org.opensearch.eval.Constants; import org.opensearch.eval.engine.SearchEngine; import java.util.ArrayList; From 6c4b2412de07e7586d1f03dc6b42cb57a456621c Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 31 Dec 2024 10:02:37 -0500 Subject: [PATCH 10/41] Working on converting to a standalone app. --- .../eval/engine/OpenSearchEngine.java | 47 ++++++++ .../opensearch/eval/engine/SearchEngine.java | 17 +++ .../eval/runners/AbstractQuerySetRunner.java | 100 +----------------- .../runners/OpenSearchQuerySetRunner.java | 7 +- 4 files changed, 69 insertions(+), 102 deletions(-) diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java index 77da718..b7c0ea3 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java @@ -14,11 +14,16 @@ import org.apache.logging.log4j.Logger; import org.opensearch.client.json.jackson.JacksonJsonpMapper; import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.opensearch._types.FieldValue; import org.opensearch.client.opensearch._types.Refresh; import org.opensearch.client.opensearch._types.Time; import org.opensearch.client.opensearch._types.mapping.IntegerNumberProperty; import org.opensearch.client.opensearch._types.mapping.Property; import org.opensearch.client.opensearch._types.mapping.TypeMapping; +import org.opensearch.client.opensearch._types.query_dsl.BoolQuery; +import org.opensearch.client.opensearch._types.query_dsl.MatchQuery; +import org.opensearch.client.opensearch._types.query_dsl.Query; +import org.opensearch.client.opensearch._types.query_dsl.TermQuery; import org.opensearch.client.opensearch.core.BulkRequest; import org.opensearch.client.opensearch.core.BulkResponse; import org.opensearch.client.opensearch.core.IndexRequest; @@ -43,6 +48,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -120,6 +126,47 @@ public String indexQuerySet(final QuerySet querySet) throws IOException { } + @Override + public QuerySet getQuerySet(String querySetId) throws IOException { + + final Query query = Query.of(q -> q.term(m -> m.field("_id").value(FieldValue.of(querySetId)))); + + final SearchResponse searchResponse = client.search(s -> s.index(Constants.QUERY_SETS_INDEX_NAME).query(query).size(1), QuerySet.class); + + // TODO: Handle the query set not being found. + + return searchResponse.hits().hits().get(0).source(); + + } + + @Override + public Double getJudgmentValue(final String judgmentsId, final String userQuery, final String documentId) throws Exception { + + var boolQuery = BoolQuery.of(bq -> bq + .must( + List.of( + MatchQuery.of(mq -> mq.query(FieldValue.of("judgments_id")).field(judgmentsId)).toQuery(), + MatchQuery.of(mq -> mq.query(FieldValue.of("query")).field(userQuery)).toQuery(), + MatchQuery.of(mq -> mq.query(FieldValue.of("document_id")).field(documentId)).toQuery() + ) + ) + ); + + final Query query = Query.of(q -> q.bool(boolQuery)); + + final SearchResponse searchResponse = client.search(s -> s.index(Constants.JUDGMENTS_INDEX_NAME) + .query(query) + .from(0) + .size(1), Judgment.class); + + if(searchResponse.hits().hits().isEmpty()) { + return Double.NaN; + } else { + return searchResponse.hits().hits().get(0).source().getJudgment(); + } + + } + @Override public Collection getUbiQueries() throws IOException { diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java index 611308b..b0f8191 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java @@ -30,4 +30,21 @@ public abstract class SearchEngine { public abstract String indexQuerySet(QuerySet querySet) throws IOException; public abstract Collection getUbiQueries() throws IOException; + /** + * Gets a query set from the index. + * @param querySetId The ID of the query set to get. + * @return The query set as a collection of maps of query to frequency + * @throws IOException Thrown if the query set cannot be retrieved. + */ + public abstract QuerySet getQuerySet(String querySetId) throws IOException; + + /** + * Get a judgment from the index. + * @param judgmentsId The ID of the judgments to find. + * @param query The user query. + * @param documentId The document ID. + * @return The value of the judgment, or NaN if the judgment cannot be found. + */ + public abstract Double getJudgmentValue(final String judgmentsId, final String query, final String documentId) throws Exception; + } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java index 3260ae0..6fad96f 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java @@ -10,15 +10,10 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.client.opensearch.core.SearchRequest; -import org.opensearch.client.opensearch.core.SearchResponse; -import org.opensearch.eval.Constants; import org.opensearch.eval.engine.SearchEngine; import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.Map; /** * Base class for query set runners. Classes that extend this class @@ -58,99 +53,6 @@ abstract QuerySetRunResult run(String querySetId, final String judgmentsId, fina */ abstract void save(QuerySetRunResult result) throws Exception; - /** - * Gets a query set from the index. - * @param querySetId The ID of the query set to get. - * @return The query set as a collection of maps of query to frequency - * @throws Exception Thrown if the query set cannot be retrieved. - */ - public final Collection> getQuerySet(final String querySetId) throws Exception { - - // Get the query set. - final SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); - sourceBuilder.query(QueryBuilders.matchQuery("_id", querySetId)); - - // Will be at most one match. - sourceBuilder.from(0); - sourceBuilder.size(1); - - final SearchRequest searchRequest = new SearchRequest(Constants.QUERY_SETS_INDEX_NAME).source(sourceBuilder); - - // TODO: Don't use .get() - final SearchResponse searchResponse = client.search(searchRequest).get(); - - if(searchResponse.getHits().getHits().length > 0) { - - // The queries from the query set that will be run. - return (Collection>) searchResponse.getHits().getAt(0).getSourceAsMap().get("queries"); - - } else { - - LOGGER.error("Unable to get query set with ID {}", querySetId); - - // The query set was not found. - throw new RuntimeException("The query set with ID " + querySetId + " was not found."); - - } - - } - - /** - * Get a judgment from the index. - * @param judgmentsId The ID of the judgments to find. - * @param query The user query. - * @param documentId The document ID. - * @return The value of the judgment, or NaN if the judgment cannot be found. - */ - public Double getJudgmentValue(final String judgmentsId, final String query, final String documentId) throws Exception { - - // Find a judgment that matches the judgments_id, query_id, and document_id fields in the index. - - final BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); - boolQueryBuilder.must(QueryBuilders.termQuery("judgments_id", judgmentsId)); - boolQueryBuilder.must(QueryBuilders.termQuery("query", query)); - boolQueryBuilder.must(QueryBuilders.termQuery("document_id", documentId)); - - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(boolQueryBuilder); - - // Will be a max of 1 result since we are getting the judgments by ID. - searchSourceBuilder.from(0); - searchSourceBuilder.size(1); - - // Only include the judgment field in the response. - final String[] includeFields = new String[] {"judgment"}; - final String[] excludeFields = new String[] {}; - searchSourceBuilder.fetchSource(includeFields, excludeFields); - - final SearchRequest searchRequest = new SearchRequest(Constants.JUDGMENTS_INDEX_NAME).source(searchSourceBuilder); - - Double judgment = Double.NaN; - - final SearchResponse searchResponse = client.search(searchRequest).get(); - - if (searchResponse.getHits().getHits().length > 0) { - - final Map j = searchResponse.getHits().getAt(0).getSourceAsMap(); - - // LOGGER.debug("Judgment contains a value: {}", j.get("judgment")); - - // TODO: Why does this not exist in some cases? - if(j.containsKey("judgment")) { - judgment = (Double) j.get("judgment"); - } - - } else { - - // No judgment for this query/doc pair exists. - judgment = Double.NaN; - - } - - return judgment; - - } - /** * Gets the judgments for a query / document pairs. * @param judgmentsId The judgments collection for which the judgment to retrieve belongs. @@ -174,7 +76,7 @@ protected RelevanceScores getRelevanceScores(final String judgmentsId, final Str final String documentId = orderedDocumentIds.get(i); // Find the judgment value for this combination of query and documentId from the index. - final Double judgmentValue = getJudgmentValue(judgmentsId, query, documentId); + final Double judgmentValue = searchEngine.getJudgmentValue(judgmentsId, query, documentId); // If a judgment for this query/doc pair is not found, Double.NaN will be returned. if(!Double.isNaN(judgmentValue)) { diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java index c4f2380..b761359 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java @@ -16,6 +16,7 @@ import org.opensearch.eval.metrics.NdcgSearchMetric; import org.opensearch.eval.metrics.PrecisionSearchMetric; import org.opensearch.eval.metrics.SearchMetric; +import org.opensearch.eval.model.data.QuerySet; import org.opensearch.eval.utils.TimeUtils; import java.util.ArrayList; @@ -48,15 +49,15 @@ public QuerySetRunResult run(final String querySetId, final String judgmentsId, final String searchPipeline, final String idField, final String query, final int k, final double threshold) throws Exception { - final Collection> querySet = getQuerySet(querySetId); - LOGGER.info("Found {} queries in query set {}", querySet.size(), querySetId); + final QuerySet querySet = searchEngine.getQuerySet(querySetId); + LOGGER.info("Found {} queries in query set {}", querySet.getQuerySetQueries().size(), querySetId); try { // The results of each query. final List queryResults = new ArrayList<>(); - for (Map queryMap : querySet) { + for (Map queryMap : querySet.getQuerySetQueries()) { // Loop over each query in the map and run each one. for (final String userQuery : queryMap.keySet()) { From 9ee0b9b9e0dd36da309a99910ed0ac4e65b5d831 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 31 Dec 2024 10:17:59 -0500 Subject: [PATCH 11/41] Working on converting to a standalone app. --- .../eval/engine/OpenSearchEngine.java | 36 ++++++- .../opensearch/eval/engine/SearchEngine.java | 3 + .../eval/model/data/QueryResultMetric.java | 97 +++++++++++++++++++ .../data/RankAggregatedClickThrough.java | 1 - .../runners/OpenSearchQuerySetRunner.java | 63 ++++-------- 5 files changed, 152 insertions(+), 48 deletions(-) create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QueryResultMetric.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java index b7c0ea3..334da48 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java @@ -23,7 +23,6 @@ import org.opensearch.client.opensearch._types.query_dsl.BoolQuery; import org.opensearch.client.opensearch._types.query_dsl.MatchQuery; import org.opensearch.client.opensearch._types.query_dsl.Query; -import org.opensearch.client.opensearch._types.query_dsl.TermQuery; import org.opensearch.client.opensearch.core.BulkRequest; import org.opensearch.client.opensearch.core.BulkResponse; import org.opensearch.client.opensearch.core.IndexRequest; @@ -41,14 +40,17 @@ import org.opensearch.eval.model.ClickthroughRate; import org.opensearch.eval.model.data.ClickThroughRate; import org.opensearch.eval.model.data.Judgment; +import org.opensearch.eval.model.data.QueryResultMetric; import org.opensearch.eval.model.data.QuerySet; import org.opensearch.eval.model.data.RankAggregatedClickThrough; import org.opensearch.eval.model.ubi.query.UbiQuery; import org.opensearch.eval.utils.TimeUtils; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -94,6 +96,22 @@ public boolean doesIndexExist(final String index) throws IOException { } + @Override + public boolean createIndex(final String index, final String mappingJson) throws IOException { + + final InputStream stream = new ByteArrayInputStream(mappingJson.getBytes(StandardCharsets.UTF_8)); + + final CreateIndexRequest createIndexRequest = new CreateIndexRequest.Builder() + .index(index) + .mappings(m -> m.withJson(stream)) + .build(); + + stream.close(); + + return Boolean.TRUE.equals(client.indices().create(createIndexRequest).acknowledged()); + + } + @Override public boolean createIndex(String index, Map mapping) throws IOException { @@ -470,6 +488,20 @@ public void indexClickthroughRates(final Map> clic } + @Override + public void indexQueryResultMetric(final QueryResultMetric queryResultMetric) throws Exception { + + // TODO: Use bulk imports. + + final IndexRequest indexRequest = new IndexRequest.Builder() + .index(Constants.DASHBOARD_METRICS_INDEX_NAME) + .id(queryResultMetric.getId()) + .document(queryResultMetric).build(); + + client.index(indexRequest); + + } + /** * Index the judgments. * @param judgments A collection of {@link Judgment judgments}. diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java index b0f8191..4c24464 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java @@ -2,6 +2,7 @@ import org.opensearch.eval.model.ClickthroughRate; import org.opensearch.eval.model.data.Judgment; +import org.opensearch.eval.model.data.QueryResultMetric; import org.opensearch.eval.model.data.QuerySet; import org.opensearch.eval.model.ubi.query.UbiQuery; @@ -14,6 +15,7 @@ public abstract class SearchEngine { public abstract boolean doesIndexExist(String index) throws IOException; public abstract boolean createIndex(String index, Map mapping) throws IOException; + public abstract boolean createIndex(String index, String mapping) throws IOException; public abstract boolean deleteIndex(String index) throws IOException; public abstract String getUserQuery(final String queryId) throws Exception; @@ -22,6 +24,7 @@ public abstract class SearchEngine { public abstract void indexRankAggregatedClickthrough(final Map rankAggregatedClickThrough) throws Exception; public abstract void indexClickthroughRates(final Map> clickthroughRates) throws Exception; public abstract String indexJudgments(final Collection judgments) throws Exception; + public abstract void indexQueryResultMetric(final QueryResultMetric queryResultMetric) throws Exception; public abstract boolean bulkIndex(String index, Map documents) throws IOException; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QueryResultMetric.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QueryResultMetric.java new file mode 100644 index 0000000..c7a14e7 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QueryResultMetric.java @@ -0,0 +1,97 @@ +package org.opensearch.eval.model.data; + +import java.util.UUID; + +public class QueryResultMetric extends AbstractData { + + private String datetime; + private String searchConfig; + private String querySetId; + private String query; + private String metric; + private double value; + private String application; + private String evaluationId; + private double frogsPercent; + + public QueryResultMetric(String id) { + super(id); + } + + public QueryResultMetric() { + super(UUID.randomUUID().toString()); + } + + public String getDatetime() { + return datetime; + } + + public void setDatetime(String datetime) { + this.datetime = datetime; + } + + public String getSearchConfig() { + return searchConfig; + } + + public void setSearchConfig(String searchConfig) { + this.searchConfig = searchConfig; + } + + public String getQuerySetId() { + return querySetId; + } + + public void setQuerySetId(String querySetId) { + this.querySetId = querySetId; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public String getMetric() { + return metric; + } + + public void setMetric(String metric) { + this.metric = metric; + } + + public double getValue() { + return value; + } + + public void setValue(double value) { + this.value = value; + } + + public String getApplication() { + return application; + } + + public void setApplication(String application) { + this.application = application; + } + + public String getEvaluationId() { + return evaluationId; + } + + public void setEvaluationId(String evaluationId) { + this.evaluationId = evaluationId; + } + + public double getFrogsPercent() { + return frogsPercent; + } + + public void setFrogsPercent(double frogsPercent) { + this.frogsPercent = frogsPercent; + } + +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java index 770b9cf..4844aef 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java @@ -23,7 +23,6 @@ public RankAggregatedClickThrough() { super(UUID.randomUUID().toString()); } - public int getPosition() { return position; } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java index b761359..a22074f 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java @@ -10,12 +10,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.eval.Constants; import org.opensearch.eval.engine.SearchEngine; import org.opensearch.eval.metrics.DcgSearchMetric; import org.opensearch.eval.metrics.NdcgSearchMetric; import org.opensearch.eval.metrics.PrecisionSearchMetric; import org.opensearch.eval.metrics.SearchMetric; +import org.opensearch.eval.model.data.QueryResultMetric; import org.opensearch.eval.model.data.QuerySet; import org.opensearch.eval.utils.TimeUtils; @@ -26,6 +26,8 @@ import java.util.Map; import java.util.UUID; +import static org.opensearch.eval.Constants.DASHBOARD_METRICS_INDEX_NAME; + /** * A {@link AbstractQuerySetRunner} for Amazon OpenSearch. */ @@ -180,7 +182,7 @@ public void save(final QuerySetRunResult result) throws Exception { // See https://github.com/o19s/opensearch-search-quality-evaluation/blob/main/opensearch-dashboard-prototyping/METRICS_SCHEMA.md // See https://github.com/o19s/opensearch-search-quality-evaluation/blob/main/opensearch-dashboard-prototyping/sample_data.ndjson - final boolean dashboardMetricsIndexExists = searchEngine.doesIndexExist(Constants.DASHBOARD_METRICS_INDEX_NAME); + final boolean dashboardMetricsIndexExists = searchEngine.doesIndexExist(DASHBOARD_METRICS_INDEX_NAME); if (!dashboardMetricsIndexExists) { @@ -200,64 +202,35 @@ public void save(final QuerySetRunResult result) throws Exception { " }\n" + " }"; - // Create the judgments index. - final CreateIndexRequest createIndexRequest = new CreateIndexRequest(SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME).mapping(mapping); - - client.admin().indices().create(createIndexRequest, new ActionListener<>() { - - @Override - public void onResponse(CreateIndexResponse createIndexResponse) { - LOGGER.info("{} index created.", SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME); - } - - @Override - public void onFailure(Exception ex) { - LOGGER.error("Unable to create the {} index.", SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME, ex); - } - - }); + // TODO: Make sure the index gets created successfully. + searchEngine.createIndex(DASHBOARD_METRICS_INDEX_NAME, mapping); } - final BulkRequest bulkRequest = new BulkRequest(); final String timestamp = TimeUtils.getTimestamp(); for(final QueryResult queryResult : result.getQueryResults()) { for(final SearchMetric searchMetric : queryResult.getSearchMetrics()) { - // TODO: Make sure all of these items have values. - final Map metrics = new HashMap<>(); - metrics.put("datetime", timestamp); - metrics.put("search_config", "research_1"); - metrics.put("query_set_id", result.getQuerySetId()); - metrics.put("query", queryResult.getQuery()); - metrics.put("metric", searchMetric.getName()); - metrics.put("value", searchMetric.getValue()); - metrics.put("application", "sample_data"); - metrics.put("evaluation_id", result.getRunId()); - metrics.put("frogs_percent", queryResult.getFrogs()); - - // TODO: This is using the index name from the sample data. - bulkRequest.add(new IndexRequest("sqe_metrics_sample_data").source(metrics)); + final QueryResultMetric queryResultMetric = new QueryResultMetric(); + queryResultMetric.setDatetime(timestamp); + queryResultMetric.setSearchConfig("research_1"); + queryResultMetric.setQuerySetId(result.getQuerySetId()); + queryResultMetric.setQuery(queryResult.getQuery()); + queryResultMetric.setMetric(searchMetric.getName()); + queryResultMetric.setValue(searchMetric.getValue()); + queryResultMetric.setApplication("sample_data"); + queryResultMetric.setEvaluationId(result.getRunId()); + queryResultMetric.setFrogsPercent(queryResult.getFrogs()); + + searchEngine.indexQueryResultMetric(queryResultMetric); } } - client.bulk(bulkRequest, new ActionListener<>() { - - @Override - public void onResponse(BulkResponse bulkItemResponses) { - LOGGER.info("Successfully indexed {} metrics.", bulkItemResponses.getItems().length); - } - - @Override - public void onFailure(Exception ex) { - LOGGER.error("Unable to bulk index metrics.", ex); - } - }); } From 83633bce5c70546f2575d4bd03971bca24619c48 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 31 Dec 2024 10:42:19 -0500 Subject: [PATCH 12/41] Working on converting to a standalone app. --- .../pom.xml | 5 + .../eval/engine/OpenSearchEngine.java | 117 ++++++++++++------ .../opensearch/eval/engine/SearchEngine.java | 3 + .../runners/OpenSearchQuerySetRunner.java | 76 ++---------- .../eval/samplers/AllQueriesQuerySampler.java | 20 +-- 5 files changed, 104 insertions(+), 117 deletions(-) diff --git a/opensearch-search-quality-evaluation-framework/pom.xml b/opensearch-search-quality-evaluation-framework/pom.xml index 568973a..b7b31c1 100644 --- a/opensearch-search-quality-evaluation-framework/pom.xml +++ b/opensearch-search-quality-evaluation-framework/pom.xml @@ -17,6 +17,11 @@ opensearch-java 2.19.0 + + com.fasterxml.jackson.core + jackson-databind + 2.18.2 + commons-cli commons-cli diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java index 334da48..69adda6 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java @@ -8,6 +8,7 @@ */ package org.opensearch.eval.engine; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.gson.Gson; import org.apache.hc.core5.http.HttpHost; import org.apache.logging.log4j.LogManager; @@ -23,15 +24,18 @@ import org.opensearch.client.opensearch._types.query_dsl.BoolQuery; import org.opensearch.client.opensearch._types.query_dsl.MatchQuery; import org.opensearch.client.opensearch._types.query_dsl.Query; +import org.opensearch.client.opensearch._types.query_dsl.WrapperQuery; import org.opensearch.client.opensearch.core.BulkRequest; import org.opensearch.client.opensearch.core.BulkResponse; import org.opensearch.client.opensearch.core.IndexRequest; import org.opensearch.client.opensearch.core.ScrollRequest; import org.opensearch.client.opensearch.core.ScrollResponse; +import org.opensearch.client.opensearch.core.SearchRequest; import org.opensearch.client.opensearch.core.SearchResponse; import org.opensearch.client.opensearch.core.bulk.BulkOperation; import org.opensearch.client.opensearch.core.bulk.IndexOperation; import org.opensearch.client.opensearch.core.search.Hit; +import org.opensearch.client.opensearch.core.search.TrackHits; import org.opensearch.client.opensearch.indices.CreateIndexRequest; import org.opensearch.client.opensearch.indices.ExistsRequest; import org.opensearch.client.transport.OpenSearchTransport; @@ -43,6 +47,7 @@ import org.opensearch.eval.model.data.QueryResultMetric; import org.opensearch.eval.model.data.QuerySet; import org.opensearch.eval.model.data.RankAggregatedClickThrough; +import org.opensearch.eval.model.ubi.event.UbiEvent; import org.opensearch.eval.model.ubi.query.UbiQuery; import org.opensearch.eval.utils.TimeUtils; @@ -51,6 +56,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Base64; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -60,6 +66,7 @@ import static org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel.INDEX_QUERY_DOC_CTR; import static org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel.INDEX_RANK_AGGREGATED_CTR; +import static org.opensearch.eval.runners.OpenSearchQuerySetRunner.QUERY_PLACEHOLDER; /** * Functionality for interacting with OpenSearch. @@ -303,25 +310,18 @@ public UbiQuery getQueryFromQueryId(final String queryId) throws Exception { LOGGER.debug("Getting query from query ID {}", queryId); - final String query = "{\"match\": {\"query_id\": \"" + queryId + "\" }}"; - final WrapperQueryBuilder qb = QueryBuilders.wrapperQuery(query); - - // The query_id should be unique anyway, but we are limiting it to a single result anyway. - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(qb); - searchSourceBuilder.from(0); - searchSourceBuilder.size(1); - - final String[] indexes = {Constants.UBI_QUERIES_INDEX_NAME}; + final SearchRequest searchRequest = new SearchRequest.Builder().query(q -> q.match(m -> m.field("query_id").query(FieldValue.of(queryId)))) + .index(Constants.UBI_QUERIES_INDEX_NAME) + .from(0) + .size(1) + .build(); - final SearchRequest searchRequest = new SearchRequest(indexes, searchSourceBuilder); - final SearchResponse response = client.search(searchRequest).get(); + final SearchResponse searchResponse = client.search(searchRequest, UbiQuery.class); // If this does not return a query then we cannot calculate the judgments. Each even should have a query associated with it. - if(response.getHits().getHits() != null & response.getHits().getHits().length > 0) { + if(searchResponse.hits().hits() != null & !searchResponse.hits().hits().isEmpty()) { - final SearchHit hit = response.getHits().getHits()[0]; - return gson.fromJson(hit.getSourceAsString(), UbiQuery.class); + return searchResponse.hits().hits().get(0).source(); } else { @@ -332,24 +332,65 @@ public UbiQuery getQueryFromQueryId(final String queryId) throws Exception { } - private Collection getQueryIdsHavingUserQuery(final String userQuery) throws Exception { + @Override + public List runQuery(final String index, final String query, final int k, final String userQuery, final String idField) throws IOException { + + // Replace the query placeholder with the user query. + final String parsedQuery = query.replace(QUERY_PLACEHOLDER, userQuery); + + final String encodedQuery = Base64.getEncoder().encodeToString(parsedQuery.getBytes(StandardCharsets.UTF_8)); + + final WrapperQuery wrapperQuery = new WrapperQuery.Builder() + .query(encodedQuery) + .build(); + + final SearchRequest searchRequest = new SearchRequest.Builder() + .index(index) + .query(q -> q.wrapper(wrapperQuery)) + .from(0) + .size(k) + .build(); + - final String query = "{\"match\": {\"user_query\": \"" + userQuery + "\" }}"; - final WrapperQueryBuilder qb = QueryBuilders.wrapperQuery(query); + // TODO: Handle the searchPipeline if it is not null. + // TODO: Only return the idField since that's all we need. - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(qb); + final SearchResponse searchResponse = client.search(searchRequest, ObjectNode.class); - final String[] indexes = {Constants.UBI_QUERIES_INDEX_NAME}; + final List orderedDocumentIds = new ArrayList<>(); - final SearchRequest searchRequest = new SearchRequest(indexes, searchSourceBuilder); - final SearchResponse response = client.search(searchRequest).get(); + for (int i = 0; i < searchResponse.hits().hits().size(); i++) { + + final String documentId; + + if ("_id".equals(idField)) { + documentId = searchResponse.hits().hits().get(i).id(); + } else { + // TODO: Need to check this field actually exists. + // TODO: Does this work? + documentId = searchResponse.hits().hits().get(i).fields().get(idField).toString(); + } + + orderedDocumentIds.add(documentId); + + } + + return orderedDocumentIds; + + } + + private Collection getQueryIdsHavingUserQuery(final String userQuery) throws Exception { + + final SearchRequest searchRequest = new SearchRequest.Builder().query(q -> q.match(m -> m.field("user_query").query(FieldValue.of(userQuery)))) + .index(Constants.UBI_QUERIES_INDEX_NAME) + .build(); + + final SearchResponse searchResponse = client.search(searchRequest, UbiQuery.class); final Collection queryIds = new ArrayList<>(); - for(final SearchHit hit : response.getHits().getHits()) { - final String queryId = hit.getSourceAsMap().get("query_id").toString(); - queryIds.add(queryId); + for (int i = 0; i < searchResponse.hits().hits().size(); i++) { + queryIds.add(searchResponse.hits().hits().get(i).source().getQueryId()); } return queryIds; @@ -394,22 +435,22 @@ public long getCountOfQueriesForUserQueryHavingResultInRankR(final String userQu " }\n" + " }"; - final WrapperQueryBuilder qb = QueryBuilders.wrapperQuery(query); + final String encodedQuery = Base64.getEncoder().encodeToString(query.getBytes(StandardCharsets.UTF_8)); - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(qb); - searchSourceBuilder.trackTotalHits(true); - searchSourceBuilder.size(0); + final WrapperQuery wrapperQuery = new WrapperQuery.Builder() + .query(encodedQuery) + .build(); - final String[] indexes = {Constants.UBI_EVENTS_INDEX_NAME}; + final SearchRequest searchRequest = new SearchRequest.Builder() + .index(Constants.UBI_EVENTS_INDEX_NAME) + .query(q -> q.wrapper(wrapperQuery)) + .size(0) + .trackTotalHits(TrackHits.of(t -> t.enabled(true))) + .build(); - final SearchRequest searchRequest = new SearchRequest(indexes, searchSourceBuilder); - final SearchResponse response = client.search(searchRequest).get(); + final SearchResponse searchResponse = client.search(searchRequest, UbiEvent.class); - // Won't be null as long as trackTotalHits is true. - if(response.getHits().getTotalHits() != null) { - countOfTimesShownAtRank += response.getHits().getTotalHits().value; - } + countOfTimesShownAtRank += searchResponse.hits().total().value(); } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java index 4c24464..f644b49 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Set; @@ -30,6 +31,8 @@ public abstract class SearchEngine { public abstract Collection getJudgments(final String index) throws IOException; + public abstract List runQuery(final String index, final String query, final int k, final String userQuery, final String idField) throws IOException; + public abstract String indexQuerySet(QuerySet querySet) throws IOException; public abstract Collection getUbiQueries() throws IOException; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java index a22074f..ea99a2b 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java @@ -64,80 +64,28 @@ public QuerySetRunResult run(final String querySetId, final String judgmentsId, // Loop over each query in the map and run each one. for (final String userQuery : queryMap.keySet()) { - // Replace the query placeholder with the user query. - final String parsedQuery = query.replace(QUERY_PLACEHOLDER, userQuery); - - // Build the query from the one that was passed in. - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - - searchSourceBuilder.query(QueryBuilders.wrapperQuery(parsedQuery)); - searchSourceBuilder.from(0); - searchSourceBuilder.size(k); - - final String[] includeFields = new String[]{idField}; - final String[] excludeFields = new String[]{}; - searchSourceBuilder.fetchSource(includeFields, excludeFields); - - // LOGGER.info(searchSourceBuilder.toString()); - - final SearchRequest searchRequest = new SearchRequest(index); - searchRequest.source(searchSourceBuilder); - - if (searchPipeline != null) { - searchSourceBuilder.pipeline(searchPipeline); - searchRequest.pipeline(searchPipeline); - } - // This is to keep OpenSearch from rejecting queries. // TODO: Look at using the Workload Management in 2.18.0. Thread.sleep(50); - client.search(searchRequest, new ActionListener<>() { - - @Override - public void onResponse(final SearchResponse searchResponse) { + final List orderedDocumentIds = searchEngine.runQuery(index, query, k, userQuery, idField); - final List orderedDocumentIds = new ArrayList<>(); + try { - for (final SearchHit hit : searchResponse.getHits().getHits()) { + final RelevanceScores relevanceScores = getRelevanceScores(judgmentsId, userQuery, orderedDocumentIds, k); - final String documentId; + // Calculate the metrics for this query. + final SearchMetric dcgSearchMetric = new DcgSearchMetric(k, relevanceScores.getRelevanceScores()); + final SearchMetric ndcgSearchmetric = new NdcgSearchMetric(k, relevanceScores.getRelevanceScores()); + final SearchMetric precisionSearchMetric = new PrecisionSearchMetric(k, threshold, relevanceScores.getRelevanceScores()); - if ("_id".equals(idField)) { - documentId = hit.getId(); - } else { - // TODO: Need to check this field actually exists. - documentId = hit.getSourceAsMap().get(idField).toString(); - } + final Collection searchMetrics = List.of(dcgSearchMetric, ndcgSearchmetric, precisionSearchMetric); - orderedDocumentIds.add(documentId); + queryResults.add(new QueryResult(userQuery, orderedDocumentIds, k, searchMetrics, relevanceScores.getFrogs())); - } - - try { - - final RelevanceScores relevanceScores = getRelevanceScores(judgmentsId, userQuery, orderedDocumentIds, k); - - // Calculate the metrics for this query. - final SearchMetric dcgSearchMetric = new DcgSearchMetric(k, relevanceScores.getRelevanceScores()); - final SearchMetric ndcgSearchmetric = new NdcgSearchMetric(k, relevanceScores.getRelevanceScores()); - final SearchMetric precisionSearchMetric = new PrecisionSearchMetric(k, threshold, relevanceScores.getRelevanceScores()); - - final Collection searchMetrics = List.of(dcgSearchMetric, ndcgSearchmetric, precisionSearchMetric); - - queryResults.add(new QueryResult(userQuery, orderedDocumentIds, k, searchMetrics, relevanceScores.getFrogs())); - - } catch (Exception ex) { - LOGGER.error("Unable to get relevance scores for judgments {} and user query {}.", judgmentsId, userQuery, ex); - } - - } - - @Override - public void onFailure(Exception ex) { - LOGGER.error("Unable to search using query: {}", searchSourceBuilder.toString(), ex); - } - }); + } catch (Exception ex) { + LOGGER.error("Unable to get relevance scores for judgments {} and user query {}.", judgmentsId, userQuery, ex); + } } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java index be78e64..bc241fa 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java @@ -8,9 +8,10 @@ */ package org.opensearch.eval.samplers; -import org.opensearch.eval.Constants; import org.opensearch.eval.engine.SearchEngine; +import org.opensearch.eval.model.ubi.query.UbiQuery; +import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -41,24 +42,13 @@ public String getName() { @Override public String sample() throws Exception { - // Get queries from the UBI queries index. - // TODO: This needs to use scroll or something else. - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(QueryBuilders.matchAllQuery()); - searchSourceBuilder.from(0); - searchSourceBuilder.size(parameters.getQuerySetSize()); - - final SearchRequest searchRequest = new SearchRequest(Constants.UBI_QUERIES_INDEX_NAME).source(searchSourceBuilder); - - // TODO: Don't use .get() - final SearchResponse searchResponse = client.search(searchRequest).get(); + final Collection ubiQueries = searchEngine.getUbiQueries(); final Map queries = new HashMap<>(); - for(final SearchHit hit : searchResponse.getHits().getHits()) { + for(final UbiQuery ubiQuery : ubiQueries) { - final Map fields = hit.getSourceAsMap(); - queries.merge(fields.get("user_query").toString(), 1L, Long::sum); + queries.merge(ubiQuery.getUserQuery(), 1L, Long::sum); // Will be useful for paging once implemented. if(queries.size() > parameters.getQuerySetSize()) { From 7a2d577813003b376186757b482c462ed493bdd5 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 31 Dec 2024 12:17:45 -0500 Subject: [PATCH 13/41] Working on converting to a standalone app. --- .../eval/engine/OpenSearchEngine.java | 117 +++++++++++++++++- .../opensearch/eval/engine/SearchEngine.java | 7 ++ .../clickmodel/coec/CoecClickModel.java | 116 +---------------- 3 files changed, 124 insertions(+), 116 deletions(-) diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java index 69adda6..e258b43 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java @@ -59,11 +59,14 @@ import java.util.Base64; import java.util.Collection; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import static org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel.EVENT_CLICK; +import static org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel.EVENT_IMPRESSION; import static org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel.INDEX_QUERY_DOC_CTR; import static org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel.INDEX_RANK_AGGREGATED_CTR; import static org.opensearch.eval.runners.OpenSearchQuerySetRunner.QUERY_PLACEHOLDER; @@ -351,7 +354,6 @@ public List runQuery(final String index, final String query, final int k .size(k) .build(); - // TODO: Handle the searchPipeline if it is not null. // TODO: Only return the idField since that's all we need. @@ -379,6 +381,119 @@ public List runQuery(final String index, final String query, final int k } + @Override + public Map> getClickthroughRate(final int maxRank) throws Exception { + + final Map> queriesToClickthroughRates = new HashMap<>(); + + // For each query: + // - Get each document returned in that query (in the QueryResponse object). + // - Calculate the click-through rate for the document. (clicks/impressions) + + // TODO: Allow for a time period and for a specific application. + + final String query = "{\n" + + " \"bool\": {\n" + + " \"should\": [\n" + + " {\n" + + " \"term\": {\n" + + " \"action_name\": \"click\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"term\": {\n" + + " \"action_name\": \"impression\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"must\": [\n" + + " {\n" + + " \"range\": {\n" + + " \"event_attributes.position.ordinal\": {\n" + + " \"lte\": " + maxRank + "\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " }"; + + final String encodedQuery = Base64.getEncoder().encodeToString(query.getBytes(StandardCharsets.UTF_8)); + + final WrapperQuery wrapperQuery = new WrapperQuery.Builder() + .query(encodedQuery) + .build(); + + final SearchRequest searchRequest = new SearchRequest.Builder() + .index(Constants.UBI_EVENTS_INDEX_NAME) + .query(q -> q.wrapper(wrapperQuery)) + .from(0) + .size(1000) + .scroll(Time.of(t -> t.offset(1000))) + .build(); + + final SearchResponse searchResponse = client.search(searchRequest, UbiEvent.class); + + String scrollId = searchResponse.scrollId(); + List> searchHits = searchResponse.hits().hits(); + + while (searchHits != null && !searchHits.isEmpty()) { + + for (int i = 0; i < searchResponse.hits().hits().size(); i++) { + + final UbiEvent ubiEvent = searchResponse.hits().hits().get(i).source(); + + // We need to the hash of the query_id because two users can both search + // for "computer" and those searches will have different query IDs, but they are the same search. + final String userQuery = getUserQuery(ubiEvent.getQueryId()); + + // userQuery will be null if there is not a query for this event in ubi_queries. + if (userQuery != null) { + + // Get the clicks for this queryId from the map, or an empty list if this is a new query. + final Set clickthroughRates = queriesToClickthroughRates.getOrDefault(userQuery, new LinkedHashSet<>()); + + // Get the ClickthroughRate object for the object that was interacted with. + final ClickthroughRate clickthroughRate = clickthroughRates.stream().filter(p -> p.getObjectId().equals(ubiEvent.getEventAttributes().getObject().getObjectId())).findFirst().orElse(new ClickthroughRate(ubiEvent.getEventAttributes().getObject().getObjectId())); + + if (EVENT_CLICK.equalsIgnoreCase(ubiEvent.getActionName())) { + //LOGGER.info("Logging a CLICK on " + ubiEvent.getEventAttributes().getObject().getObjectId()); + clickthroughRate.logClick(); + } else if (EVENT_IMPRESSION.equalsIgnoreCase(ubiEvent.getActionName())) { + //LOGGER.info("Logging an IMPRESSION on " + ubiEvent.getEventAttributes().getObject().getObjectId()); + clickthroughRate.logImpression(); + } else { + LOGGER.warn("Invalid event action name: {}", ubiEvent.getActionName()); + } + + clickthroughRates.add(clickthroughRate); + queriesToClickthroughRates.put(userQuery, clickthroughRates); + // LOGGER.debug("clickthroughRate = {}", queriesToClickthroughRates.size()); + + } + + } + + //LOGGER.info("Doing scroll to next results"); + // TODO: Getting a warning in the log that "QueryGroup _id can't be null, It should be set before accessing it. This is abnormal behaviour" + // I don't remember seeing this prior to 2.18.0 but it's possible I just didn't see it. + // https://github.com/opensearch-project/OpenSearch/blob/f105e4eb2ede1556b5dd3c743bea1ab9686ebccf/server/src/main/java/org/opensearch/wlm/QueryGroupTask.java#L73 + + final ScrollRequest scrollRequest = new ScrollRequest.Builder().scrollId(scrollId).build(); + final ScrollResponse scrollResponse = client.scroll(scrollRequest, UbiEvent.class); + + scrollId = scrollResponse.scrollId(); + searchHits = scrollResponse.hits().hits(); + + } + + indexClickthroughRates(queriesToClickthroughRates); + + return queriesToClickthroughRates; + + } + + private Collection getQueryIdsHavingUserQuery(final String userQuery) throws Exception { final SearchRequest searchRequest = new SearchRequest.Builder().query(q -> q.match(m -> m.field("user_query").query(FieldValue.of(userQuery)))) diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java index f644b49..d246419 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java @@ -53,4 +53,11 @@ public abstract class SearchEngine { */ public abstract Double getJudgmentValue(final String judgmentsId, final String query, final String documentId) throws Exception; + /** + * Gets the clickthrough rates for each query and its results. + * @return A map of user_query to the clickthrough rate for each query result. + * @throws IOException Thrown when a problem accessing OpenSearch. + */ + public abstract Map> getClickthroughRate(final int maxRank) throws Exception; + } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java index d879947..4333538 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java @@ -68,7 +68,7 @@ public String calculateJudgments() throws Exception { // Calculate and index the click-through rate for query/doc pairs. LOGGER.info("Beginning calculation of clickthrough rates."); - final Map> clickthroughRates = getClickthroughRate(); + final Map> clickthroughRates = searchEngine.getClickthroughRate(maxRank); LOGGER.info("Clickthrough rates for number of queries: {}", clickthroughRates.size()); showClickthroughRates(clickthroughRates); @@ -151,120 +151,6 @@ public String calculateCoec(final Map rankAggregatedClickThroug } - /** - * Gets the clickthrough rates for each query and its results. - * @return A map of user_query to the clickthrough rate for each query result. - * @throws IOException Thrown when a problem accessing OpenSearch. - */ - private Map> getClickthroughRate() throws Exception { - - // For each query: - // - Get each document returned in that query (in the QueryResponse object). - // - Calculate the click-through rate for the document. (clicks/impressions) - - // TODO: Allow for a time period and for a specific application. - - final String query = "{\n" + - " \"bool\": {\n" + - " \"should\": [\n" + - " {\n" + - " \"term\": {\n" + - " \"action_name\": \"click\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"term\": {\n" + - " \"action_name\": \"impression\"\n" + - " }\n" + - " }\n" + - " ],\n" + - " \"must\": [\n" + - " {\n" + - " \"range\": {\n" + - " \"event_attributes.position.ordinal\": {\n" + - " \"lte\": " + parameters.getMaxRank() + "\n" + - " }\n" + - " }\n" + - " }\n" + - " ]\n" + - " }\n" + - " }"; - - final BoolQueryBuilder queryBuilder = new BoolQueryBuilder().must(new WrapperQueryBuilder(query)); - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(queryBuilder).size(1000); - final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(10L)); - - final SearchRequest searchRequest = Requests - .searchRequest(Constants.UBI_EVENTS_INDEX_NAME) - .source(searchSourceBuilder) - .scroll(scroll); - - // TODO Don't use .get() - SearchResponse searchResponse = client.search(searchRequest).get(); - - String scrollId = searchResponse.getScrollId(); - SearchHit[] searchHits = searchResponse.getHits().getHits(); - - final Map> queriesToClickthroughRates = new HashMap<>(); - - while (searchHits != null && searchHits.length > 0) { - - for (final SearchHit hit : searchHits) { - - final UbiEvent ubiEvent = gson.fromJson(hit.getSourceAsString(), UbiEvent.class); - - // We need to the hash of the query_id because two users can both search - // for "computer" and those searches will have different query IDs, but they are the same search. - final String userQuery = searchEngine.getUserQuery(ubiEvent.getQueryId()); - - // userQuery will be null if there is not a query for this event in ubi_queries. - if(userQuery != null) { - - // Get the clicks for this queryId from the map, or an empty list if this is a new query. - final Set clickthroughRates = queriesToClickthroughRates.getOrDefault(userQuery, new LinkedHashSet<>()); - - // Get the ClickthroughRate object for the object that was interacted with. - final ClickthroughRate clickthroughRate = clickthroughRates.stream().filter(p -> p.getObjectId().equals(ubiEvent.getEventAttributes().getObject().getObjectId())).findFirst().orElse(new ClickthroughRate(ubiEvent.getEventAttributes().getObject().getObjectId())); - - if (EVENT_CLICK.equalsIgnoreCase(ubiEvent.getActionName())) { - //LOGGER.info("Logging a CLICK on " + ubiEvent.getEventAttributes().getObject().getObjectId()); - clickthroughRate.logClick(); - } else if (EVENT_IMPRESSION.equalsIgnoreCase(ubiEvent.getActionName())) { - //LOGGER.info("Logging an IMPRESSION on " + ubiEvent.getEventAttributes().getObject().getObjectId()); - clickthroughRate.logImpression(); - } else { - LOGGER.warn("Invalid event action name: {}", ubiEvent.getActionName()); - } - - clickthroughRates.add(clickthroughRate); - queriesToClickthroughRates.put(userQuery, clickthroughRates); - // LOGGER.debug("clickthroughRate = {}", queriesToClickthroughRates.size()); - - } - - } - - final SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); - scrollRequest.scroll(scroll); - - //LOGGER.info("Doing scroll to next results"); - // TODO: Getting a warning in the log that "QueryGroup _id can't be null, It should be set before accessing it. This is abnormal behaviour" - // I don't remember seeing this prior to 2.18.0 but it's possible I just didn't see it. - // https://github.com/opensearch-project/OpenSearch/blob/f105e4eb2ede1556b5dd3c743bea1ab9686ebccf/server/src/main/java/org/opensearch/wlm/QueryGroupTask.java#L73 - searchResponse = client.searchScroll(scrollRequest).get(); - //LOGGER.info("Scroll complete."); - - scrollId = searchResponse.getScrollId(); - - searchHits = searchResponse.getHits().getHits(); - - } - - searchEngine.indexClickthroughRates(queriesToClickthroughRates); - - return queriesToClickthroughRates; - - } /** * Calculate the rank-aggregated click through from the UBI events. From 2899bcfe088dd65586fd9d1bc8afe0d6d82e3f10 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Wed, 1 Jan 2025 09:07:57 -0500 Subject: [PATCH 14/41] Working on converting to a standalone app. --- .../pom.xml | 8 +- .../eval/engine/OpenSearchEngine.java | 162 ++++++++++++++++++ .../opensearch/eval/engine/SearchEngine.java | 7 + .../clickmodel/coec/CoecClickModel.java | 111 +----------- .../opensearch/eval/model/data/Judgment.java | 16 ++ .../eval/metrics/DcgSearchMetricTest.java | 8 +- .../eval/metrics/NdcgSearchMetricTest.java | 8 +- .../metrics/PrecisionSearchMetricTest.java | 7 +- 8 files changed, 211 insertions(+), 116 deletions(-) diff --git a/opensearch-search-quality-evaluation-framework/pom.xml b/opensearch-search-quality-evaluation-framework/pom.xml index b7b31c1..4acbca2 100644 --- a/opensearch-search-quality-evaluation-framework/pom.xml +++ b/opensearch-search-quality-evaluation-framework/pom.xml @@ -9,7 +9,7 @@ https://www.ubisearch.dev UTF-8 - 17 + 21 @@ -52,5 +52,11 @@ gson 2.11.0 + + org.junit.jupiter + junit-jupiter-api + 5.11.4 + test + \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java index e258b43..2aef41a 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java @@ -13,17 +13,24 @@ import org.apache.hc.core5.http.HttpHost; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.client.json.JsonData; import org.opensearch.client.json.jackson.JacksonJsonpMapper; import org.opensearch.client.opensearch.OpenSearchClient; import org.opensearch.client.opensearch._types.FieldValue; import org.opensearch.client.opensearch._types.Refresh; +import org.opensearch.client.opensearch._types.SortOrder; import org.opensearch.client.opensearch._types.Time; +import org.opensearch.client.opensearch._types.aggregations.Aggregate; +import org.opensearch.client.opensearch._types.aggregations.Aggregation; +import org.opensearch.client.opensearch._types.aggregations.StringTermsAggregate; +import org.opensearch.client.opensearch._types.aggregations.StringTermsBucket; import org.opensearch.client.opensearch._types.mapping.IntegerNumberProperty; import org.opensearch.client.opensearch._types.mapping.Property; import org.opensearch.client.opensearch._types.mapping.TypeMapping; import org.opensearch.client.opensearch._types.query_dsl.BoolQuery; import org.opensearch.client.opensearch._types.query_dsl.MatchQuery; import org.opensearch.client.opensearch._types.query_dsl.Query; +import org.opensearch.client.opensearch._types.query_dsl.RangeQuery; import org.opensearch.client.opensearch._types.query_dsl.WrapperQuery; import org.opensearch.client.opensearch.core.BulkRequest; import org.opensearch.client.opensearch.core.BulkResponse; @@ -493,6 +500,161 @@ public Map> getClickthroughRate(final int maxRank) } + @Override + public Map getRankAggregatedClickThrough(final int maxRank) throws Exception { + + final Map rankAggregatedClickThrough = new HashMap<>(); + + final RangeQuery rangeQuery = RangeQuery.of(r -> r + .field("event_attributes.position.ordinal") + .lte(JsonData.of(maxRank)) + ); + + // TODO: Is this the same as: final BucketOrder bucketOrder = BucketOrder.key(true); + final List> sort = new ArrayList<>(); + sort.add(Map.of("_key", SortOrder.Asc)); + + final Aggregation positionsAggregator = Aggregation.of(a -> a + .terms(t -> t + .field("event_attributes.position.ordinal") + .name("By_Position") + .size(maxRank) + .order(sort) + ) + ); + + final Aggregation actionNameAggregation = Aggregation.of(a -> a + .terms(t -> t + .field("action_name") + .name("By_Action") + .size(maxRank) + .order(sort) + ) + ); + + final Map aggregations = new HashMap<>(); + aggregations.put("By_Position", positionsAggregator); + aggregations.put("By_Action", actionNameAggregation); + + // TODO: Allow for a time period and for a specific application. + final SearchRequest searchRequest = new SearchRequest.Builder() + .index(Constants.UBI_EVENTS_INDEX_NAME) + .aggregations(aggregations) + .query(q -> q.range(rangeQuery)) + .from(0) + .size(0) + .build(); + + final SearchResponse searchResponse = client.search(searchRequest, Void.class); + + final Map aggs = searchResponse.aggregations(); + final StringTermsAggregate byAction = aggs.get("By_Action").sterms(); + final List byActionBuckets = byAction.buckets().array(); + + final Map clickCounts = new HashMap<>(); + final Map impressionCounts = new HashMap<>(); + + for (final StringTermsBucket bucket : byActionBuckets) { + System.out.println("Key: " + bucket.key() + ", Doc Count: " + bucket.docCount()); + +// // Handle the "impression" bucket. +// if(EVENT_IMPRESSION.equalsIgnoreCase(bucket.key())) { +// +// final Aggregate positionTerms = bucket.aggregations().get("By_Position"); +// +// final Collection positionBuckets = positionTerms.getBuckets(); +// +// for(final Terms.Bucket positionBucket : positionBuckets) { +// LOGGER.debug("Inserting impression event from position {} with click count {}", positionBucket.getKey(), (double) positionBucket.getDocCount()); +// impressionCounts.put(Integer.valueOf(positionBucket.getKey().toString()), (double) positionBucket.getDocCount()); +// } +// +// } +// +// // Handle the "click" bucket. +// if(EVENT_CLICK.equalsIgnoreCase(bucket.key())) { +// +// final Aggregate positionTerms = actionBucket.getAggregations().get("By_Position"); +// final Collection positionBuckets = positionTerms.getBuckets(); +// +// for(final Terms.Bucket positionBucket : positionBuckets) { +// LOGGER.debug("Inserting client event from position {} with click count {}", positionBucket.getKey(), (double) positionBucket.getDocCount()); +// clickCounts.put(Integer.valueOf(positionBucket.getKey().toString()), (double) positionBucket.getDocCount()); +// } +// +// } + + } + + +// final Terms actionTerms = searchResponse.getAggregations().get("By_Action"); +// final Collection actionBuckets = actionTerms.getBuckets(); +// +// LOGGER.debug("Aggregation query: {}", searchSourceBuilder.toString()); +// +// for(final Terms.Bucket actionBucket : actionBuckets) { +// +// // Handle the "impression" bucket. +// if(EVENT_IMPRESSION.equalsIgnoreCase(actionBucket.getKey().toString())) { +// +// final Terms positionTerms = actionBucket.getAggregations().get("By_Position"); +// final Collection positionBuckets = positionTerms.getBuckets(); +// +// for(final Terms.Bucket positionBucket : positionBuckets) { +// LOGGER.debug("Inserting impression event from position {} with click count {}", positionBucket.getKey(), (double) positionBucket.getDocCount()); +// impressionCounts.put(Integer.valueOf(positionBucket.getKey().toString()), (double) positionBucket.getDocCount()); +// } +// +// } +// +// // Handle the "click" bucket. +// if(EVENT_CLICK.equalsIgnoreCase(actionBucket.getKey().toString())) { +// +// final Terms positionTerms = actionBucket.getAggregations().get("By_Position"); +// final Collection positionBuckets = positionTerms.getBuckets(); +// +// for(final Terms.Bucket positionBucket : positionBuckets) { +// LOGGER.debug("Inserting client event from position {} with click count {}", positionBucket.getKey(), (double) positionBucket.getDocCount()); +// clickCounts.put(Integer.valueOf(positionBucket.getKey().toString()), (double) positionBucket.getDocCount()); +// } +// +// } +// +// } + + for(int rank = 0; rank < maxRank; rank++) { + + if(impressionCounts.containsKey(rank)) { + + if(clickCounts.containsKey(rank)) { + + // Calculate the CTR by dividing the number of clicks by the number of impressions. + LOGGER.info("Position = {}, Impression Counts = {}, Click Count = {}", rank, impressionCounts.get(rank), clickCounts.get(rank)); + rankAggregatedClickThrough.put(rank, clickCounts.get(rank) / impressionCounts.get(rank)); + + } else { + + // This document has impressions but no clicks, so it's CTR is zero. + LOGGER.info("Position = {}, Impression Counts = {}, Impressions but no clicks so CTR is 0", rank, clickCounts.get(rank)); + rankAggregatedClickThrough.put(rank, 0.0); + + } + + } else { + + // No impressions so the clickthrough rate is 0. + LOGGER.info("No impressions for rank {}, so using CTR of 0", rank); + rankAggregatedClickThrough.put(rank, (double) 0); + + } + + } + + indexRankAggregatedClickthrough(rankAggregatedClickThrough); + + return rankAggregatedClickThrough; + + } private Collection getQueryIdsHavingUserQuery(final String userQuery) throws Exception { diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java index d246419..9166961 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java @@ -60,4 +60,11 @@ public abstract class SearchEngine { */ public abstract Map> getClickthroughRate(final int maxRank) throws Exception; + /** + * Calculate the rank-aggregated click through from the UBI events. + * @return A map of positions to clickthrough rates. + * @throws IOException Thrown when a problem accessing OpenSearch. + */ + public abstract Map getRankAggregatedClickThrough(int maxRank) throws Exception; + } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java index 4333538..66fdd9f 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java @@ -11,19 +11,14 @@ import com.google.gson.Gson; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.eval.Constants; import org.opensearch.eval.engine.SearchEngine; import org.opensearch.eval.judgments.clickmodel.ClickModel; +import org.opensearch.eval.judgments.queryhash.IncrementalUserQueryHash; import org.opensearch.eval.model.ClickthroughRate; import org.opensearch.eval.model.data.Judgment; -import org.opensearch.eval.model.ubi.event.UbiEvent; -import org.opensearch.eval.judgments.queryhash.IncrementalUserQueryHash; import org.opensearch.eval.utils.MathUtils; -import java.io.IOException; import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.Map; import java.util.Set; @@ -62,7 +57,7 @@ public String calculateJudgments() throws Exception { // Calculate and index the rank-aggregated click-through. LOGGER.info("Beginning calculation of rank-aggregated click-through."); - final Map rankAggregatedClickThrough = getRankAggregatedClickThrough(); + final Map rankAggregatedClickThrough = searchEngine.getRankAggregatedClickThrough(maxRank); LOGGER.info("Rank-aggregated clickthrough positions: {}", rankAggregatedClickThrough.size()); showRankAggregatedClickThrough(rankAggregatedClickThrough); @@ -151,108 +146,6 @@ public String calculateCoec(final Map rankAggregatedClickThroug } - - /** - * Calculate the rank-aggregated click through from the UBI events. - * @return A map of positions to clickthrough rates. - * @throws IOException Thrown when a problem accessing OpenSearch. - */ - public Map getRankAggregatedClickThrough() throws Exception { - - final Map rankAggregatedClickThrough = new HashMap<>(); - - // TODO: Allow for a time period and for a specific application. - - final QueryBuilder findRangeNumber = QueryBuilders.rangeQuery("event_attributes.position.ordinal").lte(parameters.getMaxRank()); - final QueryBuilder queryBuilder = new BoolQueryBuilder().must(findRangeNumber); - - // Order the aggregations by key and not by value. - final BucketOrder bucketOrder = BucketOrder.key(true); - - final TermsAggregationBuilder positionsAggregator = AggregationBuilders.terms("By_Position").field("event_attributes.position.ordinal").order(bucketOrder).size(parameters.getMaxRank()); - final TermsAggregationBuilder actionNameAggregation = AggregationBuilders.terms("By_Action").field("action_name").subAggregation(positionsAggregator).order(bucketOrder).size(parameters.getMaxRank()); - - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder() - .query(queryBuilder) - .aggregation(actionNameAggregation) - .from(0) - .size(0); - - final SearchRequest searchRequest = new SearchRequest(Constants.UBI_EVENTS_INDEX_NAME).source(searchSourceBuilder); - final SearchResponse searchResponse = client.search(searchRequest).get(); - - final Map clickCounts = new HashMap<>(); - final Map impressionCounts = new HashMap<>(); - - final Terms actionTerms = searchResponse.getAggregations().get("By_Action"); - final Collection actionBuckets = actionTerms.getBuckets(); - - LOGGER.debug("Aggregation query: {}", searchSourceBuilder.toString()); - - for(final Terms.Bucket actionBucket : actionBuckets) { - - // Handle the "impression" bucket. - if(EVENT_IMPRESSION.equalsIgnoreCase(actionBucket.getKey().toString())) { - - final Terms positionTerms = actionBucket.getAggregations().get("By_Position"); - final Collection positionBuckets = positionTerms.getBuckets(); - - for(final Terms.Bucket positionBucket : positionBuckets) { - LOGGER.debug("Inserting impression event from position {} with click count {}", positionBucket.getKey(), (double) positionBucket.getDocCount()); - impressionCounts.put(Integer.valueOf(positionBucket.getKey().toString()), (double) positionBucket.getDocCount()); - } - - } - - // Handle the "click" bucket. - if(EVENT_CLICK.equalsIgnoreCase(actionBucket.getKey().toString())) { - - final Terms positionTerms = actionBucket.getAggregations().get("By_Position"); - final Collection positionBuckets = positionTerms.getBuckets(); - - for(final Terms.Bucket positionBucket : positionBuckets) { - LOGGER.debug("Inserting client event from position {} with click count {}", positionBucket.getKey(), (double) positionBucket.getDocCount()); - clickCounts.put(Integer.valueOf(positionBucket.getKey().toString()), (double) positionBucket.getDocCount()); - } - - } - - } - - for(int rank = 0; rank < parameters.getMaxRank(); rank++) { - - if(impressionCounts.containsKey(rank)) { - - if(clickCounts.containsKey(rank)) { - - // Calculate the CTR by dividing the number of clicks by the number of impressions. - LOGGER.info("Position = {}, Impression Counts = {}, Click Count = {}", rank, impressionCounts.get(rank), clickCounts.get(rank)); - rankAggregatedClickThrough.put(rank, clickCounts.get(rank) / impressionCounts.get(rank)); - - } else { - - // This document has impressions but no clicks, so it's CTR is zero. - LOGGER.info("Position = {}, Impression Counts = {}, Impressions but no clicks so CTR is 0", rank, clickCounts.get(rank)); - rankAggregatedClickThrough.put(rank, 0.0); - - } - - } else { - - // No impressions so the clickthrough rate is 0. - LOGGER.info("No impressions for rank {}, so using CTR of 0", rank); - rankAggregatedClickThrough.put(rank, (double) 0); - - } - - } - - searchEngine.indexRankAggregatedClickthrough(rankAggregatedClickThrough); - - return rankAggregatedClickThrough; - - } - private void showJudgments(final Collection judgments) { for(final Judgment judgment : judgments) { diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/Judgment.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/Judgment.java index 3285598..408ce02 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/Judgment.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/Judgment.java @@ -14,6 +14,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.UUID; /** * A judgment of a search result's quality for a given query. @@ -45,6 +46,21 @@ public Judgment(final String id, final String queryId, final String query, final this.judgment = judgment; } + /** + * Creates a new judgment. + * @param queryId The query ID for the judgment. + * @param query The query for the judgment. + * @param document The document in the judgment. + * @param judgment The judgment value. + */ + public Judgment(final String queryId, final String query, final String document, final double judgment) { + super(UUID.randomUUID().toString()); + this.queryId = queryId; + this.query = query; + this.document = document; + this.judgment = judgment; + } + public String toJudgmentString() { return queryId + ", " + query + ", " + document + ", " + MathUtils.round(judgment); } diff --git a/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java b/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java index f3755f3..13da235 100644 --- a/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java +++ b/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java @@ -8,12 +8,15 @@ */ package org.opensearch.eval.metrics; -import org.opensearch.test.OpenSearchTestCase; +import org.junit.jupiter.api.Test; import java.util.List; -public class DcgSearchMetricTest extends OpenSearchTestCase { +import static org.junit.jupiter.api.Assertions.assertEquals; +public class DcgSearchMetricTest { + + @Test public void testCalculate() { final int k = 10; @@ -26,6 +29,7 @@ public void testCalculate() { } + @Test public void testCalculateAllZeros() { final int k = 10; diff --git a/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java b/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java index 08795f8..cd7c037 100644 --- a/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java +++ b/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java @@ -8,12 +8,15 @@ */ package org.opensearch.eval.metrics; -import org.opensearch.test.OpenSearchTestCase; +import org.junit.jupiter.api.Test; import java.util.List; -public class NdcgSearchMetricTest extends OpenSearchTestCase { +import static org.junit.jupiter.api.Assertions.assertEquals; +public class NdcgSearchMetricTest { + + @Test public void testCalculate() { final int k = 10; @@ -26,6 +29,7 @@ public void testCalculate() { } + @Test public void testCalculateAllZeros() { final int k = 10; diff --git a/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java b/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java index b6c260f..ed3e5f3 100644 --- a/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java +++ b/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java @@ -8,12 +8,15 @@ */ package org.opensearch.eval.metrics; -import org.opensearch.test.OpenSearchTestCase; +import org.junit.jupiter.api.Test; import java.util.List; -public class PrecisionSearchMetricTest extends OpenSearchTestCase { +import static org.junit.jupiter.api.Assertions.assertEquals; +public class PrecisionSearchMetricTest { + + @Test public void testCalculate() { final int k = 10; From 5b7888bbee105e8b16cfa5a1289a3ab01e8d4943 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Wed, 1 Jan 2025 09:37:32 -0500 Subject: [PATCH 15/41] Working on converting to a standalone app. --- data/esci/index.sh | 2 +- .../Dockerfile | 3 + .../docker-compose.yaml | 47 ++++++ .../pom.xml | 145 +++++++++++------- .../run.sh | 4 + .../SearchQualityEvaluationFramework.java | 10 -- ... SearchQualityEvaluationFrameworkApp.java} | 27 +++- .../eval/engine/OpenSearchEngine.java | 21 ++- .../eval/model/ubi/event/EventAttributes.java | 2 + .../eval/model/ubi/event/EventObject.java | 3 + .../eval/model/ubi/event/Position.java | 1 + .../eval/model/ubi/event/UbiEvent.java | 73 +++++++++ .../eval/model/ubi/query/UbiQuery.java | 16 ++ 13 files changed, 271 insertions(+), 83 deletions(-) create mode 100644 opensearch-search-quality-evaluation-framework/Dockerfile create mode 100644 opensearch-search-quality-evaluation-framework/docker-compose.yaml create mode 100755 opensearch-search-quality-evaluation-framework/run.sh delete mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationFramework.java rename opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/{SearchQualityEvaluationApp.java => SearchQualityEvaluationFrameworkApp.java} (93%) diff --git a/data/esci/index.sh b/data/esci/index.sh index d1ebd45..95f915f 100755 --- a/data/esci/index.sh +++ b/data/esci/index.sh @@ -7,4 +7,4 @@ echo "Initializing UBI..." curl -s -X POST "http://localhost:9200/_plugins/ubi/initialize" echo "Indexing queries and events..." -curl -s -T "http://localhost:9200/_bulk?pretty" -H "Content-Type: application/x-ndjson" --data-binary @ubi_queries_events.ndjson +curl -X POST 'http://localhost:9200/index-name/_bulk?pretty' --data-binary @ubi_queries_events.ndjson -H "Content-Type: application/x-ndjson" \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/Dockerfile b/opensearch-search-quality-evaluation-framework/Dockerfile new file mode 100644 index 0000000..2af9056 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/Dockerfile @@ -0,0 +1,3 @@ +FROM opensearchproject/opensearch:2.18.0 + +RUN /usr/share/opensearch/bin/opensearch-plugin install --batch https://github.com/opensearch-project/user-behavior-insights/releases/download/2.18.0.2/opensearch-ubi-2.18.0.2.zip diff --git a/opensearch-search-quality-evaluation-framework/docker-compose.yaml b/opensearch-search-quality-evaluation-framework/docker-compose.yaml new file mode 100644 index 0000000..c2ab3b7 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/docker-compose.yaml @@ -0,0 +1,47 @@ +services: + + opensearch_sef: + build: . + container_name: opensearch_sef + environment: + discovery.type: single-node + node.name: opensearch + plugins.security.disabled: "true" + logger.level: info + OPENSEARCH_INITIAL_ADMIN_PASSWORD: SuperSecretPassword_123 + http.max_content_length: 500mb + OPENSEARCH_JAVA_OPTS: "-Xms16g -Xmx16g" + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + ports: + - "9200:9200" + - "9600:9600" + networks: + - opensearch-net + volumes: + - opensearch-data1:/usr/share/opensearch/data + + opensearch_sef_dashboards: + image: opensearchproject/opensearch-dashboards:2.18.0 + container_name: opensearch_sef_dashboards + ports: + - "5601:5601" + environment: + OPENSEARCH_HOSTS: '["http://opensearch_sef:9200"]' + DISABLE_SECURITY_DASHBOARDS_PLUGIN: "true" + depends_on: + - opensearch_sef + networks: + - opensearch-net + +volumes: + opensearch-data1: + +networks: + opensearch-net: + driver: bridge diff --git a/opensearch-search-quality-evaluation-framework/pom.xml b/opensearch-search-quality-evaluation-framework/pom.xml index 4acbca2..56cc564 100644 --- a/opensearch-search-quality-evaluation-framework/pom.xml +++ b/opensearch-search-quality-evaluation-framework/pom.xml @@ -1,62 +1,89 @@ - 4.0.0 - org.opensearch - search-evaluation-framework - 1.0.0-SNAPSHOT - search-evaluation-framework - https://www.ubisearch.dev - - UTF-8 - 21 - - - - org.opensearch.client - opensearch-java - 2.19.0 - - - com.fasterxml.jackson.core - jackson-databind - 2.18.2 - - - commons-cli - commons-cli - 1.9.0 - - - org.apache.logging.log4j - log4j-core - 2.24.3 - - - org.apache.httpcomponents.core5 - httpcore5 - 5.3.1 - - - org.apache.httpcomponents.client5 - httpclient5 - 5.4.1 - - - commons-logging - commons-logging - 1.3.4 - - - com.google.code.gson - gson - 2.11.0 - - - org.junit.jupiter - junit-jupiter-api - 5.11.4 - test - - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + org.opensearch + search-evaluation-framework + 1.0.0-SNAPSHOT + search-evaluation-framework + https://www.ubisearch.dev + + UTF-8 + 21 + + + + + maven-assembly-plugin + + + + true + org.opensearch.eval.SearchQualityEvaluationFrameworkApp + + + + jar-with-dependencies + + + + + make-my-jar-with-dependencies + package + + single + + + + + + + + + org.opensearch.client + opensearch-java + 2.19.0 + + + com.fasterxml.jackson.core + jackson-databind + 2.18.2 + + + commons-cli + commons-cli + 1.9.0 + + + org.apache.logging.log4j + log4j-core + 2.24.3 + + + org.apache.httpcomponents.core5 + httpcore5 + 5.3.1 + + + org.apache.httpcomponents.client5 + httpclient5 + 5.4.1 + + + commons-logging + commons-logging + 1.3.4 + + + com.google.code.gson + gson + 2.11.0 + + + org.junit.jupiter + junit-jupiter-api + 5.11.4 + test + + \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/run.sh b/opensearch-search-quality-evaluation-framework/run.sh new file mode 100755 index 0000000..9447483 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/run.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e + +# Create a click model. +java -jar ./target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -c \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationFramework.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationFramework.java deleted file mode 100644 index 245ee64..0000000 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationFramework.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.opensearch.eval; - -public class SearchQualityEvaluationFramework { - - public void main(String[] args) { - - - } - -} \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationApp.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationFrameworkApp.java similarity index 93% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationApp.java rename to opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationFrameworkApp.java index 2f71b38..ed621e1 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationApp.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationFrameworkApp.java @@ -15,23 +15,38 @@ import org.apache.commons.cli.ParseException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.eval.engine.OpenSearchEngine; +import org.opensearch.eval.engine.SearchEngine; +import org.opensearch.eval.judgments.clickmodel.ClickModel; +import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel; +import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModelParameters; -public class SearchQualityEvaluationApp { +public class SearchQualityEvaluationFrameworkApp { - private static final Logger LOGGER = LogManager.getLogger(SearchQualityEvaluationApp.class); + private static final Logger LOGGER = LogManager.getLogger(SearchQualityEvaluationFrameworkApp.class); - public static void main(String args[]) throws ParseException { + public static void main(String[] args) throws Exception { + + System.out.println("Search Quality Evaluation Framework"); final Options options = new Options(); - options.addOption("c", true, "create a click model"); + options.addOption("c", false, "create a click model"); options.addOption("q", true, "run a query set"); final CommandLineParser parser = new DefaultParser(); final CommandLine cmd = parser.parse(options, args); if(cmd.hasOption("c")) { - final String clickModel = cmd.getOptionValue("c"); - // TODO: Create the click model. + + //final String clickModel = cmd.getOptionValue("c"); + System.out.println("Creating click model..."); + + final SearchEngine searchEngine = new OpenSearchEngine(); + final CoecClickModelParameters coecClickModelParameters = new CoecClickModelParameters(10); + + final ClickModel cm = new CoecClickModel(searchEngine, coecClickModelParameters); + cm.calculateJudgments(); + } else { } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java index 2aef41a..a83be75 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java @@ -9,7 +9,6 @@ package org.opensearch.eval.engine; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.gson.Gson; import org.apache.hc.core5.http.HttpHost; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -86,13 +85,13 @@ public class OpenSearchEngine extends SearchEngine { private static final Logger LOGGER = LogManager.getLogger(OpenSearchEngine.class.getName()); private final OpenSearchClient client; - private final Gson gson = new Gson(); // Used to cache the query ID->user_query to avoid unnecessary lookups to OpenSearch. private static final Map userQueryCache = new HashMap<>(); public OpenSearchEngine() { + // TODO: Parameterize the host. final HttpHost[] hosts = new HttpHost[] { new HttpHost("http", "localhost", 9200) }; @@ -207,7 +206,9 @@ public Collection getUbiQueries() throws IOException { final Collection ubiQueries = new ArrayList<>(); - final SearchResponse searchResponse = client.search(s -> s.index(Constants.UBI_QUERIES_INDEX_NAME).size(1000).scroll(Time.of(t -> t.offset(1000))), UbiQuery.class); + final Time scrollTime = new Time.Builder().time("10m").build(); + + final SearchResponse searchResponse = client.search(s -> s.index(Constants.UBI_QUERIES_INDEX_NAME).size(1000).scroll(scrollTime), UbiQuery.class); String scrollId = searchResponse.scrollId(); List> searchHits = searchResponse.hits().hits(); @@ -234,7 +235,9 @@ public Collection getJudgments(final String index) throws IOException final Collection judgments = new ArrayList<>(); - final SearchResponse searchResponse = client.search(s -> s.index(index).size(1000).scroll(Time.of(t -> t.offset(1000))), Judgment.class); + final Time scrollTime = new Time.Builder().time("10m").build(); + + final SearchResponse searchResponse = client.search(s -> s.index(index).size(1000).scroll(scrollTime), Judgment.class); String scrollId = searchResponse.scrollId(); List> searchHits = searchResponse.hits().hits(); @@ -431,12 +434,14 @@ public Map> getClickthroughRate(final int maxRank) .query(encodedQuery) .build(); + final Time scrollTime = new Time.Builder().time("10m").build(); + final SearchRequest searchRequest = new SearchRequest.Builder() .index(Constants.UBI_EVENTS_INDEX_NAME) .query(q -> q.wrapper(wrapperQuery)) .from(0) .size(1000) - .scroll(Time.of(t -> t.offset(1000))) + .scroll(scrollTime) .build(); final SearchResponse searchResponse = client.search(searchRequest, UbiEvent.class); @@ -517,7 +522,7 @@ public Map getRankAggregatedClickThrough(final int maxRank) thr final Aggregation positionsAggregator = Aggregation.of(a -> a .terms(t -> t .field("event_attributes.position.ordinal") - .name("By_Position") + //.name("By_Position") .size(maxRank) .order(sort) ) @@ -526,7 +531,7 @@ public Map getRankAggregatedClickThrough(final int maxRank) thr final Aggregation actionNameAggregation = Aggregation.of(a -> a .terms(t -> t .field("action_name") - .name("By_Action") + //.name("By_Action") .size(maxRank) .order(sort) ) @@ -545,6 +550,8 @@ public Map getRankAggregatedClickThrough(final int maxRank) thr .size(0) .build(); + System.out.println(searchRequest.toJsonString()); + final SearchResponse searchResponse = client.search(searchRequest, Void.class); final Map aggs = searchResponse.aggregations(); diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventAttributes.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventAttributes.java index 1be71c0..7d73781 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventAttributes.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventAttributes.java @@ -8,6 +8,7 @@ */ package org.opensearch.eval.model.ubi.event; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.annotations.SerializedName; /** @@ -18,6 +19,7 @@ public class EventAttributes { @SerializedName("object") private EventObject object; + @JsonProperty("session_id") @SerializedName("session_id") private String sessionId; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java index 27c9982..0bce425 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java @@ -8,13 +8,16 @@ */ package org.opensearch.eval.model.ubi.event; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.annotations.SerializedName; public class EventObject { + @JsonProperty("object_id_field") @SerializedName("object_id_field") private String objectIdField; + @JsonProperty("object_id") @SerializedName("object_id") private String objectId; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/Position.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/Position.java index 5671ed8..ec66369 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/Position.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/Position.java @@ -8,6 +8,7 @@ */ package org.opensearch.eval.model.ubi.event; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.annotations.SerializedName; /** diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java index c29a053..279356a 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java @@ -8,6 +8,7 @@ */ package org.opensearch.eval.model.ubi.event; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.annotations.SerializedName; /** @@ -15,18 +16,43 @@ */ public class UbiEvent { + @JsonProperty("action_name") @SerializedName("action_name") private String actionName; + @JsonProperty("client_id") @SerializedName("client_id") private String clientId; + @JsonProperty("query_id") @SerializedName("query_id") private String queryId; + @JsonProperty("session_id") + @SerializedName("session_id") + private String sessionId; + + @SerializedName("application") + private String application; + + @JsonProperty("event_attributes") @SerializedName("event_attributes") private EventAttributes eventAttributes; + @JsonProperty("user_query") + @SerializedName("user_query") + private String userQuery; + + @JsonProperty("message_type") + @SerializedName("message_type") + private String messageType; + + @JsonProperty("message") + @SerializedName("message") + private String message; + + private String timestamp; + /** * Creates a new representation of an UBI event. */ @@ -79,4 +105,51 @@ public void setEventAttributes(EventAttributes eventAttributes) { this.eventAttributes = eventAttributes; } + public String getApplication() { + return application; + } + + public void setApplication(String application) { + this.application = application; + } + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + public String getUserQuery() { + return userQuery; + } + + public void setUserQuery(String userQuery) { + this.userQuery = userQuery; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getMessageType() { + return messageType; + } + + public void setMessageType(String messageType) { + this.messageType = messageType; + } } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java index 0e87ab0..eae7507 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java @@ -8,6 +8,7 @@ */ package org.opensearch.eval.model.ubi.query; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.annotations.SerializedName; import java.util.Map; @@ -20,21 +21,29 @@ public class UbiQuery { @SerializedName("timestamp") private String timestamp; + @JsonProperty("query_id") @SerializedName("query_id") private String queryId; + @JsonProperty("client_id") @SerializedName("client_id") private String clientId; + @JsonProperty("user_query") @SerializedName("user_query") private String userQuery; @SerializedName("query") private String query; + @SerializedName("application") + private String application; + + @JsonProperty("query_attributes") @SerializedName("query_attributes") private Map queryAttributes; + @JsonProperty("query_response") @SerializedName("query_response") private QueryResponse queryResponse; @@ -157,4 +166,11 @@ public void setQueryResponse(QueryResponse queryResponse) { this.queryResponse = queryResponse; } + public String getApplication() { + return application; + } + + public void setApplication(String application) { + this.application = application; + } } From 41aa00feab26cc3c13dd73ef8ae74e7a61255928 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Wed, 1 Jan 2025 10:08:36 -0500 Subject: [PATCH 16/41] Working on converting to a standalone app. --- .../eval/engine/OpenSearchEngine.java | 129 ++++++++---------- .../clickmodel/coec/CoecClickModel.java | 2 +- .../eval/model/ClickthroughRate.java | 14 +- .../eval/model/data/AbstractData.java | 6 +- .../eval/model/data/ClickThroughRate.java | 12 ++ .../src/main/resources/log4j2.xml | 13 ++ 6 files changed, 90 insertions(+), 86 deletions(-) create mode 100644 opensearch-search-quality-evaluation-framework/src/main/resources/log4j2.xml diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java index a83be75..9e1e02b 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java @@ -21,6 +21,7 @@ import org.opensearch.client.opensearch._types.Time; import org.opensearch.client.opensearch._types.aggregations.Aggregate; import org.opensearch.client.opensearch._types.aggregations.Aggregation; +import org.opensearch.client.opensearch._types.aggregations.LongTermsBucket; import org.opensearch.client.opensearch._types.aggregations.StringTermsAggregate; import org.opensearch.client.opensearch._types.aggregations.StringTermsBucket; import org.opensearch.client.opensearch._types.mapping.IntegerNumberProperty; @@ -334,7 +335,11 @@ public UbiQuery getQueryFromQueryId(final String queryId) throws Exception { // If this does not return a query then we cannot calculate the judgments. Each even should have a query associated with it. if(searchResponse.hits().hits() != null & !searchResponse.hits().hits().isEmpty()) { - return searchResponse.hits().hits().get(0).source(); + final UbiQuery ubiQuery = searchResponse.hits().hits().get(0).source(); + + LOGGER.info("Found query: {}", ubiQuery.toString()); + + return ubiQuery; } else { @@ -478,6 +483,12 @@ public Map> getClickthroughRate(final int maxRank) LOGGER.warn("Invalid event action name: {}", ubiEvent.getActionName()); } + // Safeguard to avoid having clicks without events. + // When the clicks is > 0 and impressions == 0, set the impressions to the number of clicks. + if(clickthroughRate.getClicks() > 0 && clickthroughRate.getImpressions() == 0) { + clickthroughRate.setImpressions(clickthroughRate.getClicks()); + } + clickthroughRates.add(clickthroughRate); queriesToClickthroughRates.put(userQuery, clickthroughRates); // LOGGER.debug("clickthroughRate = {}", queriesToClickthroughRates.size()); @@ -491,6 +502,10 @@ public Map> getClickthroughRate(final int maxRank) // I don't remember seeing this prior to 2.18.0 but it's possible I just didn't see it. // https://github.com/opensearch-project/OpenSearch/blob/f105e4eb2ede1556b5dd3c743bea1ab9686ebccf/server/src/main/java/org/opensearch/wlm/QueryGroupTask.java#L73 + if(scrollId == null) { + break; + } + final ScrollRequest scrollRequest = new ScrollRequest.Builder().scrollId(scrollId).build(); final ScrollResponse scrollResponse = client.scroll(scrollRequest, UbiEvent.class); @@ -522,7 +537,6 @@ public Map getRankAggregatedClickThrough(final int maxRank) thr final Aggregation positionsAggregator = Aggregation.of(a -> a .terms(t -> t .field("event_attributes.position.ordinal") - //.name("By_Position") .size(maxRank) .order(sort) ) @@ -531,14 +545,12 @@ public Map getRankAggregatedClickThrough(final int maxRank) thr final Aggregation actionNameAggregation = Aggregation.of(a -> a .terms(t -> t .field("action_name") - //.name("By_Action") .size(maxRank) .order(sort) - ) + ).aggregations(Map.of("By_Position", positionsAggregator)) ); final Map aggregations = new HashMap<>(); - aggregations.put("By_Position", positionsAggregator); aggregations.put("By_Action", actionNameAggregation); // TODO: Allow for a time period and for a specific application. @@ -562,72 +574,36 @@ public Map getRankAggregatedClickThrough(final int maxRank) thr final Map impressionCounts = new HashMap<>(); for (final StringTermsBucket bucket : byActionBuckets) { - System.out.println("Key: " + bucket.key() + ", Doc Count: " + bucket.docCount()); - -// // Handle the "impression" bucket. -// if(EVENT_IMPRESSION.equalsIgnoreCase(bucket.key())) { -// -// final Aggregate positionTerms = bucket.aggregations().get("By_Position"); -// -// final Collection positionBuckets = positionTerms.getBuckets(); -// -// for(final Terms.Bucket positionBucket : positionBuckets) { -// LOGGER.debug("Inserting impression event from position {} with click count {}", positionBucket.getKey(), (double) positionBucket.getDocCount()); -// impressionCounts.put(Integer.valueOf(positionBucket.getKey().toString()), (double) positionBucket.getDocCount()); -// } -// -// } -// -// // Handle the "click" bucket. -// if(EVENT_CLICK.equalsIgnoreCase(bucket.key())) { -// -// final Aggregate positionTerms = actionBucket.getAggregations().get("By_Position"); -// final Collection positionBuckets = positionTerms.getBuckets(); -// -// for(final Terms.Bucket positionBucket : positionBuckets) { -// LOGGER.debug("Inserting client event from position {} with click count {}", positionBucket.getKey(), (double) positionBucket.getDocCount()); -// clickCounts.put(Integer.valueOf(positionBucket.getKey().toString()), (double) positionBucket.getDocCount()); -// } -// -// } - } + // Handle the "impression" bucket. + if(EVENT_IMPRESSION.equalsIgnoreCase(bucket.key())) { + + final Aggregate positionTerms = bucket.aggregations().get("By_Position"); + + final List positionBuckets = positionTerms.lterms().buckets().array(); + + for(final LongTermsBucket positionBucket : positionBuckets) { + LOGGER.debug("Inserting impression event from position {} with click count {}", positionBucket.key(), (double) positionBucket.docCount()); + impressionCounts.put(Integer.valueOf(positionBucket.key()), (double) positionBucket.docCount()); + } + + } + // Handle the "click" bucket. + if(EVENT_CLICK.equalsIgnoreCase(bucket.key())) { -// final Terms actionTerms = searchResponse.getAggregations().get("By_Action"); -// final Collection actionBuckets = actionTerms.getBuckets(); -// -// LOGGER.debug("Aggregation query: {}", searchSourceBuilder.toString()); -// -// for(final Terms.Bucket actionBucket : actionBuckets) { -// -// // Handle the "impression" bucket. -// if(EVENT_IMPRESSION.equalsIgnoreCase(actionBucket.getKey().toString())) { -// -// final Terms positionTerms = actionBucket.getAggregations().get("By_Position"); -// final Collection positionBuckets = positionTerms.getBuckets(); -// -// for(final Terms.Bucket positionBucket : positionBuckets) { -// LOGGER.debug("Inserting impression event from position {} with click count {}", positionBucket.getKey(), (double) positionBucket.getDocCount()); -// impressionCounts.put(Integer.valueOf(positionBucket.getKey().toString()), (double) positionBucket.getDocCount()); -// } -// -// } -// -// // Handle the "click" bucket. -// if(EVENT_CLICK.equalsIgnoreCase(actionBucket.getKey().toString())) { -// -// final Terms positionTerms = actionBucket.getAggregations().get("By_Position"); -// final Collection positionBuckets = positionTerms.getBuckets(); -// -// for(final Terms.Bucket positionBucket : positionBuckets) { -// LOGGER.debug("Inserting client event from position {} with click count {}", positionBucket.getKey(), (double) positionBucket.getDocCount()); -// clickCounts.put(Integer.valueOf(positionBucket.getKey().toString()), (double) positionBucket.getDocCount()); -// } -// -// } -// -// } + final Aggregate positionTerms = bucket.aggregations().get("By_Position"); + + final List positionBuckets = positionTerms.lterms().buckets().array(); + + for(final LongTermsBucket positionBucket : positionBuckets) { + LOGGER.debug("Inserting client event from position {} with click count {}", positionBucket.key(), (double) positionBucket.docCount()); + impressionCounts.put(Integer.valueOf(positionBucket.key()), (double) positionBucket.docCount()); + } + + } + + } for(int rank = 0; rank < maxRank; rank++) { @@ -795,14 +771,17 @@ public void indexClickthroughRates(final Map> clic final String id = UUID.randomUUID().toString(); - final ClickThroughRate clickThroughRate = new ClickThroughRate(); - clickThroughRate.setUserQuery(userQuery); - clickThroughRate.setClicks(clickthroughRate.getClicks()); - clickThroughRate.setEvents(clickthroughRate.getImpressions()); - clickThroughRate.setCtr(clickthroughRate.getClickthroughRate()); - clickThroughRate.setObjectId(clickthroughRate.getObjectId()); + final ClickThroughRate ctr = new ClickThroughRate(id); + ctr.setUserQuery(userQuery); + ctr.setClicks(clickthroughRate.getClicks()); + ctr.setEvents(clickthroughRate.getImpressions()); + ctr.setCtr(clickthroughRate.getClickthroughRate()); + ctr.setObjectId(clickthroughRate.getObjectId()); + + LOGGER.debug("Clickthrough rate: {}", ctr); - final IndexRequest indexRequest = new IndexRequest.Builder().index(INDEX_QUERY_DOC_CTR).id(id).document(clickThroughRate).build(); + // TODO: This index needs created. + final IndexRequest indexRequest = new IndexRequest.Builder().index(INDEX_QUERY_DOC_CTR).id(id).document(ctr).build(); client.index(indexRequest); } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java index 66fdd9f..020ac1d 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java @@ -110,7 +110,7 @@ public String calculateCoec(final Map rankAggregatedClickThroug } // Numerator is sum of clicks at all ranks up to the maxRank. - final int totalNumberClicksForQueryResult = ctr.getClicks(); + final long totalNumberClicksForQueryResult = ctr.getClicks(); // Divide the numerator by the denominator (value). final double judgmentValue; diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ClickthroughRate.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ClickthroughRate.java index 2736ffe..7523fd2 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ClickthroughRate.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ClickthroughRate.java @@ -16,8 +16,8 @@ public class ClickthroughRate { private final String objectId; - private int clicks; - private int impressions; + private long clicks; + private long impressions; /** * Creates a new clickthrough rate for an object. @@ -35,7 +35,7 @@ public ClickthroughRate(final String objectId) { * @param clicks The count of clicks. * @param impressions The count of events. */ - public ClickthroughRate(final String objectId, final int clicks, final int impressions) { + public ClickthroughRate(final String objectId, final long clicks, final long impressions) { this.objectId = objectId; this.clicks = clicks; this.impressions = impressions; @@ -61,6 +61,10 @@ public void logImpression() { impressions++; } + public void setImpressions(long impressions) { + this.impressions = impressions; + } + /** * Calculate the clickthrough rate. * @return The clickthrough rate as clicks divided by events. @@ -73,7 +77,7 @@ public double getClickthroughRate() { * Gets the count of clicks. * @return The count of clicks. */ - public int getClicks() { + public long getClicks() { return clicks; } @@ -81,7 +85,7 @@ public int getClicks() { * Gets the count of events. * @return The count of events. */ - public int getImpressions() { + public long getImpressions() { return impressions; } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java index 1aaab07..bfbd31e 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java @@ -10,7 +10,7 @@ public abstract class AbstractData { - private String id; + private final String id; public AbstractData(final String id) { this.id = id; @@ -20,8 +20,4 @@ public String getId() { return id; } - public void setId(final String id) { - this.id = id; - } - } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java index b389130..46271d9 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java @@ -26,6 +26,18 @@ public ClickThroughRate(String id) { super(id); } + @Override + public String toString() { + return "ClickThroughRate{" + + "id='" + getId() + '\'' + + ", userQuery='" + userQuery + '\'' + + ", clicks=" + clicks + + ", events=" + events + + ", ctr=" + ctr + + ", objectId='" + objectId + '\'' + + '}'; + } + public String getUserQuery() { return userQuery; } diff --git a/opensearch-search-quality-evaluation-framework/src/main/resources/log4j2.xml b/opensearch-search-quality-evaluation-framework/src/main/resources/log4j2.xml new file mode 100644 index 0000000..a52723f --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file From b5d34cfc8673ed68bf992670abc6439793c441fd Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Wed, 1 Jan 2025 15:31:31 -0500 Subject: [PATCH 17/41] Adding running of query set to CLI. --- .../pom.xml | 2 +- .../queryset.json | 10 ++ .../run.sh | 5 +- ...tyEvaluationFrameworkApp.java => App.java} | 98 ++++++++-------- .../eval/runners/AbstractQuerySetRunner.java | 15 +-- .../runners/OpenSearchQuerySetRunner.java | 24 ++-- .../eval/runners/RunQuerySetParameters.java | 106 ++++++++++++++++++ 7 files changed, 181 insertions(+), 79 deletions(-) create mode 100644 opensearch-search-quality-evaluation-framework/queryset.json rename opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/{SearchQualityEvaluationFrameworkApp.java => App.java} (77%) create mode 100644 opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/RunQuerySetParameters.java diff --git a/opensearch-search-quality-evaluation-framework/pom.xml b/opensearch-search-quality-evaluation-framework/pom.xml index 56cc564..40d7c56 100644 --- a/opensearch-search-quality-evaluation-framework/pom.xml +++ b/opensearch-search-quality-evaluation-framework/pom.xml @@ -19,7 +19,7 @@ true - org.opensearch.eval.SearchQualityEvaluationFrameworkApp + org.opensearch.eval.App diff --git a/opensearch-search-quality-evaluation-framework/queryset.json b/opensearch-search-quality-evaluation-framework/queryset.json new file mode 100644 index 0000000..0f53ef3 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/queryset.json @@ -0,0 +1,10 @@ +{ + "query_set_id": "", + "judgments_id": "", + "index": "", + "search_pipeline": "", + "id_field": "", + "k": 10, + "threshold": 1.0, + "query": {} +} diff --git a/opensearch-search-quality-evaluation-framework/run.sh b/opensearch-search-quality-evaluation-framework/run.sh index 9447483..fdffd21 100755 --- a/opensearch-search-quality-evaluation-framework/run.sh +++ b/opensearch-search-quality-evaluation-framework/run.sh @@ -1,4 +1,7 @@ #!/bin/bash -e # Create a click model. -java -jar ./target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -c \ No newline at end of file +java -jar ./target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -c coec + +# Run a query set. +#java -jar ./target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -r queryset.json diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationFrameworkApp.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/App.java similarity index 77% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationFrameworkApp.java rename to opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/App.java index ed621e1..3c5b09c 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/SearchQualityEvaluationFrameworkApp.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/App.java @@ -8,11 +8,11 @@ */ package org.opensearch.eval; +import com.google.gson.Gson; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Options; -import org.apache.commons.cli.ParseException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.eval.engine.OpenSearchEngine; @@ -20,34 +20,67 @@ import org.opensearch.eval.judgments.clickmodel.ClickModel; import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel; import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModelParameters; +import org.opensearch.eval.runners.OpenSearchQuerySetRunner; +import org.opensearch.eval.runners.RunQuerySetParameters; -public class SearchQualityEvaluationFrameworkApp { +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; - private static final Logger LOGGER = LogManager.getLogger(SearchQualityEvaluationFrameworkApp.class); +public class App { + + private static final Logger LOGGER = LogManager.getLogger(App.class); public static void main(String[] args) throws Exception { System.out.println("Search Quality Evaluation Framework"); + final Gson gson = new Gson(); + final Options options = new Options(); - options.addOption("c", false, "create a click model"); - options.addOption("q", true, "run a query set"); + options.addOption("c", true, "create a click model"); + options.addOption("r", true, "run a query set"); final CommandLineParser parser = new DefaultParser(); final CommandLine cmd = parser.parse(options, args); + final SearchEngine searchEngine = new OpenSearchEngine(); + if(cmd.hasOption("c")) { //final String clickModel = cmd.getOptionValue("c"); System.out.println("Creating click model..."); - final SearchEngine searchEngine = new OpenSearchEngine(); - final CoecClickModelParameters coecClickModelParameters = new CoecClickModelParameters(10); + final String clickModelType = cmd.getOptionValue("c"); + + if(CoecClickModel.CLICK_MODEL_NAME.equalsIgnoreCase(clickModelType)) { + + final CoecClickModelParameters coecClickModelParameters = new CoecClickModelParameters(10); + + final ClickModel cm = new CoecClickModel(searchEngine, coecClickModelParameters); + cm.calculateJudgments(); + + } else { + System.err.println("Invalid click model type. Valid models are 'coec'."); + } + + } else if (cmd.hasOption("r")) { - final ClickModel cm = new CoecClickModel(searchEngine, coecClickModelParameters); - cm.calculateJudgments(); + System.out.println("Running query set..."); - } else { + final String querySetOptionsFile = cmd.getOptionValue("q"); + final File file = new File(querySetOptionsFile); + + if(file.exists()) { + + final RunQuerySetParameters runQuerySetParameters = gson.fromJson(Files.readString(file.toPath(), StandardCharsets.UTF_8), RunQuerySetParameters.class); + + final OpenSearchQuerySetRunner openSearchQuerySetRunner = new OpenSearchQuerySetRunner(searchEngine); + openSearchQuerySetRunner.run(runQuerySetParameters); + + } else { + System.err.println("The query set run parameters file does not exist."); + } } @@ -123,50 +156,7 @@ public static void main(String[] args) throws Exception { // return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "{\"error\": \"" + request.method() + " is not allowed.\"}")); // } // -// // Handle running query sets. -// } else if(QUERYSET_RUN_URL.equalsIgnoreCase(request.path())) { -// -// final String querySetId = request.param("id"); -// final String judgmentsId = request.param("judgments_id"); -// final String index = request.param("index"); -// final String searchPipeline = request.param("search_pipeline", null); -// final String idField = request.param("id_field", "_id"); -// final int k = Integer.parseInt(request.param("k", "10")); -// final double threshold = Double.parseDouble(request.param("threshold", "1.0")); -// -// if(querySetId == null || querySetId.isEmpty() || judgmentsId == null || judgmentsId.isEmpty() || index == null || index.isEmpty()) { -// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing required parameters.\"}")); -// } -// -// if(k < 1) { -// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"k must be a positive integer.\"}")); -// } -// -// if(!request.hasContent()) { -// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing query in body.\"}")); -// } -// -// // Get the query JSON from the content. -// final String query = new String(BytesReference.toBytes(request.content()), Charset.defaultCharset()); -// -// // Validate the query has a QUERY_PLACEHOLDER. -// if(!query.contains(QUERY_PLACEHOLDER)) { -// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing query placeholder in query.\"}")); -// } -// -// try { -// -// final OpenSearchQuerySetRunner openSearchQuerySetRunner = new OpenSearchQuerySetRunner(client); -// final QuerySetRunResult querySetRunResult = openSearchQuerySetRunner.run(querySetId, judgmentsId, index, searchPipeline, idField, query, k, threshold); -// openSearchQuerySetRunner.save(querySetRunResult); -// -// } catch (Exception ex) { -// LOGGER.error("Unable to run query set. Verify query set and judgments exist.", ex); -// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, ex.getMessage())); -// } -// -// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"message\": \"Run initiated for query set " + querySetId + "\"}")); -// + // // Handle the on-demand creation of implicit judgments. // } else if(IMPLICIT_JUDGMENTS_URL.equalsIgnoreCase(request.path())) { // diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java index 6fad96f..ad5f5dd 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java @@ -31,21 +31,10 @@ public AbstractQuerySetRunner(final SearchEngine searchEngine) { /** * Runs the query set. - * @param querySetId The ID of the query set to run. - * @param judgmentsId The ID of the judgments set to use for search metric calculation. - * @param index The name of the index to run the query sets against. - * @param searchPipeline The name of the search pipeline to use, or null to not use a search pipeline. - * @param idField The field in the index that is used to uniquely identify a document. - * @param query The query that will be used to run the query set. - * @param k The k used for metrics calculation, i.e. DCG@k. - * @param threshold The cutoff for binary judgments. A judgment score greater than or equal - * to this value will be assigned a binary judgment value of 1. A judgment score - * less than this value will be assigned a binary judgment value of 0. + * @param querySetParameters A {@link RunQuerySetParameters parameters}. * @return The query set {@link QuerySetRunResult results} and calculated metrics. */ - abstract QuerySetRunResult run(String querySetId, final String judgmentsId, final String index, final String searchPipeline, - final String idField, final String query, final int k, - final double threshold) throws Exception; + abstract QuerySetRunResult run(RunQuerySetParameters querySetParameters) throws Exception; /** * Saves the query set results to a persistent store, which may be the search engine itself. diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java index ea99a2b..00dbddc 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java @@ -47,12 +47,10 @@ public OpenSearchQuerySetRunner(final SearchEngine searchEngine) { } @Override - public QuerySetRunResult run(final String querySetId, final String judgmentsId, final String index, - final String searchPipeline, final String idField, final String query, - final int k, final double threshold) throws Exception { + public QuerySetRunResult run(final RunQuerySetParameters querySetParameters) throws Exception { - final QuerySet querySet = searchEngine.getQuerySet(querySetId); - LOGGER.info("Found {} queries in query set {}", querySet.getQuerySetQueries().size(), querySetId); + final QuerySet querySet = searchEngine.getQuerySet(querySetParameters.getQuerySetId()); + LOGGER.info("Found {} queries in query set {}", querySet.getQuerySetQueries().size(), querySetParameters.getQuerySetId()); try { @@ -68,23 +66,29 @@ public QuerySetRunResult run(final String querySetId, final String judgmentsId, // TODO: Look at using the Workload Management in 2.18.0. Thread.sleep(50); - final List orderedDocumentIds = searchEngine.runQuery(index, query, k, userQuery, idField); + final List orderedDocumentIds = searchEngine.runQuery( + querySetParameters.getIndex(), + querySetParameters.getQuery(), + querySetParameters.getK(), + userQuery, + querySetParameters.getIdField()); try { - final RelevanceScores relevanceScores = getRelevanceScores(judgmentsId, userQuery, orderedDocumentIds, k); + final int k = querySetParameters.getK(); + final RelevanceScores relevanceScores = getRelevanceScores(querySetParameters.getJudgmentsId(), userQuery, orderedDocumentIds, k); // Calculate the metrics for this query. final SearchMetric dcgSearchMetric = new DcgSearchMetric(k, relevanceScores.getRelevanceScores()); final SearchMetric ndcgSearchmetric = new NdcgSearchMetric(k, relevanceScores.getRelevanceScores()); - final SearchMetric precisionSearchMetric = new PrecisionSearchMetric(k, threshold, relevanceScores.getRelevanceScores()); + final SearchMetric precisionSearchMetric = new PrecisionSearchMetric(k, querySetParameters.getThreshold(), relevanceScores.getRelevanceScores()); final Collection searchMetrics = List.of(dcgSearchMetric, ndcgSearchmetric, precisionSearchMetric); queryResults.add(new QueryResult(userQuery, orderedDocumentIds, k, searchMetrics, relevanceScores.getFrogs())); } catch (Exception ex) { - LOGGER.error("Unable to get relevance scores for judgments {} and user query {}.", judgmentsId, userQuery, ex); + LOGGER.error("Unable to get relevance scores for judgments {} and user query {}.", querySetParameters.getJudgmentsId(), userQuery, ex); } } @@ -110,7 +114,7 @@ public QuerySetRunResult run(final String querySetId, final String judgmentsId, } final String querySetRunId = UUID.randomUUID().toString(); - final QuerySetRunResult querySetRunResult = new QuerySetRunResult(querySetRunId, querySetId, queryResults, querySetMetrics); + final QuerySetRunResult querySetRunResult = new QuerySetRunResult(querySetRunId, querySetParameters.getQuerySetId(), queryResults, querySetMetrics); LOGGER.info("Query set run complete: {}", querySetRunId); diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/RunQuerySetParameters.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/RunQuerySetParameters.java new file mode 100644 index 0000000..bf8d523 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/RunQuerySetParameters.java @@ -0,0 +1,106 @@ +package org.opensearch.eval.runners; + +import com.google.gson.annotations.SerializedName; + +public class RunQuerySetParameters { + + @SerializedName("query_set_id") + private String querySetId; + + @SerializedName("judgments_id") + private String judgmentsId; + + @SerializedName("index") + private String index; + + @SerializedName("search_pipeline") + private String searchPipeline; + + @SerializedName("id_field") + private String idField; + + @SerializedName("query") + private String query; + + @SerializedName("k") + private int k; + + @SerializedName("threshold") + private double threshold; + +// * @param querySetId The ID of the query set to run. +// * @param judgmentsId The ID of the judgments set to use for search metric calculation. +// * @param index The name of the index to run the query sets against. +// * @param searchPipeline The name of the search pipeline to use, or null to not use a search pipeline. +// * @param idField The field in the index that is used to uniquely identify a document. +// * @param query The query that will be used to run the query set. +// * @param k The k used for metrics calculation, i.e. DCG@k. +// * @param threshold The cutoff for binary judgments. A judgment score greater than or equal +// * to this value will be assigned a binary judgment value of 1. A judgment score +// * less than this value will be assigned a binary judgment value of 0. + + public String getQuerySetId() { + return querySetId; + } + + public void setQuerySetId(String querySetId) { + this.querySetId = querySetId; + } + + public String getJudgmentsId() { + return judgmentsId; + } + + public void setJudgmentsId(String judgmentsId) { + this.judgmentsId = judgmentsId; + } + + public String getIndex() { + return index; + } + + public void setIndex(String index) { + this.index = index; + } + + public String getSearchPipeline() { + return searchPipeline; + } + + public void setSearchPipeline(String searchPipeline) { + this.searchPipeline = searchPipeline; + } + + public String getIdField() { + return idField; + } + + public void setIdField(String idField) { + this.idField = idField; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public int getK() { + return k; + } + + public void setK(int k) { + this.k = k; + } + + public double getThreshold() { + return threshold; + } + + public void setThreshold(double threshold) { + this.threshold = threshold; + } + +} From a1ad39dbf94c6ec37608541a944b0ea157aad234 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Wed, 1 Jan 2025 16:03:59 -0500 Subject: [PATCH 18/41] Wiring up CLI options. --- .../run.sh | 3 + .../sampling.json | 7 ++ .../main/java/org/opensearch/eval/App.java | 85 ++++++++++++------- .../samplers/AbstractSamplerParameters.java | 8 +- .../AllQueriesQuerySamplerParameters.java | 2 +- ...obabilityProportionalToSizeParameters.java | 2 +- ...bilityProportionalToSizeQuerySampler.java} | 6 +- 7 files changed, 76 insertions(+), 37 deletions(-) create mode 100644 opensearch-search-quality-evaluation-framework/sampling.json rename opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/{ProbabilityProportionalToSizeAbstractQuerySampler.java => ProbabilityProportionalToSizeQuerySampler.java} (94%) diff --git a/opensearch-search-quality-evaluation-framework/run.sh b/opensearch-search-quality-evaluation-framework/run.sh index fdffd21..bb192ec 100755 --- a/opensearch-search-quality-evaluation-framework/run.sh +++ b/opensearch-search-quality-evaluation-framework/run.sh @@ -3,5 +3,8 @@ # Create a click model. java -jar ./target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -c coec +# Create a query set using sampling. +#java -jar ./target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -s sampling.json + # Run a query set. #java -jar ./target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -r queryset.json diff --git a/opensearch-search-quality-evaluation-framework/sampling.json b/opensearch-search-quality-evaluation-framework/sampling.json new file mode 100644 index 0000000..5d711d9 --- /dev/null +++ b/opensearch-search-quality-evaluation-framework/sampling.json @@ -0,0 +1,7 @@ +{ + "sampler": "all", + "name": "", + "description": "", + "sampling": "", + "querySetSize": 500 +} diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/App.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/App.java index 3c5b09c..7e148e5 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/App.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/App.java @@ -9,6 +9,9 @@ package org.opensearch.eval; import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; @@ -22,6 +25,10 @@ import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModelParameters; import org.opensearch.eval.runners.OpenSearchQuerySetRunner; import org.opensearch.eval.runners.RunQuerySetParameters; +import org.opensearch.eval.samplers.AllQueriesQuerySampler; +import org.opensearch.eval.samplers.AllQueriesQuerySamplerParameters; +import org.opensearch.eval.samplers.ProbabilityProportionalToSizeParameters; +import org.opensearch.eval.samplers.ProbabilityProportionalToSizeQuerySampler; import java.io.File; import java.nio.charset.StandardCharsets; @@ -39,6 +46,7 @@ public static void main(String[] args) throws Exception { final Options options = new Options(); options.addOption("c", true, "create a click model"); + options.addOption("s", true, "create a query set using sampling"); options.addOption("r", true, "run a query set"); final CommandLineParser parser = new DefaultParser(); @@ -82,6 +90,51 @@ public static void main(String[] args) throws Exception { System.err.println("The query set run parameters file does not exist."); } + } else if (cmd.hasOption("s")) { + + final String samplerOptionsFile = cmd.getOptionValue("s"); + final File file = new File(samplerOptionsFile); + + if(file.exists()) { + + final String jsonString = Files.readString(file.toPath(), StandardCharsets.UTF_8); + final JsonElement jsonElement = JsonParser.parseString(jsonString); + final JsonObject jsonObject = jsonElement.getAsJsonObject(); + final String samplerType = jsonObject.get("sampler").getAsString(); + + if("all".equalsIgnoreCase(samplerType)) { + + final AllQueriesQuerySamplerParameters parameters = gson.fromJson(jsonString, AllQueriesQuerySamplerParameters.class); + + final AllQueriesQuerySampler sampler = new AllQueriesQuerySampler(searchEngine, parameters); + final String querySetId = sampler.sample(); + + System.out.println("Query set created: " + querySetId); + + } else if("pptss".equalsIgnoreCase(samplerType)) { + + final ProbabilityProportionalToSizeParameters parameters = gson.fromJson(jsonString, ProbabilityProportionalToSizeParameters.class); + + final ProbabilityProportionalToSizeQuerySampler sampler = new ProbabilityProportionalToSizeQuerySampler(searchEngine, parameters); + final String querySetId = sampler.sample(); + + System.out.println("Query set created: " + querySetId); + + } else { + + System.err.println("Invalid sampler."); + + } + + } else { + System.err.println("The query set run parameters file does not exist."); + } + + + } else { + + System.err.println("Invalid options."); + } } @@ -125,37 +178,7 @@ public static void main(String[] args) throws Exception { // } catch(Exception ex) { // return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "{\"error\": \"" + ex.getMessage() + "\"}")); // } -// -// -// // Create a query set by using PPTSS sampling. -// } else if (ProbabilityProportionalToSizeAbstractQuerySampler.NAME.equalsIgnoreCase(sampling)) { -// -// LOGGER.info("Creating query set using PPTSS"); -// -// final ProbabilityProportionalToSizeParameters parameters = new ProbabilityProportionalToSizeParameters(name, description, sampling, querySetSize); -// final ProbabilityProportionalToSizeAbstractQuerySampler sampler = new ProbabilityProportionalToSizeAbstractQuerySampler(client, parameters); -// -// try { -// -// // Sample and index the queries. -// final String querySetId = sampler.sample(); -// -// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"query_set\": \"" + querySetId + "\"}")); -// -// } catch(Exception ex) { -// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "{\"error\": \"" + ex.getMessage() + "\"}")); -// } -// -// } else { -// // An Invalid sampling method was provided in the request. -// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Invalid sampling method: " + sampling + "\"}")); -// } -// -// } else { -// // Invalid HTTP method for this endpoint. -// return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "{\"error\": \"" + request.method() + " is not allowed.\"}")); -// } -// + // // Handle the on-demand creation of implicit judgments. // } else if(IMPLICIT_JUDGMENTS_URL.equalsIgnoreCase(request.path())) { diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java index c8d731a..4388fcc 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java @@ -10,12 +10,14 @@ public class AbstractSamplerParameters { + private final String sampler; private final String name; private final String description; private final String sampling; private final int querySetSize; - public AbstractSamplerParameters(final String name, final String description, final String sampling, final int querySetSize) { + public AbstractSamplerParameters(final String sampler, final String name, final String description, final String sampling, final int querySetSize) { + this.sampler = sampler; this.name = name; this.description = description; this.sampling = sampling; @@ -38,4 +40,8 @@ public int getQuerySetSize() { return querySetSize; } + public String getSampler() { + return sampler; + } + } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java index 3149668..dc317e2 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java @@ -11,7 +11,7 @@ public class AllQueriesQuerySamplerParameters extends AbstractSamplerParameters { public AllQueriesQuerySamplerParameters(final String name, final String description, final String sampling, final int querySetSize) { - super(name, description, sampling, querySetSize); + super("all", name, description, sampling, querySetSize); } } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java index d5e4311..242941c 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java @@ -11,7 +11,7 @@ public class ProbabilityProportionalToSizeParameters extends AbstractSamplerParameters { public ProbabilityProportionalToSizeParameters(final String name, final String description, final String sampling, final int querySetSize) { - super(name, description, sampling, querySetSize); + super("pptss", name, description, sampling, querySetSize); } } diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeQuerySampler.java similarity index 94% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java rename to opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeQuerySampler.java index ebef273..0546cd8 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeQuerySampler.java @@ -25,11 +25,11 @@ * See https://opensourceconnections.com/blog/2022/10/13/how-to-succeed-with-explicit-relevance-evaluation-using-probability-proportional-to-size-sampling/ * for more information on PPTSS. */ -public class ProbabilityProportionalToSizeAbstractQuerySampler extends AbstractQuerySampler { +public class ProbabilityProportionalToSizeQuerySampler extends AbstractQuerySampler { public static final String NAME = "pptss"; - private static final Logger LOGGER = LogManager.getLogger(ProbabilityProportionalToSizeAbstractQuerySampler.class); + private static final Logger LOGGER = LogManager.getLogger(ProbabilityProportionalToSizeQuerySampler.class); private final SearchEngine searchEngine; private final ProbabilityProportionalToSizeParameters parameters; @@ -39,7 +39,7 @@ public class ProbabilityProportionalToSizeAbstractQuerySampler extends AbstractQ * @param searchEngine The OpenSearch {@link SearchEngine engine}. * @param parameters The {@link ProbabilityProportionalToSizeParameters parameters} for the sampling. */ - public ProbabilityProportionalToSizeAbstractQuerySampler(final SearchEngine searchEngine, final ProbabilityProportionalToSizeParameters parameters) { + public ProbabilityProportionalToSizeQuerySampler(final SearchEngine searchEngine, final ProbabilityProportionalToSizeParameters parameters) { this.searchEngine = searchEngine; this.parameters = parameters; } From bd2ffb3d781080c6e971e5cac2b10159358e6ef3 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Wed, 1 Jan 2025 16:56:07 -0500 Subject: [PATCH 19/41] Replacing strings. --- .../src/main/java/org/opensearch/eval/App.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/App.java b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/App.java index 7e148e5..f21a499 100644 --- a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/App.java +++ b/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/App.java @@ -102,7 +102,7 @@ public static void main(String[] args) throws Exception { final JsonObject jsonObject = jsonElement.getAsJsonObject(); final String samplerType = jsonObject.get("sampler").getAsString(); - if("all".equalsIgnoreCase(samplerType)) { + if(AllQueriesQuerySampler.NAME.equalsIgnoreCase(samplerType)) { final AllQueriesQuerySamplerParameters parameters = gson.fromJson(jsonString, AllQueriesQuerySamplerParameters.class); @@ -111,7 +111,7 @@ public static void main(String[] args) throws Exception { System.out.println("Query set created: " + querySetId); - } else if("pptss".equalsIgnoreCase(samplerType)) { + } else if(ProbabilityProportionalToSizeQuerySampler.NAME.equalsIgnoreCase(samplerType)) { final ProbabilityProportionalToSizeParameters parameters = gson.fromJson(jsonString, ProbabilityProportionalToSizeParameters.class); From eb8bb371f4d8f0d32c042f0504db134223a630ce Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Thu, 2 Jan 2025 10:49:58 -0500 Subject: [PATCH 20/41] Moving the app into the root directory. Signed-off-by: jzonthemtn --- .gitignore | 14 + .../Dockerfile => Dockerfile | 0 .../NOTICE.txt => NOTICE.txt | 0 README.md | 63 +---- build.gradle | 4 - ...docker-compose.yaml => docker-compose.yaml | 0 gradle/libs.versions.toml | 12 - gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 0 bytes gradle/wrapper/gradle-wrapper.properties | 7 - gradlew | 252 ------------------ gradlew.bat | 94 ------- .../.gitignore | 0 .../Dockerfile | 0 .../LICENSE.txt | 0 .../NOTICE.txt | 0 .../README.md | 0 .../aggs.sh | 0 .../build.gradle | 0 .../coec.png | Bin .../coec_definition.png | Bin .../docker-compose.yaml | 0 .../gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 .../licenses/gson-2.11.0.jar.sha1 | 0 .../licenses/gson-LICENSE.txt | 0 .../licenses/gson-NOTICE.txt | 0 .../runbook}/requirements.txt | 0 .../runbook}/search-quality-eval.ipynb | 0 .../scripts/cleanup.sh | 0 .../scripts/create-judgments-now.sh | 0 .../scripts/create-judgments-schedule.sh | 0 .../scripts/create-query-set-no-sampling.sh | 0 .../create-query-set-using-pptss-sampling.sh | 0 .../scripts/delete-query-sets.sh | 0 .../scripts/get-click-through-rates.sh | 0 .../scripts/get-judgments.sh | 0 .../scripts/get-metrics.sh | 0 .../scripts/get-models.sh | 0 .../scripts/get-query-set.sh | 0 .../scripts/get-query-sets.sh | 0 .../get-rank-aggregated-clickthrough.sh | 0 .../scripts/index-sample-events.sh | 0 .../scripts/initialize-ubi.sh | 0 .../scripts/run-query-set.sh | 0 .../scripts/test-neural-query.sh | 0 .../scripts/walkthrough.sh | 0 .../settings.gradle | 0 .../SearchQualityEvaluationJobParameter.java | 0 .../SearchQualityEvaluationJobRunner.java | 0 .../eval/SearchQualityEvaluationPlugin.java | 0 .../SearchQualityEvaluationRestHandler.java | 0 .../eval/judgments/clickmodel/ClickModel.java | 0 .../clickmodel/ClickModelParameters.java | 0 .../clickmodel/coec/CoecClickModel.java | 0 .../coec/CoecClickModelParameters.java | 0 .../judgments/model/ClickthroughRate.java | 0 .../eval/judgments/model/Judgment.java | 0 .../eval/judgments/model/QuerySetQuery.java | 0 .../model/ubi/event/EventAttributes.java | 0 .../model/ubi/event/EventObject.java | 0 .../judgments/model/ubi/event/Position.java | 0 .../judgments/model/ubi/event/UbiEvent.java | 0 .../model/ubi/query/QueryResponse.java | 0 .../judgments/model/ubi/query/UbiQuery.java | 0 .../opensearch/OpenSearchHelper.java | 0 .../queryhash/IncrementalUserQueryHash.java | 0 .../judgments/queryhash/UserQueryHash.java | 0 .../eval/metrics/DcgSearchMetric.java | 0 .../eval/metrics/NdcgSearchMetric.java | 0 .../eval/metrics/PrecisionSearchMetric.java | 0 .../opensearch/eval/metrics/SearchMetric.java | 0 .../eval/runners/AbstractQuerySetRunner.java | 0 .../runners/OpenSearchQuerySetRunner.java | 0 .../opensearch/eval/runners/QueryResult.java | 0 .../eval/runners/QuerySetRunResult.java | 0 .../eval/runners/RelevanceScores.java | 0 .../eval/samplers/AbstractQuerySampler.java | 0 .../samplers/AbstractSamplerParameters.java | 0 .../eval/samplers/AllQueriesQuerySampler.java | 0 .../AllQueriesQuerySamplerParameters.java | 0 ...roportionalToSizeAbstractQuerySampler.java | 0 ...obabilityProportionalToSizeParameters.java | 0 .../org/opensearch/eval/utils/MathUtils.java | 0 .../org/opensearch/eval/utils/TimeUtils.java | 0 .../plugin-metadata/plugin-security.policy | 0 ...rch.jobscheduler.spi.JobSchedulerExtension | 0 .../eval/metrics/DcgSearchMetricTest.java | 0 .../eval/metrics/NdcgSearchMetricTest.java | 0 .../metrics/PrecisionSearchMetricTest.java | 0 .../useful_queries.txt | 0 .../.gitignore | 14 - .../LICENSE.txt | 175 ------------ .../README.md | 72 ----- .../pom.xml => pom.xml | 9 + .../queryset.json => queryset.json | 0 .../run.sh => run.sh | 0 .../sampling.json => sampling.json | 0 settings.gradle | 3 - .../main/java/org/opensearch/eval/App.java | 0 .../java/org/opensearch/eval/Constants.java | 0 .../eval/engine/OpenSearchEngine.java | 0 .../opensearch/eval/engine/SearchEngine.java | 0 .../eval/judgments/clickmodel/ClickModel.java | 0 .../clickmodel/ClickModelParameters.java | 0 .../clickmodel/coec/CoecClickModel.java | 0 .../coec/CoecClickModelParameters.java | 0 .../queryhash/IncrementalUserQueryHash.java | 0 .../judgments/queryhash/UserQueryHash.java | 0 .../eval/metrics/DcgSearchMetric.java | 0 .../eval/metrics/NdcgSearchMetric.java | 0 .../eval/metrics/PrecisionSearchMetric.java | 0 .../opensearch/eval/metrics/SearchMetric.java | 0 .../eval/model/ClickthroughRate.java | 0 .../opensearch/eval/model/QuerySetQuery.java | 0 .../eval/model/data/AbstractData.java | 0 .../eval/model/data/ClickThroughRate.java | 0 .../opensearch/eval/model/data/Judgment.java | 0 .../eval/model/data/QueryResultMetric.java | 0 .../opensearch/eval/model/data/QuerySet.java | 0 .../data/RankAggregatedClickThrough.java | 0 .../eval/model/ubi/event/EventAttributes.java | 0 .../eval/model/ubi/event/EventObject.java | 0 .../eval/model/ubi/event/Position.java | 0 .../eval/model/ubi/event/UbiEvent.java | 0 .../eval/model/ubi/query/QueryResponse.java | 0 .../eval/model/ubi/query/UbiQuery.java | 0 .../eval/runners/AbstractQuerySetRunner.java | 0 .../runners/OpenSearchQuerySetRunner.java | 0 .../opensearch/eval/runners/QueryResult.java | 0 .../eval/runners/QuerySetRunResult.java | 0 .../eval/runners/RelevanceScores.java | 0 .../eval/runners/RunQuerySetParameters.java | 0 .../eval/samplers/AbstractQuerySampler.java | 0 .../samplers/AbstractSamplerParameters.java | 0 .../eval/samplers/AllQueriesQuerySampler.java | 0 .../AllQueriesQuerySamplerParameters.java | 0 ...obabilityProportionalToSizeParameters.java | 0 ...abilityProportionalToSizeQuerySampler.java | 0 .../org/opensearch/eval/utils/MathUtils.java | 0 .../org/opensearch/eval/utils/TimeUtils.java | 0 .../src => src}/main/resources/log4j2.xml | 0 .../eval/metrics/DcgSearchMetricTest.java | 0 .../eval/metrics/NdcgSearchMetricTest.java | 0 .../metrics/PrecisionSearchMetricTest.java | 0 145 files changed, 28 insertions(+), 691 deletions(-) rename opensearch-search-quality-evaluation-framework/Dockerfile => Dockerfile (100%) rename opensearch-search-quality-evaluation-framework/NOTICE.txt => NOTICE.txt (100%) delete mode 100644 build.gradle rename opensearch-search-quality-evaluation-framework/docker-compose.yaml => docker-compose.yaml (100%) delete mode 100644 gradle/libs.versions.toml delete mode 100644 gradle/wrapper/gradle-wrapper.jar delete mode 100644 gradle/wrapper/gradle-wrapper.properties delete mode 100755 gradlew delete mode 100644 gradlew.bat rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/.gitignore (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/Dockerfile (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/LICENSE.txt (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/NOTICE.txt (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/README.md (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/aggs.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/build.gradle (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/coec.png (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/coec_definition.png (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/docker-compose.yaml (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/gradle.properties (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/gradle/wrapper/gradle-wrapper.jar (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/gradle/wrapper/gradle-wrapper.properties (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/licenses/gson-2.11.0.jar.sha1 (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/licenses/gson-LICENSE.txt (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/licenses/gson-NOTICE.txt (100%) rename {runbook => legacy-plugin/runbook}/requirements.txt (100%) rename {runbook => legacy-plugin/runbook}/search-quality-eval.ipynb (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/cleanup.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/create-judgments-now.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/create-judgments-schedule.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/create-query-set-no-sampling.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/create-query-set-using-pptss-sampling.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/delete-query-sets.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/get-click-through-rates.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/get-judgments.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/get-metrics.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/get-models.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/get-query-set.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/get-query-sets.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/get-rank-aggregated-clickthrough.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/index-sample-events.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/initialize-ubi.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/run-query-set.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/test-neural-query.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/scripts/walkthrough.sh (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/settings.gradle (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/model/Judgment.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/src/main/java/org/opensearch/eval/metrics/SearchMetric.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/src/main/java/org/opensearch/eval/runners/QueryResult.java (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/src/main/java/org/opensearch/eval/runners/RelevanceScores.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/src/main/java/org/opensearch/eval/utils/MathUtils.java (100%) rename {opensearch-search-quality-evaluation-framework => legacy-plugin}/src/main/java/org/opensearch/eval/utils/TimeUtils.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/plugin-metadata/plugin-security.policy (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/main/resources/META-INF/services/org.opensearch.jobscheduler.spi.JobSchedulerExtension (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java (100%) rename {opensearch-search-quality-evaluation-plugin => legacy-plugin}/useful_queries.txt (100%) delete mode 100644 opensearch-search-quality-evaluation-plugin/.gitignore delete mode 100644 opensearch-search-quality-evaluation-plugin/LICENSE.txt delete mode 100644 opensearch-search-quality-evaluation-plugin/README.md rename opensearch-search-quality-evaluation-framework/pom.xml => pom.xml (91%) rename opensearch-search-quality-evaluation-framework/queryset.json => queryset.json (100%) rename opensearch-search-quality-evaluation-framework/run.sh => run.sh (100%) rename opensearch-search-quality-evaluation-framework/sampling.json => sampling.json (100%) delete mode 100644 settings.gradle rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/App.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/Constants.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/engine/OpenSearchEngine.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/engine/SearchEngine.java (100%) rename {opensearch-search-quality-evaluation-plugin/src => src}/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java (100%) rename {opensearch-search-quality-evaluation-plugin/src => src}/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java (100%) rename {opensearch-search-quality-evaluation-plugin/src => src}/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java (100%) rename {opensearch-search-quality-evaluation-plugin/src => src}/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java (100%) rename {opensearch-search-quality-evaluation-plugin/src => src}/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java (100%) rename {opensearch-search-quality-evaluation-plugin/src => src}/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java (100%) rename {opensearch-search-quality-evaluation-plugin/src => src}/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java (100%) rename {opensearch-search-quality-evaluation-plugin/src => src}/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java (100%) rename {opensearch-search-quality-evaluation-plugin/src => src}/main/java/org/opensearch/eval/metrics/SearchMetric.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/model/ClickthroughRate.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/model/QuerySetQuery.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/model/data/AbstractData.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/model/data/ClickThroughRate.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/model/data/Judgment.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/model/data/QueryResultMetric.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/model/data/QuerySet.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/model/ubi/event/EventAttributes.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/model/ubi/event/EventObject.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/model/ubi/event/Position.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/model/ubi/query/QueryResponse.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java (100%) rename {opensearch-search-quality-evaluation-plugin/src => src}/main/java/org/opensearch/eval/runners/QueryResult.java (100%) rename {opensearch-search-quality-evaluation-plugin/src => src}/main/java/org/opensearch/eval/runners/QuerySetRunResult.java (100%) rename {opensearch-search-quality-evaluation-plugin/src => src}/main/java/org/opensearch/eval/runners/RelevanceScores.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/runners/RunQuerySetParameters.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeQuerySampler.java (100%) rename {opensearch-search-quality-evaluation-plugin/src => src}/main/java/org/opensearch/eval/utils/MathUtils.java (100%) rename {opensearch-search-quality-evaluation-plugin/src => src}/main/java/org/opensearch/eval/utils/TimeUtils.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/main/resources/log4j2.xml (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java (100%) rename {opensearch-search-quality-evaluation-framework/src => src}/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java (100%) diff --git a/.gitignore b/.gitignore index 9beb788..488f99c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,17 @@ gradle-app.setting .project .classpath +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +# intellij files +.idea/ +*.iml +*.ipr +*.iws +build-idea/ +out/ + diff --git a/opensearch-search-quality-evaluation-framework/Dockerfile b/Dockerfile similarity index 100% rename from opensearch-search-quality-evaluation-framework/Dockerfile rename to Dockerfile diff --git a/opensearch-search-quality-evaluation-framework/NOTICE.txt b/NOTICE.txt similarity index 100% rename from opensearch-search-quality-evaluation-framework/NOTICE.txt rename to NOTICE.txt diff --git a/README.md b/README.md index 3c061fd..9fdc516 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,11 @@ -# Search Evaluation Framework +# OpenSearch Evaluation Framework -This repository contains the search quality evaluation framework as described in the [RFC](https://github.com/opensearch-project/OpenSearch/issues/15354). +This is an application to perform search evaluation. -Note: Some of the data files in this repository are tracked by `git lfs`. +## Building -## Repository Contents - -* `data` - The data directory contains scripts for creating random UBI queries and events for purposes of development and testing. -* `opensearch-search-quality-evaluation-plugin` - An OpenSearch plugin that extends the OpenSearch Scheduler plugin that provides the ability to generate scheduled (and on-demand) implicit judgments from UBI data. -* `opensearch-search-quality-implicit-judgments` - A standalone Java application to generate implicit judgments from indexed UBI data. - -## OpenSearch Search Quality Evaluation Plugin - -This is an OpenSearch plugin that extends the OpenSearch Scheduler plugin that provides the ability to generate scheduled (and on-demand) implicit judgments from UBI data. - -To use the plugin: - -``` -./gradlew build -cd opensearch-search-quality-evaluation-plugin -docker compose build -docker compose up -``` - -To create a schedule to generate implicit judgments: +Build the project from the top-level directory to build all projects. ``` -curl -s -X POST "http://localhost:9200/_plugins/search_quality_eval/schedule?id=1&click_model=coec&job_name=test&interval=60" | jq -``` - -See the created job: - -``` -curl -s http://localhost:9200/search_quality_eval_scheduled_jobs/_search | jq -``` - -To run an on-demand job without scheduling: - +mvn clean install ``` -curl -X POST "http://localhost:9200/_plugins/search_quality_eval/judgments?click_model=coec&max_rank=20" | jq -``` - -To see the job runs: - -``` -curl -X POST "http://localhost:9200/search_quality_eval_completed_jobs/_search" | jq -``` - -See the first 10 judgments: - -``` -curl -s http://localhost:9200/judgments/_search | jq -``` - -## OpenSearch Search Quality Implicit Judgments - -This is a standalone Java application to generate implicit judgments from indexed UBI data. It runs outside OpenSearch and queries the UBI indexes to get the data for calculating the implicit judgments. - -To run it, run the `org.opensearch.eval.App` class. This will connect to OpenSearch running on `localhost:9200`. It expects the `ubi_events` and `ubi_queries` indexes to exist and be populated. - -## License - -This code is licensed under the Apache 2.0 License. See [LICENSE.txt](LICENSE.txt). diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 4cf5a69..0000000 --- a/build.gradle +++ /dev/null @@ -1,4 +0,0 @@ -allprojects { - group 'org.opensearch' - version '1.0.0-SNAPSHOT' -} \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/docker-compose.yaml b/docker-compose.yaml similarity index 100% rename from opensearch-search-quality-evaluation-framework/docker-compose.yaml rename to docker-compose.yaml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml deleted file mode 100644 index 1aab14a..0000000 --- a/gradle/libs.versions.toml +++ /dev/null @@ -1,12 +0,0 @@ -# This file was generated by the Gradle 'init' task. -# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format - -[versions] -commons-math3 = "3.6.1" -guava = "33.2.1-jre" -junit = "4.13.2" - -[libraries] -commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = "commons-math3" } -guava = { module = "com.google.guava:guava", version.ref = "guava" } -junit = { module = "junit:junit", version.ref = "junit" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index a4b76b9530d66f5e68d973ea569d8e19de379189..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43583 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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 GZ~q4|mN%#X diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9355b41..0000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew deleted file mode 100755 index f5feea6..0000000 --- a/gradlew +++ /dev/null @@ -1,252 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index 9d21a21..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/opensearch-search-quality-evaluation-framework/.gitignore b/legacy-plugin/.gitignore similarity index 100% rename from opensearch-search-quality-evaluation-framework/.gitignore rename to legacy-plugin/.gitignore diff --git a/opensearch-search-quality-evaluation-plugin/Dockerfile b/legacy-plugin/Dockerfile similarity index 100% rename from opensearch-search-quality-evaluation-plugin/Dockerfile rename to legacy-plugin/Dockerfile diff --git a/opensearch-search-quality-evaluation-framework/LICENSE.txt b/legacy-plugin/LICENSE.txt similarity index 100% rename from opensearch-search-quality-evaluation-framework/LICENSE.txt rename to legacy-plugin/LICENSE.txt diff --git a/opensearch-search-quality-evaluation-plugin/NOTICE.txt b/legacy-plugin/NOTICE.txt similarity index 100% rename from opensearch-search-quality-evaluation-plugin/NOTICE.txt rename to legacy-plugin/NOTICE.txt diff --git a/opensearch-search-quality-evaluation-framework/README.md b/legacy-plugin/README.md similarity index 100% rename from opensearch-search-quality-evaluation-framework/README.md rename to legacy-plugin/README.md diff --git a/opensearch-search-quality-evaluation-plugin/aggs.sh b/legacy-plugin/aggs.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/aggs.sh rename to legacy-plugin/aggs.sh diff --git a/opensearch-search-quality-evaluation-plugin/build.gradle b/legacy-plugin/build.gradle similarity index 100% rename from opensearch-search-quality-evaluation-plugin/build.gradle rename to legacy-plugin/build.gradle diff --git a/opensearch-search-quality-evaluation-plugin/coec.png b/legacy-plugin/coec.png similarity index 100% rename from opensearch-search-quality-evaluation-plugin/coec.png rename to legacy-plugin/coec.png diff --git a/opensearch-search-quality-evaluation-plugin/coec_definition.png b/legacy-plugin/coec_definition.png similarity index 100% rename from opensearch-search-quality-evaluation-plugin/coec_definition.png rename to legacy-plugin/coec_definition.png diff --git a/opensearch-search-quality-evaluation-plugin/docker-compose.yaml b/legacy-plugin/docker-compose.yaml similarity index 100% rename from opensearch-search-quality-evaluation-plugin/docker-compose.yaml rename to legacy-plugin/docker-compose.yaml diff --git a/opensearch-search-quality-evaluation-plugin/gradle.properties b/legacy-plugin/gradle.properties similarity index 100% rename from opensearch-search-quality-evaluation-plugin/gradle.properties rename to legacy-plugin/gradle.properties diff --git a/opensearch-search-quality-evaluation-plugin/gradle/wrapper/gradle-wrapper.jar b/legacy-plugin/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from opensearch-search-quality-evaluation-plugin/gradle/wrapper/gradle-wrapper.jar rename to legacy-plugin/gradle/wrapper/gradle-wrapper.jar diff --git a/opensearch-search-quality-evaluation-plugin/gradle/wrapper/gradle-wrapper.properties b/legacy-plugin/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from opensearch-search-quality-evaluation-plugin/gradle/wrapper/gradle-wrapper.properties rename to legacy-plugin/gradle/wrapper/gradle-wrapper.properties diff --git a/opensearch-search-quality-evaluation-plugin/licenses/gson-2.11.0.jar.sha1 b/legacy-plugin/licenses/gson-2.11.0.jar.sha1 similarity index 100% rename from opensearch-search-quality-evaluation-plugin/licenses/gson-2.11.0.jar.sha1 rename to legacy-plugin/licenses/gson-2.11.0.jar.sha1 diff --git a/opensearch-search-quality-evaluation-plugin/licenses/gson-LICENSE.txt b/legacy-plugin/licenses/gson-LICENSE.txt similarity index 100% rename from opensearch-search-quality-evaluation-plugin/licenses/gson-LICENSE.txt rename to legacy-plugin/licenses/gson-LICENSE.txt diff --git a/opensearch-search-quality-evaluation-plugin/licenses/gson-NOTICE.txt b/legacy-plugin/licenses/gson-NOTICE.txt similarity index 100% rename from opensearch-search-quality-evaluation-plugin/licenses/gson-NOTICE.txt rename to legacy-plugin/licenses/gson-NOTICE.txt diff --git a/runbook/requirements.txt b/legacy-plugin/runbook/requirements.txt similarity index 100% rename from runbook/requirements.txt rename to legacy-plugin/runbook/requirements.txt diff --git a/runbook/search-quality-eval.ipynb b/legacy-plugin/runbook/search-quality-eval.ipynb similarity index 100% rename from runbook/search-quality-eval.ipynb rename to legacy-plugin/runbook/search-quality-eval.ipynb diff --git a/opensearch-search-quality-evaluation-plugin/scripts/cleanup.sh b/legacy-plugin/scripts/cleanup.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/cleanup.sh rename to legacy-plugin/scripts/cleanup.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/create-judgments-now.sh b/legacy-plugin/scripts/create-judgments-now.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/create-judgments-now.sh rename to legacy-plugin/scripts/create-judgments-now.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/create-judgments-schedule.sh b/legacy-plugin/scripts/create-judgments-schedule.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/create-judgments-schedule.sh rename to legacy-plugin/scripts/create-judgments-schedule.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/create-query-set-no-sampling.sh b/legacy-plugin/scripts/create-query-set-no-sampling.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/create-query-set-no-sampling.sh rename to legacy-plugin/scripts/create-query-set-no-sampling.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/create-query-set-using-pptss-sampling.sh b/legacy-plugin/scripts/create-query-set-using-pptss-sampling.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/create-query-set-using-pptss-sampling.sh rename to legacy-plugin/scripts/create-query-set-using-pptss-sampling.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/delete-query-sets.sh b/legacy-plugin/scripts/delete-query-sets.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/delete-query-sets.sh rename to legacy-plugin/scripts/delete-query-sets.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/get-click-through-rates.sh b/legacy-plugin/scripts/get-click-through-rates.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/get-click-through-rates.sh rename to legacy-plugin/scripts/get-click-through-rates.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/get-judgments.sh b/legacy-plugin/scripts/get-judgments.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/get-judgments.sh rename to legacy-plugin/scripts/get-judgments.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/get-metrics.sh b/legacy-plugin/scripts/get-metrics.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/get-metrics.sh rename to legacy-plugin/scripts/get-metrics.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/get-models.sh b/legacy-plugin/scripts/get-models.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/get-models.sh rename to legacy-plugin/scripts/get-models.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/get-query-set.sh b/legacy-plugin/scripts/get-query-set.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/get-query-set.sh rename to legacy-plugin/scripts/get-query-set.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/get-query-sets.sh b/legacy-plugin/scripts/get-query-sets.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/get-query-sets.sh rename to legacy-plugin/scripts/get-query-sets.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/get-rank-aggregated-clickthrough.sh b/legacy-plugin/scripts/get-rank-aggregated-clickthrough.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/get-rank-aggregated-clickthrough.sh rename to legacy-plugin/scripts/get-rank-aggregated-clickthrough.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/index-sample-events.sh b/legacy-plugin/scripts/index-sample-events.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/index-sample-events.sh rename to legacy-plugin/scripts/index-sample-events.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/initialize-ubi.sh b/legacy-plugin/scripts/initialize-ubi.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/initialize-ubi.sh rename to legacy-plugin/scripts/initialize-ubi.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/run-query-set.sh b/legacy-plugin/scripts/run-query-set.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/run-query-set.sh rename to legacy-plugin/scripts/run-query-set.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/test-neural-query.sh b/legacy-plugin/scripts/test-neural-query.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/test-neural-query.sh rename to legacy-plugin/scripts/test-neural-query.sh diff --git a/opensearch-search-quality-evaluation-plugin/scripts/walkthrough.sh b/legacy-plugin/scripts/walkthrough.sh similarity index 100% rename from opensearch-search-quality-evaluation-plugin/scripts/walkthrough.sh rename to legacy-plugin/scripts/walkthrough.sh diff --git a/opensearch-search-quality-evaluation-plugin/settings.gradle b/legacy-plugin/settings.gradle similarity index 100% rename from opensearch-search-quality-evaluation-plugin/settings.gradle rename to legacy-plugin/settings.gradle diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java b/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java rename to legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java b/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java rename to legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java b/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java rename to legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java b/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java rename to legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/Judgment.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/Judgment.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/Judgment.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/Judgment.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java rename to legacy-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java b/legacy-plugin/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java rename to legacy-plugin/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java b/legacy-plugin/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java rename to legacy-plugin/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java b/legacy-plugin/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java rename to legacy-plugin/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/SearchMetric.java b/legacy-plugin/src/main/java/org/opensearch/eval/metrics/SearchMetric.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/metrics/SearchMetric.java rename to legacy-plugin/src/main/java/org/opensearch/eval/metrics/SearchMetric.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java b/legacy-plugin/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java rename to legacy-plugin/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java b/legacy-plugin/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java rename to legacy-plugin/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/QueryResult.java b/legacy-plugin/src/main/java/org/opensearch/eval/runners/QueryResult.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/QueryResult.java rename to legacy-plugin/src/main/java/org/opensearch/eval/runners/QueryResult.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java b/legacy-plugin/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java rename to legacy-plugin/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/RelevanceScores.java b/legacy-plugin/src/main/java/org/opensearch/eval/runners/RelevanceScores.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/RelevanceScores.java rename to legacy-plugin/src/main/java/org/opensearch/eval/runners/RelevanceScores.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java b/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java rename to legacy-plugin/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java b/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java rename to legacy-plugin/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java b/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java rename to legacy-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java b/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java rename to legacy-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java b/legacy-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java rename to legacy-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java b/legacy-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java rename to legacy-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/utils/MathUtils.java b/legacy-plugin/src/main/java/org/opensearch/eval/utils/MathUtils.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/utils/MathUtils.java rename to legacy-plugin/src/main/java/org/opensearch/eval/utils/MathUtils.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/utils/TimeUtils.java b/legacy-plugin/src/main/java/org/opensearch/eval/utils/TimeUtils.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/utils/TimeUtils.java rename to legacy-plugin/src/main/java/org/opensearch/eval/utils/TimeUtils.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/plugin-metadata/plugin-security.policy b/legacy-plugin/src/main/plugin-metadata/plugin-security.policy similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/plugin-metadata/plugin-security.policy rename to legacy-plugin/src/main/plugin-metadata/plugin-security.policy diff --git a/opensearch-search-quality-evaluation-plugin/src/main/resources/META-INF/services/org.opensearch.jobscheduler.spi.JobSchedulerExtension b/legacy-plugin/src/main/resources/META-INF/services/org.opensearch.jobscheduler.spi.JobSchedulerExtension similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/resources/META-INF/services/org.opensearch.jobscheduler.spi.JobSchedulerExtension rename to legacy-plugin/src/main/resources/META-INF/services/org.opensearch.jobscheduler.spi.JobSchedulerExtension diff --git a/opensearch-search-quality-evaluation-plugin/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java b/legacy-plugin/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java rename to legacy-plugin/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java diff --git a/opensearch-search-quality-evaluation-plugin/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java b/legacy-plugin/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java rename to legacy-plugin/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java diff --git a/opensearch-search-quality-evaluation-plugin/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java b/legacy-plugin/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java rename to legacy-plugin/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java diff --git a/opensearch-search-quality-evaluation-plugin/useful_queries.txt b/legacy-plugin/useful_queries.txt similarity index 100% rename from opensearch-search-quality-evaluation-plugin/useful_queries.txt rename to legacy-plugin/useful_queries.txt diff --git a/opensearch-search-quality-evaluation-plugin/.gitignore b/opensearch-search-quality-evaluation-plugin/.gitignore deleted file mode 100644 index 6c884e1..0000000 --- a/opensearch-search-quality-evaluation-plugin/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -# Ignore Gradle project-specific cache directory -.gradle - -# Ignore Gradle build output directory -build - -# intellij files -.idea/ -*.iml -*.ipr -*.iws -build-idea/ -out/ - diff --git a/opensearch-search-quality-evaluation-plugin/LICENSE.txt b/opensearch-search-quality-evaluation-plugin/LICENSE.txt deleted file mode 100644 index 67db858..0000000 --- a/opensearch-search-quality-evaluation-plugin/LICENSE.txt +++ /dev/null @@ -1,175 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. diff --git a/opensearch-search-quality-evaluation-plugin/README.md b/opensearch-search-quality-evaluation-plugin/README.md deleted file mode 100644 index 215ccce..0000000 --- a/opensearch-search-quality-evaluation-plugin/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# OpenSearch Evaluation Framework - -This is an OpenSearch plugin built on the OpenSearch job scheduler plugin. - -## API Endpoints - -| Method | Endpoint | Description | -|--------|-----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| -| `POST` | `/_plugins/search_quality_eval/queryset` | Create a query set by sampling from the `ubi_queries` index. The `name`, `description`, and `sampling` method parameters are required. | -| `POST` | `/_plugins/search_quality_eval/run` | Initiate a run of a query set. The `name` of the query set is a required parameter. | -| `POST` | `/_plugins/search_quality_eval/judgments` | Generate implicit judgments from UBI events and queries now. | -| `POST` | `/_plugins/search_quality_eval/schedule` | Create a scheduled job to generate implicit judgments. | - - -## Building - -Build the project from the top-level directory to build all projects. - -``` -cd .. -./gradlew build -``` - -## Running in Docker - -From this directory: - -``` -docker compose build && docker compose up -``` - -Verify the plugin is installed: - -``` -curl http://localhost:9200/_cat/plugins -``` - -In the list returned you should see: - -``` -opensearch search-quality-evaluation-plugin 2.17.1.0-SNAPSHOT -``` - -To create a schedule to generate implicit judgments: - -``` -curl -s -X POST "http://localhost:9200/_plugins/search_quality_eval/schedule?id=1&click_model=coec&job_name=test&interval=60" | jq -``` - -See the created job: - -``` -curl -s http://localhost:9200/search_quality_eval_scheduled_jobs/_search | jq -``` - -To run an on-demand job without scheduling: - -``` -curl -X POST "http://localhost:9200/_plugins/search_quality_eval/judgments?click_model=coec&max_rank=20" | jq -``` - -To see the job runs: - -``` -curl -X POST "http://localhost:9200/search_quality_eval_completed_jobs/_search" | jq -``` - -See the first 10 judgments: - -``` -curl -s http://localhost:9200/judgments/_search | jq -``` \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/pom.xml b/pom.xml similarity index 91% rename from opensearch-search-quality-evaluation-framework/pom.xml rename to pom.xml index 40d7c56..056e96e 100644 --- a/opensearch-search-quality-evaluation-framework/pom.xml +++ b/pom.xml @@ -36,6 +36,15 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + 21 + + diff --git a/opensearch-search-quality-evaluation-framework/queryset.json b/queryset.json similarity index 100% rename from opensearch-search-quality-evaluation-framework/queryset.json rename to queryset.json diff --git a/opensearch-search-quality-evaluation-framework/run.sh b/run.sh similarity index 100% rename from opensearch-search-quality-evaluation-framework/run.sh rename to run.sh diff --git a/opensearch-search-quality-evaluation-framework/sampling.json b/sampling.json similarity index 100% rename from opensearch-search-quality-evaluation-framework/sampling.json rename to sampling.json diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index b6e6b20..0000000 --- a/settings.gradle +++ /dev/null @@ -1,3 +0,0 @@ -rootProject.name = 'search-evaluation-framework' -include 'opensearch-search-quality-evaluation-plugin' -include 'opensearch-search-quality-evaluation-framework' \ No newline at end of file diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/App.java b/src/main/java/org/opensearch/eval/App.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/App.java rename to src/main/java/org/opensearch/eval/App.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/Constants.java b/src/main/java/org/opensearch/eval/Constants.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/Constants.java rename to src/main/java/org/opensearch/eval/Constants.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java b/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java rename to src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java b/src/main/java/org/opensearch/eval/engine/SearchEngine.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/engine/SearchEngine.java rename to src/main/java/org/opensearch/eval/engine/SearchEngine.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java b/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java rename to src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java b/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java rename to src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java b/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java rename to src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java b/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java rename to src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java b/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java rename to src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java b/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java rename to src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java b/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java rename to src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java b/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java rename to src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java b/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java rename to src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/metrics/SearchMetric.java b/src/main/java/org/opensearch/eval/metrics/SearchMetric.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/metrics/SearchMetric.java rename to src/main/java/org/opensearch/eval/metrics/SearchMetric.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ClickthroughRate.java b/src/main/java/org/opensearch/eval/model/ClickthroughRate.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ClickthroughRate.java rename to src/main/java/org/opensearch/eval/model/ClickthroughRate.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/QuerySetQuery.java b/src/main/java/org/opensearch/eval/model/QuerySetQuery.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/QuerySetQuery.java rename to src/main/java/org/opensearch/eval/model/QuerySetQuery.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java b/src/main/java/org/opensearch/eval/model/data/AbstractData.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/AbstractData.java rename to src/main/java/org/opensearch/eval/model/data/AbstractData.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java b/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java rename to src/main/java/org/opensearch/eval/model/data/ClickThroughRate.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/Judgment.java b/src/main/java/org/opensearch/eval/model/data/Judgment.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/Judgment.java rename to src/main/java/org/opensearch/eval/model/data/Judgment.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QueryResultMetric.java b/src/main/java/org/opensearch/eval/model/data/QueryResultMetric.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QueryResultMetric.java rename to src/main/java/org/opensearch/eval/model/data/QueryResultMetric.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QuerySet.java b/src/main/java/org/opensearch/eval/model/data/QuerySet.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/QuerySet.java rename to src/main/java/org/opensearch/eval/model/data/QuerySet.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java b/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java rename to src/main/java/org/opensearch/eval/model/data/RankAggregatedClickThrough.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventAttributes.java b/src/main/java/org/opensearch/eval/model/ubi/event/EventAttributes.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventAttributes.java rename to src/main/java/org/opensearch/eval/model/ubi/event/EventAttributes.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java b/src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java rename to src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/Position.java b/src/main/java/org/opensearch/eval/model/ubi/event/Position.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/Position.java rename to src/main/java/org/opensearch/eval/model/ubi/event/Position.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java b/src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java rename to src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/query/QueryResponse.java b/src/main/java/org/opensearch/eval/model/ubi/query/QueryResponse.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/query/QueryResponse.java rename to src/main/java/org/opensearch/eval/model/ubi/query/QueryResponse.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java b/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java rename to src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java b/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java rename to src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java b/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java rename to src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/runners/QueryResult.java b/src/main/java/org/opensearch/eval/runners/QueryResult.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/runners/QueryResult.java rename to src/main/java/org/opensearch/eval/runners/QueryResult.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java b/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java rename to src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/runners/RelevanceScores.java b/src/main/java/org/opensearch/eval/runners/RelevanceScores.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/runners/RelevanceScores.java rename to src/main/java/org/opensearch/eval/runners/RelevanceScores.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/RunQuerySetParameters.java b/src/main/java/org/opensearch/eval/runners/RunQuerySetParameters.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/runners/RunQuerySetParameters.java rename to src/main/java/org/opensearch/eval/runners/RunQuerySetParameters.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java b/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java rename to src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java b/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java rename to src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java b/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java rename to src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java b/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java rename to src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java b/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java rename to src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeQuerySampler.java b/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeQuerySampler.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeQuerySampler.java rename to src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeQuerySampler.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/utils/MathUtils.java b/src/main/java/org/opensearch/eval/utils/MathUtils.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/utils/MathUtils.java rename to src/main/java/org/opensearch/eval/utils/MathUtils.java diff --git a/opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/utils/TimeUtils.java b/src/main/java/org/opensearch/eval/utils/TimeUtils.java similarity index 100% rename from opensearch-search-quality-evaluation-plugin/src/main/java/org/opensearch/eval/utils/TimeUtils.java rename to src/main/java/org/opensearch/eval/utils/TimeUtils.java diff --git a/opensearch-search-quality-evaluation-framework/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/main/resources/log4j2.xml rename to src/main/resources/log4j2.xml diff --git a/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java b/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java rename to src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java diff --git a/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java b/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java rename to src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java diff --git a/opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java b/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java similarity index 100% rename from opensearch-search-quality-evaluation-framework/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java rename to src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java From b2ca178fb14a9ef8db5d58181902891faddece1e Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Thu, 2 Jan 2025 10:51:44 -0500 Subject: [PATCH 21/41] Moving scripts. Signed-off-by: jzonthemtn --- queryset.json => cli-runner/queryset.json | 0 run.sh => cli-runner/run.sh | 0 sampling.json => cli-runner/sampling.json | 0 create-index.sh | 7 ---- events-mapping.json | 45 ----------------------- 5 files changed, 52 deletions(-) rename queryset.json => cli-runner/queryset.json (100%) rename run.sh => cli-runner/run.sh (100%) rename sampling.json => cli-runner/sampling.json (100%) delete mode 100755 create-index.sh delete mode 100644 events-mapping.json diff --git a/queryset.json b/cli-runner/queryset.json similarity index 100% rename from queryset.json rename to cli-runner/queryset.json diff --git a/run.sh b/cli-runner/run.sh similarity index 100% rename from run.sh rename to cli-runner/run.sh diff --git a/sampling.json b/cli-runner/sampling.json similarity index 100% rename from sampling.json rename to cli-runner/sampling.json diff --git a/create-index.sh b/create-index.sh deleted file mode 100755 index 2566073..0000000 --- a/create-index.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -e - -echo "Creating ubi_events index" - -curl -X DELETE http://localhost:9200/ubi_events -curl -X PUT http://localhost:9200/ubi_events -H "Content-Type: application/json" -curl -X PUT http://localhost:9200/ubi_events/_mapping -H "Content-Type: application/json" -d @./events-mapping.json diff --git a/events-mapping.json b/events-mapping.json deleted file mode 100644 index cdb1393..0000000 --- a/events-mapping.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "properties": { - "application": { "type": "keyword", "ignore_above": 256 }, - "action_name": { "type": "keyword", "ignore_above": 100 }, - "client_id": { "type": "keyword", "ignore_above": 100 }, - "message": { "type": "keyword", "ignore_above": 1024 }, - "message_type": { "type": "keyword", "ignore_above": 100 }, - "timestamp": { - "type": "date", - "format":"strict_date_time", - "ignore_malformed": true, - "doc_values": true - }, - "event_attributes": { - "dynamic": true, - "properties": { - "position": { - "properties": { - "ordinal": { "type": "integer" }, - "x": { "type": "integer" }, - "y": { "type": "integer" }, - "page_depth": { "type": "integer" }, - "scroll_depth": { "type": "integer" }, - "trail": { "type": "text", - "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } - } - } - } - }, - "object": { - "properties": { - "internal_id": { "type": "keyword" }, - "object_id": { "type": "keyword", "ignore_above": 256 }, - "object_id_field": { "type": "keyword", "ignore_above": 100 }, - "name": { "type": "keyword", "ignore_above": 256 }, - "description": { "type": "text", - "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } - }, - "object_detail": { "type": "object" } - } - } - } - } - } -} \ No newline at end of file From 873449be9703b6014f7d186831032f2983d74ddb Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Thu, 2 Jan 2025 10:54:12 -0500 Subject: [PATCH 22/41] Updating github action. --- .github/workflows/build.yml | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b2dc353..9c5e159 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,23 +2,14 @@ name: Build and Test on: [push, pull_request, workflow_dispatch] jobs: build: - runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.experimental }} - strategy: - matrix: - os: [ubuntu-latest, macOS-latest, windows-latest] - jdk: [ 11, 17, 21 ] - experimental: [false] - steps: - - uses: actions/checkout@v4 - - name: Set up JDK ${{ matrix.jdk }} - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: ${{ matrix.jdk }} - cache: gradle - - name: Assemble target plugin - uses: gradle/gradle-build-action@v3 - with: - cache-disabled: true - arguments: -Dtests.security.manager=false assemble + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn --batch-mode --update-snapshots verify From 71678d3c6f594f00ba764fc5125b5d2af9c11a64 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Thu, 2 Jan 2025 10:55:05 -0500 Subject: [PATCH 23/41] Updating github action. --- .github/workflows/build.yml | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9c5e159..ab65db6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,14 +2,28 @@ name: Build and Test on: [push, pull_request, workflow_dispatch] jobs: build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 21 - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - cache: maven - - name: Build with Maven - run: mvn --batch-mode --update-snapshots verify + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.experimental }} + strategy: + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] + jdk: [ 21 ] + experimental: [false] + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - name: Cache Maven packages + uses: actions/cache@v1 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v4 + with: + distribution: adopt + java-version: ${{ matrix.jdk }} + cache: maven + - name: Build + run: mvn --batch-mode test \ No newline at end of file From d17d116dc48d9569bf094f2ca755e874239dee72 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Thu, 2 Jan 2025 10:57:49 -0500 Subject: [PATCH 24/41] Updating github action. --- .github/workflows/build.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab65db6..65dc7ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,17 +13,10 @@ jobs: - uses: actions/checkout@v4 with: lfs: true - - name: Cache Maven packages - uses: actions/cache@v1 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - name: Set up JDK ${{ matrix.jdk }} uses: actions/setup-java@v4 with: distribution: adopt java-version: ${{ matrix.jdk }} - cache: maven - name: Build run: mvn --batch-mode test \ No newline at end of file From 640b0adc36f14dcadf4c9e7afd3e7506cf58a91d Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Thu, 2 Jan 2025 10:58:45 -0500 Subject: [PATCH 25/41] Updating paths. --- cli-runner/run.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli-runner/run.sh b/cli-runner/run.sh index bb192ec..1812bec 100755 --- a/cli-runner/run.sh +++ b/cli-runner/run.sh @@ -1,10 +1,10 @@ #!/bin/bash -e # Create a click model. -java -jar ./target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -c coec +java -jar ../target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -c coec # Create a query set using sampling. -#java -jar ./target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -s sampling.json +#java -jar ../target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -s sampling.json # Run a query set. -#java -jar ./target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -r queryset.json +#java -jar ../target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -r queryset.json From b0689d7a0561553775511c425812098f31f08849 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Thu, 16 Jan 2025 09:26:47 -0500 Subject: [PATCH 26/41] Wiring up more of the query set runner. --- cli-runner/create-click-model.sh | 4 ++ cli-runner/{run.sh => create-query-set.sh} | 5 +- cli-runner/get-click-through-rates.sh | 3 + cli-runner/get-judgments.sh | 6 ++ cli-runner/get-metrics.sh | 3 + cli-runner/get-models.sh | 10 ++++ cli-runner/get-query-set.sh | 5 ++ cli-runner/get-query-sets.sh | 3 + cli-runner/initialize-ubi.sh | 5 ++ cli-runner/queryset.json | 10 ++-- cli-runner/run-query-set.sh | 4 ++ src/main/java/org/opensearch/eval/App.java | 4 +- .../eval/engine/OpenSearchEngine.java | 58 ++++++++++--------- .../opensearch/eval/engine/SearchEngine.java | 3 +- .../runners/OpenSearchQuerySetRunner.java | 5 +- .../eval/samplers/AllQueriesQuerySampler.java | 2 +- 16 files changed, 87 insertions(+), 43 deletions(-) create mode 100755 cli-runner/create-click-model.sh rename cli-runner/{run.sh => create-query-set.sh} (59%) create mode 100755 cli-runner/get-click-through-rates.sh create mode 100755 cli-runner/get-judgments.sh create mode 100755 cli-runner/get-metrics.sh create mode 100755 cli-runner/get-models.sh create mode 100755 cli-runner/get-query-set.sh create mode 100755 cli-runner/get-query-sets.sh create mode 100755 cli-runner/initialize-ubi.sh create mode 100755 cli-runner/run-query-set.sh diff --git a/cli-runner/create-click-model.sh b/cli-runner/create-click-model.sh new file mode 100755 index 0000000..09c6d13 --- /dev/null +++ b/cli-runner/create-click-model.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e + +# Create a click model. +java -jar ../target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -c coec diff --git a/cli-runner/run.sh b/cli-runner/create-query-set.sh similarity index 59% rename from cli-runner/run.sh rename to cli-runner/create-query-set.sh index 1812bec..53066f8 100755 --- a/cli-runner/run.sh +++ b/cli-runner/create-query-set.sh @@ -1,10 +1,7 @@ #!/bin/bash -e -# Create a click model. -java -jar ../target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -c coec - # Create a query set using sampling. -#java -jar ../target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -s sampling.json +java -jar ../target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -s sampling.json # Run a query set. #java -jar ../target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -r queryset.json diff --git a/cli-runner/get-click-through-rates.sh b/cli-runner/get-click-through-rates.sh new file mode 100755 index 0000000..16377a2 --- /dev/null +++ b/cli-runner/get-click-through-rates.sh @@ -0,0 +1,3 @@ +#!/bin/bash -e + +curl -s "http://localhost:9200/click_through_rates/_search" | jq diff --git a/cli-runner/get-judgments.sh b/cli-runner/get-judgments.sh new file mode 100755 index 0000000..ee3fa15 --- /dev/null +++ b/cli-runner/get-judgments.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e + +#JUDGMENT_ID=$1 +#curl -s "http://localhost:9200/judgments/_doc/${JUDGMENT_ID}" | jq + +curl -s "http://localhost:9200/judgments/_search" | jq diff --git a/cli-runner/get-metrics.sh b/cli-runner/get-metrics.sh new file mode 100755 index 0000000..4789332 --- /dev/null +++ b/cli-runner/get-metrics.sh @@ -0,0 +1,3 @@ +#!/bin/bash -e + +curl -s "http://localhost:9200/sqe_metrics_sample_data/_search" | jq diff --git a/cli-runner/get-models.sh b/cli-runner/get-models.sh new file mode 100755 index 0000000..da6c0a5 --- /dev/null +++ b/cli-runner/get-models.sh @@ -0,0 +1,10 @@ +#!/bin/bash -e + +# Get the search pipeline. +curl -s http://localhost:9200/_search/pipeline/hybrid-search-pipeline | jq + +#curl -s "http://localhost:9200/_plugins/_ml/models/_search" -H "Content-Type: application/json" -d'{ +# "query": { +# "match_all": {} +# } +# }' | jq diff --git a/cli-runner/get-query-set.sh b/cli-runner/get-query-set.sh new file mode 100755 index 0000000..c2ddce0 --- /dev/null +++ b/cli-runner/get-query-set.sh @@ -0,0 +1,5 @@ +#!/bin/bash -e + +QUERY_SET_ID="${1}" + +curl -s "http://localhost:9200/search_quality_eval_query_sets/_doc/${QUERY_SET_ID}" | jq diff --git a/cli-runner/get-query-sets.sh b/cli-runner/get-query-sets.sh new file mode 100755 index 0000000..0bcb3ff --- /dev/null +++ b/cli-runner/get-query-sets.sh @@ -0,0 +1,3 @@ +#!/bin/bash -e + +curl -s "http://localhost:9200/search_quality_eval_query_sets/_search" | jq diff --git a/cli-runner/initialize-ubi.sh b/cli-runner/initialize-ubi.sh new file mode 100755 index 0000000..46f0e2d --- /dev/null +++ b/cli-runner/initialize-ubi.sh @@ -0,0 +1,5 @@ +#!/bin/bash -e + +curl -s -X DELETE http://localhost:9200/ubi_queries,ubi_events | jq + +curl -s -X POST http://localhost:9200/_plugins/ubi/initialize | jq diff --git a/cli-runner/queryset.json b/cli-runner/queryset.json index 0f53ef3..c8a712f 100644 --- a/cli-runner/queryset.json +++ b/cli-runner/queryset.json @@ -1,10 +1,10 @@ { - "query_set_id": "", - "judgments_id": "", - "index": "", + "query_set_id": "20b2c5d3-42bf-4aaf-843e-d806e9fcd3c1", + "judgments_id": "69a69c21-f0bb-443e-bf75-bca389704fd0", + "index": "ecommerce", "search_pipeline": "", - "id_field": "", + "id_field": "asin", "k": 10, "threshold": 1.0, - "query": {} + "query": "{\"match\": {\"description\": \"#$query##\"}}" } diff --git a/cli-runner/run-query-set.sh b/cli-runner/run-query-set.sh new file mode 100755 index 0000000..91035ee --- /dev/null +++ b/cli-runner/run-query-set.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e + +# Run a query set. +java -jar ../target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -r queryset.json diff --git a/src/main/java/org/opensearch/eval/App.java b/src/main/java/org/opensearch/eval/App.java index f21a499..64c1440 100644 --- a/src/main/java/org/opensearch/eval/App.java +++ b/src/main/java/org/opensearch/eval/App.java @@ -76,7 +76,7 @@ public static void main(String[] args) throws Exception { System.out.println("Running query set..."); - final String querySetOptionsFile = cmd.getOptionValue("q"); + final String querySetOptionsFile = cmd.getOptionValue("r"); final File file = new File(querySetOptionsFile); if(file.exists()) { @@ -122,7 +122,7 @@ public static void main(String[] args) throws Exception { } else { - System.err.println("Invalid sampler."); + System.err.println("Invalid sampler: " + samplerType); } diff --git a/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java b/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java index 9e1e02b..e27163d 100644 --- a/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java +++ b/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java @@ -24,9 +24,6 @@ import org.opensearch.client.opensearch._types.aggregations.LongTermsBucket; import org.opensearch.client.opensearch._types.aggregations.StringTermsAggregate; import org.opensearch.client.opensearch._types.aggregations.StringTermsBucket; -import org.opensearch.client.opensearch._types.mapping.IntegerNumberProperty; -import org.opensearch.client.opensearch._types.mapping.Property; -import org.opensearch.client.opensearch._types.mapping.TypeMapping; import org.opensearch.client.opensearch._types.query_dsl.BoolQuery; import org.opensearch.client.opensearch._types.query_dsl.MatchQuery; import org.opensearch.client.opensearch._types.query_dsl.Query; @@ -129,20 +126,6 @@ public boolean createIndex(final String index, final String mappingJson) throws } - @Override - public boolean createIndex(String index, Map mapping) throws IOException { - - // TODO: Build the mapping. - final TypeMapping mapping2 = new TypeMapping.Builder() - .properties("age", new Property.Builder().integer(new IntegerNumberProperty.Builder().build()).build()) - .build(); - - final CreateIndexRequest createIndexRequest = new CreateIndexRequest.Builder().index(index).mappings(mapping2).build(); - - return Boolean.TRUE.equals(client.indices().create(createIndexRequest).acknowledged()); - - } - @Override public boolean deleteIndex(String index) throws IOException { @@ -351,7 +334,9 @@ public UbiQuery getQueryFromQueryId(final String queryId) throws Exception { } @Override - public List runQuery(final String index, final String query, final int k, final String userQuery, final String idField) throws IOException { + public List runQuery(final String index, final String query, final int k, final String userQuery, final String idField, final String pipeline) throws IOException { + + LOGGER.info("Running query on index {}, k = {}, userQuery = {}, idField = {}, pipeline = {}, query = {}", index, k, userQuery, idField, pipeline, query); // Replace the query placeholder with the user query. final String parsedQuery = query.replace(QUERY_PLACEHOLDER, userQuery); @@ -362,20 +347,37 @@ public List runQuery(final String index, final String query, final int k .query(encodedQuery) .build(); - final SearchRequest searchRequest = new SearchRequest.Builder() - .index(index) - .query(q -> q.wrapper(wrapperQuery)) - .from(0) - .size(k) - .build(); - - // TODO: Handle the searchPipeline if it is not null. // TODO: Only return the idField since that's all we need. + final SearchRequest searchRequest; + + if(!pipeline.isEmpty()) { + + searchRequest = new SearchRequest.Builder() + .index(index) + .query(q -> q.wrapper(wrapperQuery)) + .from(0) + .size(k) + .pipeline(pipeline) + .build(); + + } else { + + searchRequest = new SearchRequest.Builder() + .index(index) + .query(q -> q.wrapper(wrapperQuery)) + .from(0) + .size(k) + .build(); + + } final SearchResponse searchResponse = client.search(searchRequest, ObjectNode.class); final List orderedDocumentIds = new ArrayList<>(); + LOGGER.info("Encoded query: {}", encodedQuery); + LOGGER.info("Found hits: {}", searchResponse.hits().hits().size()); + for (int i = 0; i < searchResponse.hits().hits().size(); i++) { final String documentId; @@ -385,7 +387,9 @@ public List runQuery(final String index, final String query, final int k } else { // TODO: Need to check this field actually exists. // TODO: Does this work? - documentId = searchResponse.hits().hits().get(i).fields().get(idField).toString(); + final Hit hit = searchResponse.hits().hits().get(i); + documentId = hit.source().get(idField).toString(); + } orderedDocumentIds.add(documentId); diff --git a/src/main/java/org/opensearch/eval/engine/SearchEngine.java b/src/main/java/org/opensearch/eval/engine/SearchEngine.java index 9166961..3529a76 100644 --- a/src/main/java/org/opensearch/eval/engine/SearchEngine.java +++ b/src/main/java/org/opensearch/eval/engine/SearchEngine.java @@ -15,7 +15,6 @@ public abstract class SearchEngine { public abstract boolean doesIndexExist(String index) throws IOException; - public abstract boolean createIndex(String index, Map mapping) throws IOException; public abstract boolean createIndex(String index, String mapping) throws IOException; public abstract boolean deleteIndex(String index) throws IOException; @@ -31,7 +30,7 @@ public abstract class SearchEngine { public abstract Collection getJudgments(final String index) throws IOException; - public abstract List runQuery(final String index, final String query, final int k, final String userQuery, final String idField) throws IOException; + public abstract List runQuery(final String index, final String query, final int k, final String userQuery, final String idField, final String pipeline) throws IOException; public abstract String indexQuerySet(QuerySet querySet) throws IOException; public abstract Collection getUbiQueries() throws IOException; diff --git a/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java b/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java index 00dbddc..751ec02 100644 --- a/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java +++ b/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java @@ -35,7 +35,7 @@ public class OpenSearchQuerySetRunner extends AbstractQuerySetRunner { private static final Logger LOGGER = LogManager.getLogger(OpenSearchQuerySetRunner.class); - public static final String QUERY_PLACEHOLDER = "#?query##"; + public static final String QUERY_PLACEHOLDER = "#$query##"; /** * Creates a new query set runner @@ -71,7 +71,8 @@ public QuerySetRunResult run(final RunQuerySetParameters querySetParameters) thr querySetParameters.getQuery(), querySetParameters.getK(), userQuery, - querySetParameters.getIdField()); + querySetParameters.getIdField(), + querySetParameters.getSearchPipeline()); try { diff --git a/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java b/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java index bc241fa..8d6e61a 100644 --- a/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java +++ b/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java @@ -20,7 +20,7 @@ */ public class AllQueriesQuerySampler extends AbstractQuerySampler { - public static final String NAME = "none"; + public static final String NAME = "all"; private final SearchEngine searchEngine; private final AllQueriesQuerySamplerParameters parameters; From 66fa2525857ea806604b368d72814f61799c6b4c Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Fri, 17 Jan 2025 11:44:59 -0500 Subject: [PATCH 27/41] Changing to generic client for the user query running. --- docker-compose.yaml | 2 +- {cli-runner => scripts}/create-click-model.sh | 0 {cli-runner => scripts}/create-query-set.sh | 0 .../get-click-through-rates.sh | 0 {cli-runner => scripts}/get-judgments.sh | 0 {cli-runner => scripts}/get-metrics.sh | 0 {cli-runner => scripts}/get-models.sh | 0 {cli-runner => scripts}/get-query-set.sh | 0 {cli-runner => scripts}/get-query-sets.sh | 0 {cli-runner => scripts}/initialize-ubi.sh | 0 {cli-runner => scripts}/queryset.json | 4 +- {cli-runner => scripts}/run-query-set.sh | 0 {cli-runner => scripts}/sampling.json | 0 .../eval/engine/OpenSearchEngine.java | 128 ++++++++++++------ .../runners/OpenSearchQuerySetRunner.java | 2 +- 15 files changed, 90 insertions(+), 46 deletions(-) rename {cli-runner => scripts}/create-click-model.sh (100%) rename {cli-runner => scripts}/create-query-set.sh (100%) rename {cli-runner => scripts}/get-click-through-rates.sh (100%) rename {cli-runner => scripts}/get-judgments.sh (100%) rename {cli-runner => scripts}/get-metrics.sh (100%) rename {cli-runner => scripts}/get-models.sh (100%) rename {cli-runner => scripts}/get-query-set.sh (100%) rename {cli-runner => scripts}/get-query-sets.sh (100%) rename {cli-runner => scripts}/initialize-ubi.sh (100%) rename {cli-runner => scripts}/queryset.json (63%) rename {cli-runner => scripts}/run-query-set.sh (100%) rename {cli-runner => scripts}/sampling.json (100%) diff --git a/docker-compose.yaml b/docker-compose.yaml index c2ab3b7..8e7a4bd 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -7,7 +7,7 @@ services: discovery.type: single-node node.name: opensearch plugins.security.disabled: "true" - logger.level: info + logger.level: debug OPENSEARCH_INITIAL_ADMIN_PASSWORD: SuperSecretPassword_123 http.max_content_length: 500mb OPENSEARCH_JAVA_OPTS: "-Xms16g -Xmx16g" diff --git a/cli-runner/create-click-model.sh b/scripts/create-click-model.sh similarity index 100% rename from cli-runner/create-click-model.sh rename to scripts/create-click-model.sh diff --git a/cli-runner/create-query-set.sh b/scripts/create-query-set.sh similarity index 100% rename from cli-runner/create-query-set.sh rename to scripts/create-query-set.sh diff --git a/cli-runner/get-click-through-rates.sh b/scripts/get-click-through-rates.sh similarity index 100% rename from cli-runner/get-click-through-rates.sh rename to scripts/get-click-through-rates.sh diff --git a/cli-runner/get-judgments.sh b/scripts/get-judgments.sh similarity index 100% rename from cli-runner/get-judgments.sh rename to scripts/get-judgments.sh diff --git a/cli-runner/get-metrics.sh b/scripts/get-metrics.sh similarity index 100% rename from cli-runner/get-metrics.sh rename to scripts/get-metrics.sh diff --git a/cli-runner/get-models.sh b/scripts/get-models.sh similarity index 100% rename from cli-runner/get-models.sh rename to scripts/get-models.sh diff --git a/cli-runner/get-query-set.sh b/scripts/get-query-set.sh similarity index 100% rename from cli-runner/get-query-set.sh rename to scripts/get-query-set.sh diff --git a/cli-runner/get-query-sets.sh b/scripts/get-query-sets.sh similarity index 100% rename from cli-runner/get-query-sets.sh rename to scripts/get-query-sets.sh diff --git a/cli-runner/initialize-ubi.sh b/scripts/initialize-ubi.sh similarity index 100% rename from cli-runner/initialize-ubi.sh rename to scripts/initialize-ubi.sh diff --git a/cli-runner/queryset.json b/scripts/queryset.json similarity index 63% rename from cli-runner/queryset.json rename to scripts/queryset.json index c8a712f..3576cdf 100644 --- a/cli-runner/queryset.json +++ b/scripts/queryset.json @@ -2,9 +2,9 @@ "query_set_id": "20b2c5d3-42bf-4aaf-843e-d806e9fcd3c1", "judgments_id": "69a69c21-f0bb-443e-bf75-bca389704fd0", "index": "ecommerce", - "search_pipeline": "", + "search_pipeline": "hybrid_pipeline", "id_field": "asin", "k": 10, "threshold": 1.0, - "query": "{\"match\": {\"description\": \"#$query##\"}}" + "query": "{\"query\": {\"match\": {\"description\": \"#$query##\"}}}" } diff --git a/cli-runner/run-query-set.sh b/scripts/run-query-set.sh similarity index 100% rename from cli-runner/run-query-set.sh rename to scripts/run-query-set.sh diff --git a/cli-runner/sampling.json b/scripts/sampling.json similarity index 100% rename from cli-runner/sampling.json rename to scripts/sampling.json diff --git a/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java b/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java index e27163d..aae03ba 100644 --- a/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java +++ b/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java @@ -8,6 +8,7 @@ */ package org.opensearch.eval.engine; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.hc.core5.http.HttpHost; import org.apache.logging.log4j.LogManager; @@ -40,6 +41,10 @@ import org.opensearch.client.opensearch.core.bulk.IndexOperation; import org.opensearch.client.opensearch.core.search.Hit; import org.opensearch.client.opensearch.core.search.TrackHits; +import org.opensearch.client.opensearch.generic.Bodies; +import org.opensearch.client.opensearch.generic.OpenSearchGenericClient; +import org.opensearch.client.opensearch.generic.Requests; +import org.opensearch.client.opensearch.generic.Response; import org.opensearch.client.opensearch.indices.CreateIndexRequest; import org.opensearch.client.opensearch.indices.ExistsRequest; import org.opensearch.client.transport.OpenSearchTransport; @@ -336,66 +341,102 @@ public UbiQuery getQueryFromQueryId(final String queryId) throws Exception { @Override public List runQuery(final String index, final String query, final int k, final String userQuery, final String idField, final String pipeline) throws IOException { - LOGGER.info("Running query on index {}, k = {}, userQuery = {}, idField = {}, pipeline = {}, query = {}", index, k, userQuery, idField, pipeline, query); - // Replace the query placeholder with the user query. final String parsedQuery = query.replace(QUERY_PLACEHOLDER, userQuery); - final String encodedQuery = Base64.getEncoder().encodeToString(parsedQuery.getBytes(StandardCharsets.UTF_8)); + LOGGER.debug("Running query on index {}, k = {}, userQuery = {}, idField = {}, pipeline = {}, query = {}", index, k, userQuery, idField, pipeline, parsedQuery); - final WrapperQuery wrapperQuery = new WrapperQuery.Builder() - .query(encodedQuery) - .build(); + // Use a generic client to get around https://github.com/opensearch-project/OpenSearch/issues/16829 + // Refer to https://code.dblock.org/2023/10/16/making-raw-json-rest-requests-to-opensearch.html#:~:text=build()%3B,Here's%20a%20search%20example.&See%20the%20updated%20documentation%20and%20working%20demo%20for%20more%20information. + final OpenSearchGenericClient genericClient = client.generic().withClientOptions(OpenSearchGenericClient.ClientOptions.throwOnHttpErrors()); - // TODO: Only return the idField since that's all we need. - final SearchRequest searchRequest; + final Map params = new HashMap<>(); if(!pipeline.isEmpty()) { - - searchRequest = new SearchRequest.Builder() - .index(index) - .query(q -> q.wrapper(wrapperQuery)) - .from(0) - .size(k) - .pipeline(pipeline) - .build(); - - } else { - - searchRequest = new SearchRequest.Builder() - .index(index) - .query(q -> q.wrapper(wrapperQuery)) - .from(0) - .size(k) - .build(); - + params.put("search_pipeline", pipeline); } - final SearchResponse searchResponse = client.search(searchRequest, ObjectNode.class); - - final List orderedDocumentIds = new ArrayList<>(); + final Response searchResponse = genericClient.execute( + Requests.builder() + .endpoint(index + "/_search") + .method("POST") + .query(params) + .json(parsedQuery) + .build()); - LOGGER.info("Encoded query: {}", encodedQuery); - LOGGER.info("Found hits: {}", searchResponse.hits().hits().size()); + final JsonNode json = searchResponse.getBody() + .map(b -> Bodies.json(b, JsonNode.class, client._transport().jsonpMapper())) + .orElse(null); - for (int i = 0; i < searchResponse.hits().hits().size(); i++) { + final List orderedDocumentIds = new ArrayList<>(); - final String documentId; + final JsonNode hits = json.get("hits").get("hits"); + for (int i = 0; i < hits.size(); i++) { - if ("_id".equals(idField)) { - documentId = searchResponse.hits().hits().get(i).id(); + if(hits.get(i).get("_source").get(idField) != null) { + orderedDocumentIds.add(hits.get(i).get("_source").get(idField).asText()); } else { - // TODO: Need to check this field actually exists. - // TODO: Does this work? - final Hit hit = searchResponse.hits().hits().get(i); - documentId = hit.source().get(idField).toString(); - + LOGGER.info("The requested idField {} does not exist.", idField); } - orderedDocumentIds.add(documentId); - } + // The following commented code uses a wrapper query. +// final String encodedQuery = Base64.getEncoder().encodeToString(parsedQuery.getBytes(StandardCharsets.UTF_8)); + +// final WrapperQuery wrapperQuery = new WrapperQuery.Builder() +// .query(encodedQuery) +// .build(); + + // TODO: Only return the idField since that's all we need. + // final SearchRequest searchRequest; + +// if(!pipeline.isEmpty()) { +// +// searchRequest = new SearchRequest.Builder() +// .index(index) +// .query(q -> q.wrapper(wrapperQuery)) +// .from(0) +// .size(k) +// .pipeline(pipeline) +// .build(); +// +// } else { +// +// searchRequest = new SearchRequest.Builder() +// .index(index) +// .query(q -> q.wrapper(wrapperQuery)) +// .from(0) +// .size(k) +// .build(); +// +// } + +// final SearchResponse searchResponse = client.search(searchRequest, ObjectNode.class); + +// final List orderedDocumentIds = new ArrayList<>(); +// +// LOGGER.info("Encoded query: {}", encodedQuery); +// LOGGER.info("Found hits: {}", searchResponse.hits().hits().size()); +// +// for (int i = 0; i < searchResponse.hits().hits().size(); i++) { +// +// final String documentId; +// +// if ("_id".equals(idField)) { +// documentId = searchResponse.hits().hits().get(i).id(); +// } else { +// // TODO: Need to check this field actually exists. +// // TODO: Does this work? +// final Hit hit = searchResponse.hits().hits().get(i); +// documentId = hit.source().get(idField).toString(); +// +// } +// +// orderedDocumentIds.add(documentId); +// +// } + return orderedDocumentIds; } @@ -453,6 +494,9 @@ public Map> getClickthroughRate(final int maxRank) .scroll(scrollTime) .build(); + // Use the generic client to send the raw json. + // https://code.dblock.org/2023/10/16/making-raw-json-rest-requests-to-opensearch.html#:~:text=build()%3B,Here's%20a%20search%20example.&See%20the%20updated%20documentation%20and%20working%20demo%20for%20more%20information. + final SearchResponse searchResponse = client.search(searchRequest, UbiEvent.class); String scrollId = searchResponse.scrollId(); diff --git a/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java b/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java index 751ec02..4d8b4dc 100644 --- a/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java +++ b/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java @@ -122,7 +122,7 @@ public QuerySetRunResult run(final RunQuerySetParameters querySetParameters) thr return querySetRunResult; } catch (Exception ex) { - throw new RuntimeException("Unable to run query set.", ex); + throw new RuntimeException("Unable to run query set. If using a search_pipeline make sure the pipeline exists.", ex); } } From b9ed3259f69eecf2244b3b7f4ca798463669e1c4 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Fri, 17 Jan 2025 11:50:17 -0500 Subject: [PATCH 28/41] Updating comment. --- src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java b/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java index aae03ba..8a4b7cc 100644 --- a/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java +++ b/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java @@ -347,7 +347,7 @@ public List runQuery(final String index, final String query, final int k LOGGER.debug("Running query on index {}, k = {}, userQuery = {}, idField = {}, pipeline = {}, query = {}", index, k, userQuery, idField, pipeline, parsedQuery); // Use a generic client to get around https://github.com/opensearch-project/OpenSearch/issues/16829 - // Refer to https://code.dblock.org/2023/10/16/making-raw-json-rest-requests-to-opensearch.html#:~:text=build()%3B,Here's%20a%20search%20example.&See%20the%20updated%20documentation%20and%20working%20demo%20for%20more%20information. + // Refer to https://code.dblock.org/2023/10/16/making-raw-json-rest-requests-to-opensearch.html final OpenSearchGenericClient genericClient = client.generic().withClientOptions(OpenSearchGenericClient.ClientOptions.throwOnHttpErrors()); final Map params = new HashMap<>(); From 3bc20654c75705f3fe62755a4173cadf3e2b4d56 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Fri, 17 Jan 2025 12:56:49 -0500 Subject: [PATCH 29/41] Adding script to make a search pipeline. --- scripts/create-pipeline.sh | 26 ++++++++++++++++++++++++++ scripts/get-metrics.sh | 3 --- 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100755 scripts/create-pipeline.sh delete mode 100755 scripts/get-metrics.sh diff --git a/scripts/create-pipeline.sh b/scripts/create-pipeline.sh new file mode 100755 index 0000000..24342d7 --- /dev/null +++ b/scripts/create-pipeline.sh @@ -0,0 +1,26 @@ +#!/bin/bash -e + +curl -X PUT http://localhost:9200/_search/pipeline/hybrid_pipeline -H "Content-type: application/json" -d' +{ + "request_processors": [ + { + "filter_query" : { + "tag" : "tag1", + "description" : "This processor is going to restrict to publicly visible documents", + "query" : { + "term": { + "visibility": "public" + } + } + } + } + ], + "response_processors": [ + { + "rename_field": { + "field": "message", + "target_field": "notification" + } + } + ] +}' diff --git a/scripts/get-metrics.sh b/scripts/get-metrics.sh deleted file mode 100755 index 4789332..0000000 --- a/scripts/get-metrics.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -e - -curl -s "http://localhost:9200/sqe_metrics_sample_data/_search" | jq From f76c5f9b9863a3d549bdc48f8ea718706c86c628 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Mon, 20 Jan 2025 14:58:21 -0500 Subject: [PATCH 30/41] Parameterizing the opensearch host. Signed-off-by: jzonthemtn --- pom.xml | 2 ++ scripts/create-click-model.sh | 2 +- .../{sampling.json => create-query-set.json} | 0 scripts/create-query-set.sh | 5 +---- .../create-pipeline.sh | 0 .../get-click-through-rates.sh | 0 .../{ => opensearch-scripts}/get-judgments.sh | 0 scripts/{ => opensearch-scripts}/get-models.sh | 0 .../{ => opensearch-scripts}/get-query-set.sh | 0 .../{ => opensearch-scripts}/get-query-sets.sh | 0 .../{ => opensearch-scripts}/initialize-ubi.sh | 0 scripts/{queryset.json => run-query-set.json} | 0 scripts/run-query-set.sh | 2 +- src/main/java/org/opensearch/eval/App.java | 18 ++++++++++++++---- .../eval/engine/OpenSearchEngine.java | 6 +++--- 15 files changed, 22 insertions(+), 13 deletions(-) rename scripts/{sampling.json => create-query-set.json} (100%) rename scripts/{ => opensearch-scripts}/create-pipeline.sh (100%) rename scripts/{ => opensearch-scripts}/get-click-through-rates.sh (100%) rename scripts/{ => opensearch-scripts}/get-judgments.sh (100%) rename scripts/{ => opensearch-scripts}/get-models.sh (100%) rename scripts/{ => opensearch-scripts}/get-query-set.sh (100%) rename scripts/{ => opensearch-scripts}/get-query-sets.sh (100%) rename scripts/{ => opensearch-scripts}/initialize-ubi.sh (100%) rename scripts/{queryset.json => run-query-set.json} (100%) diff --git a/pom.xml b/pom.xml index 056e96e..75f41e9 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ maven-assembly-plugin + false true @@ -25,6 +26,7 @@ jar-with-dependencies + search-evaluation-framework diff --git a/scripts/create-click-model.sh b/scripts/create-click-model.sh index 09c6d13..e4f343c 100755 --- a/scripts/create-click-model.sh +++ b/scripts/create-click-model.sh @@ -1,4 +1,4 @@ #!/bin/bash -e # Create a click model. -java -jar ../target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -c coec +java -jar ../target/search-evaluation-framework.jar -o http://localhost:9200 -c coec diff --git a/scripts/sampling.json b/scripts/create-query-set.json similarity index 100% rename from scripts/sampling.json rename to scripts/create-query-set.json diff --git a/scripts/create-query-set.sh b/scripts/create-query-set.sh index 53066f8..7a221c6 100755 --- a/scripts/create-query-set.sh +++ b/scripts/create-query-set.sh @@ -1,7 +1,4 @@ #!/bin/bash -e # Create a query set using sampling. -java -jar ../target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -s sampling.json - -# Run a query set. -#java -jar ../target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -r queryset.json +java -jar ../target/search-evaluation-framework.jar -s create-query-set.json diff --git a/scripts/create-pipeline.sh b/scripts/opensearch-scripts/create-pipeline.sh similarity index 100% rename from scripts/create-pipeline.sh rename to scripts/opensearch-scripts/create-pipeline.sh diff --git a/scripts/get-click-through-rates.sh b/scripts/opensearch-scripts/get-click-through-rates.sh similarity index 100% rename from scripts/get-click-through-rates.sh rename to scripts/opensearch-scripts/get-click-through-rates.sh diff --git a/scripts/get-judgments.sh b/scripts/opensearch-scripts/get-judgments.sh similarity index 100% rename from scripts/get-judgments.sh rename to scripts/opensearch-scripts/get-judgments.sh diff --git a/scripts/get-models.sh b/scripts/opensearch-scripts/get-models.sh similarity index 100% rename from scripts/get-models.sh rename to scripts/opensearch-scripts/get-models.sh diff --git a/scripts/get-query-set.sh b/scripts/opensearch-scripts/get-query-set.sh similarity index 100% rename from scripts/get-query-set.sh rename to scripts/opensearch-scripts/get-query-set.sh diff --git a/scripts/get-query-sets.sh b/scripts/opensearch-scripts/get-query-sets.sh similarity index 100% rename from scripts/get-query-sets.sh rename to scripts/opensearch-scripts/get-query-sets.sh diff --git a/scripts/initialize-ubi.sh b/scripts/opensearch-scripts/initialize-ubi.sh similarity index 100% rename from scripts/initialize-ubi.sh rename to scripts/opensearch-scripts/initialize-ubi.sh diff --git a/scripts/queryset.json b/scripts/run-query-set.json similarity index 100% rename from scripts/queryset.json rename to scripts/run-query-set.json diff --git a/scripts/run-query-set.sh b/scripts/run-query-set.sh index 91035ee..9e0f720 100755 --- a/scripts/run-query-set.sh +++ b/scripts/run-query-set.sh @@ -1,4 +1,4 @@ #!/bin/bash -e # Run a query set. -java -jar ../target/search-evaluation-framework-1.0.0-SNAPSHOT-jar-with-dependencies.jar -r queryset.json +java -jar ../target/search-evaluation-framework.jar -o http://localhost:9200 -r run-query-set.json diff --git a/src/main/java/org/opensearch/eval/App.java b/src/main/java/org/opensearch/eval/App.java index 64c1440..7bcc314 100644 --- a/src/main/java/org/opensearch/eval/App.java +++ b/src/main/java/org/opensearch/eval/App.java @@ -31,6 +31,7 @@ import org.opensearch.eval.samplers.ProbabilityProportionalToSizeQuerySampler; import java.io.File; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -45,14 +46,23 @@ public static void main(String[] args) throws Exception { final Gson gson = new Gson(); final Options options = new Options(); - options.addOption("c", true, "create a click model"); - options.addOption("s", true, "create a query set using sampling"); - options.addOption("r", true, "run a query set"); + options.addOption("c", "create-click-model", true, "create a click model"); + options.addOption("s", "create-query-set", true, "create a query set using sampling"); + options.addOption("r", "run-query-set", true, "run a query set"); + options.addOption("o", "opensearch", true, "OpenSearch URL, e.g. http://localhost:9200"); final CommandLineParser parser = new DefaultParser(); final CommandLine cmd = parser.parse(options, args); - final SearchEngine searchEngine = new OpenSearchEngine(); + final URI uri; + if(cmd.hasOption("o")) { + uri = URI.create(cmd.getOptionValue("o")); + } else { + System.out.println("No OpenSearch host given so defaulting to http://localhost:9200"); + uri = URI.create("http://localhost:9200"); + } + + final SearchEngine searchEngine = new OpenSearchEngine(uri); if(cmd.hasOption("c")) { diff --git a/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java b/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java index 8a4b7cc..621d5ef 100644 --- a/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java +++ b/src/main/java/org/opensearch/eval/engine/OpenSearchEngine.java @@ -63,6 +63,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; @@ -92,11 +93,10 @@ public class OpenSearchEngine extends SearchEngine { // Used to cache the query ID->user_query to avoid unnecessary lookups to OpenSearch. private static final Map userQueryCache = new HashMap<>(); - public OpenSearchEngine() { + public OpenSearchEngine(final URI uri) { - // TODO: Parameterize the host. final HttpHost[] hosts = new HttpHost[] { - new HttpHost("http", "localhost", 9200) + HttpHost.create(uri) }; final OpenSearchTransport transport = ApacheHttpClient5TransportBuilder From b58b3fefbd173501a41c2bf351be7e8a08510202 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 21 Jan 2025 08:27:27 -0500 Subject: [PATCH 31/41] Updating pipeline name. --- scripts/run-query-set.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/run-query-set.json b/scripts/run-query-set.json index 3576cdf..aa725f4 100644 --- a/scripts/run-query-set.json +++ b/scripts/run-query-set.json @@ -2,7 +2,7 @@ "query_set_id": "20b2c5d3-42bf-4aaf-843e-d806e9fcd3c1", "judgments_id": "69a69c21-f0bb-443e-bf75-bca389704fd0", "index": "ecommerce", - "search_pipeline": "hybrid_pipeline", + "search_pipeline": "filter_pipeline", "id_field": "asin", "k": 10, "threshold": 1.0, From bfb6b7f994bd37aacf3cb1b6e002a80646096f38 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 21 Jan 2025 08:30:44 -0500 Subject: [PATCH 32/41] Fixing query size with >=. --- .../org/opensearch/eval/samplers/AllQueriesQuerySampler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java b/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java index 8d6e61a..54a78b5 100644 --- a/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java +++ b/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java @@ -51,7 +51,7 @@ public String sample() throws Exception { queries.merge(ubiQuery.getUserQuery(), 1L, Long::sum); // Will be useful for paging once implemented. - if(queries.size() > parameters.getQuerySetSize()) { + if(queries.size() >= parameters.getQuerySetSize()) { break; } From e1542624665d1ef9354743b3154c39693f64f450 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 21 Jan 2025 08:33:46 -0500 Subject: [PATCH 33/41] Adding userId to UbiEvent. --- .../opensearch/eval/model/ubi/event/UbiEvent.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java b/src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java index 279356a..34306af 100644 --- a/src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java +++ b/src/main/java/org/opensearch/eval/model/ubi/event/UbiEvent.java @@ -16,6 +16,10 @@ */ public class UbiEvent { + @JsonProperty("user_id") + @SerializedName("user_id") + private String userId; + @JsonProperty("action_name") @SerializedName("action_name") private String actionName; @@ -152,4 +156,12 @@ public String getMessageType() { public void setMessageType(String messageType) { this.messageType = messageType; } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } } From 5a2cb162682c0b7d8d3358565aca91eea7030566 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 21 Jan 2025 09:16:21 -0500 Subject: [PATCH 34/41] Persisting the query run results. --- scripts/opensearch-scripts/get-metrics.sh | 3 +++ scripts/opensearch-scripts/get-models.sh | 10 ---------- scripts/run-query-set.json | 6 +++--- .../eval/judgments/clickmodel/coec/CoecClickModel.java | 2 +- .../eval/runners/OpenSearchQuerySetRunner.java | 4 ++++ 5 files changed, 11 insertions(+), 14 deletions(-) create mode 100755 scripts/opensearch-scripts/get-metrics.sh delete mode 100755 scripts/opensearch-scripts/get-models.sh diff --git a/scripts/opensearch-scripts/get-metrics.sh b/scripts/opensearch-scripts/get-metrics.sh new file mode 100755 index 0000000..4789332 --- /dev/null +++ b/scripts/opensearch-scripts/get-metrics.sh @@ -0,0 +1,3 @@ +#!/bin/bash -e + +curl -s "http://localhost:9200/sqe_metrics_sample_data/_search" | jq diff --git a/scripts/opensearch-scripts/get-models.sh b/scripts/opensearch-scripts/get-models.sh deleted file mode 100755 index da6c0a5..0000000 --- a/scripts/opensearch-scripts/get-models.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -e - -# Get the search pipeline. -curl -s http://localhost:9200/_search/pipeline/hybrid-search-pipeline | jq - -#curl -s "http://localhost:9200/_plugins/_ml/models/_search" -H "Content-Type: application/json" -d'{ -# "query": { -# "match_all": {} -# } -# }' | jq diff --git a/scripts/run-query-set.json b/scripts/run-query-set.json index aa725f4..ebccf1a 100644 --- a/scripts/run-query-set.json +++ b/scripts/run-query-set.json @@ -1,8 +1,8 @@ { - "query_set_id": "20b2c5d3-42bf-4aaf-843e-d806e9fcd3c1", - "judgments_id": "69a69c21-f0bb-443e-bf75-bca389704fd0", + "query_set_id": "6d2a8d56-a9e6-4f99-9a81-482974d11c32", + "judgments_id": "c2d46e06-0e37-4370-b547-296b554c646e", "index": "ecommerce", - "search_pipeline": "filter_pipeline", + "search_pipeline": "", "id_field": "asin", "k": 10, "threshold": 1.0, diff --git a/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java b/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java index 020ac1d..684fbf7 100644 --- a/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java +++ b/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java @@ -109,7 +109,7 @@ public String calculateCoec(final Map rankAggregatedClickThroug } - // Numerator is sum of clicks at all ranks up to the maxRank. + // Numerator is the sum of clicks at all ranks up to the maxRank. final long totalNumberClicksForQueryResult = ctr.getClicks(); // Divide the numerator by the denominator (value). diff --git a/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java b/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java index 4d8b4dc..3593544 100644 --- a/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java +++ b/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java @@ -117,6 +117,8 @@ public QuerySetRunResult run(final RunQuerySetParameters querySetParameters) thr final String querySetRunId = UUID.randomUUID().toString(); final QuerySetRunResult querySetRunResult = new QuerySetRunResult(querySetRunId, querySetParameters.getQuerySetId(), queryResults, querySetMetrics); + save(querySetRunResult); + LOGGER.info("Query set run complete: {}", querySetRunId); return querySetRunResult; @@ -130,6 +132,8 @@ public QuerySetRunResult run(final RunQuerySetParameters querySetParameters) thr @Override public void save(final QuerySetRunResult result) throws Exception { + LOGGER.info("Indexing query run results."); + // Now, index the metrics as expected by the dashboards. // See https://github.com/o19s/opensearch-search-quality-evaluation/blob/main/opensearch-dashboard-prototyping/METRICS_SCHEMA.md From fbb87c40b5f4ea1e25fc114ef4bbbd503900381b Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 21 Jan 2025 11:58:25 -0500 Subject: [PATCH 35/41] Removing unneeded class. Removing plugin implementation. Signed-off-by: jzonthemtn --- legacy-plugin/.gitignore | 14 - legacy-plugin/Dockerfile | 6 - legacy-plugin/LICENSE.txt | 175 ----- legacy-plugin/NOTICE.txt | 1 - legacy-plugin/README.md | 72 -- legacy-plugin/aggs.sh | 20 - legacy-plugin/build.gradle | 105 --- legacy-plugin/coec.png | Bin 163989 -> 0 bytes legacy-plugin/coec_definition.png | Bin 295731 -> 0 bytes legacy-plugin/docker-compose.yaml | 47 -- legacy-plugin/gradle.properties | 2 - .../gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - legacy-plugin/licenses/gson-2.11.0.jar.sha1 | 1 - legacy-plugin/licenses/gson-LICENSE.txt | 202 ------ legacy-plugin/licenses/gson-NOTICE.txt | 0 legacy-plugin/runbook/requirements.txt | 2 - .../runbook/search-quality-eval.ipynb | 683 ------------------ legacy-plugin/scripts/cleanup.sh | 6 - legacy-plugin/scripts/create-judgments-now.sh | 7 - .../scripts/create-judgments-schedule.sh | 6 - .../scripts/create-query-set-no-sampling.sh | 5 - .../create-query-set-using-pptss-sampling.sh | 5 - legacy-plugin/scripts/delete-query-sets.sh | 3 - .../scripts/get-click-through-rates.sh | 3 - legacy-plugin/scripts/get-judgments.sh | 6 - legacy-plugin/scripts/get-metrics.sh | 3 - legacy-plugin/scripts/get-models.sh | 10 - legacy-plugin/scripts/get-query-set.sh | 5 - legacy-plugin/scripts/get-query-sets.sh | 3 - .../get-rank-aggregated-clickthrough.sh | 3 - legacy-plugin/scripts/index-sample-events.sh | 267 ------- legacy-plugin/scripts/initialize-ubi.sh | 5 - legacy-plugin/scripts/run-query-set.sh | 57 -- legacy-plugin/scripts/test-neural-query.sh | 24 - legacy-plugin/scripts/walkthrough.sh | 21 - legacy-plugin/settings.gradle | 1 - .../SearchQualityEvaluationJobParameter.java | 248 ------- .../SearchQualityEvaluationJobRunner.java | 168 ----- .../eval/SearchQualityEvaluationPlugin.java | 213 ------ .../SearchQualityEvaluationRestHandler.java | 417 ----------- .../eval/judgments/clickmodel/ClickModel.java | 23 - .../clickmodel/ClickModelParameters.java | 13 - .../clickmodel/coec/CoecClickModel.java | 422 ----------- .../coec/CoecClickModelParameters.java | 55 -- .../judgments/model/ClickthroughRate.java | 96 --- .../eval/judgments/model/Judgment.java | 97 --- .../eval/judgments/model/QuerySetQuery.java | 29 - .../model/ubi/event/EventAttributes.java | 82 --- .../model/ubi/event/EventObject.java | 58 -- .../judgments/model/ubi/event/Position.java | 42 -- .../judgments/model/ubi/event/UbiEvent.java | 82 --- .../model/ubi/query/QueryResponse.java | 58 -- .../judgments/model/ubi/query/UbiQuery.java | 160 ---- .../opensearch/OpenSearchHelper.java | 342 --------- .../queryhash/IncrementalUserQueryHash.java | 51 -- .../judgments/queryhash/UserQueryHash.java | 23 - .../eval/metrics/DcgSearchMetric.java | 64 -- .../eval/metrics/NdcgSearchMetric.java | 62 -- .../eval/metrics/PrecisionSearchMetric.java | 63 -- .../opensearch/eval/metrics/SearchMetric.java | 70 -- .../eval/runners/AbstractQuerySetRunner.java | 208 ------ .../runners/OpenSearchQuerySetRunner.java | 290 -------- .../opensearch/eval/runners/QueryResult.java | 72 -- .../eval/runners/QuerySetRunResult.java | 108 --- .../eval/runners/RelevanceScores.java | 32 - .../eval/samplers/AbstractQuerySampler.java | 98 --- .../samplers/AbstractSamplerParameters.java | 41 -- .../eval/samplers/AllQueriesQuerySampler.java | 79 -- .../AllQueriesQuerySamplerParameters.java | 17 - ...roportionalToSizeAbstractQuerySampler.java | 176 ----- ...obabilityProportionalToSizeParameters.java | 17 - .../org/opensearch/eval/utils/MathUtils.java | 26 - .../org/opensearch/eval/utils/TimeUtils.java | 35 - .../plugin-metadata/plugin-security.policy | 4 - ...rch.jobscheduler.spi.JobSchedulerExtension | 1 - .../eval/metrics/DcgSearchMetricTest.java | 41 -- .../eval/metrics/NdcgSearchMetricTest.java | 41 -- .../metrics/PrecisionSearchMetricTest.java | 30 - legacy-plugin/useful_queries.txt | 151 ---- .../eval/model/ubi/query/QueryResponse.java | 58 -- .../eval/model/ubi/query/UbiQuery.java | 31 +- 82 files changed, 12 insertions(+), 6257 deletions(-) delete mode 100644 legacy-plugin/.gitignore delete mode 100644 legacy-plugin/Dockerfile delete mode 100644 legacy-plugin/LICENSE.txt delete mode 100644 legacy-plugin/NOTICE.txt delete mode 100644 legacy-plugin/README.md delete mode 100755 legacy-plugin/aggs.sh delete mode 100644 legacy-plugin/build.gradle delete mode 100644 legacy-plugin/coec.png delete mode 100644 legacy-plugin/coec_definition.png delete mode 100644 legacy-plugin/docker-compose.yaml delete mode 100644 legacy-plugin/gradle.properties delete mode 100644 legacy-plugin/gradle/wrapper/gradle-wrapper.jar delete mode 100644 legacy-plugin/gradle/wrapper/gradle-wrapper.properties delete mode 100644 legacy-plugin/licenses/gson-2.11.0.jar.sha1 delete mode 100644 legacy-plugin/licenses/gson-LICENSE.txt delete mode 100644 legacy-plugin/licenses/gson-NOTICE.txt delete mode 100644 legacy-plugin/runbook/requirements.txt delete mode 100644 legacy-plugin/runbook/search-quality-eval.ipynb delete mode 100755 legacy-plugin/scripts/cleanup.sh delete mode 100755 legacy-plugin/scripts/create-judgments-now.sh delete mode 100755 legacy-plugin/scripts/create-judgments-schedule.sh delete mode 100755 legacy-plugin/scripts/create-query-set-no-sampling.sh delete mode 100755 legacy-plugin/scripts/create-query-set-using-pptss-sampling.sh delete mode 100755 legacy-plugin/scripts/delete-query-sets.sh delete mode 100755 legacy-plugin/scripts/get-click-through-rates.sh delete mode 100755 legacy-plugin/scripts/get-judgments.sh delete mode 100755 legacy-plugin/scripts/get-metrics.sh delete mode 100755 legacy-plugin/scripts/get-models.sh delete mode 100755 legacy-plugin/scripts/get-query-set.sh delete mode 100755 legacy-plugin/scripts/get-query-sets.sh delete mode 100755 legacy-plugin/scripts/get-rank-aggregated-clickthrough.sh delete mode 100755 legacy-plugin/scripts/index-sample-events.sh delete mode 100755 legacy-plugin/scripts/initialize-ubi.sh delete mode 100755 legacy-plugin/scripts/run-query-set.sh delete mode 100755 legacy-plugin/scripts/test-neural-query.sh delete mode 100755 legacy-plugin/scripts/walkthrough.sh delete mode 100644 legacy-plugin/settings.gradle delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/Judgment.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/metrics/SearchMetric.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/runners/QueryResult.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/runners/RelevanceScores.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/utils/MathUtils.java delete mode 100644 legacy-plugin/src/main/java/org/opensearch/eval/utils/TimeUtils.java delete mode 100644 legacy-plugin/src/main/plugin-metadata/plugin-security.policy delete mode 100644 legacy-plugin/src/main/resources/META-INF/services/org.opensearch.jobscheduler.spi.JobSchedulerExtension delete mode 100644 legacy-plugin/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java delete mode 100644 legacy-plugin/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java delete mode 100644 legacy-plugin/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java delete mode 100644 legacy-plugin/useful_queries.txt delete mode 100644 src/main/java/org/opensearch/eval/model/ubi/query/QueryResponse.java diff --git a/legacy-plugin/.gitignore b/legacy-plugin/.gitignore deleted file mode 100644 index 6c884e1..0000000 --- a/legacy-plugin/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -# Ignore Gradle project-specific cache directory -.gradle - -# Ignore Gradle build output directory -build - -# intellij files -.idea/ -*.iml -*.ipr -*.iws -build-idea/ -out/ - diff --git a/legacy-plugin/Dockerfile b/legacy-plugin/Dockerfile deleted file mode 100644 index 02f56c8..0000000 --- a/legacy-plugin/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM opensearchproject/opensearch:2.18.0 - -RUN /usr/share/opensearch/bin/opensearch-plugin install --batch https://github.com/opensearch-project/user-behavior-insights/releases/download/2.18.0.2/opensearch-ubi-2.18.0.2.zip - -ADD ./build/distributions/search-quality-evaluation-plugin-0.0.1.zip /tmp/search-quality-evaluation-plugin.zip -RUN /usr/share/opensearch/bin/opensearch-plugin install --batch file:/tmp/search-quality-evaluation-plugin.zip diff --git a/legacy-plugin/LICENSE.txt b/legacy-plugin/LICENSE.txt deleted file mode 100644 index 67db858..0000000 --- a/legacy-plugin/LICENSE.txt +++ /dev/null @@ -1,175 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. diff --git a/legacy-plugin/NOTICE.txt b/legacy-plugin/NOTICE.txt deleted file mode 100644 index be5c6b3..0000000 --- a/legacy-plugin/NOTICE.txt +++ /dev/null @@ -1 +0,0 @@ -Copyright Open Source Connections or its affiliates. All Rights Reserved. diff --git a/legacy-plugin/README.md b/legacy-plugin/README.md deleted file mode 100644 index 215ccce..0000000 --- a/legacy-plugin/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# OpenSearch Evaluation Framework - -This is an OpenSearch plugin built on the OpenSearch job scheduler plugin. - -## API Endpoints - -| Method | Endpoint | Description | -|--------|-----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| -| `POST` | `/_plugins/search_quality_eval/queryset` | Create a query set by sampling from the `ubi_queries` index. The `name`, `description`, and `sampling` method parameters are required. | -| `POST` | `/_plugins/search_quality_eval/run` | Initiate a run of a query set. The `name` of the query set is a required parameter. | -| `POST` | `/_plugins/search_quality_eval/judgments` | Generate implicit judgments from UBI events and queries now. | -| `POST` | `/_plugins/search_quality_eval/schedule` | Create a scheduled job to generate implicit judgments. | - - -## Building - -Build the project from the top-level directory to build all projects. - -``` -cd .. -./gradlew build -``` - -## Running in Docker - -From this directory: - -``` -docker compose build && docker compose up -``` - -Verify the plugin is installed: - -``` -curl http://localhost:9200/_cat/plugins -``` - -In the list returned you should see: - -``` -opensearch search-quality-evaluation-plugin 2.17.1.0-SNAPSHOT -``` - -To create a schedule to generate implicit judgments: - -``` -curl -s -X POST "http://localhost:9200/_plugins/search_quality_eval/schedule?id=1&click_model=coec&job_name=test&interval=60" | jq -``` - -See the created job: - -``` -curl -s http://localhost:9200/search_quality_eval_scheduled_jobs/_search | jq -``` - -To run an on-demand job without scheduling: - -``` -curl -X POST "http://localhost:9200/_plugins/search_quality_eval/judgments?click_model=coec&max_rank=20" | jq -``` - -To see the job runs: - -``` -curl -X POST "http://localhost:9200/search_quality_eval_completed_jobs/_search" | jq -``` - -See the first 10 judgments: - -``` -curl -s http://localhost:9200/judgments/_search | jq -``` \ No newline at end of file diff --git a/legacy-plugin/aggs.sh b/legacy-plugin/aggs.sh deleted file mode 100755 index 5cf0e24..0000000 --- a/legacy-plugin/aggs.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -e - -curl -X GET http://localhost:9200/ubi_events/_search -H "Content-Type: application/json" -d' -{ - "size": 0, - "aggs": { - "By_Action": { - "terms": { - "field": "action_name" - }, - "aggs": { - "By_Position": { - "terms": { - "field": "event_attributes.position.ordinal" - } - } - } - } - } -}' | jq \ No newline at end of file diff --git a/legacy-plugin/build.gradle b/legacy-plugin/build.gradle deleted file mode 100644 index dcfa2da..0000000 --- a/legacy-plugin/build.gradle +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -apply plugin: 'java' -apply plugin: 'idea' -apply plugin: 'opensearch.opensearchplugin' -apply plugin: 'opensearch.yaml-rest-test' - -opensearchplugin { - name 'search-quality-evaluation-plugin' - description 'OpenSearch Search Quality Evaluation' - classname 'org.opensearch.eval.SearchQualityEvaluationPlugin' - extendedPlugins = ['opensearch-job-scheduler'] -} - -ext { - projectSubstitutions = [:] - licenseFile = rootProject.file('LICENSE.txt') - noticeFile = rootProject.file('NOTICE.txt') -} - -test { - include "**/Test*.class" - include "**/*Test.class" - include "**/*Test.class" - include "**/*TestCase.class" -} - -group = 'org.opensearch' -version = "${evalVersion}" - -// disabling some unnecessary validations for this plugin -testingConventions.enabled = false -forbiddenApis.ignoreFailures = true -loggerUsageCheck.enabled = false -validateNebulaPom.enabled = false -thirdPartyAudit.enabled = false - -buildscript { - repositories { - mavenLocal() - maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } - mavenCentral() - maven { url "https://plugins.gradle.org/m2/" } - } - - dependencies { - classpath "org.opensearch.gradle:build-tools:${opensearchVersion}" - } -} - -repositories { - mavenLocal() - mavenCentral() - maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } -} - -dependencies { - runtimeOnly "org.apache.logging.log4j:log4j-core:${versions.log4j}" - compileOnly "org.opensearch:opensearch-job-scheduler:${opensearchVersion}.0" - compileOnly "org.opensearch:opensearch-job-scheduler-spi:${opensearchVersion}.0" - compileOnly "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}" - compileOnly "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" - compileOnly "org.apache.httpcomponents:httpcore:${versions.httpcore}" - compileOnly "org.apache.httpcomponents:httpclient:${versions.httpclient}" - compileOnly "commons-logging:commons-logging:${versions.commonslogging}" - implementation "com.google.code.gson:gson:2.11.0" - yamlRestTestImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j}" -} - -publishing { - repositories { - maven { - name = "Snapshots" - url = "https://aws.oss.sonatype.org/content/repositories/snapshots" - credentials { - username "$System.env.SONATYPE_USERNAME" - password "$System.env.SONATYPE_PASSWORD" - } - } - } - publications { - pluginZip(MavenPublication) { publication -> - pom { - name = "opensearch-eval" - description = "OpenSearch User Behavior Insights plugin" - groupId = "org.opensearch.plugin" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - developers { - developer { - name = "OpenSearch" - url = "https://github.com/opensearch-project/eval" - } - } - } - } - } -} \ No newline at end of file diff --git a/legacy-plugin/coec.png b/legacy-plugin/coec.png deleted file mode 100644 index 65e297ab35497551d5defdaa92cde0951b0127b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163989 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

T~AnZFqehYtqLXSNcun;-Rgtbk;M7 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 diff --git a/legacy-plugin/coec_definition.png b/legacy-plugin/coec_definition.png deleted file mode 100644 index e5ed3f99659809a96f226ef976e56c2a169f74ec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 295731 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 PajygXX6z?!d`FF%+`(_T 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

^XJ@| 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 diff --git a/legacy-plugin/docker-compose.yaml b/legacy-plugin/docker-compose.yaml deleted file mode 100644 index c2ab3b7..0000000 --- a/legacy-plugin/docker-compose.yaml +++ /dev/null @@ -1,47 +0,0 @@ -services: - - opensearch_sef: - build: . - container_name: opensearch_sef - environment: - discovery.type: single-node - node.name: opensearch - plugins.security.disabled: "true" - logger.level: info - OPENSEARCH_INITIAL_ADMIN_PASSWORD: SuperSecretPassword_123 - http.max_content_length: 500mb - OPENSEARCH_JAVA_OPTS: "-Xms16g -Xmx16g" - ulimits: - memlock: - soft: -1 - hard: -1 - nofile: - soft: 65536 - hard: 65536 - ports: - - "9200:9200" - - "9600:9600" - networks: - - opensearch-net - volumes: - - opensearch-data1:/usr/share/opensearch/data - - opensearch_sef_dashboards: - image: opensearchproject/opensearch-dashboards:2.18.0 - container_name: opensearch_sef_dashboards - ports: - - "5601:5601" - environment: - OPENSEARCH_HOSTS: '["http://opensearch_sef:9200"]' - DISABLE_SECURITY_DASHBOARDS_PLUGIN: "true" - depends_on: - - opensearch_sef - networks: - - opensearch-net - -volumes: - opensearch-data1: - -networks: - opensearch-net: - driver: bridge diff --git a/legacy-plugin/gradle.properties b/legacy-plugin/gradle.properties deleted file mode 100644 index 2659a68..0000000 --- a/legacy-plugin/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -opensearchVersion = 2.18.0 -evalVersion = 0.0.1 diff --git a/legacy-plugin/gradle/wrapper/gradle-wrapper.jar b/legacy-plugin/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM diff --git a/legacy-plugin/gradle/wrapper/gradle-wrapper.properties b/legacy-plugin/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 2bbac7d..0000000 --- a/legacy-plugin/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file diff --git a/legacy-plugin/licenses/gson-2.11.0.jar.sha1 b/legacy-plugin/licenses/gson-2.11.0.jar.sha1 deleted file mode 100644 index 0414a49..0000000 --- a/legacy-plugin/licenses/gson-2.11.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -527175ca6d81050b53bdd4c457a6d6e017626b0e \ No newline at end of file diff --git a/legacy-plugin/licenses/gson-LICENSE.txt b/legacy-plugin/licenses/gson-LICENSE.txt deleted file mode 100644 index 7a4a3ea..0000000 --- a/legacy-plugin/licenses/gson-LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/legacy-plugin/licenses/gson-NOTICE.txt b/legacy-plugin/licenses/gson-NOTICE.txt deleted file mode 100644 index e69de29..0000000 diff --git a/legacy-plugin/runbook/requirements.txt b/legacy-plugin/runbook/requirements.txt deleted file mode 100644 index 934b71e..0000000 --- a/legacy-plugin/runbook/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -notebook==7.2.2 -bash_kernel==0.9.3 diff --git a/legacy-plugin/runbook/search-quality-eval.ipynb b/legacy-plugin/runbook/search-quality-eval.ipynb deleted file mode 100644 index 578fcb9..0000000 --- a/legacy-plugin/runbook/search-quality-eval.ipynb +++ /dev/null @@ -1,683 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 14, - "id": "7ec95d0b-308a-4863-bc6b-c82e8553551e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"acknowledged\":true}\n" - ] - } - ], - "source": [ - "# Delete all indexes to start fresh.\n", - "# curl -X DELETE http://localhost:9200/ubi_queries,ubi_events,\n", - "# curl -X DELETE http://localhost:9200/judgments\n", - "curl -X DELETE http://localhost:9200/search_quality_eval_query_sets" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fc25e25a-7200-4298-858b-4d606b32829a", - "metadata": {}, - "outputs": [], - "source": [ - "# Create the UBI indexes\n", - "curl -X PUT http://localhost:9200/ubi_queries/_mappings -H \"Content-Type: application/json\" -d'\n", - "{\n", - " \"properties\": {\n", - " \"timestamp\": { \"type\": \"date\", \"format\": \"strict_date_time\" },\n", - " \"query_id\": { \"type\": \"keyword\", \"ignore_above\": 100 },\n", - " \"query\": { \"type\": \"text\" },\n", - " \"query_response_id\": { \"type\": \"keyword\", \"ignore_above\": 100 },\n", - " \"query_response_hit_ids\": { \"type\": \"keyword\" },\n", - " \"user_query\": { \"type\": \"keyword\", \"ignore_above\": 256 },\n", - " \"query_attributes\": { \"type\": \"flat_object\" },\n", - " \"client_id\": { \"type\": \"keyword\", \"ignore_above\": 100 },\n", - " \"application\": { \"type\": \"keyword\", \"ignore_above\": 100 }\n", - " }\n", - "}'\n", - "\n", - "curl -X PUT http://localhost:9200/ubi_events/_mappings -H \"Content-Type: application/json\" -d'\n", - "{\n", - " \"properties\": {\n", - " \"application\": { \"type\": \"keyword\", \"ignore_above\": 256 },\n", - " \"action_name\": { \"type\": \"keyword\", \"ignore_above\": 100 },\n", - " \"client_id\": { \"type\": \"keyword\", \"ignore_above\": 100 },\n", - " \"query_id\": { \"type\": \"keyword\", \"ignore_above\": 100 },\n", - " \"message\": { \"type\": \"keyword\", \"ignore_above\": 1024 },\n", - " \"message_type\": { \"type\": \"keyword\", \"ignore_above\": 100 },\n", - " \"timestamp\": {\n", - " \"type\": \"date\",\n", - " \"format\":\"strict_date_time\",\n", - " \"ignore_malformed\": true,\n", - " \"doc_values\": true\n", - " },\n", - " \"event_attributes\": {\n", - " \"dynamic\": true,\n", - " \"properties\": {\n", - " \"position\": {\n", - " \"properties\": {\n", - " \"ordinal\": { \"type\": \"integer\" },\n", - " \"x\": { \"type\": \"integer\" },\n", - " \"y\": { \"type\": \"integer\" },\n", - " \"page_depth\": { \"type\": \"integer\" },\n", - " \"scroll_depth\": { \"type\": \"integer\" },\n", - " \"trail\": { \"type\": \"text\",\n", - " \"fields\": { \"keyword\": { \"type\": \"keyword\", \"ignore_above\": 256 }\n", - " }\n", - " }\n", - " }\n", - " },\n", - " \"object\": {\n", - " \"properties\": {\n", - " \"internal_id\": { \"type\": \"keyword\" },\n", - " \"object_id\": { \"type\": \"keyword\", \"ignore_above\": 256 },\n", - " \"object_id_field\": { \"type\": \"keyword\", \"ignore_above\": 100 },\n", - " \"name\": { \"type\": \"keyword\", \"ignore_above\": 256 },\n", - " \"description\": { \"type\": \"text\",\n", - " \"fields\": { \"keyword\": { \"type\": \"keyword\", \"ignore_above\": 256 } }\n", - " },\n", - " \"object_detail\": { \"type\": \"object\" }\n", - " }\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cc97c07b-e182-4248-be0d-d3cd6d0c4471", - "metadata": {}, - "outputs": [], - "source": [ - "# Index the ESCI data.\n", - "cd ./../data/esci && ./index.sh 1> /dev/null 2> /dev/null" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "b6793210-e45b-4445-ad1f-c4b56c80cb1a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"count\":100000,\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0}}\n", - "{\"count\":528374,\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0}}\n" - ] - } - ], - "source": [ - "# See the query and events count from the indexed ESCI data.\n", - "curl -s http://localhost:9200/ubi_queries/_count\n", - "curl -s http://localhost:9200/ubi_events/_count" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "21d85f25-97a6-480f-84da-d03b6af682ca", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"message\": \"Implicit judgment generation initiated.\"}\n" - ] - } - ], - "source": [ - "# Create implicit judgments now.\n", - "curl -s -X POST \"http://localhost:9200/_plugins/search_quality_eval/judgments?click_model=coec&max_rank=20\"\n", - "\n", - "# Schedule implicit judgments creation.\n", - "# curl -s -X POST \"http://localhost:9200/_plugins/search_quality_eval/schedule?id=1&click_model=coec&max_rank=20&job_name=test2&interval=10\" | jq\n", - "# curl -s \"http://localhost:9200/search_quality_eval_scheduler/_search\" | jq" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "e04e1282-c744-4ead-a65d-66d1b731391a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"took\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m3\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"timed_out\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_shards\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"total\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"successful\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"skipped\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m0\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"failed\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m0\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"hits\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"total\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"value\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m5000\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"relation\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"eq\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"max_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"hits\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m[\n", - " \u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"_index\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"judgments\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"6ae380c3-4313-4b43-b145-31fbd27a115c\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_source\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"query_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"1\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"judgment\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m20.949720670391063\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"query\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"k cups\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"document\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"B07P23H371\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"_index\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"judgments\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"400c3242-a4f5-4897-85a2-6db1d7aaf0b4\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_source\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"query_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"1\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"judgment\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m22.609514837494114\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"query\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"k cups\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"document\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"B00K2RY8GI\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"_index\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"judgments\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"8151d6df-0ab6-428a-ba83-450df26987df\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_source\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"query_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"1\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"judgment\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m17.46724890829694\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"query\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"k cups\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"document\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"B082D6NC6P\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"_index\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"judgments\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"36a399cf-eb78-489d-8795-a408b7da4f26\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_source\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"query_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"1\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"judgment\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m17.557060446450965\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"query\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"k cups\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"document\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"B06XSFHTWM\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"_index\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"judgments\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"972740a4-8494-4e08-82b4-07bccfa44ec3\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_source\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"query_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"1\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"judgment\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m8.660508083140877\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"query\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"k cups\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"document\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"B081NTR1XX\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"_index\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"judgments\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"2a5133a2-6716-451e-a605-bdf038bb2312\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_source\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"query_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"2\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"judgment\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m13.966480446927374\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"query\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"chips\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"document\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"B00BHFELOS\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"_index\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"judgments\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"7b0d830a-ac6d-483b-abd0-1ddf2a3de36a\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_source\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"query_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"2\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"judgment\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m6.550218340611353\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"query\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"chips\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"document\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"B0170B0RUY\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"_index\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"judgments\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"5926100c-9992-40b5-b20b-a2bf9d8b7bb3\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_source\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"query_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"2\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"judgment\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m10.032605969400551\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"query\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"chips\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"document\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"B00ID0OURI\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"_index\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"judgments\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"80e7be09-a366-424c-9048-40037c168dcd\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_source\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"query_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"2\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"judgment\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m0\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"query\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"chips\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"document\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"B079RMSR88\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"_index\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"judgments\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"63db1764-ab04-4c7e-8ddc-95ac0010c3b8\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_source\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"query_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"2\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"judgment\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m0\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"query\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"chips\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"document\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"B07XJWNF9R\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m]\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - "\u001b[1;39m}\u001b[0m\n" - ] - } - ], - "source": [ - "# See the judgments.\n", - "curl -s http://localhost:9200/judgments/_search | jq" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "6b6f847d-5142-4d0e-83b1-6127410bfa67", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"query_set\": \"d4857274-3216-4add-afeb-e8c30562270e\"}\n" - ] - } - ], - "source": [ - "# Create a query set using PPTSS sampling from the UBI queries.\n", - "curl -s -X POST \"http://localhost:9200/_plugins/search_quality_eval/queryset?name=test&description=fake&sampling=pptss&query_set_size=50\"" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "2cc4ee64-f3b8-4a6b-8050-1fa9e70fc9ba", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"took\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"timed_out\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_shards\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"total\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"successful\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"skipped\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m0\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"failed\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m0\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"hits\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"total\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"value\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m4\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"relation\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"eq\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"max_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"hits\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m[\n", - " \u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"_index\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"search_quality_eval_query_sets\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"46c14597-b4d2-4872-af9c-c5b4ffdef876\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_source\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"sampling\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"pptss\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"name\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"test\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"description\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"fake\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"created_at\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1732544453255\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"queries\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m[\n", - " \u001b[0;32m\"k cups\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"chips\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"bicycle\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"shirts long sleeves\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"best bra without underwire\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"lamp base only without shade\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"fitbit charge 3\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"laptop\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"ipod\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"hot pink nike shoes\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"instant pot\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"halloween costumes for women\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"hydro flask\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"face mask\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"barbie dolls\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple airpods\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"printer\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"wood graining tool for painting\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"lightning cable\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"iphone\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"toy story\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"paper towels\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"moses sandals for women\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"rings\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"watches for women\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"jerzees long sleeve t shirt\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"frozen 2\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"water shoes\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"dog harness for large dogs\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"youth slippers size 7\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"gun\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"curious george yellow hat\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple pen\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"vacuum cleaner\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple headphones\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"echo dot\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"moncler\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"womens dresses\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"diffusers for essential oils\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"weighted blanket\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"beats wireless headphones\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"rugs\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple watch series 3\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"simple grunge outfits\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"smart watch\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"nike\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"chromebook\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"ban idiots not guns\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"3 ring binder\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple watch\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"womens slippers\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"shoes\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"womens shoes\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"futon frames full size without mattress\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"gifts for men\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"mini fridge\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"tv\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"xo hat the weeknd\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"cut up high waisted shorts\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"harry potter\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"airsoft guns\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"gaming chair\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"reusable camping trash bag\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"skirt\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"ratchet belts for men without buckle\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"macbook\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"iphone xr\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"patio furniture\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"twin mattress\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"jeans american eagle for men\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"carhartt double front pants for men\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"sewing machine\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"compression socks\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"foot massager\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"kerug filter\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"fire stick\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"vintage sewing kit\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"desk\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"iphone 11\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"ipad\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"office chair\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"cut out tank tops for women\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"gaming laptop\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"yoga mat\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"laptops\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"dildo\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"compression socks women\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple earphones\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"wireless earbuds\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"monitor\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"shoes for women\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"nxt crossbow\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"batman\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"aquarium\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple earbuds\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"marshmallows without gelatin\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"bluetooth earbuds\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"airpods\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m]\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"_index\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"search_quality_eval_query_sets\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"78334998-6fc8-4f96-94a9-78ea0b4c694f\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_source\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"sampling\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"pptss\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"name\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"test\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"description\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"fake\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"created_at\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1732544477679\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"queries\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m[\n", - " \u001b[0;32m\"yeezy\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"tv\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"k cups\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"headset with microphone\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"fitbit charge 3\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"laptop\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"airsoft guns\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"hot pink nike shoes\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"instant pot\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"ratchet belts for men without buckle\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"iphone xr\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"patio furniture\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"twin mattress\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"hydro flask\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"face mask\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"carhartt double front pants for men\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"compression socks\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"printer\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"iphone\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"kerug filter\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"plus size womens clothing\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"rings\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"yeti\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"scrunchies\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"frozen 2\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"where the crawdads sing\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"youth slippers size 7\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"gun\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"office chair\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"weighted blanket\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"beats wireless headphones\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"yoga mat\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"laptops\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"dildo\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple watch series 3\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"simple grunge outfits\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"compression socks women\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"iphone headphones\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"chromebook\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"wireless earbuds\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"ban idiots not guns\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"puma high tops\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple watch\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"monitor\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"shoes\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"womens shoes\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"aquarium\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"futon frames full size without mattress\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple watch 3\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"gifts for men\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"airpods\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m]\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"_index\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"search_quality_eval_query_sets\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"b5f3c63c-429d-4870-9e9b-8154ef834924\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_source\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"sampling\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"pptss\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"name\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"test\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"description\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"fake\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"created_at\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1732544512268\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"queries\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m[\n", - " \u001b[0;32m\"tv\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"xo hat the weeknd\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"best bra without underwire\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"king platform bed without headboard\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"headset with microphone\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"aloy costume\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"fitbit charge 3\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"laptop\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"halo\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"gaming chair\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"hot pink nike shoes\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"reusable camping trash bag\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"music\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"patio furniture\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"sunglasses\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"face mask\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"printer\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"paper towels\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"kerug filter\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"blackout curtains\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"desk\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"iphone 11\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"ipad\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"youth slippers size 7\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple pen\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"vacuum cleaner\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"echo dot\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"moncler\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"portable charger\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"yoga mat\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"laptops\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"dildo\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"air freshener\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"boots for women\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"compression socks women\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"nike\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"chromebook\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"ban idiots not guns\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"wireless earbuds\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"monitor\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple watch\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"shoes for women\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"shoes\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"batman\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"futon frames full size without mattress\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple earbuds\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"bluetooth earbuds\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"gifts for men\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"toilet paper\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"airpods\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"mini fridge\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m]\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m,\n", - " \u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"_index\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"search_quality_eval_query_sets\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_id\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"d4857274-3216-4add-afeb-e8c30562270e\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_score\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"_source\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n", - " \u001b[0m\u001b[34;1m\"sampling\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"pptss\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"name\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"test\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"description\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"fake\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"created_at\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39m1732544554130\u001b[0m\u001b[1;39m,\n", - " \u001b[0m\u001b[34;1m\"queries\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m[\n", - " \u001b[0;32m\"tv\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"chips\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"bicycle\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"shirts long sleeves\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"aloy costume\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"laptop\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"reusable camping trash bag\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"projector screen\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"iphone xr\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"ratchet belts for men without buckle\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"macbook\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"calm and quiet for dogs\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"twin mattress\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"face mask\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"sewing machine\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"printer\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"compression socks\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"computer desk\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"toy story\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"foot massager\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"paper towels\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"fire stick\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"plus size womens clothing\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"blackout curtains\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"yeti\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"ipad\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"scrunchies\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"frozen 2\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"water shoes\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"where the crawdads sing\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"youth slippers size 7\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple headphones\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"echo dot\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"gaming laptop\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"laptops\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"dildo\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple watch series 3\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"simple grunge outfits\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"boots for women\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"chromebook\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"wireless earbuds\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"3 ring binder\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple watch\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"puma high tops\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"shoes for women\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"shoes\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple earbuds\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"apple watch 3\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"toilet paper\"\u001b[0m\u001b[1;39m,\n", - " \u001b[0;32m\"airpods\"\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m]\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m]\u001b[0m\u001b[1;39m\n", - " \u001b[1;39m}\u001b[0m\u001b[1;39m\n", - "\u001b[1;39m}\u001b[0m\n" - ] - } - ], - "source": [ - "# Look at the query sets.\n", - "curl -s http://localhost:9200/search_quality_eval_query_sets/_search | jq" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "abe28eb7-fe7b-47b9-aec0-f3090cd38ca7", - "metadata": {}, - "outputs": [], - "source": [ - "# Run a query set.\n", - "curl -s http://localhost:9200/search_quality_eval_run?id=${QUERY_SET_ID}" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Bash", - "language": "bash", - "name": "bash" - }, - "language_info": { - "codemirror_mode": "shell", - "file_extension": ".sh", - "mimetype": "text/x-sh", - "name": "bash" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/legacy-plugin/scripts/cleanup.sh b/legacy-plugin/scripts/cleanup.sh deleted file mode 100755 index 77b1fd4..0000000 --- a/legacy-plugin/scripts/cleanup.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -e - -curl -s -X DELETE "http://localhost:9200/judgments,search_quality_eval_completed_jobs,search_quality_eval_query_sets_run_results" | jq -curl -s -X DELETE "http://localhost:9200/search_quality_eval_completed_jobs" | jq -curl -s -X DELETE "http://localhost:9200/search_quality_eval_query_sets_run_results" | jq -curl -s -X DELETE "http://localhost:9200/ubi_queries,ubi_events" | jq diff --git a/legacy-plugin/scripts/create-judgments-now.sh b/legacy-plugin/scripts/create-judgments-now.sh deleted file mode 100755 index 8bc505a..0000000 --- a/legacy-plugin/scripts/create-judgments-now.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -e - -echo "Deleting existing judgments index..." -curl -s -X DELETE http://localhost:9200/judgments - -echo "Creating judgments..." -curl -s -X POST "http://localhost:9200/_plugins/search_quality_eval/judgments?click_model=coec&max_rank=50" diff --git a/legacy-plugin/scripts/create-judgments-schedule.sh b/legacy-plugin/scripts/create-judgments-schedule.sh deleted file mode 100755 index 72a79ed..0000000 --- a/legacy-plugin/scripts/create-judgments-schedule.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -e - -curl -s -X POST "http://localhost:9200/_plugins/search_quality_eval/schedule?id=1&click_model=coec&max_rank=20&job_name=test2&interval=10" | jq - -echo "Scheduled jobs:" -curl -s "http://localhost:9200/search_quality_eval_scheduler/_search" | jq diff --git a/legacy-plugin/scripts/create-query-set-no-sampling.sh b/legacy-plugin/scripts/create-query-set-no-sampling.sh deleted file mode 100755 index ace0404..0000000 --- a/legacy-plugin/scripts/create-query-set-no-sampling.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -e - -curl -s -X DELETE "http://localhost:9200/search_quality_eval_query_sets" - -curl -s -X POST "http://localhost:9200/_plugins/search_quality_eval/queryset?name=test&description=fake&sampling=none&query_set_size=10" diff --git a/legacy-plugin/scripts/create-query-set-using-pptss-sampling.sh b/legacy-plugin/scripts/create-query-set-using-pptss-sampling.sh deleted file mode 100755 index 572bc8d..0000000 --- a/legacy-plugin/scripts/create-query-set-using-pptss-sampling.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -e - -curl -s -X DELETE "http://localhost:9200/search_quality_eval_query_sets" - -curl -s -X POST "http://localhost:9200/_plugins/search_quality_eval/queryset?name=test&description=fake&sampling=pptss&query_set_size=20" diff --git a/legacy-plugin/scripts/delete-query-sets.sh b/legacy-plugin/scripts/delete-query-sets.sh deleted file mode 100755 index f495800..0000000 --- a/legacy-plugin/scripts/delete-query-sets.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -e - -curl -s -X DELETE "http://localhost:9200/search_quality_eval_query_sets" diff --git a/legacy-plugin/scripts/get-click-through-rates.sh b/legacy-plugin/scripts/get-click-through-rates.sh deleted file mode 100755 index 16377a2..0000000 --- a/legacy-plugin/scripts/get-click-through-rates.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -e - -curl -s "http://localhost:9200/click_through_rates/_search" | jq diff --git a/legacy-plugin/scripts/get-judgments.sh b/legacy-plugin/scripts/get-judgments.sh deleted file mode 100755 index ee3fa15..0000000 --- a/legacy-plugin/scripts/get-judgments.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -e - -#JUDGMENT_ID=$1 -#curl -s "http://localhost:9200/judgments/_doc/${JUDGMENT_ID}" | jq - -curl -s "http://localhost:9200/judgments/_search" | jq diff --git a/legacy-plugin/scripts/get-metrics.sh b/legacy-plugin/scripts/get-metrics.sh deleted file mode 100755 index 4789332..0000000 --- a/legacy-plugin/scripts/get-metrics.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -e - -curl -s "http://localhost:9200/sqe_metrics_sample_data/_search" | jq diff --git a/legacy-plugin/scripts/get-models.sh b/legacy-plugin/scripts/get-models.sh deleted file mode 100755 index da6c0a5..0000000 --- a/legacy-plugin/scripts/get-models.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -e - -# Get the search pipeline. -curl -s http://localhost:9200/_search/pipeline/hybrid-search-pipeline | jq - -#curl -s "http://localhost:9200/_plugins/_ml/models/_search" -H "Content-Type: application/json" -d'{ -# "query": { -# "match_all": {} -# } -# }' | jq diff --git a/legacy-plugin/scripts/get-query-set.sh b/legacy-plugin/scripts/get-query-set.sh deleted file mode 100755 index c2ddce0..0000000 --- a/legacy-plugin/scripts/get-query-set.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -e - -QUERY_SET_ID="${1}" - -curl -s "http://localhost:9200/search_quality_eval_query_sets/_doc/${QUERY_SET_ID}" | jq diff --git a/legacy-plugin/scripts/get-query-sets.sh b/legacy-plugin/scripts/get-query-sets.sh deleted file mode 100755 index 0bcb3ff..0000000 --- a/legacy-plugin/scripts/get-query-sets.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -e - -curl -s "http://localhost:9200/search_quality_eval_query_sets/_search" | jq diff --git a/legacy-plugin/scripts/get-rank-aggregated-clickthrough.sh b/legacy-plugin/scripts/get-rank-aggregated-clickthrough.sh deleted file mode 100755 index 639a012..0000000 --- a/legacy-plugin/scripts/get-rank-aggregated-clickthrough.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -e - -curl -s "http://localhost:9200/rank_aggregated_ctr/_search" | jq diff --git a/legacy-plugin/scripts/index-sample-events.sh b/legacy-plugin/scripts/index-sample-events.sh deleted file mode 100755 index e50c828..0000000 --- a/legacy-plugin/scripts/index-sample-events.sh +++ /dev/null @@ -1,267 +0,0 @@ -curl -s -X DELETE http://localhost:9200/ubi_events,ubi_queries - -curl -s -X POST http://localhost:9200/_plugins/ubi/initialize - -curl -s -X PUT http://localhost:9200/ubi_events/_doc/1 -H "Content-Type: application/json" -d' -{ - "application": "esci_ubi_sample", - "action_name": "impression", - "query_id": "bd43b377-67ff-4165-8753-58bbdb3392c5", - "session_id": "fdb13692-d42c-4d1d-950b-b8814c963de2", - "client_id": "28ccfb32-fbd7-4514-9051-cea719db42de", - "timestamp": "2024-12-11T04:56:49.419Z", - "user_query": "tv", - "message_type": null, - "message": null, - "event_attributes": { - "object": { - "object_id": "B07JW53H22", - "object_id_field": "product_id" - }, - "position": { - "ordinal": 1 - } - } -}' | jq - -curl -s -X PUT http://localhost:9200/ubi_events/_doc/2 -H "Content-Type: application/json" -d' -{ - "application": "esci_ubi_sample", - "action_name": "impression", - "query_id": "bd43b377-67ff-4165-8753-58bbdb3392c5", - "session_id": "fdb13692-d42c-4d1d-950b-b8814c963de2", - "client_id": "28ccfb32-fbd7-4514-9051-cea719db42de", - "timestamp": "2024-12-11T04:56:49.419Z", - "user_query": "tv", - "message_type": null, - "message": null, - "event_attributes": { - "object": { - "object_id": "B07JW53H22", - "object_id_field": "product_id" - }, - "position": { - "ordinal": 1 - } - } -}' | jq - -curl -s -X PUT http://localhost:9200/ubi_events/_doc/3 -H "Content-Type: application/json" -d' -{ - "application": "esci_ubi_sample", - "action_name": "impression", - "query_id": "bd43b377-67ff-4165-8753-58bbdb3392c5", - "session_id": "fdb13692-d42c-4d1d-950b-b8814c963de2", - "client_id": "28ccfb32-fbd7-4514-9051-cea719db42de", - "timestamp": "2024-12-11T04:56:49.419Z", - "user_query": "tv", - "message_type": null, - "message": null, - "event_attributes": { - "object": { - "object_id": "B07JW53H22", - "object_id_field": "product_id" - }, - "position": { - "ordinal": 1 - } - } -}' | jq - -curl -s -X PUT http://localhost:9200/ubi_events/_doc/4 -H "Content-Type: application/json" -d' -{ - "application": "esci_ubi_sample", - "action_name": "impression", - "query_id": "bd43b377-67ff-4165-8753-58bbdb3392c5", - "session_id": "fdb13692-d42c-4d1d-950b-b8814c963de2", - "client_id": "28ccfb32-fbd7-4514-9051-cea719db42de", - "timestamp": "2024-12-11T04:56:49.419Z", - "user_query": "tv", - "message_type": null, - "message": null, - "event_attributes": { - "object": { - "object_id": "B07JW53H22", - "object_id_field": "product_id" - }, - "position": { - "ordinal": 1 - } - } -}' | jq - -curl -s -X PUT http://localhost:9200/ubi_events/_doc/5 -H "Content-Type: application/json" -d' -{ - "application": "esci_ubi_sample", - "action_name": "click", - "query_id": "bd43b377-67ff-4165-8753-58bbdb3392c5", - "session_id": "fdb13692-d42c-4d1d-950b-b8814c963de2", - "client_id": "28ccfb32-fbd7-4514-9051-cea719db42de", - "timestamp": "2024-12-11T04:56:49.419Z", - "user_query": "tv", - "message_type": null, - "message": null, - "event_attributes": { - "object": { - "object_id": "B07JW53H22", - "object_id_field": "product_id" - }, - "position": { - "ordinal": 1 - } - } -}' | jq - -curl -s -X PUT http://localhost:9200/ubi_events/_doc/6 -H "Content-Type: application/json" -d' -{ - "application": "esci_ubi_sample", - "action_name": "impression", - "query_id": "dc6872a3-1f4c-46b2-ad84-7add603b4c73", - "session_id": "a8f7d668-12b9-4cf3-a56f-22700b9e9b89", - "client_id": "a654b87b-a8cd-423b-996f-a169de13d4fb", - "timestamp": "2024-12-11T00:16:42.278Z", - "user_query": "airpods", - "message_type": null, - "message": null, - "event_attributes": { - "object": { - "object_id": "B088FVYG44", - "object_id_field": "product_id" - }, - "position": { - "ordinal": 1 - } - } -}' | jq - -curl -s -X PUT http://localhost:9200/ubi_events/_doc/7 -H "Content-Type: application/json" -d' -{ - "application": "esci_ubi_sample", - "action_name": "impression", - "query_id": "dc6872a3-1f4c-46b2-ad84-7add603b4c73", - "session_id": "a8f7d668-12b9-4cf3-a56f-22700b9e9b89", - "client_id": "a654b87b-a8cd-423b-996f-a169de13d4fb", - "timestamp": "2024-12-11T00:16:42.278Z", - "user_query": "airpods", - "message_type": null, - "message": null, - "event_attributes": { - "object": { - "object_id": "B088FVYG44", - "object_id_field": "product_id" - }, - "position": { - "ordinal": 1 - } - } -}' | jq - -curl -s -X PUT http://localhost:9200/ubi_events/_doc/8 -H "Content-Type: application/json" -d' -{ - "application": "esci_ubi_sample", - "action_name": "impression", - "query_id": "dc6872a3-1f4c-46b2-ad84-7add603b4c73", - "session_id": "a8f7d668-12b9-4cf3-a56f-22700b9e9b89", - "client_id": "a654b87b-a8cd-423b-996f-a169de13d4fb", - "timestamp": "2024-12-11T00:16:42.278Z", - "user_query": "airpods", - "message_type": null, - "message": null, - "event_attributes": { - "object": { - "object_id": "B088FVYG44", - "object_id_field": "product_id" - }, - "position": { - "ordinal": 1 - } - } -}' | jq - -curl -s -X PUT http://localhost:9200/ubi_events/_doc/9 -H "Content-Type: application/json" -d' -{ - "application": "esci_ubi_sample", - "action_name": "impression", - "query_id": "dc6872a3-1f4c-46b2-ad84-7add603b4c73", - "session_id": "a8f7d668-12b9-4cf3-a56f-22700b9e9b89", - "client_id": "a654b87b-a8cd-423b-996f-a169de13d4fb", - "timestamp": "2024-12-11T00:16:42.278Z", - "user_query": "airpods", - "message_type": null, - "message": null, - "event_attributes": { - "object": { - "object_id": "B088FVYG44", - "object_id_field": "product_id" - }, - "position": { - "ordinal": 1 - } - } -}' | jq - -curl -s -X PUT http://localhost:9200/ubi_events/_doc/10 -H "Content-Type: application/json" -d' -{ - "application": "esci_ubi_sample", - "action_name": "click", - "query_id": "dc6872a3-1f4c-46b2-ad84-7add603b4c73", - "session_id": "a8f7d668-12b9-4cf3-a56f-22700b9e9b89", - "client_id": "a654b87b-a8cd-423b-996f-a169de13d4fb", - "timestamp": "2024-12-11T00:16:42.278Z", - "user_query": "airpods", - "message_type": null, - "message": null, - "event_attributes": { - "object": { - "object_id": "B088FVYG44", - "object_id_field": "product_id" - }, - "position": { - "ordinal": 1 - } - } -}' | jq - -curl -s -X PUT http://localhost:9200/ubi_events/_doc/11 -H "Content-Type: application/json" -d' -{ - "application": "esci_ubi_sample", - "action_name": "click", - "query_id": "dc6872a3-1f4c-46b2-ad84-7add603b4c73", - "session_id": "a8f7d668-12b9-4cf3-a56f-22700b9e9b89", - "client_id": "a654b87b-a8cd-423b-996f-a169de13d4fb", - "timestamp": "2024-12-11T00:16:42.278Z", - "user_query": "airpods", - "message_type": null, - "message": null, - "event_attributes": { - "object": { - "object_id": "B088FVYG44", - "object_id_field": "product_id" - }, - "position": { - "ordinal": 1 - } - } -}' | jq - -curl -s -X PUT http://localhost:9200/ubi_events/_doc/12 -H "Content-Type: application/json" -d' -{ - "application": "esci_ubi_sample", - "action_name": "click", - "query_id": "dc6872a3-1f4c-46b2-ad84-7add603b4c73", - "session_id": "a8f7d668-12b9-4cf3-a56f-22700b9e9b89", - "client_id": "a654b87b-a8cd-423b-996f-a169de13d4fb", - "timestamp": "2024-12-11T00:16:42.278Z", - "user_query": "airpods", - "message_type": null, - "message": null, - "event_attributes": { - "object": { - "object_id": "B088FVYG44", - "object_id_field": "product_id" - }, - "position": { - "ordinal": 1 - } - } -}' | jq \ No newline at end of file diff --git a/legacy-plugin/scripts/initialize-ubi.sh b/legacy-plugin/scripts/initialize-ubi.sh deleted file mode 100755 index 37883fa..0000000 --- a/legacy-plugin/scripts/initialize-ubi.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -e - -curl -s -X DELETE http://localhost:9200/ubi_queries,ubi_events - -curl -s -X POST http://localhost:9200/_plugins/ubi/initialize diff --git a/legacy-plugin/scripts/run-query-set.sh b/legacy-plugin/scripts/run-query-set.sh deleted file mode 100755 index f76e26c..0000000 --- a/legacy-plugin/scripts/run-query-set.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -e - -QUERY_SET_ID="09fc2a69-9bc0-49ea-9747-aefd66528858" -JUDGMENTS_ID="cd7b72c9-21fa-4500-abf3-722438ab3ad4" -INDEX="ecommerce" -ID_FIELD="asin" -K="50" -THRESHOLD="1.0" # Default value - -curl -s -X DELETE "http://localhost:9200/sqe_metrics_sample_data" - -# Keyword search -curl -s -X POST "http://localhost:9200/_plugins/search_quality_eval/run?id=${QUERY_SET_ID}&judgments_id=${JUDGMENTS_ID}&index=${INDEX}&id_field=${ID_FIELD}&k=${K}" \ - -H "Content-Type: application/json" \ - --data-binary '{ - "multi_match": { - "query": "#$query##", - "fields": ["id", "title", "category", "bullets", "description", "attrs.Brand", "attrs.Color"] - } - }' - -## Neural search -#curl -s -X POST "http://localhost:9200/_plugins/search_quality_eval/run?id=${QUERY_SET_ID}&judgments_id=${JUDGMENTS_ID}&index=${INDEX}&id_field=${ID_FIELD}&k=${K}&search_pipeline=neural-search-pipeline" \ -# -H "Content-Type: application/json" \ -# --data-binary '{ -# "neural": { -# "title_embedding": { -# "query_text": ""#$query##", -# "k": "50" -# } -# } -# }' - -# Hybrid search -#curl -s -X POST "http://localhost:9200/_plugins/search_quality_eval/run?id=${QUERY_SET_ID}&judgments_id=${JUDGMENTS_ID}&index=${INDEX}&id_field=${ID_FIELD}&k=${K}&search_pipeline=hybrid-search-pipeline" \ -# -H "Content-Type: application/json" \ -# --data-binary '{ -# "hybrid": { -# "queries": [ -# { -# "match": { -# "title": { -# "query": "#$query##" -# } -# } -# }, -# { -# "neural": { -# "title_embedding": { -# "query_text": "#$query##", -# "k": "50" -# } -# } -# } -# ] -# } -# }' diff --git a/legacy-plugin/scripts/test-neural-query.sh b/legacy-plugin/scripts/test-neural-query.sh deleted file mode 100755 index c3d518f..0000000 --- a/legacy-plugin/scripts/test-neural-query.sh +++ /dev/null @@ -1,24 +0,0 @@ -curl -s "http://localhost:9200/ecommerce/_search?search_pipeline=hybrid-search-pipeline" -H "Content-Type: application/json" -d' -{ - "query": { - "hybrid": { - "queries": [ - { - "match": { - "title": { - "query": "shoes" - } - } - }, - { - "neural": { - "title_embedding": { - "query_text": "shoes", - "k": "50" - } - } - } - ] - } - } -}' | jq \ No newline at end of file diff --git a/legacy-plugin/scripts/walkthrough.sh b/legacy-plugin/scripts/walkthrough.sh deleted file mode 100755 index faf9f75..0000000 --- a/legacy-plugin/scripts/walkthrough.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -e - -# Example walkthrough end-to-end for the plugin. - -# Delete existing UBI indexes and create new ones. -curl -s -X DELETE "http://localhost:9200/ubi_queries,ubi_events" -curl -s -X POST "http://localhost:9200/_plugins/ubi/initialize" - -# IMPORTANT: Now index data (UBI and ESCI). - -# Create judgments. -curl -s -X POST "http://localhost:9200/_plugins/search_quality_eval/judgments?click_model=coec&max_rank=20" - -# Create a query set. -curl -s -X POST "http://localhost:9200/_plugins/search_quality_eval/queryset?name=test&description=fake&sampling=pptss&query_set_size=100" - -# Run the query set. -./run-query-set.sh ${QUERY_SET_ID} - -# Look at the results. -curl -s "http://localhost:9200/search_quality_eval_query_sets_run_results/_search" | jq diff --git a/legacy-plugin/settings.gradle b/legacy-plugin/settings.gradle deleted file mode 100644 index ef059e1..0000000 --- a/legacy-plugin/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'search-evaluation-framework' diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java b/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java deleted file mode 100644 index 2ea5379..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobParameter.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval; - -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.jobscheduler.spi.ScheduledJobParameter; -import org.opensearch.jobscheduler.spi.schedule.Schedule; - -import java.io.IOException; -import java.time.Instant; - -public class SearchQualityEvaluationJobParameter implements ScheduledJobParameter { - - /** - * The name of the parameter for providing a name for the scheduled job. - */ - public static final String NAME_FIELD = "name"; - - /** - * The name of the parameter for creating a job as enabled or disabled. - */ - public static final String ENABLED_FILED = "enabled"; - - /** - * The name of the parameter for specifying when the job was last updated. - */ - public static final String LAST_UPDATE_TIME_FIELD = "last_update_time"; - - /** - * The name of the parameter for specifying a readable time for when the job was last updated. - */ - public static final String LAST_UPDATE_TIME_FIELD_READABLE = "last_update_time_field"; - public static final String SCHEDULE_FIELD = "schedule"; - public static final String ENABLED_TIME_FILED = "enabled_time"; - public static final String ENABLED_TIME_FILED_READABLE = "enabled_time_field"; - public static final String LOCK_DURATION_SECONDS = "lock_duration_seconds"; - public static final String JITTER = "jitter"; - - /** - * The name of the parameter that allows for specifying the type of click model to use. - */ - public static final String CLICK_MODEL = "click_model"; - - /** - * The name of the parameter that allows for setting a max rank value to use during judgment generation. - */ - public static final String MAX_RANK = "max_rank"; - - // Properties from ScheduledJobParameter. - private String jobName; - private Instant lastUpdateTime; - private Instant enabledTime; - private boolean enabled; - private Schedule schedule; - private Long lockDurationSeconds; - private Double jitter; - - // Custom properties. - private String clickModel; - private int maxRank; - - public SearchQualityEvaluationJobParameter() { - - } - - public SearchQualityEvaluationJobParameter(final String name, final Schedule schedule, - final Long lockDurationSeconds, final Double jitter, - final String clickModel, final int maxRank) { - this.jobName = name; - this.schedule = schedule; - this.enabled = true; - this.lockDurationSeconds = lockDurationSeconds; - this.jitter = jitter; - - final Instant now = Instant.now(); - this.enabledTime = now; - this.lastUpdateTime = now; - - // Custom properties. - this.clickModel = clickModel; - this.maxRank = maxRank; - - } - - @Override - public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { - - builder.startObject(); - - builder - .field(NAME_FIELD, this.jobName) - .field(ENABLED_FILED, this.enabled) - .field(SCHEDULE_FIELD, this.schedule) - .field(CLICK_MODEL, this.clickModel) - .field(MAX_RANK, this.maxRank); - - if (this.enabledTime != null) { - builder.timeField(ENABLED_TIME_FILED, ENABLED_TIME_FILED_READABLE, this.enabledTime.toEpochMilli()); - } - - if (this.lastUpdateTime != null) { - builder.timeField(LAST_UPDATE_TIME_FIELD, LAST_UPDATE_TIME_FIELD_READABLE, this.lastUpdateTime.toEpochMilli()); - } - - if (this.lockDurationSeconds != null) { - builder.field(LOCK_DURATION_SECONDS, this.lockDurationSeconds); - } - - if (this.jitter != null) { - builder.field(JITTER, this.jitter); - } - - builder.endObject(); - - return builder; - - } - - @Override - public String getName() { - return this.jobName; - } - - @Override - public Instant getLastUpdateTime() { - return this.lastUpdateTime; - } - - @Override - public Instant getEnabledTime() { - return this.enabledTime; - } - - @Override - public Schedule getSchedule() { - return this.schedule; - } - - @Override - public boolean isEnabled() { - return this.enabled; - } - - @Override - public Long getLockDurationSeconds() { - return this.lockDurationSeconds; - } - - @Override - public Double getJitter() { - return jitter; - } - - /** - * Sets the name of the job. - * @param jobName The name of the job. - */ - public void setJobName(String jobName) { - this.jobName = jobName; - } - - /** - * Sets when the job was last updated. - * @param lastUpdateTime An {@link Instant} of when the job was last updated. - */ - public void setLastUpdateTime(Instant lastUpdateTime) { - this.lastUpdateTime = lastUpdateTime; - } - - /** - * Sets when the job was enabled. - * @param enabledTime An {@link Instant} of when the job was enabled. - */ - public void setEnabledTime(Instant enabledTime) { - this.enabledTime = enabledTime; - } - - /** - * Sets whether the job is enabled. - * @param enabled A boolean representing whether the job is enabled. - */ - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - /** - * Sets the schedule for the job. - * @param schedule A {@link Schedule} for the job. - */ - public void setSchedule(Schedule schedule) { - this.schedule = schedule; - } - - /** - * Sets the lock duration for the cluster when running the job. - * @param lockDurationSeconds The lock duration in seconds. - */ - public void setLockDurationSeconds(Long lockDurationSeconds) { - this.lockDurationSeconds = lockDurationSeconds; - } - - /** - * Sets the jitter for the job. - * @param jitter The jitter for the job. - */ - public void setJitter(Double jitter) { - this.jitter = jitter; - } - - /** - * Gets the type of click model to use for implicit judgment generation. - * @return The type of click model to use for implicit judgment generation. - */ - public String getClickModel() { - return clickModel; - } - - /** - * Sets the click model type to use for implicit judgment generation. - * @param clickModel The click model type to use for implicit judgment generation. - */ - public void setClickModel(String clickModel) { - this.clickModel = clickModel; - } - - /** - * Gets the max rank to use when generating implicit judgments. - * @return The max rank to use when generating implicit judgments. - */ - public int getMaxRank() { - return maxRank; - } - - /** - * Sets the max rank to use when generating implicit judgments. - * @param maxRank The max rank to use when generating implicit judgments. - */ - public void setMaxRank(int maxRank) { - this.maxRank = maxRank; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java b/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java deleted file mode 100644 index 442ae4c..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationJobRunner.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.index.IndexResponse; -import org.opensearch.action.support.WriteRequest; -import org.opensearch.client.Client; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.action.ActionListener; -import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel; -import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModelParameters; -import org.opensearch.jobscheduler.spi.JobExecutionContext; -import org.opensearch.jobscheduler.spi.ScheduledJobParameter; -import org.opensearch.jobscheduler.spi.ScheduledJobRunner; -import org.opensearch.jobscheduler.spi.utils.LockService; -import org.opensearch.threadpool.ThreadPool; - -import java.util.HashMap; -import java.util.Map; - -/** - * Job runner for scheduled implicit judgments jobs. - */ -public class SearchQualityEvaluationJobRunner implements ScheduledJobRunner { - - private static final Logger LOGGER = LogManager.getLogger(SearchQualityEvaluationJobRunner.class); - - private static SearchQualityEvaluationJobRunner INSTANCE; - - /** - * Gets a singleton instance of this class. - * @return A {@link SearchQualityEvaluationJobRunner}. - */ - public static SearchQualityEvaluationJobRunner getJobRunnerInstance() { - - LOGGER.info("Getting job runner instance"); - - if (INSTANCE != null) { - return INSTANCE; - } - - synchronized (SearchQualityEvaluationJobRunner.class) { - if (INSTANCE == null) { - INSTANCE = new SearchQualityEvaluationJobRunner(); - } - return INSTANCE; - } - - } - - private ClusterService clusterService; - private ThreadPool threadPool; - private Client client; - - private SearchQualityEvaluationJobRunner() { - - } - - public void setClusterService(ClusterService clusterService) { - this.clusterService = clusterService; - } - - public void setThreadPool(ThreadPool threadPool) { - this.threadPool = threadPool; - } - - public void setClient(Client client) { - this.client = client; - } - - @Override - public void runJob(final ScheduledJobParameter jobParameter, final JobExecutionContext context) { - - if(!(jobParameter instanceof SearchQualityEvaluationJobParameter)) { - throw new IllegalStateException( - "Job parameter is not instance of SampleJobParameter, type: " + jobParameter.getClass().getCanonicalName() - ); - } - - if(this.clusterService == null) { - throw new IllegalStateException("ClusterService is not initialized."); - } - - if(this.threadPool == null) { - throw new IllegalStateException("ThreadPool is not initialized."); - } - - final LockService lockService = context.getLockService(); - - final Runnable runnable = () -> { - - if (jobParameter.getLockDurationSeconds() != null) { - - lockService.acquireLock(jobParameter, context, ActionListener.wrap(lock -> { - - if (lock == null) { - return; - } - - final SearchQualityEvaluationJobParameter searchQualityEvaluationJobParameter = (SearchQualityEvaluationJobParameter) jobParameter; - - final long startTime = System.currentTimeMillis(); - final String judgmentsId; - - if("coec".equalsIgnoreCase(searchQualityEvaluationJobParameter.getClickModel())) { - - LOGGER.info("Beginning implicit judgment generation using clicks-over-expected-clicks."); - final CoecClickModelParameters coecClickModelParameters = new CoecClickModelParameters(searchQualityEvaluationJobParameter.getMaxRank()); - final CoecClickModel coecClickModel = new CoecClickModel(client, coecClickModelParameters); - - judgmentsId = coecClickModel.calculateJudgments(); - - } else { - - // Invalid click model. - throw new IllegalArgumentException("Invalid click model: " + searchQualityEvaluationJobParameter.getClickModel()); - - } - - final long elapsedTime = System.currentTimeMillis() - startTime; - LOGGER.info("Implicit judgment generation completed in {} ms", elapsedTime); - - final Map job = new HashMap<>(); - job.put("name", searchQualityEvaluationJobParameter.getName()); - job.put("click_model", searchQualityEvaluationJobParameter.getClickModel()); - job.put("started", startTime); - job.put("duration", elapsedTime); - job.put("judgments", judgmentsId); - job.put("invocation", "scheduled"); - job.put("max_rank", searchQualityEvaluationJobParameter.getMaxRank()); - - final IndexRequest indexRequest = new IndexRequest() - .index(SearchQualityEvaluationPlugin.COMPLETED_JOBS_INDEX_NAME) - .id(judgmentsId) - .source(job) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); - - client.index(indexRequest, new ActionListener<>() { - @Override - public void onResponse(IndexResponse indexResponse) { - LOGGER.info("Successfully indexed implicit judgments {}", judgmentsId); - } - - @Override - public void onFailure(Exception ex) { - LOGGER.error("Unable to index implicit judgments", ex); - } - }); - - }, exception -> { throw new IllegalStateException("Failed to acquire lock."); })); - } - - }; - - threadPool.generic().submit(runnable); - - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java b/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java deleted file mode 100644 index 6a7b581..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationPlugin.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.client.Client; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.node.DiscoveryNodes; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.settings.ClusterSettings; -import org.opensearch.common.settings.IndexScopedSettings; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.settings.SettingsFilter; -import org.opensearch.core.common.io.stream.NamedWriteableRegistry; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.core.xcontent.XContentParserUtils; -import org.opensearch.env.Environment; -import org.opensearch.env.NodeEnvironment; -import org.opensearch.jobscheduler.spi.JobSchedulerExtension; -import org.opensearch.jobscheduler.spi.ScheduledJobParser; -import org.opensearch.jobscheduler.spi.ScheduledJobRunner; -import org.opensearch.jobscheduler.spi.schedule.ScheduleParser; -import org.opensearch.plugins.ActionPlugin; -import org.opensearch.plugins.Plugin; -import org.opensearch.repositories.RepositoriesService; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestHandler; -import org.opensearch.script.ScriptService; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.watcher.ResourceWatcherService; - -import java.io.IOException; -import java.time.Instant; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.function.Supplier; - -/** - * Main class for the Search Quality Evaluation plugin. - */ -public class SearchQualityEvaluationPlugin extends Plugin implements ActionPlugin, JobSchedulerExtension { - - private static final Logger LOGGER = LogManager.getLogger(SearchQualityEvaluationPlugin.class); - - /** - * The name of the UBI index containing the queries. This should not be changed. - */ - public static final String UBI_QUERIES_INDEX_NAME = "ubi_queries"; - - /** - * The name of the UBI index containing the events. This should not be changed. - */ - public static final String UBI_EVENTS_INDEX_NAME = "ubi_events"; - - /** - * The name of the index to store the scheduled jobs to create implicit judgments. - */ - public static final String SCHEDULED_JOBS_INDEX_NAME = "search_quality_eval_scheduled_jobs"; - - /** - * The name of the index to store the completed jobs to create implicit judgments. - */ - public static final String COMPLETED_JOBS_INDEX_NAME = "search_quality_eval_completed_jobs"; - - /** - * The name of the index that stores the query sets. - */ - public static final String QUERY_SETS_INDEX_NAME = "search_quality_eval_query_sets"; - - /** - * The name of the index that stores the metrics for the dashboard. - */ - public static final String DASHBOARD_METRICS_INDEX_NAME = "sqe_metrics_sample_data"; - - /** - * The name of the index that stores the implicit judgments. - */ - public static final String JUDGMENTS_INDEX_NAME = "judgments"; - - @Override - public Collection createComponents( - final Client client, - final ClusterService clusterService, - final ThreadPool threadPool, - final ResourceWatcherService resourceWatcherService, - final ScriptService scriptService, - final NamedXContentRegistry xContentRegistry, - final Environment environment, - final NodeEnvironment nodeEnvironment, - final NamedWriteableRegistry namedWriteableRegistry, - final IndexNameExpressionResolver indexNameExpressionResolver, - final Supplier repositoriesServiceSupplier - ) { - - LOGGER.info("Creating search evaluation framework components"); - final SearchQualityEvaluationJobRunner jobRunner = SearchQualityEvaluationJobRunner.getJobRunnerInstance(); - jobRunner.setClusterService(clusterService); - jobRunner.setThreadPool(threadPool); - jobRunner.setClient(client); - - return Collections.emptyList(); - - } - - @Override - public String getJobType() { - return "scheduler_search_quality_eval"; - } - - @Override - public String getJobIndex() { - LOGGER.info("Getting job index name"); - return SCHEDULED_JOBS_INDEX_NAME; - } - - @Override - public ScheduledJobRunner getJobRunner() { - LOGGER.info("Creating job runner"); - return SearchQualityEvaluationJobRunner.getJobRunnerInstance(); - } - - @Override - public ScheduledJobParser getJobParser() { - - return (parser, id, jobDocVersion) -> { - - final SearchQualityEvaluationJobParameter jobParameter = new SearchQualityEvaluationJobParameter(); - XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); - - while (!parser.nextToken().equals(XContentParser.Token.END_OBJECT)) { - - final String fieldName = parser.currentName(); - - parser.nextToken(); - - switch (fieldName) { - case SearchQualityEvaluationJobParameter.NAME_FIELD: - jobParameter.setJobName(parser.text()); - break; - case SearchQualityEvaluationJobParameter.ENABLED_FILED: - jobParameter.setEnabled(parser.booleanValue()); - break; - case SearchQualityEvaluationJobParameter.ENABLED_TIME_FILED: - jobParameter.setEnabledTime(parseInstantValue(parser)); - break; - case SearchQualityEvaluationJobParameter.LAST_UPDATE_TIME_FIELD: - jobParameter.setLastUpdateTime(parseInstantValue(parser)); - break; - case SearchQualityEvaluationJobParameter.SCHEDULE_FIELD: - jobParameter.setSchedule(ScheduleParser.parse(parser)); - break; - case SearchQualityEvaluationJobParameter.LOCK_DURATION_SECONDS: - jobParameter.setLockDurationSeconds(parser.longValue()); - break; - case SearchQualityEvaluationJobParameter.JITTER: - jobParameter.setJitter(parser.doubleValue()); - break; - case SearchQualityEvaluationJobParameter.CLICK_MODEL: - jobParameter.setClickModel(parser.text()); - break; - case SearchQualityEvaluationJobParameter.MAX_RANK: - jobParameter.setMaxRank(parser.intValue()); - break; - default: - XContentParserUtils.throwUnknownToken(parser.currentToken(), parser.getTokenLocation()); - } - - } - - return jobParameter; - - }; - - } - - private Instant parseInstantValue(final XContentParser parser) throws IOException { - - if (XContentParser.Token.VALUE_NULL.equals(parser.currentToken())) { - return null; - } - - if (parser.currentToken().isValue()) { - return Instant.ofEpochMilli(parser.longValue()); - } - - XContentParserUtils.throwUnknownToken(parser.currentToken(), parser.getTokenLocation()); - return null; - - } - - @Override - public List getRestHandlers( - final Settings settings, - final RestController restController, - final ClusterSettings clusterSettings, - final IndexScopedSettings indexScopedSettings, - final SettingsFilter settingsFilter, - final IndexNameExpressionResolver indexNameExpressionResolver, - final Supplier nodesInCluster - ) { - return Collections.singletonList(new SearchQualityEvaluationRestHandler()); - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java b/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java deleted file mode 100644 index ba56f04..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/SearchQualityEvaluationRestHandler.java +++ /dev/null @@ -1,417 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.action.admin.indices.create.CreateIndexRequest; -import org.opensearch.action.admin.indices.create.CreateIndexResponse; -import org.opensearch.action.admin.indices.exists.indices.IndicesExistsRequest; -import org.opensearch.action.admin.indices.exists.indices.IndicesExistsResponse; -import org.opensearch.action.delete.DeleteRequest; -import org.opensearch.action.delete.DeleteResponse; -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.index.IndexResponse; -import org.opensearch.action.support.WriteRequest; -import org.opensearch.client.node.NodeClient; -import org.opensearch.common.xcontent.json.JsonXContent; -import org.opensearch.core.action.ActionListener; -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.core.rest.RestStatus; -import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel; -import org.opensearch.eval.judgments.clickmodel.coec.CoecClickModelParameters; -import org.opensearch.eval.runners.OpenSearchQuerySetRunner; -import org.opensearch.eval.runners.QuerySetRunResult; -import org.opensearch.eval.samplers.AllQueriesQuerySampler; -import org.opensearch.eval.samplers.AllQueriesQuerySamplerParameters; -import org.opensearch.eval.samplers.ProbabilityProportionalToSizeAbstractQuerySampler; -import org.opensearch.eval.samplers.ProbabilityProportionalToSizeParameters; -import org.opensearch.jobscheduler.spi.schedule.IntervalSchedule; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.RestResponse; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.ExecutionException; - -import static org.opensearch.eval.SearchQualityEvaluationPlugin.JUDGMENTS_INDEX_NAME; - -public class SearchQualityEvaluationRestHandler extends BaseRestHandler { - - private static final Logger LOGGER = LogManager.getLogger(SearchQualityEvaluationRestHandler.class); - - /** - * URL for the implicit judgment scheduling. - */ - public static final String SCHEDULING_URL = "/_plugins/search_quality_eval/schedule"; - - /** - * URL for on-demand implicit judgment generation. - */ - public static final String IMPLICIT_JUDGMENTS_URL = "/_plugins/search_quality_eval/judgments"; - - /** - * URL for managing query sets. - */ - public static final String QUERYSET_MANAGEMENT_URL = "/_plugins/search_quality_eval/queryset"; - - /** - * URL for initiating query sets to run on-demand. - */ - public static final String QUERYSET_RUN_URL = "/_plugins/search_quality_eval/run"; - - /** - * The placeholder in the query that gets replaced by the query term when running a query set. - */ - public static final String QUERY_PLACEHOLDER = "#$query##"; - - @Override - public String getName() { - return "Search Quality Evaluation Framework"; - } - - @Override - public List routes() { - return List.of( - new Route(RestRequest.Method.POST, IMPLICIT_JUDGMENTS_URL), - new Route(RestRequest.Method.POST, SCHEDULING_URL), - new Route(RestRequest.Method.DELETE, SCHEDULING_URL), - new Route(RestRequest.Method.POST, QUERYSET_MANAGEMENT_URL), - new Route(RestRequest.Method.POST, QUERYSET_RUN_URL)); - } - - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - - // Handle managing query sets. - if(QUERYSET_MANAGEMENT_URL.equalsIgnoreCase(request.path())) { - - // Creating a new query set by sampling the UBI queries. - if (request.method().equals(RestRequest.Method.POST)) { - - final String name = request.param("name"); - final String description = request.param("description"); - final String sampling = request.param("sampling", "pptss"); - final int querySetSize = Integer.parseInt(request.param("query_set_size", "1000")); - - // Create a query set by finding all the unique user_query terms. - if (AllQueriesQuerySampler.NAME.equalsIgnoreCase(sampling)) { - - // If we are not sampling queries, the query sets should just be directly - // indexed into OpenSearch using the `ubi_queries` index directly. - - try { - - final AllQueriesQuerySamplerParameters parameters = new AllQueriesQuerySamplerParameters(name, description, sampling, querySetSize); - final AllQueriesQuerySampler sampler = new AllQueriesQuerySampler(client, parameters); - - // Sample and index the queries. - final String querySetId = sampler.sample(); - - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"query_set\": \"" + querySetId + "\"}")); - - } catch(Exception ex) { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "{\"error\": \"" + ex.getMessage() + "\"}")); - } - - - // Create a query set by using PPTSS sampling. - } else if (ProbabilityProportionalToSizeAbstractQuerySampler.NAME.equalsIgnoreCase(sampling)) { - - LOGGER.info("Creating query set using PPTSS"); - - final ProbabilityProportionalToSizeParameters parameters = new ProbabilityProportionalToSizeParameters(name, description, sampling, querySetSize); - final ProbabilityProportionalToSizeAbstractQuerySampler sampler = new ProbabilityProportionalToSizeAbstractQuerySampler(client, parameters); - - try { - - // Sample and index the queries. - final String querySetId = sampler.sample(); - - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"query_set\": \"" + querySetId + "\"}")); - - } catch(Exception ex) { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "{\"error\": \"" + ex.getMessage() + "\"}")); - } - - } else { - // An Invalid sampling method was provided in the request. - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Invalid sampling method: " + sampling + "\"}")); - } - - } else { - // Invalid HTTP method for this endpoint. - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "{\"error\": \"" + request.method() + " is not allowed.\"}")); - } - - // Handle running query sets. - } else if(QUERYSET_RUN_URL.equalsIgnoreCase(request.path())) { - - final String querySetId = request.param("id"); - final String judgmentsId = request.param("judgments_id"); - final String index = request.param("index"); - final String searchPipeline = request.param("search_pipeline", null); - final String idField = request.param("id_field", "_id"); - final int k = Integer.parseInt(request.param("k", "10")); - final double threshold = Double.parseDouble(request.param("threshold", "1.0")); - - if(querySetId == null || querySetId.isEmpty() || judgmentsId == null || judgmentsId.isEmpty() || index == null || index.isEmpty()) { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing required parameters.\"}")); - } - - if(k < 1) { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"k must be a positive integer.\"}")); - } - - if(!request.hasContent()) { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing query in body.\"}")); - } - - // Get the query JSON from the content. - final String query = new String(BytesReference.toBytes(request.content()), Charset.defaultCharset()); - - // Validate the query has a QUERY_PLACEHOLDER. - if(!query.contains(QUERY_PLACEHOLDER)) { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Missing query placeholder in query.\"}")); - } - - try { - - final OpenSearchQuerySetRunner openSearchQuerySetRunner = new OpenSearchQuerySetRunner(client); - final QuerySetRunResult querySetRunResult = openSearchQuerySetRunner.run(querySetId, judgmentsId, index, searchPipeline, idField, query, k, threshold); - openSearchQuerySetRunner.save(querySetRunResult); - - } catch (Exception ex) { - LOGGER.error("Unable to run query set. Verify query set and judgments exist.", ex); - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, ex.getMessage())); - } - - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"message\": \"Run initiated for query set " + querySetId + "\"}")); - - // Handle the on-demand creation of implicit judgments. - } else if(IMPLICIT_JUDGMENTS_URL.equalsIgnoreCase(request.path())) { - - if (request.method().equals(RestRequest.Method.POST)) { - - //final long startTime = System.currentTimeMillis(); - final String clickModel = request.param("click_model", "coec"); - final int maxRank = Integer.parseInt(request.param("max_rank", "20")); - - if (CoecClickModel.CLICK_MODEL_NAME.equalsIgnoreCase(clickModel)) { - - final CoecClickModelParameters coecClickModelParameters = new CoecClickModelParameters(maxRank); - final CoecClickModel coecClickModel = new CoecClickModel(client, coecClickModelParameters); - - final String judgmentsId; - - // TODO: Run this in a separate thread. - try { - - // Create the judgments index. - createJudgmentsIndex(client); - - judgmentsId = coecClickModel.calculateJudgments(); - - // judgmentsId will be null if no judgments were created (and indexed). - if(judgmentsId == null) { - // TODO: Is Bad Request the appropriate error? Perhaps Conflict is more appropriate? - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"No judgments were created. Check the queries and events data.\"}")); - } - -// final long elapsedTime = System.currentTimeMillis() - startTime; -// -// final Map job = new HashMap<>(); -// job.put("name", "manual_generation"); -// job.put("click_model", clickModel); -// job.put("started", startTime); -// job.put("duration", elapsedTime); -// job.put("invocation", "on_demand"); -// job.put("judgments_id", judgmentsId); -// job.put("max_rank", maxRank); -// -// final String jobId = UUID.randomUUID().toString(); -// -// final IndexRequest indexRequest = new IndexRequest() -// .index(SearchQualityEvaluationPlugin.COMPLETED_JOBS_INDEX_NAME) -// .id(jobId) -// .source(job) -// .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); -// -// client.index(indexRequest, new ActionListener<>() { -// @Override -// public void onResponse(final IndexResponse indexResponse) { -// LOGGER.debug("Click model job completed successfully: {}", jobId); -// } -// -// @Override -// public void onFailure(final Exception ex) { -// LOGGER.error("Unable to run job with ID {}", jobId, ex); -// throw new RuntimeException("Unable to run job", ex); -// } -// }); - - } catch (Exception ex) { - throw new RuntimeException("Unable to generate judgments.", ex); - } - - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"judgments_id\": \"" + judgmentsId + "\"}")); - - } else { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "{\"error\": \"Invalid click model.\"}")); - } - - } else { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "{\"error\": \"" + request.method() + " is not allowed.\"}")); - } - - // Handle the scheduling of creating implicit judgments. - } else if(SCHEDULING_URL.equalsIgnoreCase(request.path())) { - - if (request.method().equals(RestRequest.Method.POST)) { - - // Get the job parameters from the request. - final String id = request.param("id"); - final String jobName = request.param("job_name", UUID.randomUUID().toString()); - final String lockDurationSecondsString = request.param("lock_duration_seconds", "600"); - final Long lockDurationSeconds = lockDurationSecondsString != null ? Long.parseLong(lockDurationSecondsString) : null; - final String jitterString = request.param("jitter"); - final Double jitter = jitterString != null ? Double.parseDouble(jitterString) : null; - final String clickModel = request.param("click_model"); - final int maxRank = Integer.parseInt(request.param("max_rank", "20")); - - // Validate the request parameters. - if (id == null || clickModel == null) { - throw new IllegalArgumentException("The id and click_model parameters must be provided."); - } - - // Read the start_time. - final Instant startTime; - if (request.param("start_time") == null) { - startTime = Instant.now(); - } else { - startTime = Instant.ofEpochMilli(Long.parseLong(request.param("start_time"))); - } - - // Read the interval. - final int interval; - if (request.param("interval") == null) { - // Default to every 24 hours. - interval = 1440; - } else { - interval = Integer.parseInt(request.param("interval")); - } - - final SearchQualityEvaluationJobParameter jobParameter = new SearchQualityEvaluationJobParameter( - jobName, new IntervalSchedule(startTime, interval, ChronoUnit.MINUTES), lockDurationSeconds, - jitter, clickModel, maxRank - ); - - final IndexRequest indexRequest = new IndexRequest().index(SearchQualityEvaluationPlugin.SCHEDULED_JOBS_INDEX_NAME) - .id(id) - .source(jobParameter.toXContent(JsonXContent.contentBuilder(), null)) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); - - return restChannel -> { - - // index the job parameter - client.index(indexRequest, new ActionListener<>() { - - @Override - public void onResponse(final IndexResponse indexResponse) { - - try { - - final RestResponse restResponse = new BytesRestResponse( - RestStatus.OK, - indexResponse.toXContent(JsonXContent.contentBuilder(), null) - ); - LOGGER.info("Created implicit judgments schedule for click-model {}: Job name {}, running every {} minutes starting {}", clickModel, jobName, interval, startTime); - - restChannel.sendResponse(restResponse); - - } catch (IOException e) { - restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, e.getMessage())); - } - - } - - @Override - public void onFailure(Exception e) { - restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, e.getMessage())); - } - }); - - }; - - // Delete a scheduled job to make implicit judgments. - } else if (request.method().equals(RestRequest.Method.DELETE)) { - - final String id = request.param("id"); - final DeleteRequest deleteRequest = new DeleteRequest().index(SearchQualityEvaluationPlugin.SCHEDULED_JOBS_INDEX_NAME).id(id); - - return restChannel -> client.delete(deleteRequest, new ActionListener<>() { - @Override - public void onResponse(final DeleteResponse deleteResponse) { - restChannel.sendResponse(new BytesRestResponse(RestStatus.OK, "{\"message\": \"Scheduled job deleted.\"}")); - } - - @Override - public void onFailure(Exception e) { - restChannel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, e.getMessage())); - } - }); - - } else { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "{\"error\": \"" + request.method() + " is not allowed.\"}")); - } - - } else { - return restChannel -> restChannel.sendResponse(new BytesRestResponse(RestStatus.NOT_FOUND, "{\"error\": \"" + request.path() + " was not found.\"}")); - } - - } - - private void createJudgmentsIndex(final NodeClient client) throws Exception { - - // If the judgments index does not exist we need to create it. - final IndicesExistsRequest indicesExistsRequest = new IndicesExistsRequest(JUDGMENTS_INDEX_NAME); - - final IndicesExistsResponse indicesExistsResponse = client.admin().indices().exists(indicesExistsRequest).get(); - - if(!indicesExistsResponse.isExists()) { - - // TODO: Read this mapping from a resource file instead. - final String mapping = "{\n" + - " \"properties\": {\n" + - " \"judgments_id\": { \"type\": \"keyword\" },\n" + - " \"query_id\": { \"type\": \"keyword\" },\n" + - " \"query\": { \"type\": \"keyword\" },\n" + - " \"document_id\": { \"type\": \"keyword\" },\n" + - " \"judgment\": { \"type\": \"double\" },\n" + - " \"timestamp\": { \"type\": \"date\", \"format\": \"strict_date_time\" }\n" + - " }\n" + - " }"; - - // Create the judgments index. - final CreateIndexRequest createIndexRequest = new CreateIndexRequest(JUDGMENTS_INDEX_NAME).mapping(mapping); - - // TODO: Don't use .get() - client.admin().indices().create(createIndexRequest).get(); - - } - - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java deleted file mode 100644 index ea83a87..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModel.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.clickmodel; - -/** - * Base class for creating click models. - */ -public abstract class ClickModel { - - /** - * Calculate implicit judgments. - * @return The judgments ID. - * @throws Exception Thrown if the judgments cannot be created. - */ - public abstract String calculateJudgments() throws Exception; - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java deleted file mode 100644 index 8c42550..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/ClickModelParameters.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.clickmodel; - -public abstract class ClickModelParameters { - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java deleted file mode 100644 index f2e8aa8..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModel.java +++ /dev/null @@ -1,422 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.clickmodel.coec; - -import com.google.gson.Gson; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.action.search.SearchScrollRequest; -import org.opensearch.client.Client; -import org.opensearch.client.Requests; -import org.opensearch.common.unit.TimeValue; -import org.opensearch.eval.SearchQualityEvaluationPlugin; -import org.opensearch.eval.judgments.clickmodel.ClickModel; -import org.opensearch.eval.judgments.model.ClickthroughRate; -import org.opensearch.eval.judgments.model.Judgment; -import org.opensearch.eval.judgments.model.ubi.event.UbiEvent; -import org.opensearch.eval.judgments.opensearch.OpenSearchHelper; -import org.opensearch.eval.judgments.queryhash.IncrementalUserQueryHash; -import org.opensearch.eval.utils.MathUtils; -import org.opensearch.index.query.BoolQueryBuilder; -import org.opensearch.index.query.QueryBuilder; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.index.query.WrapperQueryBuilder; -import org.opensearch.search.Scroll; -import org.opensearch.search.SearchHit; -import org.opensearch.search.aggregations.AggregationBuilders; -import org.opensearch.search.aggregations.BucketOrder; -import org.opensearch.search.aggregations.bucket.terms.Terms; -import org.opensearch.search.aggregations.bucket.terms.TermsAggregationBuilder; -import org.opensearch.search.builder.SearchSourceBuilder; - -import java.io.IOException; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.LinkedList; -import java.util.Map; -import java.util.Set; - -public class CoecClickModel extends ClickModel { - - public static final String CLICK_MODEL_NAME = "coec"; - - // OpenSearch indexes for COEC data. - public static final String INDEX_RANK_AGGREGATED_CTR = "rank_aggregated_ctr"; - public static final String INDEX_QUERY_DOC_CTR = "click_through_rates"; - - // UBI event names. - public static final String EVENT_CLICK = "click"; - public static final String EVENT_IMPRESSION = "impression"; - - private final CoecClickModelParameters parameters; - - private final OpenSearchHelper openSearchHelper; - - private final IncrementalUserQueryHash incrementalUserQueryHash = new IncrementalUserQueryHash(); - private final Gson gson = new Gson(); - private final Client client; - - private static final Logger LOGGER = LogManager.getLogger(CoecClickModel.class.getName()); - - public CoecClickModel(final Client client, final CoecClickModelParameters parameters) { - - this.parameters = parameters; - this.openSearchHelper = new OpenSearchHelper(client); - this.client = client; - - } - - @Override - public String calculateJudgments() throws Exception { - - final int maxRank = parameters.getMaxRank(); - - // Calculate and index the rank-aggregated click-through. - LOGGER.info("Beginning calculation of rank-aggregated click-through."); - final Map rankAggregatedClickThrough = getRankAggregatedClickThrough(); - LOGGER.info("Rank-aggregated clickthrough positions: {}", rankAggregatedClickThrough.size()); - showRankAggregatedClickThrough(rankAggregatedClickThrough); - - // Calculate and index the click-through rate for query/doc pairs. - LOGGER.info("Beginning calculation of clickthrough rates."); - final Map> clickthroughRates = getClickthroughRate(); - LOGGER.info("Clickthrough rates for number of queries: {}", clickthroughRates.size()); - showClickthroughRates(clickthroughRates); - - // Generate and index the implicit judgments. - LOGGER.info("Beginning calculation of implicit judgments."); - return calculateCoec(rankAggregatedClickThrough, clickthroughRates); - - } - - public String calculateCoec(final Map rankAggregatedClickThrough, - final Map> clickthroughRates) throws Exception { - - // Calculate the COEC. - // Numerator is the total number of clicks received by a query/result pair. - // Denominator is the expected clicks (EC) that an average result would receive after being impressed i times at rank r, - // and CTR is the average CTR for each position in the results page (up to R) computed over all queries and results. - - // Format: query_id, query, document, judgment - final Collection judgments = new LinkedList<>(); - - LOGGER.info("Count of queries: {}", clickthroughRates.size()); - - for(final String userQuery : clickthroughRates.keySet()) { - - // The clickthrough rates for this one query. - // A ClickthroughRate is a document with counts of impressions and clicks. - final Collection ctrs = clickthroughRates.get(userQuery); - - // Go through each clickthrough rate for this query. - for(final ClickthroughRate ctr : ctrs) { - - double denominatorSum = 0; - - for(int rank = 0; rank < parameters.getMaxRank(); rank++) { - - // The document's mean CTR at the rank. - final double meanCtrAtRank = rankAggregatedClickThrough.getOrDefault(rank, 0.0); - - // The number of times this document was shown as this rank. - final long countOfTimesShownAtRank = openSearchHelper.getCountOfQueriesForUserQueryHavingResultInRankR(userQuery, ctr.getObjectId(), rank); - - denominatorSum += (meanCtrAtRank * countOfTimesShownAtRank); - - } - - // Numerator is sum of clicks at all ranks up to the maxRank. - final int totalNumberClicksForQueryResult = ctr.getClicks(); - - // Divide the numerator by the denominator (value). - final double judgmentValue; - - if(denominatorSum == 0) { - judgmentValue = 0.0; - } else { - judgmentValue = totalNumberClicksForQueryResult / denominatorSum; - } - - // Hash the user query to get a query ID. - final int queryId = incrementalUserQueryHash.getHash(userQuery); - - // Add the judgment to the list. - // TODO: What to do for query ID when the values are per user_query instead? - final Judgment judgment = new Judgment(String.valueOf(queryId), userQuery, ctr.getObjectId(), judgmentValue); - judgments.add(judgment); - - } - - } - - LOGGER.info("Count of user queries: {}", clickthroughRates.size()); - LOGGER.info("Count of judgments: {}", judgments.size()); - - showJudgments(judgments); - - if(!(judgments.isEmpty())) { - return openSearchHelper.indexJudgments(judgments); - } else { - return null; - } - - } - - /** - * Gets the clickthrough rates for each query and its results. - * @return A map of user_query to the clickthrough rate for each query result. - * @throws IOException Thrown when a problem accessing OpenSearch. - */ - private Map> getClickthroughRate() throws Exception { - - // For each query: - // - Get each document returned in that query (in the QueryResponse object). - // - Calculate the click-through rate for the document. (clicks/impressions) - - // TODO: Allow for a time period and for a specific application. - - final String query = "{\n" + - " \"bool\": {\n" + - " \"should\": [\n" + - " {\n" + - " \"term\": {\n" + - " \"action_name\": \"click\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"term\": {\n" + - " \"action_name\": \"impression\"\n" + - " }\n" + - " }\n" + - " ],\n" + - " \"must\": [\n" + - " {\n" + - " \"range\": {\n" + - " \"event_attributes.position.ordinal\": {\n" + - " \"lte\": " + parameters.getMaxRank() + "\n" + - " }\n" + - " }\n" + - " }\n" + - " ]\n" + - " }\n" + - " }"; - - final BoolQueryBuilder queryBuilder = new BoolQueryBuilder().must(new WrapperQueryBuilder(query)); - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(queryBuilder).size(1000); - final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(10L)); - - final SearchRequest searchRequest = Requests - .searchRequest(SearchQualityEvaluationPlugin.UBI_EVENTS_INDEX_NAME) - .source(searchSourceBuilder) - .scroll(scroll); - - // TODO Don't use .get() - SearchResponse searchResponse = client.search(searchRequest).get(); - - String scrollId = searchResponse.getScrollId(); - SearchHit[] searchHits = searchResponse.getHits().getHits(); - - final Map> queriesToClickthroughRates = new HashMap<>(); - - while (searchHits != null && searchHits.length > 0) { - - for (final SearchHit hit : searchHits) { - - final UbiEvent ubiEvent = AccessController.doPrivileged((PrivilegedAction) () -> gson.fromJson(hit.getSourceAsString(), UbiEvent.class)); - - // We need to the hash of the query_id because two users can both search - // for "computer" and those searches will have different query IDs, but they are the same search. - final String userQuery = openSearchHelper.getUserQuery(ubiEvent.getQueryId()); - - // userQuery will be null if there is not a query for this event in ubi_queries. - if(userQuery != null) { - - // Get the clicks for this queryId from the map, or an empty list if this is a new query. - final Set clickthroughRates = queriesToClickthroughRates.getOrDefault(userQuery, new LinkedHashSet<>()); - - // Get the ClickthroughRate object for the object that was interacted with. - final ClickthroughRate clickthroughRate = clickthroughRates.stream().filter(p -> p.getObjectId().equals(ubiEvent.getEventAttributes().getObject().getObjectId())).findFirst().orElse(new ClickthroughRate(ubiEvent.getEventAttributes().getObject().getObjectId())); - - if (EVENT_CLICK.equalsIgnoreCase(ubiEvent.getActionName())) { - //LOGGER.info("Logging a CLICK on " + ubiEvent.getEventAttributes().getObject().getObjectId()); - clickthroughRate.logClick(); - } else if (EVENT_IMPRESSION.equalsIgnoreCase(ubiEvent.getActionName())) { - //LOGGER.info("Logging an IMPRESSION on " + ubiEvent.getEventAttributes().getObject().getObjectId()); - clickthroughRate.logImpression(); - } else { - LOGGER.warn("Invalid event action name: {}", ubiEvent.getActionName()); - } - - clickthroughRates.add(clickthroughRate); - queriesToClickthroughRates.put(userQuery, clickthroughRates); - // LOGGER.debug("clickthroughRate = {}", queriesToClickthroughRates.size()); - - } - - } - - final SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); - scrollRequest.scroll(scroll); - - //LOGGER.info("Doing scroll to next results"); - // TODO: Getting a warning in the log that "QueryGroup _id can't be null, It should be set before accessing it. This is abnormal behaviour" - // I don't remember seeing this prior to 2.18.0 but it's possible I just didn't see it. - // https://github.com/opensearch-project/OpenSearch/blob/f105e4eb2ede1556b5dd3c743bea1ab9686ebccf/server/src/main/java/org/opensearch/wlm/QueryGroupTask.java#L73 - searchResponse = client.searchScroll(scrollRequest).get(); - //LOGGER.info("Scroll complete."); - - scrollId = searchResponse.getScrollId(); - - searchHits = searchResponse.getHits().getHits(); - - } - - openSearchHelper.indexClickthroughRates(queriesToClickthroughRates); - - return queriesToClickthroughRates; - - } - - /** - * Calculate the rank-aggregated click through from the UBI events. - * @return A map of positions to clickthrough rates. - * @throws IOException Thrown when a problem accessing OpenSearch. - */ - public Map getRankAggregatedClickThrough() throws Exception { - - final Map rankAggregatedClickThrough = new HashMap<>(); - - // TODO: Allow for a time period and for a specific application. - - final QueryBuilder findRangeNumber = QueryBuilders.rangeQuery("event_attributes.position.ordinal").lte(parameters.getMaxRank()); - final QueryBuilder queryBuilder = new BoolQueryBuilder().must(findRangeNumber); - - // Order the aggregations by key and not by value. - final BucketOrder bucketOrder = BucketOrder.key(true); - - final TermsAggregationBuilder positionsAggregator = AggregationBuilders.terms("By_Position").field("event_attributes.position.ordinal").order(bucketOrder).size(parameters.getMaxRank()); - final TermsAggregationBuilder actionNameAggregation = AggregationBuilders.terms("By_Action").field("action_name").subAggregation(positionsAggregator).order(bucketOrder).size(parameters.getMaxRank()); - - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder() - .query(queryBuilder) - .aggregation(actionNameAggregation) - .from(0) - .size(0); - - final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.UBI_EVENTS_INDEX_NAME).source(searchSourceBuilder); - final SearchResponse searchResponse = client.search(searchRequest).get(); - - final Map clickCounts = new HashMap<>(); - final Map impressionCounts = new HashMap<>(); - - final Terms actionTerms = searchResponse.getAggregations().get("By_Action"); - final Collection actionBuckets = actionTerms.getBuckets(); - - LOGGER.debug("Aggregation query: {}", searchSourceBuilder.toString()); - - for(final Terms.Bucket actionBucket : actionBuckets) { - - // Handle the "impression" bucket. - if(EVENT_IMPRESSION.equalsIgnoreCase(actionBucket.getKey().toString())) { - - final Terms positionTerms = actionBucket.getAggregations().get("By_Position"); - final Collection positionBuckets = positionTerms.getBuckets(); - - for(final Terms.Bucket positionBucket : positionBuckets) { - LOGGER.debug("Inserting impression event from position {} with click count {}", positionBucket.getKey(), (double) positionBucket.getDocCount()); - impressionCounts.put(Integer.valueOf(positionBucket.getKey().toString()), (double) positionBucket.getDocCount()); - } - - } - - // Handle the "click" bucket. - if(EVENT_CLICK.equalsIgnoreCase(actionBucket.getKey().toString())) { - - final Terms positionTerms = actionBucket.getAggregations().get("By_Position"); - final Collection positionBuckets = positionTerms.getBuckets(); - - for(final Terms.Bucket positionBucket : positionBuckets) { - LOGGER.debug("Inserting client event from position {} with click count {}", positionBucket.getKey(), (double) positionBucket.getDocCount()); - clickCounts.put(Integer.valueOf(positionBucket.getKey().toString()), (double) positionBucket.getDocCount()); - } - - } - - } - - for(int rank = 0; rank < parameters.getMaxRank(); rank++) { - - if(impressionCounts.containsKey(rank)) { - - if(clickCounts.containsKey(rank)) { - - // Calculate the CTR by dividing the number of clicks by the number of impressions. - LOGGER.info("Position = {}, Impression Counts = {}, Click Count = {}", rank, impressionCounts.get(rank), clickCounts.get(rank)); - rankAggregatedClickThrough.put(rank, clickCounts.get(rank) / impressionCounts.get(rank)); - - } else { - - // This document has impressions but no clicks, so it's CTR is zero. - LOGGER.info("Position = {}, Impression Counts = {}, Impressions but no clicks so CTR is 0", rank, clickCounts.get(rank)); - rankAggregatedClickThrough.put(rank, 0.0); - - } - - } else { - - // No impressions so the clickthrough rate is 0. - LOGGER.info("No impressions for rank {}, so using CTR of 0", rank); - rankAggregatedClickThrough.put(rank, (double) 0); - - } - - } - - openSearchHelper.indexRankAggregatedClickthrough(rankAggregatedClickThrough); - - return rankAggregatedClickThrough; - - } - - private void showJudgments(final Collection judgments) { - - for(final Judgment judgment : judgments) { - LOGGER.info(judgment.toJudgmentString()); - } - - } - - private void showClickthroughRates(final Map> clickthroughRates) { - - for(final String userQuery : clickthroughRates.keySet()) { - - LOGGER.debug("user_query: {}", userQuery); - - for(final ClickthroughRate clickthroughRate : clickthroughRates.get(userQuery)) { - LOGGER.debug("\t - {}", clickthroughRate.toString()); - } - - } - - } - - private void showRankAggregatedClickThrough(final Map rankAggregatedClickThrough) { - - for(final int position : rankAggregatedClickThrough.keySet()) { - LOGGER.info("Position: {}, # ctr: {}", position, MathUtils.round(rankAggregatedClickThrough.get(position), parameters.getRoundingDigits())); - } - - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java deleted file mode 100644 index 36df03e..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/clickmodel/coec/CoecClickModelParameters.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.clickmodel.coec; - -import org.opensearch.eval.judgments.clickmodel.ClickModelParameters; - -/** - * The parameters for the {@link CoecClickModel}. - */ -public class CoecClickModelParameters extends ClickModelParameters { - - private final int maxRank; - private int roundingDigits = 3; - - /** - * Creates new parameters. - * @param maxRank The max rank to use when calculating the judgments. - */ - public CoecClickModelParameters(final int maxRank) { - this.maxRank = maxRank; - } - - /** - * Creates new parameters. - * @param maxRank The max rank to use when calculating the judgments. - * @param roundingDigits The number of decimal places to round calculated values to. - */ - public CoecClickModelParameters(final int maxRank, final int roundingDigits) { - this.maxRank = maxRank; - this.roundingDigits = roundingDigits; - } - - /** - * Gets the max rank for the implicit judgments calculation. - * @return The max rank for the implicit judgments calculation. - */ - public int getMaxRank() { - return maxRank; - } - - /** - * Gets the number of rounding digits to use for judgments. - * @return The number of rounding digits to use for judgments. - */ - public int getRoundingDigits() { - return roundingDigits; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java deleted file mode 100644 index cef1f1f..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ClickthroughRate.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.model; - -import org.opensearch.eval.utils.MathUtils; - -/** - * A query result and its number of clicks and total events. - */ -public class ClickthroughRate { - - private final String objectId; - private int clicks; - private int impressions; - - /** - * Creates a new clickthrough rate for an object. - * @param objectId The ID of the object. - */ - public ClickthroughRate(final String objectId) { - this.objectId = objectId; - this.clicks = 0; - this.impressions = 0; - } - - /** - * Creates a new clickthrough rate for an object given counts of clicks and events. - * @param objectId The object ID. - * @param clicks The count of clicks. - * @param impressions The count of events. - */ - public ClickthroughRate(final String objectId, final int clicks, final int impressions) { - this.objectId = objectId; - this.clicks = clicks; - this.impressions = impressions; - } - - @Override - public String toString() { - return "object_id: " + objectId + ", clicks: " + clicks + ", events: " + impressions + ", ctr: " + MathUtils.round(getClickthroughRate()); - } - - /** - * Log a click to this object. - * This increments clicks and events. - */ - public void logClick() { - clicks++; - } - - /** - * Log an impression to this object. - */ - public void logImpression() { - impressions++; - } - - /** - * Calculate the clickthrough rate. - * @return The clickthrough rate as clicks divided by events. - */ - public double getClickthroughRate() { - return (double) clicks / impressions; - } - - /** - * Gets the count of clicks. - * @return The count of clicks. - */ - public int getClicks() { - return clicks; - } - - /** - * Gets the count of events. - * @return The count of events. - */ - public int getImpressions() { - return impressions; - } - - /** - * Gets the object ID. - * @return The object ID. - */ - public String getObjectId() { - return objectId; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/Judgment.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/Judgment.java deleted file mode 100644 index bc9955f..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/Judgment.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.model; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.eval.utils.MathUtils; - -import java.util.HashMap; -import java.util.Map; - -/** - * A judgment of a search result's quality for a given query. - */ -public class Judgment { - - private static final Logger LOGGER = LogManager.getLogger(Judgment.class.getName()); - - private final String queryId; - private final String query; - private final String document; - private final double judgment; - - /** - * Creates a new judgment. - * @param queryId The query ID for the judgment. - * @param query The query for the judgment. - * @param document The document in the jdugment. - * @param judgment The judgment value. - */ - public Judgment(final String queryId, final String query, final String document, final double judgment) { - this.queryId = queryId; - this.query = query; - this.document = document; - this.judgment = judgment; - } - - public String toJudgmentString() { - return queryId + ", " + query + ", " + document + ", " + MathUtils.round(judgment); - } - - public Map getJudgmentAsMap() { - - final Map judgmentMap = new HashMap<>(); - judgmentMap.put("query_id", queryId); - judgmentMap.put("query", query); - judgmentMap.put("document_id", document); - judgmentMap.put("judgment", judgment); - - return judgmentMap; - - } - - @Override - public String toString() { - return "query_id: " + queryId + ", query: " + query + ", document: " + document + ", judgment: " + MathUtils.round(judgment); - } - - /** - * Gets the judgment's query ID. - * @return The judgment's query ID. - */ - public String getQueryId() { - return queryId; - } - - /** - * Gets the judgment's query. - * @return The judgment's query. - */ - public String getQuery() { - return query; - } - - /** - * Gets the judgment's document. - * @return The judgment's document. - */ - public String getDocument() { - return document; - } - - /** - * Gets the judgment's value. - * @return The judgment's value. - */ - public double getJudgment() { - return judgment; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java deleted file mode 100644 index 2244df4..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/QuerySetQuery.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.model; - -public class QuerySetQuery { - - private final String query; - private final long frequency; - - public QuerySetQuery(final String query, final long frequency) { - this.query = query; - this.frequency = frequency; - } - - public String getQuery() { - return query; - } - - public long getFrequency() { - return frequency; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java deleted file mode 100644 index cf09444..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventAttributes.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.model.ubi.event; - -import com.google.gson.annotations.SerializedName; - -/** - * Attributes on an UBI event. - */ -public class EventAttributes { - - @SerializedName("object") - private EventObject object; - - @SerializedName("session_id") - private String sessionId; - - @SerializedName("position") - private Position position; - - /** - * Creates a new object. - */ - public EventAttributes() { - - } - - /** - * Gets the {@link EventObject} for the event. - * @return A {@link EventObject}. - */ - public EventObject getObject() { - return object; - } - - /** - * Sets the {@link EventObject} for the event. - * @param object A {@link EventObject}. - */ - public void setObject(EventObject object) { - this.object = object; - } - - /** - * Gets the session ID for the event. - * @return The session ID for the event. - */ - public String getSessionId() { - return sessionId; - } - - /** - * Sets the session ID for the event. - * @param sessionId The session ID for the evnet. - */ - public void setSessionId(String sessionId) { - this.sessionId = sessionId; - } - - /** - * Gets the {@link Position} associated with the event. - * @return The {@link Position} associated with the event. - */ - public Position getPosition() { - return position; - } - - /** - * Sets the {@link Position} associated with the event. - * @param position The {@link Position} associated with the event. - */ - public void setPosition(Position position) { - this.position = position; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java deleted file mode 100644 index 55595ba..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/EventObject.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.model.ubi.event; - -import com.google.gson.annotations.SerializedName; - -public class EventObject { - - @SerializedName("object_id_field") - private String objectIdField; - - @SerializedName("object_id") - private String objectId; - - @Override - public String toString() { - return "[" + objectIdField + ", " + objectId + "]"; - } - - /** - * Gets the object ID. - * @return The object ID. - */ - public String getObjectId() { - return objectId; - } - - /** - * Sets the object ID. - * @param objectId The object ID. - */ - public void setObjectId(String objectId) { - this.objectId = objectId; - } - - /** - * Gets the object ID field. - * @return The object ID field. - */ - public String getObjectIdField() { - return objectIdField; - } - - /** - * Sets the object ID field. - * @param objectIdField The object ID field. - */ - public void setObjectIdField(String objectIdField) { - this.objectIdField = objectIdField; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java deleted file mode 100644 index e3ebaad..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/Position.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.model.ubi.event; - -import com.google.gson.annotations.SerializedName; - -/** - * A position represents the location of a search result in an event. - */ -public class Position { - - @SerializedName("ordinal") - private int ordinal; - - @Override - public String toString() { - return String.valueOf(ordinal); - } - - /** - * Gets the ordinal of the position. - * @return The ordinal of the position. - */ - public int getOrdinal() { - return ordinal; - } - - /** - * Sets the ordinal of the position. - * @param ordinal The ordinal of the position. - */ - public void setOrdinal(int ordinal) { - this.ordinal = ordinal; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java deleted file mode 100644 index 61c0f8b..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/event/UbiEvent.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.model.ubi.event; - -import com.google.gson.annotations.SerializedName; - -/** - * Creates a representation of a UBI event. - */ -public class UbiEvent { - - @SerializedName("action_name") - private String actionName; - - @SerializedName("client_id") - private String clientId; - - @SerializedName("query_id") - private String queryId; - - @SerializedName("event_attributes") - private EventAttributes eventAttributes; - - /** - * Creates a new representation of an UBI event. - */ - public UbiEvent() { - - } - - @Override - public String toString() { - return actionName + ", " + clientId + ", " + queryId + ", " + eventAttributes.getObject().toString() + ", " + eventAttributes.getPosition().getOrdinal(); - } - - /** - * Gets the name of the action. - * @return The name of the action. - */ - public String getActionName() { - return actionName; - } - - /** - * Gets the client ID. - * @return The client ID. - */ - public String getClientId() { - return clientId; - } - - /** - * Gets the query ID. - * @return The query ID. - */ - public String getQueryId() { - return queryId; - } - - /** - * Gets the event attributes. - * @return The {@link EventAttributes}. - */ - public EventAttributes getEventAttributes() { - return eventAttributes; - } - - /** - * Sets the event attributes. - * @param eventAttributes The {@link EventAttributes}. - */ - public void setEventAttributes(EventAttributes eventAttributes) { - this.eventAttributes = eventAttributes; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java deleted file mode 100644 index 5d45ee0..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/QueryResponse.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.model.ubi.query; - -import java.util.List; - -/** - * A query response for a {@link UbiQuery query}. - */ -public class QueryResponse { - - private final String queryId; - private final String queryResponseId; - private final List queryResponseHitIds; - - /** - * Creates a query response. - * @param queryId The ID of the query. - * @param queryResponseId The ID of the query response. - * @param queryResponseHitIds A list of IDs for the hits in the query. - */ - public QueryResponse(final String queryId, final String queryResponseId, final List queryResponseHitIds) { - this.queryId = queryId; - this.queryResponseId = queryResponseId; - this.queryResponseHitIds = queryResponseHitIds; - } - - /** - * Gets the query ID. - * @return The query ID. - */ - public String getQueryId() { - return queryId; - } - - /** - * Gets the query response ID. - * @return The query response ID. - */ - public String getQueryResponseId() { - return queryResponseId; - } - - /** - * Gets the list of query response hit IDs. - * @return A list of query response hit IDs. - */ - public List getQueryResponseHitIds() { - return queryResponseHitIds; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java deleted file mode 100644 index 0b7ca0b..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/model/ubi/query/UbiQuery.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.model.ubi.query; - -import com.google.gson.annotations.SerializedName; - -import java.util.Map; - -/** - * Represents a UBI query. - */ -public class UbiQuery { - - @SerializedName("timestamp") - private String timestamp; - - @SerializedName("query_id") - private String queryId; - - @SerializedName("client_id") - private String clientId; - - @SerializedName("user_query") - private String userQuery; - - @SerializedName("query") - private String query; - - @SerializedName("query_attributes") - private Map queryAttributes; - - @SerializedName("query_response") - private QueryResponse queryResponse; - - /** - * Creates a new UBI query object. - */ - public UbiQuery() { - - } - - /** - * Gets the timestamp for the query. - * @return The timestamp for the query. - */ - public String getTimestamp() { - return timestamp; - } - - /** - * Sets the timestamp for the query. - * @param timestamp The timestamp for the query. - */ - public void setTimestamp(String timestamp) { - this.timestamp = timestamp; - } - - /** - * Gets the query ID. - * @return The query ID. - */ - public String getQueryId() { - return queryId; - } - - /** - * Sets the query ID. - * @param queryId The query ID. - */ - public void setQueryId(String queryId) { - this.queryId = queryId; - } - - /** - * Sets the client ID. - * @param clientId The client ID. - */ - public void setClientId(String clientId) { - this.clientId = clientId; - } - - /** - * Gets the client ID. - * @return The client ID. - */ - public String getClientId() { - return clientId; - } - - /** - * Gets the user query. - * @return The user query. - */ - public String getUserQuery() { - return userQuery; - } - - /** - * Sets the user query. - * @param userQuery The user query. - */ - public void setUserQuery(String userQuery) { - this.userQuery = userQuery; - } - - /** - * Gets the query. - * @return The query. - */ - public String getQuery() { - return query; - } - - /** - * Sets the query. - * @param query The query. - */ - public void setQuery(String query) { - this.query = query; - } - - /** - * Sets the query attributes. - * @return The query attributes. - */ - public Map getQueryAttributes() { - return queryAttributes; - } - - /** - * Sets the query attributes. - * @param queryAttributes The query attributes. - */ - public void setQueryAttributes(Map queryAttributes) { - this.queryAttributes = queryAttributes; - } - - /** - * Gets the query responses. - * @return The query responses. - */ - public QueryResponse getQueryResponse() { - return queryResponse; - } - - /** - * Sets the query responses. - * @param queryResponse The query responses. - */ - public void setQueryResponse(QueryResponse queryResponse) { - this.queryResponse = queryResponse; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java deleted file mode 100644 index 3c391b3..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/opensearch/OpenSearchHelper.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.opensearch; - -import com.google.gson.Gson; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.action.bulk.BulkRequest; -import org.opensearch.action.bulk.BulkResponse; -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.client.Client; -import org.opensearch.core.action.ActionListener; -import org.opensearch.eval.judgments.model.ClickthroughRate; -import org.opensearch.eval.judgments.model.Judgment; -import org.opensearch.eval.judgments.model.ubi.query.UbiQuery; -import org.opensearch.eval.utils.TimeUtils; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.index.query.WrapperQueryBuilder; -import org.opensearch.search.SearchHit; -import org.opensearch.search.builder.SearchSourceBuilder; - -import java.io.IOException; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -import static org.opensearch.eval.SearchQualityEvaluationPlugin.JUDGMENTS_INDEX_NAME; -import static org.opensearch.eval.SearchQualityEvaluationPlugin.UBI_EVENTS_INDEX_NAME; -import static org.opensearch.eval.SearchQualityEvaluationPlugin.UBI_QUERIES_INDEX_NAME; -import static org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel.INDEX_QUERY_DOC_CTR; -import static org.opensearch.eval.judgments.clickmodel.coec.CoecClickModel.INDEX_RANK_AGGREGATED_CTR; - -/** - * Functionality for interacting with OpenSearch. - * TODO: Move these functions out of this class. - */ -public class OpenSearchHelper { - - private static final Logger LOGGER = LogManager.getLogger(OpenSearchHelper.class.getName()); - - private final Client client; - private final Gson gson = new Gson(); - - // Used to cache the query ID->user_query to avoid unnecessary lookups to OpenSearch. - private static final Map userQueryCache = new HashMap<>(); - - public OpenSearchHelper(final Client client) { - this.client = client; - } - - /** - * Gets the user query for a given query ID. - * @param queryId The query ID. - * @return The user query. - * @throws IOException Thrown when there is a problem accessing OpenSearch. - */ - public String getUserQuery(final String queryId) throws Exception { - - // If it's in the cache just get it and return it. - if(userQueryCache.containsKey(queryId)) { - return userQueryCache.get(queryId); - } - - // Cache it and return it. - final UbiQuery ubiQuery = getQueryFromQueryId(queryId); - - // ubiQuery will be null if the query does not exist. - if(ubiQuery != null) { - - userQueryCache.put(queryId, ubiQuery.getUserQuery()); - return ubiQuery.getUserQuery(); - - } else { - - return null; - - } - - } - - /** - * Gets the query object for a given query ID. - * @param queryId The query ID. - * @return A {@link UbiQuery} object for the given query ID. - * @throws Exception Thrown if the query cannot be retrieved. - */ - public UbiQuery getQueryFromQueryId(final String queryId) throws Exception { - - LOGGER.debug("Getting query from query ID {}", queryId); - - final String query = "{\"match\": {\"query_id\": \"" + queryId + "\" }}"; - final WrapperQueryBuilder qb = QueryBuilders.wrapperQuery(query); - - // The query_id should be unique anyway, but we are limiting it to a single result anyway. - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(qb); - searchSourceBuilder.from(0); - searchSourceBuilder.size(1); - - final String[] indexes = {UBI_QUERIES_INDEX_NAME}; - - final SearchRequest searchRequest = new SearchRequest(indexes, searchSourceBuilder); - final SearchResponse response = client.search(searchRequest).get(); - - // If this does not return a query then we cannot calculate the judgments. Each even should have a query associated with it. - if(response.getHits().getHits() != null & response.getHits().getHits().length > 0) { - - final SearchHit hit = response.getHits().getHits()[0]; - return AccessController.doPrivileged((PrivilegedAction) () -> gson.fromJson(hit.getSourceAsString(), UbiQuery.class)); - - } else { - - LOGGER.warn("No query exists for query ID {} to calculate judgments.", queryId); - return null; - - } - - } - - private Collection getQueryIdsHavingUserQuery(final String userQuery) throws Exception { - - final String query = "{\"match\": {\"user_query\": \"" + userQuery + "\" }}"; - final WrapperQueryBuilder qb = QueryBuilders.wrapperQuery(query); - - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(qb); - - final String[] indexes = {UBI_QUERIES_INDEX_NAME}; - - final SearchRequest searchRequest = new SearchRequest(indexes, searchSourceBuilder); - final SearchResponse response = client.search(searchRequest).get(); - - final Collection queryIds = new ArrayList<>(); - - for(final SearchHit hit : response.getHits().getHits()) { - final String queryId = hit.getSourceAsMap().get("query_id").toString(); - queryIds.add(queryId); - } - - return queryIds; - - } - - public long getCountOfQueriesForUserQueryHavingResultInRankR(final String userQuery, final String objectId, final int rank) throws Exception { - - long countOfTimesShownAtRank = 0; - - // Get all query IDs matching this user query. - final Collection queryIds = getQueryIdsHavingUserQuery(userQuery); - - // For each query ID, get the events with action_name = "impression" having a match on objectId and rank (position). - for(final String queryId : queryIds) { - - final String query = "{\n" + - " \"bool\": {\n" + - " \"must\": [\n" + - " {\n" + - " \"term\": {\n" + - " \"query_id\": \"" + queryId + "\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"term\": {\n" + - " \"action_name\": \"impression\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"term\": {\n" + - " \"event_attributes.position.ordinal\": \"" + rank + "\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"term\": {\n" + - " \"event_attributes.object.object_id\": \"" + objectId + "\"\n" + - " }\n" + - " }\n" + - " ]\n" + - " }\n" + - " }"; - - final WrapperQueryBuilder qb = QueryBuilders.wrapperQuery(query); - - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(qb); - searchSourceBuilder.trackTotalHits(true); - searchSourceBuilder.size(0); - - final String[] indexes = {UBI_EVENTS_INDEX_NAME}; - - final SearchRequest searchRequest = new SearchRequest(indexes, searchSourceBuilder); - final SearchResponse response = client.search(searchRequest).get(); - - // Won't be null as long as trackTotalHits is true. - if(response.getHits().getTotalHits() != null) { - countOfTimesShownAtRank += response.getHits().getTotalHits().value; - } - - } - - LOGGER.debug("Count of {} having {} at rank {} = {}", userQuery, objectId, rank, countOfTimesShownAtRank); - - if(countOfTimesShownAtRank > 0) { - LOGGER.debug("Count of {} having {} at rank {} = {}", userQuery, objectId, rank, countOfTimesShownAtRank); - } - - return countOfTimesShownAtRank; - - } - - /** - * Index the rank-aggregated clickthrough values. - * @param rankAggregatedClickThrough A map of position to clickthrough values. - * @throws IOException Thrown when there is a problem accessing OpenSearch. - */ - public void indexRankAggregatedClickthrough(final Map rankAggregatedClickThrough) throws Exception { - - if(!rankAggregatedClickThrough.isEmpty()) { - - // TODO: Split this into multiple bulk insert requests. - - final BulkRequest request = new BulkRequest(); - - for (final int position : rankAggregatedClickThrough.keySet()) { - - final Map jsonMap = new HashMap<>(); - jsonMap.put("position", position); - jsonMap.put("ctr", rankAggregatedClickThrough.get(position)); - - final IndexRequest indexRequest = new IndexRequest(INDEX_RANK_AGGREGATED_CTR).id(UUID.randomUUID().toString()).source(jsonMap); - - request.add(indexRequest); - - } - - client.bulk(request).get(); - - } - - } - - /** - * Index the clickthrough rates. - * @param clickthroughRates A map of query IDs to a collection of {@link ClickthroughRate} objects. - * @throws IOException Thrown when there is a problem accessing OpenSearch. - */ - public void indexClickthroughRates(final Map> clickthroughRates) throws Exception { - - if(!clickthroughRates.isEmpty()) { - - final BulkRequest request = new BulkRequest(); - - for(final String userQuery : clickthroughRates.keySet()) { - - for(final ClickthroughRate clickthroughRate : clickthroughRates.get(userQuery)) { - - final Map jsonMap = new HashMap<>(); - jsonMap.put("user_query", userQuery); - jsonMap.put("clicks", clickthroughRate.getClicks()); - jsonMap.put("events", clickthroughRate.getImpressions()); - jsonMap.put("ctr", clickthroughRate.getClickthroughRate()); - jsonMap.put("object_id", clickthroughRate.getObjectId()); - - final IndexRequest indexRequest = new IndexRequest(INDEX_QUERY_DOC_CTR) - .id(UUID.randomUUID().toString()) - .source(jsonMap); - - request.add(indexRequest); - - } - - } - - client.bulk(request, new ActionListener<>() { - - @Override - public void onResponse(BulkResponse bulkItemResponses) { - if(bulkItemResponses.hasFailures()) { - LOGGER.error("Clickthrough rates were not all successfully indexed: {}", bulkItemResponses.buildFailureMessage()); - } else { - LOGGER.debug("Clickthrough rates has been successfully indexed."); - } - } - - @Override - public void onFailure(Exception ex) { - LOGGER.error("Indexing the clickthrough rates failed.", ex); - } - - }); - - } - - } - - /** - * Index the judgments. - * @param judgments A collection of {@link Judgment judgments}. - * @throws IOException Thrown when there is a problem accessing OpenSearch. - * @return The ID of the indexed judgments. - */ - public String indexJudgments(final Collection judgments) throws Exception { - - final String judgmentsId = UUID.randomUUID().toString(); - final String timestamp = TimeUtils.getTimestamp(); - - final BulkRequest bulkRequest = new BulkRequest(); - - for(final Judgment judgment : judgments) { - - final Map j = judgment.getJudgmentAsMap(); - j.put("judgments_id", judgmentsId); - j.put("timestamp", timestamp); - - final IndexRequest indexRequest = new IndexRequest(JUDGMENTS_INDEX_NAME) - .id(UUID.randomUUID().toString()) - .source(j); - - bulkRequest.add(indexRequest); - - } - - // TODO: Don't use .get() - client.bulk(bulkRequest).get(); - - return judgmentsId; - - } - -} \ No newline at end of file diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java deleted file mode 100644 index b893f43..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/IncrementalUserQueryHash.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.queryhash; - -import java.util.HashMap; -import java.util.Map; - -/** - * Facilitates the hashing of user queries. - */ -public class IncrementalUserQueryHash implements UserQueryHash { - - private final Map userQueries; - private int count = 1; - - /** - * Creates a new instance of this class. - */ - public IncrementalUserQueryHash() { - this.userQueries = new HashMap<>(); - } - - @Override - public int getHash(String userQuery) { - - final int hash; - - if(userQueries.containsKey(userQuery)) { - - return userQueries.get(userQuery); - - } else { - - userQueries.put(userQuery, count); - hash = count; - count++; - - - } - - return hash; - - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java b/legacy-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java deleted file mode 100644 index 714f85a..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/judgments/queryhash/UserQueryHash.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.judgments.queryhash; - -/** - * In interface for creating hashes of user queries. - */ -public interface UserQueryHash { - - /** - * Creates a unique integer given a user query. - * @param userQuery The user query. - * @return A unique integer representing the user query. - */ - int getHash(String userQuery); - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java b/legacy-plugin/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java deleted file mode 100644 index 446696f..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/metrics/DcgSearchMetric.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.metrics; - -import java.util.List; - -/** - * Subclass of {@link SearchMetric} that calculates Discounted Cumulative Gain @ k. - */ -public class DcgSearchMetric extends SearchMetric { - - protected final List relevanceScores; - - /** - * Creates new DCG metrics. - * @param k The k value. - * @param relevanceScores A list of relevance scores. - */ - public DcgSearchMetric(final int k, final List relevanceScores) { - super(k); - this.relevanceScores = relevanceScores; - } - - @Override - public String getName() { - return "dcg_at_" + k; - } - - @Override - public double calculate() { - return calculateDcg(relevanceScores); - } - - protected double calculateDcg(final List relevanceScores) { - - // k should equal the size of relevanceScores. - - double dcg = 0.0; - - for (int i = 0; i < relevanceScores.size(); i++) { - - double d = log2(i + 2); - double n = Math.pow(2, relevanceScores.get(i)) - 1; - - if(d != 0) { - dcg += (n / d); - } - - } - return dcg; - - } - - private double log2(int N) { - return Math.log(N) / Math.log(2); - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java b/legacy-plugin/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java deleted file mode 100644 index a392732..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/metrics/NdcgSearchMetric.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.metrics; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -/** - * Subclass of {@link SearchMetric} that calculates Normalized Discounted Cumulative Gain @ k. - */ -public class NdcgSearchMetric extends DcgSearchMetric { - - /** - * Creates new NDCG metrics. - * @param k The k value. - * @param relevanceScores A list of relevancy scores. - */ - public NdcgSearchMetric(final int k, final List relevanceScores) { - super(k, relevanceScores); - } - - @Override - public String getName() { - return "ndcg_at_" + k; - } - - @Override - public double calculate() { - - double dcg = super.calculate(); - - if(dcg == 0) { - - // The ndcg is 0. No need to continue. - return 0; - - } else { - - final List idealRelevanceScores = new ArrayList<>(relevanceScores); - idealRelevanceScores.sort(Collections.reverseOrder()); - - double idcg = super.calculateDcg(idealRelevanceScores); - - if(idcg == 0) { - return 0; - } else { - return dcg / idcg; - } - - } - - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java b/legacy-plugin/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java deleted file mode 100644 index a2ac50b..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/metrics/PrecisionSearchMetric.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.metrics; - -import java.util.List; - -/** - * Subclass of {@link SearchMetric} that calculates Precision @ k. - */ -public class PrecisionSearchMetric extends SearchMetric { - - private final double threshold; - private final List relevanceScores; - - /** - * Creates new precision metrics. - * @param k The k value. - * @param threshold The threshold for assigning binary relevancy scores to non-binary scores. - * Scores greater than or equal to this value will be assigned a relevancy score of 1 (relevant). - * Scores less than this value will be assigned a relevancy score of 0 (not relevant). - * @param relevanceScores A list of relevance scores. - */ - public PrecisionSearchMetric(final int k, final double threshold, final List relevanceScores) { - super(k); - this.threshold = threshold; - this.relevanceScores = relevanceScores; - } - - @Override - public String getName() { - return "precision_at_" + k; - } - - @Override - public double calculate() { - - double numberOfRelevantItems = 0; - - for(final double relevanceScore : relevanceScores) { - if(relevanceScore >= threshold) { - numberOfRelevantItems++; - } - } - - return numberOfRelevantItems / (double) k; - - } - - /** - * Gets the threshold value. - * @return The threshold value. - */ - public double threshold() { - return threshold; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/metrics/SearchMetric.java b/legacy-plugin/src/main/java/org/opensearch/eval/metrics/SearchMetric.java deleted file mode 100644 index acd580a..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/metrics/SearchMetric.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.metrics; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * Base class for search metrics. - */ -public abstract class SearchMetric { - - protected static final Logger LOGGER = LogManager.getLogger(SearchMetric.class); - - protected int k; - - /** - * Gets the name of the metric, i.e. ndcg. - * @return The name of the metric. - */ - public abstract String getName(); - - /** - * Calculates the metric. - * @return The value of the metric. - */ - public abstract double calculate(); - - private Double value = Double.NaN; - - /** - * Creates the metric. - * @param k The k value. - */ - public SearchMetric(final int k) { - this.k = k; - } - - /** - * Gets the k value. - * @return The k value. - */ - public int getK() { - return k; - } - - /** - * Gets the value of the metric. If the metric has not yet been calculated, - * the metric will first be calculated by calling calculate. This - * function should be used in cases where repeated access to the metrics value is - * needed without recalculating the metrics value. - * @return The value of the metric. - */ - public double getValue() { - - if(Double.isNaN(value)) { - this.value = calculate(); - } - - return value; - - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java b/legacy-plugin/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java deleted file mode 100644 index 7ca0ad6..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/runners/AbstractQuerySetRunner.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.runners; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.client.Client; -import org.opensearch.eval.SearchQualityEvaluationPlugin; -import org.opensearch.index.query.BoolQueryBuilder; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.search.builder.SearchSourceBuilder; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -/** - * Base class for query set runners. Classes that extend this class - * should be specific to a search engine. See the {@link OpenSearchQuerySetRunner} for an example. - */ -public abstract class AbstractQuerySetRunner { - - private static final Logger LOGGER = LogManager.getLogger(AbstractQuerySetRunner.class); - - protected final Client client; - - public AbstractQuerySetRunner(final Client client) { - this.client = client; - } - - /** - * Runs the query set. - * @param querySetId The ID of the query set to run. - * @param judgmentsId The ID of the judgments set to use for search metric calculation. - * @param index The name of the index to run the query sets against. - * @param searchPipeline The name of the search pipeline to use, or null to not use a search pipeline. - * @param idField The field in the index that is used to uniquely identify a document. - * @param query The query that will be used to run the query set. - * @param k The k used for metrics calculation, i.e. DCG@k. - * @param threshold The cutoff for binary judgments. A judgment score greater than or equal - * to this value will be assigned a binary judgment value of 1. A judgment score - * less than this value will be assigned a binary judgment value of 0. - * @return The query set {@link QuerySetRunResult results} and calculated metrics. - */ - abstract QuerySetRunResult run(String querySetId, final String judgmentsId, final String index, final String searchPipeline, - final String idField, final String query, final int k, - final double threshold) throws Exception; - - /** - * Saves the query set results to a persistent store, which may be the search engine itself. - * @param result The {@link QuerySetRunResult results}. - */ - abstract void save(QuerySetRunResult result) throws Exception; - - /** - * Gets a query set from the index. - * @param querySetId The ID of the query set to get. - * @return The query set as a collection of maps of query to frequency - * @throws Exception Thrown if the query set cannot be retrieved. - */ - public final Collection> getQuerySet(final String querySetId) throws Exception { - - // Get the query set. - final SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); - sourceBuilder.query(QueryBuilders.matchQuery("_id", querySetId)); - - // Will be at most one match. - sourceBuilder.from(0); - sourceBuilder.size(1); - - final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.QUERY_SETS_INDEX_NAME).source(sourceBuilder); - - // TODO: Don't use .get() - final SearchResponse searchResponse = client.search(searchRequest).get(); - - if(searchResponse.getHits().getHits().length > 0) { - - // The queries from the query set that will be run. - return (Collection>) searchResponse.getHits().getAt(0).getSourceAsMap().get("queries"); - - } else { - - LOGGER.error("Unable to get query set with ID {}", querySetId); - - // The query set was not found. - throw new RuntimeException("The query set with ID " + querySetId + " was not found."); - - } - - } - - /** - * Get a judgment from the index. - * @param judgmentsId The ID of the judgments to find. - * @param query The user query. - * @param documentId The document ID. - * @return The value of the judgment, or NaN if the judgment cannot be found. - */ - public Double getJudgmentValue(final String judgmentsId, final String query, final String documentId) throws Exception { - - // Find a judgment that matches the judgments_id, query_id, and document_id fields in the index. - - final BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); - boolQueryBuilder.must(QueryBuilders.termQuery("judgments_id", judgmentsId)); - boolQueryBuilder.must(QueryBuilders.termQuery("query", query)); - boolQueryBuilder.must(QueryBuilders.termQuery("document_id", documentId)); - - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(boolQueryBuilder); - - // Will be a max of 1 result since we are getting the judgments by ID. - searchSourceBuilder.from(0); - searchSourceBuilder.size(1); - - // Only include the judgment field in the response. - final String[] includeFields = new String[] {"judgment"}; - final String[] excludeFields = new String[] {}; - searchSourceBuilder.fetchSource(includeFields, excludeFields); - - final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.JUDGMENTS_INDEX_NAME).source(searchSourceBuilder); - - Double judgment = Double.NaN; - - final SearchResponse searchResponse = client.search(searchRequest).get(); - - if (searchResponse.getHits().getHits().length > 0) { - - final Map j = searchResponse.getHits().getAt(0).getSourceAsMap(); - - // LOGGER.debug("Judgment contains a value: {}", j.get("judgment")); - - // TODO: Why does this not exist in some cases? - if(j.containsKey("judgment")) { - judgment = (Double) j.get("judgment"); - } - - } else { - - // No judgment for this query/doc pair exists. - judgment = Double.NaN; - - } - - return judgment; - - } - - /** - * Gets the judgments for a query / document pairs. - * @param judgmentsId The judgments collection for which the judgment to retrieve belongs. - * @param query The user query. - * @param orderedDocumentIds A list of document IDs returned for the user query. - * @param k The k used for metrics calculation, i.e. DCG@k. - * @return An ordered list of relevance scores for the query / document pairs. - * @throws Exception Thrown if a judgment cannot be retrieved. - */ - protected RelevanceScores getRelevanceScores(final String judgmentsId, final String query, final List orderedDocumentIds, final int k) throws Exception { - - // Ordered list of scores. - final List scores = new ArrayList<>(); - - // Count the number of documents without judgments. - int documentsWithoutJudgmentsCount = 0; - - // For each document (up to k), get the judgment for the document. - for (int i = 0; i < k && i < orderedDocumentIds.size(); i++) { - - final String documentId = orderedDocumentIds.get(i); - - // Find the judgment value for this combination of query and documentId from the index. - final Double judgmentValue = getJudgmentValue(judgmentsId, query, documentId); - - // If a judgment for this query/doc pair is not found, Double.NaN will be returned. - if(!Double.isNaN(judgmentValue)) { - LOGGER.info("Score found for document ID {} with judgments {} and query {} = {}", documentId, judgmentsId, query, judgmentValue); - scores.add(judgmentValue); - } else { - //LOGGER.info("No score found for document ID {} with judgments {} and query {}", documentId, judgmentsId, query); - documentsWithoutJudgmentsCount++; - } - - } - - double frogs = ((double) documentsWithoutJudgmentsCount) / orderedDocumentIds.size(); - - if(Double.isNaN(frogs)) { - frogs = 1.0; - } - - // Multiply by 100 to be a percentage. - frogs *= 100; - - LOGGER.info("frogs for query {} = {} ------- {} / {}", query, frogs, documentsWithoutJudgmentsCount, orderedDocumentIds.size()); - - return new RelevanceScores(scores, frogs); - - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java b/legacy-plugin/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java deleted file mode 100644 index a1f0c4f..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/runners/OpenSearchQuerySetRunner.java +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.runners; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.action.admin.indices.create.CreateIndexRequest; -import org.opensearch.action.admin.indices.create.CreateIndexResponse; -import org.opensearch.action.admin.indices.exists.indices.IndicesExistsRequest; -import org.opensearch.action.admin.indices.exists.indices.IndicesExistsResponse; -import org.opensearch.action.bulk.BulkRequest; -import org.opensearch.action.bulk.BulkResponse; -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.client.Client; -import org.opensearch.core.action.ActionListener; -import org.opensearch.eval.SearchQualityEvaluationPlugin; -import org.opensearch.eval.metrics.DcgSearchMetric; -import org.opensearch.eval.metrics.NdcgSearchMetric; -import org.opensearch.eval.metrics.PrecisionSearchMetric; -import org.opensearch.eval.metrics.SearchMetric; -import org.opensearch.eval.utils.TimeUtils; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.search.SearchHit; -import org.opensearch.search.builder.SearchSourceBuilder; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import static org.opensearch.eval.SearchQualityEvaluationRestHandler.QUERY_PLACEHOLDER; - -/** - * A {@link AbstractQuerySetRunner} for Amazon OpenSearch. - */ -public class OpenSearchQuerySetRunner extends AbstractQuerySetRunner { - - private static final Logger LOGGER = LogManager.getLogger(OpenSearchQuerySetRunner.class); - - /** - * Creates a new query set runner - * - * @param client An OpenSearch {@link Client}. - */ - public OpenSearchQuerySetRunner(final Client client) { - super(client); - } - - @Override - public QuerySetRunResult run(final String querySetId, final String judgmentsId, final String index, - final String searchPipeline, final String idField, final String query, - final int k, final double threshold) throws Exception { - - final Collection> querySet = getQuerySet(querySetId); - LOGGER.info("Found {} queries in query set {}", querySet.size(), querySetId); - - try { - - // The results of each query. - final List queryResults = new ArrayList<>(); - - for (Map queryMap : querySet) { - - // Loop over each query in the map and run each one. - for (final String userQuery : queryMap.keySet()) { - - // Replace the query placeholder with the user query. - final String parsedQuery = query.replace(QUERY_PLACEHOLDER, userQuery); - - // Build the query from the one that was passed in. - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - - searchSourceBuilder.query(QueryBuilders.wrapperQuery(parsedQuery)); - searchSourceBuilder.from(0); - searchSourceBuilder.size(k); - - final String[] includeFields = new String[]{idField}; - final String[] excludeFields = new String[]{}; - searchSourceBuilder.fetchSource(includeFields, excludeFields); - - // LOGGER.info(searchSourceBuilder.toString()); - - final SearchRequest searchRequest = new SearchRequest(index); - searchRequest.source(searchSourceBuilder); - - if(searchPipeline != null) { - searchSourceBuilder.pipeline(searchPipeline); - searchRequest.pipeline(searchPipeline); - } - - // This is to keep OpenSearch from rejecting queries. - // TODO: Look at using the Workload Management in 2.18.0. - Thread.sleep(50); - - client.search(searchRequest, new ActionListener<>() { - - @Override - public void onResponse(final SearchResponse searchResponse) { - - final List orderedDocumentIds = new ArrayList<>(); - - for (final SearchHit hit : searchResponse.getHits().getHits()) { - - final String documentId; - - if("_id".equals(idField)) { - documentId = hit.getId(); - } else { - // TODO: Need to check this field actually exists. - documentId = hit.getSourceAsMap().get(idField).toString(); - } - - orderedDocumentIds.add(documentId); - - } - - try { - - final RelevanceScores relevanceScores = getRelevanceScores(judgmentsId, userQuery, orderedDocumentIds, k); - - // Calculate the metrics for this query. - final SearchMetric dcgSearchMetric = new DcgSearchMetric(k, relevanceScores.getRelevanceScores()); - final SearchMetric ndcgSearchmetric = new NdcgSearchMetric(k, relevanceScores.getRelevanceScores()); - final SearchMetric precisionSearchMetric = new PrecisionSearchMetric(k, threshold, relevanceScores.getRelevanceScores()); - - final Collection searchMetrics = List.of(dcgSearchMetric, ndcgSearchmetric, precisionSearchMetric); - - queryResults.add(new QueryResult(userQuery, orderedDocumentIds, k, searchMetrics, relevanceScores.getFrogs())); - - } catch (Exception ex) { - LOGGER.error("Unable to get relevance scores for judgments {} and user query {}.", judgmentsId, userQuery, ex); - } - - } - - @Override - public void onFailure(Exception ex) { - LOGGER.error("Unable to search using query: {}", searchSourceBuilder.toString(), ex); - } - }); - - } - - } - - // Calculate the search metrics for the entire query set given the individual query set metrics. - // Sum up the metrics for each query per metric type. - final int querySetSize = queryResults.size(); - final Map sumOfMetrics = new HashMap<>(); - for(final QueryResult queryResult : queryResults) { - for(final SearchMetric searchMetric : queryResult.getSearchMetrics()) { - //LOGGER.info("Summing: {} - {}", searchMetric.getName(), searchMetric.getValue()); - sumOfMetrics.merge(searchMetric.getName(), searchMetric.getValue(), Double::sum); - } - } - - // Now divide by the number of queries. - final Map querySetMetrics = new HashMap<>(); - for(final String metric : sumOfMetrics.keySet()) { - //LOGGER.info("Dividing by the query set size: {} / {}", sumOfMetrics.get(metric), querySetSize); - querySetMetrics.put(metric, sumOfMetrics.get(metric) / querySetSize); - } - - final String querySetRunId = UUID.randomUUID().toString(); - final QuerySetRunResult querySetRunResult = new QuerySetRunResult(querySetRunId, querySetId, queryResults, querySetMetrics); - - LOGGER.info("Query set run complete: {}", querySetRunId); - - return querySetRunResult; - - } catch (Exception ex) { - throw new RuntimeException("Unable to run query set.", ex); - } - - } - - @Override - public void save(final QuerySetRunResult result) throws Exception { - - // Now, index the metrics as expected by the dashboards. - - // See https://github.com/o19s/opensearch-search-quality-evaluation/blob/main/opensearch-dashboard-prototyping/METRICS_SCHEMA.md - // See https://github.com/o19s/opensearch-search-quality-evaluation/blob/main/opensearch-dashboard-prototyping/sample_data.ndjson - - final IndicesExistsRequest indicesExistsRequest = new IndicesExistsRequest(SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME); - - client.admin().indices().exists(indicesExistsRequest, new ActionListener<>() { - - @Override - public void onResponse(IndicesExistsResponse indicesExistsResponse) { - - if(!indicesExistsResponse.isExists()) { - - // Create the index. - // TODO: Read this mapping from a resource file instead. - final String mapping = "{\n" + - " \"properties\": {\n" + - " \"datetime\": { \"type\": \"date\", \"format\": \"strict_date_time\" },\n" + - " \"search_config\": { \"type\": \"keyword\" },\n" + - " \"query_set_id\": { \"type\": \"keyword\" },\n" + - " \"query\": { \"type\": \"keyword\" },\n" + - " \"metric\": { \"type\": \"keyword\" },\n" + - " \"value\": { \"type\": \"double\" },\n" + - " \"application\": { \"type\": \"keyword\" },\n" + - " \"evaluation_id\": { \"type\": \"keyword\" },\n" + - " \"frogs_percent\": { \"type\": \"double\" }\n" + - " }\n" + - " }"; - - // Create the judgments index. - final CreateIndexRequest createIndexRequest = new CreateIndexRequest(SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME).mapping(mapping); - - client.admin().indices().create(createIndexRequest, new ActionListener<>() { - - @Override - public void onResponse(CreateIndexResponse createIndexResponse) { - LOGGER.info("{} index created.", SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME); - } - - @Override - public void onFailure(Exception ex) { - LOGGER.error("Unable to create the {} index.", SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME, ex); - } - - }); - - } - - } - - @Override - public void onFailure(Exception ex) { - LOGGER.error("Unable to determine if {} index exists.", SearchQualityEvaluationPlugin.DASHBOARD_METRICS_INDEX_NAME, ex); - } - - }); - - final BulkRequest bulkRequest = new BulkRequest(); - final String timestamp = TimeUtils.getTimestamp(); - - for(final QueryResult queryResult : result.getQueryResults()) { - - for(final SearchMetric searchMetric : queryResult.getSearchMetrics()) { - - // TODO: Make sure all of these items have values. - final Map metrics = new HashMap<>(); - metrics.put("datetime", timestamp); - metrics.put("search_config", "research_1"); - metrics.put("query_set_id", result.getQuerySetId()); - metrics.put("query", queryResult.getQuery()); - metrics.put("metric", searchMetric.getName()); - metrics.put("value", searchMetric.getValue()); - metrics.put("application", "sample_data"); - metrics.put("evaluation_id", result.getRunId()); - metrics.put("frogs_percent", queryResult.getFrogs()); - - // TODO: This is using the index name from the sample data. - bulkRequest.add(new IndexRequest("sqe_metrics_sample_data").source(metrics)); - - } - - } - - client.bulk(bulkRequest, new ActionListener<>() { - - @Override - public void onResponse(BulkResponse bulkItemResponses) { - LOGGER.info("Successfully indexed {} metrics.", bulkItemResponses.getItems().length); - } - - @Override - public void onFailure(Exception ex) { - LOGGER.error("Unable to bulk index metrics.", ex); - } - - }); - - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/runners/QueryResult.java b/legacy-plugin/src/main/java/org/opensearch/eval/runners/QueryResult.java deleted file mode 100644 index cc2b118..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/runners/QueryResult.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.runners; - -import org.opensearch.eval.metrics.SearchMetric; - -import java.util.Collection; -import java.util.List; - -/** - * Contains the search results for a single query. - */ -public class QueryResult { - - private final String query; - private final List orderedDocumentIds; - private final int k; - private final Collection searchMetrics; - private final double frogs; - - /** - * Creates the search results. - * @param query The query used to generate this result. - * @param orderedDocumentIds A list of ordered document IDs in the same order as they appeared - * in the query. - * @param k The k used for metrics calculation, i.e. DCG@k. - * @param searchMetrics A collection of {@link SearchMetric} for this query. - * @param frogs The percentage of documents not having a judgment. - */ - public QueryResult(final String query, final List orderedDocumentIds, final int k, final Collection searchMetrics, final double frogs) { - this.query = query; - this.orderedDocumentIds = orderedDocumentIds; - this.k = k; - this.searchMetrics = searchMetrics; - this.frogs = frogs; - } - - /** - * Gets the query used to generate this result. - * @return The query used to generate this result. - */ - public String getQuery() { - return query; - } - - /** - * Gets the list of ordered document IDs. - * @return A list of ordered documented IDs. - */ - public List getOrderedDocumentIds() { - return orderedDocumentIds; - } - - public int getK() { - return k; - } - - public Collection getSearchMetrics() { - return searchMetrics; - } - - public double getFrogs() { - return frogs; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java b/legacy-plugin/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java deleted file mode 100644 index 280ba9c..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/runners/QuerySetRunResult.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.runners; - -import org.opensearch.eval.metrics.SearchMetric; -import org.opensearch.eval.utils.TimeUtils; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * The results of a query set run. - */ -public class QuerySetRunResult { - - private final String runId; - private final String querySetId; - private final List queryResults; - private final Map metrics; - private final String timestamp; - - /** - * Creates a new query set run result. A random UUID is generated as the run ID. - * @param runId A unique identifier for this query set run. - * @param querySetId A unique identifier for the query set. - * @param queryResults A collection of {@link QueryResult} that contains the queries and search results. - * @param metrics A map of metric name to value. - */ - public QuerySetRunResult(final String runId, final String querySetId, final List queryResults, final Map metrics) { - this.runId = runId; - this.querySetId = querySetId; - this.queryResults = queryResults; - this.metrics = metrics; - this.timestamp = TimeUtils.getTimestamp(); - } - - /** - * Get the run's ID. - * @return The run's ID. - */ - public String getRunId() { - return runId; - } - - /** - * Gets the query set ID. - * @return The query set ID. - */ - public String getQuerySetId() { - return querySetId; - } - - /** - * Gets the search metrics. - * @return The search metrics. - */ - public Map getSearchMetrics() { - return metrics; - } - - /** - * Gets the results of the query set run. - * @return A collection of {@link QueryResult results}. - */ - public Collection getQueryResults() { - return queryResults; - } - - public String getTimestamp() { - return timestamp; - } - - public Collection> getQueryResultsAsMap() { - - final Collection> qs = new ArrayList<>(); - - for(final QueryResult queryResult : queryResults) { - - final Map q = new HashMap<>(); - - q.put("query", queryResult.getQuery()); - q.put("document_ids", queryResult.getOrderedDocumentIds()); - q.put("frogs", queryResult.getFrogs()); - - // Calculate and add each metric to the map. - for(final SearchMetric searchMetric : queryResult.getSearchMetrics()) { - q.put(searchMetric.getName(), searchMetric.calculate()); - } - - qs.add(q); - - } - - return qs; - - } - - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/runners/RelevanceScores.java b/legacy-plugin/src/main/java/org/opensearch/eval/runners/RelevanceScores.java deleted file mode 100644 index d57de40..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/runners/RelevanceScores.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.runners; - -import java.util.List; - -public class RelevanceScores { - - private List relevanceScores; - private double frogs; - - public RelevanceScores(final List relevanceScores, final double frogs) { - this.relevanceScores = relevanceScores; - this.frogs = frogs; - } - - public List getRelevanceScores() { - return relevanceScores; - } - - - public double getFrogs() { - return frogs; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java b/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java deleted file mode 100644 index 3c70f0a..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AbstractQuerySampler.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.samplers; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.index.IndexResponse; -import org.opensearch.action.support.WriteRequest; -import org.opensearch.client.node.NodeClient; -import org.opensearch.core.action.ActionListener; -import org.opensearch.eval.SearchQualityEvaluationPlugin; -import org.opensearch.eval.utils.TimeUtils; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -/** - * An interface for sampling UBI queries. - */ -public abstract class AbstractQuerySampler { - - private static final Logger LOGGER = LogManager.getLogger(AbstractQuerySampler.class); - - /** - * Gets the name of the sampler. - * @return The name of the sampler. - */ - public abstract String getName(); - - /** - * Samples the queries and inserts the query set into an index. - * @return A query set ID. - */ - public abstract String sample() throws Exception; - - /** - * Index the query set. - */ - protected String indexQuerySet(final NodeClient client, final String name, final String description, final String sampling, Map queries) throws Exception { - - LOGGER.info("Indexing {} queries for query set {}", queries.size(), name); - - final Collection> querySetQueries = new ArrayList<>(); - - // Convert the queries map to an object. - for(final String query : queries.keySet()) { - - // Map of the query itself to the frequency of the query. - final Map querySetQuery = new HashMap<>(); - querySetQuery.put(query, queries.get(query)); - - querySetQueries.add(querySetQuery); - - } - - final Map querySet = new HashMap<>(); - querySet.put("name", name); - querySet.put("description", description); - querySet.put("sampling", sampling); - querySet.put("queries", querySetQueries); - querySet.put("timestamp", TimeUtils.getTimestamp()); - - final String querySetId = UUID.randomUUID().toString(); - - // TODO: Create a mapping for the query set index. - final IndexRequest indexRequest = new IndexRequest().index(SearchQualityEvaluationPlugin.QUERY_SETS_INDEX_NAME) - .id(querySetId) - .source(querySet) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); - - client.index(indexRequest, new ActionListener<>() { - - @Override - public void onResponse(IndexResponse indexResponse) { - LOGGER.info("Indexed query set {} having name {}", querySetId, name); - } - - @Override - public void onFailure(Exception ex) { - LOGGER.error("Unable to index query set {}", querySetId, ex); - } - }); - - return querySetId; - - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java b/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java deleted file mode 100644 index c8d731a..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AbstractSamplerParameters.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.samplers; - -public class AbstractSamplerParameters { - - private final String name; - private final String description; - private final String sampling; - private final int querySetSize; - - public AbstractSamplerParameters(final String name, final String description, final String sampling, final int querySetSize) { - this.name = name; - this.description = description; - this.sampling = sampling; - this.querySetSize = querySetSize; - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - public String getSampling() { - return sampling; - } - - public int getQuerySetSize() { - return querySetSize; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java b/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java deleted file mode 100644 index 263d70a..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySampler.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.samplers; - -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.client.node.NodeClient; -import org.opensearch.eval.SearchQualityEvaluationPlugin; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.search.SearchHit; -import org.opensearch.search.builder.SearchSourceBuilder; - -import java.util.HashMap; -import java.util.Map; - -/** - * An implementation of {@link AbstractQuerySampler} that uses all UBI queries without any sampling. - */ -public class AllQueriesQuerySampler extends AbstractQuerySampler { - - public static final String NAME = "none"; - - private final NodeClient client; - private final AllQueriesQuerySamplerParameters parameters; - - /** - * Creates a new sampler. - * @param client The OpenSearch {@link NodeClient client}. - */ - public AllQueriesQuerySampler(final NodeClient client, final AllQueriesQuerySamplerParameters parameters) { - this.client = client; - this.parameters = parameters; - } - - @Override - public String getName() { - return NAME; - } - - @Override - public String sample() throws Exception { - - // Get queries from the UBI queries index. - // TODO: This needs to use scroll or something else. - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(QueryBuilders.matchAllQuery()); - searchSourceBuilder.from(0); - searchSourceBuilder.size(parameters.getQuerySetSize()); - - final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.UBI_QUERIES_INDEX_NAME).source(searchSourceBuilder); - - // TODO: Don't use .get() - final SearchResponse searchResponse = client.search(searchRequest).get(); - - final Map queries = new HashMap<>(); - - for(final SearchHit hit : searchResponse.getHits().getHits()) { - - final Map fields = hit.getSourceAsMap(); - queries.merge(fields.get("user_query").toString(), 1L, Long::sum); - - // Will be useful for paging once implemented. - if(queries.size() > parameters.getQuerySetSize()) { - break; - } - - } - - return indexQuerySet(client, parameters.getName(), parameters.getDescription(), parameters.getSampling(), queries); - - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java b/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java deleted file mode 100644 index 3149668..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/samplers/AllQueriesQuerySamplerParameters.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.samplers; - -public class AllQueriesQuerySamplerParameters extends AbstractSamplerParameters { - - public AllQueriesQuerySamplerParameters(final String name, final String description, final String sampling, final int querySetSize) { - super(name, description, sampling, querySetSize); - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java b/legacy-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java deleted file mode 100644 index 79f2c7c..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeAbstractQuerySampler.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.samplers; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.action.search.SearchScrollRequest; -import org.opensearch.client.node.NodeClient; -import org.opensearch.common.unit.TimeValue; -import org.opensearch.eval.SearchQualityEvaluationPlugin; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.search.Scroll; -import org.opensearch.search.SearchHit; -import org.opensearch.search.builder.SearchSourceBuilder; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * An implementation of {@link AbstractQuerySampler} that uses PPTSS sampling. - * See https://opensourceconnections.com/blog/2022/10/13/how-to-succeed-with-explicit-relevance-evaluation-using-probability-proportional-to-size-sampling/ - * for more information on PPTSS. - */ -public class ProbabilityProportionalToSizeAbstractQuerySampler extends AbstractQuerySampler { - - public static final String NAME = "pptss"; - - private static final Logger LOGGER = LogManager.getLogger(ProbabilityProportionalToSizeAbstractQuerySampler.class); - - private final NodeClient client; - private final ProbabilityProportionalToSizeParameters parameters; - - /** - * Creates a new PPTSS sampler. - * @param client The OpenSearch {@link NodeClient client}. - * @param parameters The {@link ProbabilityProportionalToSizeParameters parameters} for the sampling. - */ - public ProbabilityProportionalToSizeAbstractQuerySampler(final NodeClient client, final ProbabilityProportionalToSizeParameters parameters) { - this.client = client; - this.parameters = parameters; - } - - @Override - public String getName() { - return NAME; - } - - @Override - public String sample() throws Exception { - - // TODO: Can this be changed to an aggregation? - // An aggregation is limited (?) to 10,000 which could miss some queries. - - // Get queries from the UBI queries index. - final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(QueryBuilders.matchAllQuery()); - searchSourceBuilder.size(10000); - final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(10L)); - - final SearchRequest searchRequest = new SearchRequest(SearchQualityEvaluationPlugin.UBI_QUERIES_INDEX_NAME); - searchRequest.scroll(scroll); - searchRequest.source(searchSourceBuilder); - - // TODO: Don't use .get() - SearchResponse searchResponse = client.search(searchRequest).get(); - - String scrollId = searchResponse.getScrollId(); - SearchHit[] searchHits = searchResponse.getHits().getHits(); - - final Collection userQueries = new ArrayList<>(); - - while (searchHits != null && searchHits.length > 0) { - - for(final SearchHit hit : searchHits) { - final Map fields = hit.getSourceAsMap(); - userQueries.add(fields.get("user_query").toString()); - // LOGGER.info("user queries count: {} user query: {}", userQueries.size(), fields.get("user_query").toString()); - } - - final SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); - scrollRequest.scroll(scroll); - - // TODO: Don't use .get() - searchResponse = client.searchScroll(scrollRequest).get(); - - scrollId = searchResponse.getScrollId(); - searchHits = searchResponse.getHits().getHits(); - - } - - // LOGGER.info("User queries found: {}", userQueries); - - final Map weights = new HashMap<>(); - - // Increment the weight for each user query. - for(final String userQuery : userQueries) { - weights.merge(userQuery, 1L, Long::sum); - } - - // The total number of queries will be used to normalize the weights. - final long countOfQueries = userQueries.size(); - - // Calculate the normalized weights by dividing by the total number of queries. - final Map normalizedWeights = new HashMap<>(); - for(final String userQuery : weights.keySet()) { - normalizedWeights.put(userQuery, weights.get(userQuery) / (double) countOfQueries); - //LOGGER.info("{}: {}/{} = {}", userQuery, weights.get(userQuery), countOfQueries, normalizedWeights.get(userQuery)); - } - - // Ensure all normalized weights sum to 1. - final double sumOfNormalizedWeights = normalizedWeights.values().stream().reduce(0.0, Double::sum); - if(!compare(1.0, sumOfNormalizedWeights)) { - throw new RuntimeException("Summed normalized weights do not equal 1.0: Actual value: " + sumOfNormalizedWeights); - } else { - LOGGER.info("Summed normalized weights sum to {}", sumOfNormalizedWeights); - } - - final Map querySet = new HashMap<>(); - final Set randomNumbers = new HashSet<>(); - - // Generate random numbers between 0 and 1 for the size of the query set. - // Do this until our query set has reached the requested maximum size. - // This may require generating more random numbers than what was requested - // because removing duplicate user queries will require randomly picking more queries. - int count = 1; - - // TODO: How to short-circuit this such that if the same query gets picked over and over, the loop will never end. - final int max = 5000; - while(querySet.size() < parameters.getQuerySetSize() && count < max) { - - // Make a random number not yet used. - double random; - do { - random = Math.random(); - } while (randomNumbers.contains(random)); - randomNumbers.add(random); - - // Find the weight closest to the random weight in the map of deltas. - double smallestDelta = Integer.MAX_VALUE; - String closestQuery = null; - for(final String query : normalizedWeights.keySet()) { - final double delta = Math.abs(normalizedWeights.get(query) - random); - if(delta < smallestDelta) { - smallestDelta = delta; - closestQuery = query; - } - } - - querySet.put(closestQuery, weights.get(closestQuery)); - count++; - - //LOGGER.info("Generated random value: {}; Smallest delta = {}; Closest query = {}", random, smallestDelta, closestQuery); - - } - - return indexQuerySet(client, parameters.getName(), parameters.getDescription(), parameters.getSampling(), querySet); - - } - - public static boolean compare(double a, double b) { - return Math.abs(a - b) < 0.00001; - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java b/legacy-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java deleted file mode 100644 index d5e4311..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/samplers/ProbabilityProportionalToSizeParameters.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.samplers; - -public class ProbabilityProportionalToSizeParameters extends AbstractSamplerParameters { - - public ProbabilityProportionalToSizeParameters(final String name, final String description, final String sampling, final int querySetSize) { - super(name, description, sampling, querySetSize); - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/utils/MathUtils.java b/legacy-plugin/src/main/java/org/opensearch/eval/utils/MathUtils.java deleted file mode 100644 index d83adcd..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/utils/MathUtils.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.utils; - -public class MathUtils { - - private MathUtils() { - - } - - public static String round(final double value, final int decimalPlaces) { - double factor = Math.pow(10, decimalPlaces); - return String.valueOf(Math.round(value * factor) / factor); - } - - public static String round(final double value) { - return round(value, 3); - } - -} diff --git a/legacy-plugin/src/main/java/org/opensearch/eval/utils/TimeUtils.java b/legacy-plugin/src/main/java/org/opensearch/eval/utils/TimeUtils.java deleted file mode 100644 index 1948b60..0000000 --- a/legacy-plugin/src/main/java/org/opensearch/eval/utils/TimeUtils.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.utils; - -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; - -/** - * This is a utility class. - */ -public class TimeUtils { - - /** - * Generate a timestamp in the yyyy-MM-ddTHH:mm:ss.SSSZ format. - * @return A timestamp in the yyyy-MM-ddTHH:mm:ss.SSSZ format. - */ - public static String getTimestamp() { - - final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()); - formatter.setTimeZone(TimeZone.getTimeZone("UTC")); - - final Date date = new Date(); - return formatter.format(date); - - } - -} diff --git a/legacy-plugin/src/main/plugin-metadata/plugin-security.policy b/legacy-plugin/src/main/plugin-metadata/plugin-security.policy deleted file mode 100644 index eb1558f..0000000 --- a/legacy-plugin/src/main/plugin-metadata/plugin-security.policy +++ /dev/null @@ -1,4 +0,0 @@ -grant { - permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; - permission java.lang.RuntimePermission "accessDeclaredMembers"; -}; diff --git a/legacy-plugin/src/main/resources/META-INF/services/org.opensearch.jobscheduler.spi.JobSchedulerExtension b/legacy-plugin/src/main/resources/META-INF/services/org.opensearch.jobscheduler.spi.JobSchedulerExtension deleted file mode 100644 index a1f979c..0000000 --- a/legacy-plugin/src/main/resources/META-INF/services/org.opensearch.jobscheduler.spi.JobSchedulerExtension +++ /dev/null @@ -1 +0,0 @@ -org.opensearch.eval.SearchQualityEvaluationPlugin \ No newline at end of file diff --git a/legacy-plugin/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java b/legacy-plugin/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java deleted file mode 100644 index f3755f3..0000000 --- a/legacy-plugin/src/test/java/org/opensearch/eval/metrics/DcgSearchMetricTest.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.metrics; - -import org.opensearch.test.OpenSearchTestCase; - -import java.util.List; - -public class DcgSearchMetricTest extends OpenSearchTestCase { - - public void testCalculate() { - - final int k = 10; - final List relevanceScores = List.of(1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 0.0); - - final DcgSearchMetric dcgSearchMetric = new DcgSearchMetric(k, relevanceScores); - final double dcg = dcgSearchMetric.calculate(); - - assertEquals(13.864412483585935, dcg, 0.0); - - } - - public void testCalculateAllZeros() { - - final int k = 10; - final List relevanceScores = List.of(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); - - final DcgSearchMetric dcgSearchMetric = new DcgSearchMetric(k, relevanceScores); - final double dcg = dcgSearchMetric.calculate(); - - assertEquals(0.0, dcg, 0.0); - - } - -} diff --git a/legacy-plugin/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java b/legacy-plugin/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java deleted file mode 100644 index 08795f8..0000000 --- a/legacy-plugin/src/test/java/org/opensearch/eval/metrics/NdcgSearchMetricTest.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.metrics; - -import org.opensearch.test.OpenSearchTestCase; - -import java.util.List; - -public class NdcgSearchMetricTest extends OpenSearchTestCase { - - public void testCalculate() { - - final int k = 10; - final List relevanceScores = List.of(1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 0.0); - - final NdcgSearchMetric ndcgSearchMetric = new NdcgSearchMetric(k, relevanceScores); - final double ndcg = ndcgSearchMetric.calculate(); - - assertEquals(0.7151195094457645, ndcg, 0.0); - - } - - public void testCalculateAllZeros() { - - final int k = 10; - final List relevanceScores = List.of(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); - - final NdcgSearchMetric ndcgSearchMetric = new NdcgSearchMetric(k, relevanceScores); - final double ndcg = ndcgSearchMetric.calculate(); - - assertEquals(0.0, ndcg, 0.0); - - } - -} diff --git a/legacy-plugin/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java b/legacy-plugin/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java deleted file mode 100644 index b6c260f..0000000 --- a/legacy-plugin/src/test/java/org/opensearch/eval/metrics/PrecisionSearchMetricTest.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.metrics; - -import org.opensearch.test.OpenSearchTestCase; - -import java.util.List; - -public class PrecisionSearchMetricTest extends OpenSearchTestCase { - - public void testCalculate() { - - final int k = 10; - final double threshold = 1.0; - final List relevanceScores = List.of(1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 0.0); - - final PrecisionSearchMetric precisionSearchMetric = new PrecisionSearchMetric(k, threshold, relevanceScores); - final double precision = precisionSearchMetric.calculate(); - - assertEquals(0.9, precision, 0.0); - - } - -} diff --git a/legacy-plugin/useful_queries.txt b/legacy-plugin/useful_queries.txt deleted file mode 100644 index 35c8335..0000000 --- a/legacy-plugin/useful_queries.txt +++ /dev/null @@ -1,151 +0,0 @@ -DELETE ubi_events -DELETE ubi_queries - -GET ubi_events/_mapping -GET ubi_events/_search - -GET ubi_queries/_mapping -GET ubi_queries/_search - -DELETE judgments -GET judgments/_search - - -PUT ubi_queries -{ - "mappings": { - "properties": { - "timestamp": { "type": "date", "format": "strict_date_time" }, - "query_id": { "type": "keyword", "ignore_above": 100 }, - "query": { "type": "text" }, - "query_response_id": { "type": "keyword", "ignore_above": 100 }, - "query_response_hit_ids": { "type": "keyword" }, - "user_query": { "type": "keyword", "ignore_above": 256 }, - "query_attributes": { "type": "flat_object" }, - "client_id": { "type": "keyword", "ignore_above": 100 }, - "application": { "type": "keyword", "ignore_above": 100 } - } - } -} - -PUT ubi_events -{ -"mappings": { - "properties": { - "application": { "type": "keyword", "ignore_above": 256 }, - "action_name": { "type": "keyword", "ignore_above": 100 }, - "client_id": { "type": "keyword", "ignore_above": 100 }, - "query_id": { "type": "keyword", "ignore_above": 100 }, - "message": { "type": "keyword", "ignore_above": 1024 }, - "message_type": { "type": "keyword", "ignore_above": 100 }, - "timestamp": { - "type": "date", - "format":"strict_date_time", - "ignore_malformed": true, - "doc_values": true - }, - "event_attributes": { - "dynamic": true, - "properties": { - "position": { - "properties": { - "ordinal": { "type": "integer" }, - "x": { "type": "integer" }, - "y": { "type": "integer" }, - "page_depth": { "type": "integer" }, - "scroll_depth": { "type": "integer" }, - "trail": { "type": "text", - "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } - } - } - } - }, - "object": { - "properties": { - "internal_id": { "type": "keyword" }, - "object_id": { "type": "keyword", "ignore_above": 256 }, - "object_id_field": { "type": "keyword", "ignore_above": 100 }, - "name": { "type": "keyword", "ignore_above": 256 }, - "description": { "type": "text", - "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } - }, - "object_detail": { "type": "object" } - } - } - } - } - } - } -} - -GET ubi_events/_search -{ - "query": { - "range": { - "event_attributes.position.ordinal": { - "lte": 20 - } - } - } -} - -GET ubi_queries/_search -{ - "query": { - "term": { - "user_query": "batteries" - } - } -} - -GET ubi_events/_search -{ - "query": { - "bool": { - "must": [ - { - "term": { - "query_id": "cdc01f67-0b24-4c96-bb56-a89234f4fb0c" - } - }, - { - "term": { - "action_name": "click" - } - }, - { - "term": { - "event_attributes.position.ordinal": "0" - } - }, - { - "term": { - "event_attributes.object.object_id": "B0797J3DWK" - } - } - ] - } - } - } -} - -GET ubi_events/_search -{ - "size": 0, - "aggs": { - "By_Action": { - "terms": { - "field": "action_name", - "size": 20 - }, - "aggs": { - "By_Position": { - "terms": { - "field": "event_attributes.position.ordinal", - "size": 20 - } - } - } - } - } -} \ No newline at end of file diff --git a/src/main/java/org/opensearch/eval/model/ubi/query/QueryResponse.java b/src/main/java/org/opensearch/eval/model/ubi/query/QueryResponse.java deleted file mode 100644 index e257b3a..0000000 --- a/src/main/java/org/opensearch/eval/model/ubi/query/QueryResponse.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ -package org.opensearch.eval.model.ubi.query; - -import java.util.List; - -/** - * A query response for a {@link UbiQuery query}. - */ -public class QueryResponse { - - private final String queryId; - private final String queryResponseId; - private final List queryResponseHitIds; - - /** - * Creates a query response. - * @param queryId The ID of the query. - * @param queryResponseId The ID of the query response. - * @param queryResponseHitIds A list of IDs for the hits in the query. - */ - public QueryResponse(final String queryId, final String queryResponseId, final List queryResponseHitIds) { - this.queryId = queryId; - this.queryResponseId = queryResponseId; - this.queryResponseHitIds = queryResponseHitIds; - } - - /** - * Gets the query ID. - * @return The query ID. - */ - public String getQueryId() { - return queryId; - } - - /** - * Gets the query response ID. - * @return The query response ID. - */ - public String getQueryResponseId() { - return queryResponseId; - } - - /** - * Gets the list of query response hit IDs. - * @return A list of query response hit IDs. - */ - public List getQueryResponseHitIds() { - return queryResponseHitIds; - } - -} diff --git a/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java b/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java index eae7507..f1708c1 100644 --- a/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java +++ b/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.annotations.SerializedName; +import java.util.List; import java.util.Map; /** @@ -43,9 +44,9 @@ public class UbiQuery { @SerializedName("query_attributes") private Map queryAttributes; - @JsonProperty("query_response") - @SerializedName("query_response") - private QueryResponse queryResponse; + @JsonProperty("query_response_object_ids") + @SerializedName("query_response_object_ids") + private List queryResponseObjectIds; /** * Creates a new UBI query object. @@ -150,22 +151,6 @@ public void setQueryAttributes(Map queryAttributes) { this.queryAttributes = queryAttributes; } - /** - * Gets the query responses. - * @return The query responses. - */ - public QueryResponse getQueryResponse() { - return queryResponse; - } - - /** - * Sets the query responses. - * @param queryResponse The query responses. - */ - public void setQueryResponse(QueryResponse queryResponse) { - this.queryResponse = queryResponse; - } - public String getApplication() { return application; } @@ -173,4 +158,12 @@ public String getApplication() { public void setApplication(String application) { this.application = application; } + + public List getQueryResponseObjectIds() { + return queryResponseObjectIds; + } + + public void setQueryResponseObjectIds(List queryResponseObjectIds) { + this.queryResponseObjectIds = queryResponseObjectIds; + } } From b9396d48ce8675cb5a435feaebe1e888f5296286 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 21 Jan 2025 11:59:39 -0500 Subject: [PATCH 36/41] Removing unneeded docker files. Signed-off-by: jzonthemtn --- Dockerfile | 3 --- docker-compose.yaml | 47 --------------------------------------------- 2 files changed, 50 deletions(-) delete mode 100644 Dockerfile delete mode 100644 docker-compose.yaml diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 2af9056..0000000 --- a/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM opensearchproject/opensearch:2.18.0 - -RUN /usr/share/opensearch/bin/opensearch-plugin install --batch https://github.com/opensearch-project/user-behavior-insights/releases/download/2.18.0.2/opensearch-ubi-2.18.0.2.zip diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 8e7a4bd..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,47 +0,0 @@ -services: - - opensearch_sef: - build: . - container_name: opensearch_sef - environment: - discovery.type: single-node - node.name: opensearch - plugins.security.disabled: "true" - logger.level: debug - OPENSEARCH_INITIAL_ADMIN_PASSWORD: SuperSecretPassword_123 - http.max_content_length: 500mb - OPENSEARCH_JAVA_OPTS: "-Xms16g -Xmx16g" - ulimits: - memlock: - soft: -1 - hard: -1 - nofile: - soft: 65536 - hard: 65536 - ports: - - "9200:9200" - - "9600:9600" - networks: - - opensearch-net - volumes: - - opensearch-data1:/usr/share/opensearch/data - - opensearch_sef_dashboards: - image: opensearchproject/opensearch-dashboards:2.18.0 - container_name: opensearch_sef_dashboards - ports: - - "5601:5601" - environment: - OPENSEARCH_HOSTS: '["http://opensearch_sef:9200"]' - DISABLE_SECURITY_DASHBOARDS_PLUGIN: "true" - depends_on: - - opensearch_sef - networks: - - opensearch-net - -volumes: - opensearch-data1: - -networks: - opensearch-net: - driver: bridge From 52c620cce7ee5dfb873a5f0d3c64cc138c782081 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Tue, 21 Jan 2025 14:29:32 -0500 Subject: [PATCH 37/41] Updates for hybrid query. Signed-off-by: jzonthemtn --- scripts/hybrid_query.txt | 28 +++++++++++++++++++ scripts/opensearch-scripts/create-pipeline.sh | 26 ----------------- scripts/opensearch-scripts/get-pipelines.sh | 3 ++ scripts/run-query-set.json | 9 +++--- 4 files changed, 36 insertions(+), 30 deletions(-) create mode 100644 scripts/hybrid_query.txt delete mode 100755 scripts/opensearch-scripts/create-pipeline.sh create mode 100755 scripts/opensearch-scripts/get-pipelines.sh diff --git a/scripts/hybrid_query.txt b/scripts/hybrid_query.txt new file mode 100644 index 0000000..752c961 --- /dev/null +++ b/scripts/hybrid_query.txt @@ -0,0 +1,28 @@ +{ + "_source": { + "excludes": [ + "title_embedding" + ] + }, + "query": { + "hybrid": { + "queries": [ + { + "match": { + "title_text": { + "query": "#$query##" + } + } + }, + { + "neural": { + "title_embedding": { + "query_text": "#$query##", + "k": 50 + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/opensearch-scripts/create-pipeline.sh b/scripts/opensearch-scripts/create-pipeline.sh deleted file mode 100755 index 24342d7..0000000 --- a/scripts/opensearch-scripts/create-pipeline.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -e - -curl -X PUT http://localhost:9200/_search/pipeline/hybrid_pipeline -H "Content-type: application/json" -d' -{ - "request_processors": [ - { - "filter_query" : { - "tag" : "tag1", - "description" : "This processor is going to restrict to publicly visible documents", - "query" : { - "term": { - "visibility": "public" - } - } - } - } - ], - "response_processors": [ - { - "rename_field": { - "field": "message", - "target_field": "notification" - } - } - ] -}' diff --git a/scripts/opensearch-scripts/get-pipelines.sh b/scripts/opensearch-scripts/get-pipelines.sh new file mode 100755 index 0000000..876fd9d --- /dev/null +++ b/scripts/opensearch-scripts/get-pipelines.sh @@ -0,0 +1,3 @@ +#!/bin/bash -e + +curl http://localhost:9200/_search/pipeline | jq \ No newline at end of file diff --git a/scripts/run-query-set.json b/scripts/run-query-set.json index ebccf1a..6db1519 100644 --- a/scripts/run-query-set.json +++ b/scripts/run-query-set.json @@ -1,10 +1,11 @@ { - "query_set_id": "6d2a8d56-a9e6-4f99-9a81-482974d11c32", - "judgments_id": "c2d46e06-0e37-4370-b547-296b554c646e", + "query_set_id": "6b1ac777-758d-4f33-9bb6-7e3f15e77637", + "judgments_id": "76267535-0591-4e13-9e3d-8de5cb1329a6", "index": "ecommerce", - "search_pipeline": "", + "search_pipeline": "hybrid-search-pipeline", "id_field": "asin", "k": 10, "threshold": 1.0, - "query": "{\"query\": {\"match\": {\"description\": \"#$query##\"}}}" + "query": "{\"_source\": {\"excludes\": [\"title_embedding\"]},\"query\": {\"hybrid\": {\"queries\": [{\"match\": {\"title_text\": {\"query\": \"#$query##\"}}},{\"neural\": {\"title_embedding\": {\"query_text\": \"#$query##\",\"k\": 50}}}]}}}", + "not_used_query": "{\"query\": {\"match\": {\"description\": \"#$query##\"}}}" } From 95207f235f6ec968c45999adc19a52f0935e7577 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Wed, 22 Jan 2025 09:27:21 -0500 Subject: [PATCH 38/41] Changing to queryResponseHitIds. --- .../opensearch/eval/model/ubi/query/UbiQuery.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java b/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java index f1708c1..21aa73d 100644 --- a/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java +++ b/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java @@ -44,9 +44,9 @@ public class UbiQuery { @SerializedName("query_attributes") private Map queryAttributes; - @JsonProperty("query_response_object_ids") - @SerializedName("query_response_object_ids") - private List queryResponseObjectIds; + @JsonProperty("query_response_hit_ids") + @SerializedName("query_response_hit_ids") + private List queryResponseHitIds; /** * Creates a new UBI query object. @@ -159,11 +159,11 @@ public void setApplication(String application) { this.application = application; } - public List getQueryResponseObjectIds() { - return queryResponseObjectIds; + public List getQueryResponseHitIds() { + return queryResponseHitIds; } - public void setQueryResponseObjectIds(List queryResponseObjectIds) { - this.queryResponseObjectIds = queryResponseObjectIds; + public void setQueryResponseHitIds(List queryResponseHitIds) { + this.queryResponseHitIds = queryResponseHitIds; } } From d4a3d1747673ce8df615aa497bb1a2ea6dfda70a Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Wed, 22 Jan 2025 09:29:08 -0500 Subject: [PATCH 39/41] Adding additionalProperties. --- .../eval/model/ubi/event/EventObject.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java b/src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java index 0bce425..71cd693 100644 --- a/src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java +++ b/src/main/java/org/opensearch/eval/model/ubi/event/EventObject.java @@ -8,9 +8,13 @@ */ package org.opensearch.eval.model.ubi.event; +import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.annotations.SerializedName; +import java.util.HashMap; +import java.util.Map; + public class EventObject { @JsonProperty("object_id_field") @@ -21,6 +25,8 @@ public class EventObject { @SerializedName("object_id") private String objectId; + private final Map additionalProperties = new HashMap<>(); + @Override public String toString() { return "[" + objectIdField + ", " + objectId + "]"; @@ -58,4 +64,22 @@ public void setObjectIdField(String objectIdField) { this.objectIdField = objectIdField; } + /** + * Adds an unrecognized property to the additional properties map. + * @param key The property name. + * @param value The property value. + */ + @JsonAnySetter + public void addAdditionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + } + + /** + * Gets the additional properties. + * @return The additional properties map. + */ + public Map getAdditionalProperties() { + return additionalProperties; + } + } From 975ca44236a07863682b3bc22532a72711d0e271 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Wed, 22 Jan 2025 09:30:11 -0500 Subject: [PATCH 40/41] Changing to queryResponseHitIds. --- .../opensearch/eval/model/ubi/query/UbiQuery.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java b/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java index 21aa73d..30b68be 100644 --- a/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java +++ b/src/main/java/org/opensearch/eval/model/ubi/query/UbiQuery.java @@ -44,6 +44,10 @@ public class UbiQuery { @SerializedName("query_attributes") private Map queryAttributes; + @JsonProperty("query_response_id") + @SerializedName("query_response_id") + private String queryResponseId; + @JsonProperty("query_response_hit_ids") @SerializedName("query_response_hit_ids") private List queryResponseHitIds; @@ -166,4 +170,13 @@ public List getQueryResponseHitIds() { public void setQueryResponseHitIds(List queryResponseHitIds) { this.queryResponseHitIds = queryResponseHitIds; } + + public String getQueryResponseId() { + return queryResponseId; + } + + public void setQueryResponseId(String queryResponseId) { + this.queryResponseId = queryResponseId; + } + } From 483bdfb4c0d0b4362f111a718b22023374b0dcb8 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Wed, 22 Jan 2025 11:41:03 -0500 Subject: [PATCH 41/41] Renaming script. --- scripts/{create-click-model.sh => generate-judgments.sh} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/{create-click-model.sh => generate-judgments.sh} (100%) diff --git a/scripts/create-click-model.sh b/scripts/generate-judgments.sh similarity index 100% rename from scripts/create-click-model.sh rename to scripts/generate-judgments.sh