From 53df481b0ce84d532c8eb4bae54a1f79bc10c37f Mon Sep 17 00:00:00 2001 From: Mark Cohen Date: Fri, 4 Nov 2022 17:36:38 -0400 Subject: [PATCH 01/13] adding .github workflows Signed-off-by: Mark Cohen --- .github/CODEOWNERS | 2 + .github/CODEOWNERS~ | 2 + .github/ISSUE_TEMPLATE/bug_report.md | 31 ++++ .github/ISSUE_TEMPLATE/config.yml | 7 + .github/ISSUE_TEMPLATE/documentation-issue.md | 11 ++ .github/ISSUE_TEMPLATE/feature_request.md | 18 +++ .github/PULL_REQUEST_TEMPLATE.md | 15 ++ .github/draft-release-notes-config.yml | 40 ++++++ .github/workflows/CI.yml | 135 ++++++++++++++++++ .github/workflows/CI.yml~ | 135 ++++++++++++++++++ .github/workflows/backport.yml | 28 ++++ ...backwards_compatibility_tests_workflow.yml | 73 ++++++++++ ...ackwards_compatibility_tests_workflow.yml~ | 73 ++++++++++ .../workflows/create-documentation-issue.yml | 41 ++++++ .github/workflows/dco.yml | 18 +++ .github/workflows/delete_backport_branch.yml | 15 ++ .github/workflows/delete_backport_branch.yml~ | 15 ++ .../draft-release-notes-workflow.yml | 20 +++ .github/workflows/links.yml | 23 +++ 19 files changed, 702 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/CODEOWNERS~ create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation-issue.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/draft-release-notes-config.yml create mode 100644 .github/workflows/CI.yml create mode 100644 .github/workflows/CI.yml~ create mode 100644 .github/workflows/backport.yml create mode 100644 .github/workflows/backwards_compatibility_tests_workflow.yml create mode 100644 .github/workflows/backwards_compatibility_tests_workflow.yml~ create mode 100644 .github/workflows/create-documentation-issue.yml create mode 100644 .github/workflows/dco.yml create mode 100644 .github/workflows/delete_backport_branch.yml create mode 100644 .github/workflows/delete_backport_branch.yml~ create mode 100644 .github/workflows/draft-release-notes-workflow.yml create mode 100644 .github/workflows/links.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..c455c58 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# This should match the owning team set up in https://github.com/orgs/opensearch-project/teams +* @opensearch-project/search-request-processor-team \ No newline at end of file diff --git a/.github/CODEOWNERS~ b/.github/CODEOWNERS~ new file mode 100644 index 0000000..2c0e889 --- /dev/null +++ b/.github/CODEOWNERS~ @@ -0,0 +1,2 @@ +# This should match the owning team set up in https://github.com/orgs/opensearch-project/teams +* @opensearch-project/k-nn \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..29eddb9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: 🐛 Bug report +about: Create a report to help us improve +title: '[BUG]' +labels: 'bug, untriaged' +assignees: '' +--- + +**What is the bug?** +A clear and concise description of the bug. + +**How can one reproduce the bug?** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**What is the expected behavior?** +A clear and concise description of what you expected to happen. + +**What is your host/environment?** + - OS: [e.g. iOS] + - Version [e.g. 22] + - Plugins + +**Do you have any screenshots?** +If applicable, add screenshots to help explain your problem. + +**Do you have any additional context?** +Add any other context about the problem. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a8199a1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +contact_links: + - name: OpenSearch Community Support + url: https://discuss.opendistrocommunity.dev/ + about: Please ask and answer questions here. + - name: AWS/Amazon Security + url: https://aws.amazon.com/security/vulnerability-reporting/ + about: Please report security vulnerabilities here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/documentation-issue.md b/.github/ISSUE_TEMPLATE/documentation-issue.md new file mode 100644 index 0000000..c349056 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation-issue.md @@ -0,0 +1,11 @@ +**Is your feature request related to a problem?** +A new feature has been added. + +**What solution would you like?** +Document the usage of the new feature. + +**What alternatives have you considered?** +N/A + +**Do you have any additional context?** +See please diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..6198f33 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: 🎆 Feature request +about: Request a feature in this project +title: '[FEATURE]' +labels: 'enhancement, untriaged' +assignees: '' +--- +**Is your feature request related to a problem?** +A clear and concise description of what the problem is, e.g. _I'm always frustrated when [...]_ + +**What solution would you like?** +A clear and concise description of what you want to happen. + +**What alternatives have you considered?** +A clear and concise description of any alternative solutions or features you've considered. + +**Do you have any additional context?** +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..19cd65f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +### Description +[Describe what this change achieves] + +### Issues Resolved +[List any issues this PR will resolve] + +### Check List +- [ ] New functionality includes testing. + - [ ] All tests pass +- [ ] New functionality has been documented. + - [ ] New functionality has javadoc added +- [ ] Commits are signed as per the DCO using --signoff + +By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. +For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/k-NN/blob/main/CONTRIBUTING.md#developer-certificate-of-origin). diff --git a/.github/draft-release-notes-config.yml b/.github/draft-release-notes-config.yml new file mode 100644 index 0000000..d168727 --- /dev/null +++ b/.github/draft-release-notes-config.yml @@ -0,0 +1,40 @@ +# The overall template of the release notes +template: | + Compatible with OpenSearch (**set version here**). + $CHANGES + +# Setting the formatting and sorting for the release notes body +name-template: Version (set version here) +change-template: '* $TITLE (#$NUMBER)' +sort-by: merged_at +sort-direction: ascending +replacers: + - search: '##' + replace: '###' + +# Organizing the tagged PRs into categories +categories: + - title: 'Breaking Changes' + labels: + - 'Breaking Changes' + - title: 'Features' + labels: + - 'Features' + - title: 'Enhancements' + labels: + - 'Enhancements' + - title: 'Bug Fixes' + labels: + - 'Bug Fixes' + - title: 'Infrastructure' + labels: + - 'Infrastructure' + - title: 'Documentation' + labels: + - 'Documentation' + - title: 'Maintenance' + labels: + - 'Maintenance' + - title: 'Refactoring' + labels: + - 'Refactoring' \ No newline at end of file diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..dfe732a --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,135 @@ +name: Build and Test Search Request Processor +on: + schedule: + - cron: '0 0 * * *' # every night + push: + branches: + - "*" + - "feature/**" + pull_request: + branches: + - "*" + - "feature/**" + +jobs: + Build-k-NN: + strategy: + matrix: + java: [11, 17] + os: [ubuntu-latest, macos-latest, windows-latest] + + name: Build and Test Search Request Processor Plugin + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout Search Request Processor + uses: actions/checkout@v1 + + - name: Setup Java ${{ matrix.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + +# - name: Install dependencies on ubuntu +## if: startsWith(matrix.os,'ubuntu') +# run: | +# sudo apt-get install libopenblas-dev gfortran -y + +# - name: Install dependencies on macos +# if: startsWith(matrix.os, 'macos') +# run: | +# brew reinstall gcc +# export FC=/usr/local/Cellar/gcc/12.2.0/bin/gfortran + + - name: Run build + run: | + ./gradlew build + + - name: Upload Coverage Report + if: startsWith(matrix.os,'ubuntu') + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + Build-search-request-processor-Windows: + strategy: + matrix: + java: [ 11, 17 ] + + name: Build and Test Search Request Processor Plugin on Windows + runs-on: windows-latest + + steps: + - name: Checkout Search Request Processor + uses: actions/checkout@v1 + + - name: Setup Java ${{ matrix.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + +# - name: Install MinGW Using Scoop +# run: | +# iex "& {$(irm get.scoop.sh)} -RunAsAdmin" +# scoop bucket add main +# scoop install mingw + +# - name: Add MinGW to PATH +# run: | +# echo "C:/Users/runneradmin/scoop/apps/mingw/current/bin" >> $env:GITHUB_PATH +# refreshenv + +# - name: Download OpenBLAS +# run: | +# curl -L -O https://github.com/xianyi/OpenBLAS/releases/download/v0.3.21/OpenBLAS-0.3.21-x64.zip +# mkdir OpenBLAS +# Expand-Archive -Path .\OpenBLAS-0.3.21-x64.zip -DestinationPath .\OpenBLAS\ +# mkdir ./src/main/resources/windowsDependencies +# cp ./OpenBLAS/bin/libopenblas.dll ./src/main/resources/windowsDependencies/ +# rm .\OpenBLAS-0.3.21-x64.zip +# rm -r .\OpenBLAS\ + + - name: Run build + run: | + ./gradlew.bat build + +# - name: Pull and Run Docker for security tests +# run: | +# plugin=`ls build/distributions/*.zip` +# version=`echo $plugin|awk -F- '{print $4}'| cut -d. -f 1-3` +# plugin_version=`echo $plugin|awk -F- '{print $4}'| cut -d. -f 1-4` +# echo $version +# cd .. +# if docker pull opendistroforelasticsearch/opendistroforelasticsearch:$version +# then +# echo "FROM opendistroforelasticsearch/opendistroforelasticsearch:$version" >> Dockerfile +# echo "RUN if [ -d /usr/share/elasticsearch/plugins/opendistro-knn ]; then /usr/share/elasticsearch/bin/elasticsearch-plugin remove opendistro-knn; fi" >> Dockerfile +# echo "RUN yum -y update \ && yum -y groupinstall "Development Tools" \ && yum install -y unzip glibc.x86_64 cmake \ && yum clean all" >> Dockerfile +# echo "RUN git clone --recursive --branch ${GITHUB_REF##*/} https://github.com/opendistro-for-elasticsearch/k-NN.git /usr/share/elasticsearch/k-NN \ " >> Dockerfile +# echo "&& cd /usr/share/elasticsearch/k-NN/jni \ && sed -i 's/-march=native/-march=x86-64/g' external/nmslib/similarity_search/CMakeLists.txt \ && cmake . \ && make \ " >> Dockerfile +# echo "&& mkdir /tmp/jni/ && cp release/*.so /tmp/jni/ && ls -ltr /tmp/jni/ \ && cp /tmp/jni/libKNNIndex*.so /usr/lib \ && rm -rf /usr/share/elasticsearch/k-NN" >> Dockerfile +# echo "RUN cd /usr/share/elasticsearch/" >> Dockerfile +# echo "ADD k-NN/build/distributions/opendistro-knn-$plugin_version.zip /tmp/" >> Dockerfile +# echo "RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch file:/tmp/opendistro-knn-$plugin_version.zip" >> Dockerfile +# docker build -t odfe-knn:test . +# echo "imagePresent=true" >> $GITHUB_ENV +# else +# echo "imagePresent=false" >> $GITHUB_ENV +# fi +# - name: Run Docker Image +# if: env.imagePresent == 'true' +# run: | +# cd .. +# docker run -p 9200:9200 -d -p 9600:9600 -e "discovery.type=single-node" odfe-knn:test +# sleep 90 +# - name: Run k-NN Test +# if: env.imagePresent == 'true' +# run: | +# security=`curl -XGET https://localhost:9200/_cat/plugins?v -u admin:admin --insecure |grep opendistro_security|wc -l` +# if [ $security -gt 0 ] +# then +# echo "Security plugin is available. Running tests in security mode" +# ./gradlew :integTest -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername="docker-cluster" -Dhttps=true -Duser=admin -Dpassword=admin +# else +# echo "Security plugin is NOT available. Skipping tests as they are already ran part of ./gradlew build" +# fi diff --git a/.github/workflows/CI.yml~ b/.github/workflows/CI.yml~ new file mode 100644 index 0000000..31d16f2 --- /dev/null +++ b/.github/workflows/CI.yml~ @@ -0,0 +1,135 @@ +name: Build and Test k-NN +on: + schedule: + - cron: '0 0 * * *' # every night + push: + branches: + - "*" + - "feature/**" + pull_request: + branches: + - "*" + - "feature/**" + +jobs: + Build-k-NN: + strategy: + matrix: + java: [11, 17] + os: [ubuntu-latest, macos-latest] + + name: Build and Test k-NN Plugin + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout k-NN + uses: actions/checkout@v1 + + - name: Setup Java ${{ matrix.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + + - name: Install dependencies on ubuntu + if: startsWith(matrix.os,'ubuntu') + run: | + sudo apt-get install libopenblas-dev gfortran -y + + - name: Install dependencies on macos + if: startsWith(matrix.os, 'macos') + run: | + brew reinstall gcc + export FC=/usr/local/Cellar/gcc/12.2.0/bin/gfortran + + - name: Run build + run: | + ./gradlew build + + - name: Upload Coverage Report + if: startsWith(matrix.os,'ubuntu') + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + Build-k-NN-Windows: + strategy: + matrix: + java: [ 11, 17 ] + + name: Build and Test k-NN Plugin on Windows + runs-on: windows-latest + + steps: + - name: Checkout k-NN + uses: actions/checkout@v1 + + - name: Setup Java ${{ matrix.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + + - name: Install MinGW Using Scoop + run: | + iex "& {$(irm get.scoop.sh)} -RunAsAdmin" + scoop bucket add main + scoop install mingw + + - name: Add MinGW to PATH + run: | + echo "C:/Users/runneradmin/scoop/apps/mingw/current/bin" >> $env:GITHUB_PATH + refreshenv + + - name: Download OpenBLAS + run: | + curl -L -O https://github.com/xianyi/OpenBLAS/releases/download/v0.3.21/OpenBLAS-0.3.21-x64.zip + mkdir OpenBLAS + Expand-Archive -Path .\OpenBLAS-0.3.21-x64.zip -DestinationPath .\OpenBLAS\ + mkdir ./src/main/resources/windowsDependencies + cp ./OpenBLAS/bin/libopenblas.dll ./src/main/resources/windowsDependencies/ + rm .\OpenBLAS-0.3.21-x64.zip + rm -r .\OpenBLAS\ + + - name: Run build + run: | + ./gradlew.bat build + +# - name: Pull and Run Docker for security tests +# run: | +# plugin=`ls build/distributions/*.zip` +# version=`echo $plugin|awk -F- '{print $4}'| cut -d. -f 1-3` +# plugin_version=`echo $plugin|awk -F- '{print $4}'| cut -d. -f 1-4` +# echo $version +# cd .. +# if docker pull opendistroforelasticsearch/opendistroforelasticsearch:$version +# then +# echo "FROM opendistroforelasticsearch/opendistroforelasticsearch:$version" >> Dockerfile +# echo "RUN if [ -d /usr/share/elasticsearch/plugins/opendistro-knn ]; then /usr/share/elasticsearch/bin/elasticsearch-plugin remove opendistro-knn; fi" >> Dockerfile +# echo "RUN yum -y update \ && yum -y groupinstall "Development Tools" \ && yum install -y unzip glibc.x86_64 cmake \ && yum clean all" >> Dockerfile +# echo "RUN git clone --recursive --branch ${GITHUB_REF##*/} https://github.com/opendistro-for-elasticsearch/k-NN.git /usr/share/elasticsearch/k-NN \ " >> Dockerfile +# echo "&& cd /usr/share/elasticsearch/k-NN/jni \ && sed -i 's/-march=native/-march=x86-64/g' external/nmslib/similarity_search/CMakeLists.txt \ && cmake . \ && make \ " >> Dockerfile +# echo "&& mkdir /tmp/jni/ && cp release/*.so /tmp/jni/ && ls -ltr /tmp/jni/ \ && cp /tmp/jni/libKNNIndex*.so /usr/lib \ && rm -rf /usr/share/elasticsearch/k-NN" >> Dockerfile +# echo "RUN cd /usr/share/elasticsearch/" >> Dockerfile +# echo "ADD k-NN/build/distributions/opendistro-knn-$plugin_version.zip /tmp/" >> Dockerfile +# echo "RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch file:/tmp/opendistro-knn-$plugin_version.zip" >> Dockerfile +# docker build -t odfe-knn:test . +# echo "imagePresent=true" >> $GITHUB_ENV +# else +# echo "imagePresent=false" >> $GITHUB_ENV +# fi +# - name: Run Docker Image +# if: env.imagePresent == 'true' +# run: | +# cd .. +# docker run -p 9200:9200 -d -p 9600:9600 -e "discovery.type=single-node" odfe-knn:test +# sleep 90 +# - name: Run k-NN Test +# if: env.imagePresent == 'true' +# run: | +# security=`curl -XGET https://localhost:9200/_cat/plugins?v -u admin:admin --insecure |grep opendistro_security|wc -l` +# if [ $security -gt 0 ] +# then +# echo "Security plugin is available. Running tests in security mode" +# ./gradlew :integTest -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername="docker-cluster" -Dhttps=true -Duser=admin -Dpassword=admin +# else +# echo "Security plugin is NOT available. Skipping tests as they are already ran part of ./gradlew build" +# fi diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 0000000..e47d8d8 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,28 @@ +name: Backport +on: + pull_request_target: + types: + - closed + - labeled + +jobs: + backport: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + name: Backport + steps: + - name: GitHub App token + id: github_app_token + uses: tibdex/github-app-token@v1.5.0 + with: + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + installation_id: 22958780 + + - name: Backport + uses: VachaShah/backport@v1.1.4 + with: + github_token: ${{ steps.github_app_token.outputs.token }} + branch_name: backport/backport-${{ github.event.number }} diff --git a/.github/workflows/backwards_compatibility_tests_workflow.yml b/.github/workflows/backwards_compatibility_tests_workflow.yml new file mode 100644 index 0000000..eac70c8 --- /dev/null +++ b/.github/workflows/backwards_compatibility_tests_workflow.yml @@ -0,0 +1,73 @@ +name: Backwards Compatibility Tests Search Request Processor +on: + push: + branches: + - "*" + - "feature/**" + pull_request: + branches: + - "*" + - "feature/**" + +jobs: + Restart-Upgrade-BWCTests-k-NN: + strategy: + matrix: + java: [ 11, 17 ] + bwc_version : [ "2.4.0" ] + opensearch_version : [ "3.0.0-SNAPSHOT" ] + + name: SRP Restart-Upgrade BWC Tests + runs-on: ubuntu-latest + env: + BWC_VERSION_RESTART_UPGRADE: ${{ matrix.bwc_version }} + + steps: + - name: Checkout SRP + uses: actions/checkout@v1 + + - name: Setup Java ${{ matrix.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + +# - name: Install dependencies +# run: | +# sudo apt-get install libopenblas-dev gfortran -y + + + - name: Run SRP Restart-Upgrade BWC Tests from BWCVersion-${{ matrix.bwc_version }} to OpenSearch Version-${{ matrix.opensearch_version }} + run: | + echo "Running restart-upgrade backwards compatibility tests ..." + ./gradlew :qa:restart-upgrade:testRestartUpgrade -Dtests.bwc.version=$BWC_VERSION_RESTART_UPGRADE + + + Rolling-Upgrade-BWCTests-SRP: + strategy: + matrix: + java: [ 11, 17 ] + bwc_version: [ "2.4.0" ] + opensearch_version: [ "3.0.0-SNAPSHOT" ] + + name: SRP Rolling-Upgrade BWC Tests + runs-on: ubuntu-latest + env: + BWC_VERSION_ROLLING_UPGRADE: ${{ matrix.bwc_version }} + + steps: + - name: Checkout SRP + uses: actions/checkout@v1 + + - name: Setup Java ${{ matrix.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + +# - name: Install dependencies +# run: | +# sudo apt-get install libopenblas-dev gfortran -y + + - name: Run SRP Rolling-Upgrade BWC Tests from BWCVersion-${{ matrix.bwc_version }} to OpenSearch Version-${{ matrix.opensearch_version }} + run: | + echo "Running rolling-upgrade backwards compatibility tests ..." + ./gradlew :qa:rolling-upgrade:testRollingUpgrade -Dtests.bwc.version=$BWC_VERSION_ROLLING_UPGRADE diff --git a/.github/workflows/backwards_compatibility_tests_workflow.yml~ b/.github/workflows/backwards_compatibility_tests_workflow.yml~ new file mode 100644 index 0000000..b77adaf --- /dev/null +++ b/.github/workflows/backwards_compatibility_tests_workflow.yml~ @@ -0,0 +1,73 @@ +name: Backwards Compatibility Tests k-NN +on: + push: + branches: + - "*" + - "feature/**" + pull_request: + branches: + - "*" + - "feature/**" + +jobs: + Restart-Upgrade-BWCTests-k-NN: + strategy: + matrix: + java: [ 11, 17 ] + bwc_version : [ "2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0" ] + opensearch_version : [ "3.0.0-SNAPSHOT" ] + + name: k-NN Restart-Upgrade BWC Tests + runs-on: ubuntu-latest + env: + BWC_VERSION_RESTART_UPGRADE: ${{ matrix.bwc_version }} + + steps: + - name: Checkout k-NN + uses: actions/checkout@v1 + + - name: Setup Java ${{ matrix.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + + - name: Install dependencies + run: | + sudo apt-get install libopenblas-dev gfortran -y + + + - name: Run k-NN Restart-Upgrade BWC Tests from BWCVersion-${{ matrix.bwc_version }} to OpenSearch Version-${{ matrix.opensearch_version }} + run: | + echo "Running restart-upgrade backwards compatibility tests ..." + ./gradlew :qa:restart-upgrade:testRestartUpgrade -Dtests.bwc.version=$BWC_VERSION_RESTART_UPGRADE + + + Rolling-Upgrade-BWCTests-k-NN: + strategy: + matrix: + java: [ 11, 17 ] + bwc_version: [ "2.4.0" ] + opensearch_version: [ "3.0.0-SNAPSHOT" ] + + name: k-NN Rolling-Upgrade BWC Tests + runs-on: ubuntu-latest + env: + BWC_VERSION_ROLLING_UPGRADE: ${{ matrix.bwc_version }} + + steps: + - name: Checkout k-NN + uses: actions/checkout@v1 + + - name: Setup Java ${{ matrix.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + + - name: Install dependencies + run: | + sudo apt-get install libopenblas-dev gfortran -y + + - name: Run k-NN Rolling-Upgrade BWC Tests from BWCVersion-${{ matrix.bwc_version }} to OpenSearch Version-${{ matrix.opensearch_version }} + run: | + echo "Running rolling-upgrade backwards compatibility tests ..." + ./gradlew :qa:rolling-upgrade:testRollingUpgrade -Dtests.bwc.version=$BWC_VERSION_ROLLING_UPGRADE diff --git a/.github/workflows/create-documentation-issue.yml b/.github/workflows/create-documentation-issue.yml new file mode 100644 index 0000000..1ab6e8b --- /dev/null +++ b/.github/workflows/create-documentation-issue.yml @@ -0,0 +1,41 @@ +name: Create Documentation Issue +on: + pull_request: + types: + - labeled +env: + PR_NUMBER: ${{ github.event.number }} + +jobs: + create-issue: + if: ${{ github.event.label.name == 'needs-documentation' }} + runs-on: ubuntu-latest + name: Create Documentation Issue + steps: + - name: GitHub App token + id: github_app_token + uses: tibdex/github-app-token@v1.5.0 + with: + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + installation_id: 22958780 + + - name: Checkout code + uses: actions/checkout@v2 + + - name: Edit the issue template + run: | + echo "https://github.com/opensearch-project/k-NN/pull/${{ env.PR_NUMBER }}." >> ./.github/ISSUE_TEMPLATE/documentation-issue.md + + - name: Create Issue From File + id: create-issue + uses: peter-evans/create-issue-from-file@v4 + with: + title: Add documentation related to new feature + content-filepath: ./.github/ISSUE_TEMPLATE/documentation-issue.md + labels: documentation + repository: opensearch-project/documentation-website + token: ${{ steps.github_app_token.outputs.token }} + + - name: Print Issue + run: echo Created related documentation issue ${{ steps.create-issue.outputs.issue-number }} diff --git a/.github/workflows/dco.yml b/.github/workflows/dco.yml new file mode 100644 index 0000000..cf30ea8 --- /dev/null +++ b/.github/workflows/dco.yml @@ -0,0 +1,18 @@ +name: Developer Certificate of Origin Check + +on: [pull_request] + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Get PR Commits + id: 'get-pr-commits' + uses: tim-actions/get-pr-commits@v1.1.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: DCO Check + uses: tim-actions/dco@v1.1.0 + with: + commits: ${{ steps.get-pr-commits.outputs.commits }} diff --git a/.github/workflows/delete_backport_branch.yml b/.github/workflows/delete_backport_branch.yml new file mode 100644 index 0000000..a97f9cd --- /dev/null +++ b/.github/workflows/delete_backport_branch.yml @@ -0,0 +1,15 @@ +name: Delete merged branch of the backport PRs +on: + pull_request: + types: + - closed + +jobs: + delete-branch: + runs-on: ubuntu-latest + if: startsWith(github.event.pull_request.head.ref,'backport-') + steps: + - name: Delete merged branch + uses: SvanBoxel/delete-merged-branch@main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/delete_backport_branch.yml~ b/.github/workflows/delete_backport_branch.yml~ new file mode 100644 index 0000000..d654df6 --- /dev/null +++ b/.github/workflows/delete_backport_branch.yml~ @@ -0,0 +1,15 @@ +name: Delete merged branch of the backport PRs +on: + pull_request: + types: + - closed + +jobs: + delete-branch: + runs-on: ubuntu-latest + if: startsWith(github.event.pull_request.head.ref,'backport/') + steps: + - name: Delete merged branch + uses: SvanBoxel/delete-merged-branch@main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/draft-release-notes-workflow.yml b/.github/workflows/draft-release-notes-workflow.yml new file mode 100644 index 0000000..6b3d89c --- /dev/null +++ b/.github/workflows/draft-release-notes-workflow.yml @@ -0,0 +1,20 @@ +name: Release Drafter + +on: + push: + branches: + - main + +jobs: + update_release_draft: + name: Update draft release notes + runs-on: ubuntu-latest + steps: + - name: Update draft release notes + uses: release-drafter/release-drafter@v5 + with: + config-name: draft-release-notes-config.yml + name: Version (set here) + tag: (None) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml new file mode 100644 index 0000000..3d0b81a --- /dev/null +++ b/.github/workflows/links.yml @@ -0,0 +1,23 @@ +name: Link Checker +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + linkchecker: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: lychee Link Checker + id: lychee + uses: lycheeverse/lychee-action@master + with: + args: --accept=200,403,429 **/*.html **/*.md **/*.txt **/*.json + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + - name: Fail if there were link errors + run: exit ${{ steps.lychee.outputs.exit_code }} \ No newline at end of file From abc49c6810ac6cbe505506ee29df549ca60695ce Mon Sep 17 00:00:00 2001 From: Mark Cohen Date: Sun, 6 Nov 2022 12:18:53 -0500 Subject: [PATCH 02/13] * moved source for semantic-ranker plugin to root to follow 1 plugin per repo convention * moved README.md to RELEVANCE.md; will create new README for search request processor, current, and future proposed states Signed-off-by: Mark Cohen --- .github/workflows/CI.yml | 72 ----- .gitignore | 89 ++++++ RELEVANCE.md | 62 ++++ build.gradle | 131 +++++++++ gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 234 ++++++++++++++++ gradlew.bat | 89 ++++++ settings.gradle | 10 + .../relevance/SearchRelevancePlugin.java | 100 +++++++ .../actionfilter/SearchActionFilter.java | 265 ++++++++++++++++++ .../relevance/client/OpenSearchClient.java | 32 +++ .../configuration/ConfigurationUtils.java | 95 +++++++ .../relevance/configuration/Constants.java | 23 ++ .../ResultTransformerConfiguration.java | 16 ++ .../SearchConfigurationExtBuilder.java | 137 +++++++++ .../TransformerConfiguration.java | 30 ++ .../transformer/ResultTransformer.java | 40 +++ .../transformer/ResultTransformerType.java | 32 +++ .../transformer/TransformerType.java | 32 +++ .../KendraIntelligentRanker.java | 170 +++++++++++ .../client/KendraClientSettings.java | 117 ++++++++ .../client/KendraHttpClient.java | 122 ++++++++ .../client/SimpleAwsErrorHandler.java | 28 ++ .../client/SimpleResponseHandler.java | 23 ++ .../configuration/Constants.java | 31 ++ .../KendraIntelligentRankerSettings.java | 95 +++++++ ...KendraIntelligentRankingConfiguration.java | 209 ++++++++++++++ .../model/PassageScore.java | 26 ++ .../model/dto/Document.java | 72 +++++ .../model/dto/RescoreRequest.java | 40 +++ .../model/dto/RescoreResult.java | 27 ++ .../model/dto/RescoreResultItem.java | 25 ++ .../preprocess/BM25Scorer.java | 84 ++++++ .../preprocess/QueryParser.java | 128 +++++++++ .../preprocess/SlidingWindowTextSplitter.java | 159 +++++++++++ .../preprocess/TextTokenizer.java | 51 ++++ .../plugin-metadata/plugin-security.policy | 15 + .../relevance/SearchRelevancePluginIT.java | 39 +++ .../relevance/SearchRelevanceTests.java | 14 + .../preprocess/BM25ScorerTests.java | 48 ++++ .../preprocess/QueryParserTests.java | 133 +++++++++ .../SlidingWindowTextSplitterTests.java | 157 +++++++++++ .../preprocess/TextTokenizerTests.java | 38 +++ src/test/resources/splitter/input.txt | 15 + .../SearchRelevanceClientYamlTestSuiteIT.java | 26 ++ .../resources/rest-api-spec/test/10_basic.yml | 8 + 46 files changed, 3322 insertions(+), 72 deletions(-) create mode 100644 .gitignore create mode 100644 RELEVANCE.md create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/org/opensearch/search/relevance/SearchRelevancePlugin.java create mode 100644 src/main/java/org/opensearch/search/relevance/actionfilter/SearchActionFilter.java create mode 100644 src/main/java/org/opensearch/search/relevance/client/OpenSearchClient.java create mode 100644 src/main/java/org/opensearch/search/relevance/configuration/ConfigurationUtils.java create mode 100644 src/main/java/org/opensearch/search/relevance/configuration/Constants.java create mode 100644 src/main/java/org/opensearch/search/relevance/configuration/ResultTransformerConfiguration.java create mode 100644 src/main/java/org/opensearch/search/relevance/configuration/SearchConfigurationExtBuilder.java create mode 100644 src/main/java/org/opensearch/search/relevance/configuration/TransformerConfiguration.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/ResultTransformer.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/ResultTransformerType.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/TransformerType.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/KendraIntelligentRanker.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraClientSettings.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraHttpClient.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleAwsErrorHandler.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleResponseHandler.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/Constants.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankerSettings.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankingConfiguration.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/PassageScore.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/Document.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreRequest.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResult.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResultItem.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25Scorer.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParser.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitter.java create mode 100644 src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizer.java create mode 100644 src/main/plugin-metadata/plugin-security.policy create mode 100644 src/test/java/org/opensearch/search/relevance/SearchRelevancePluginIT.java create mode 100644 src/test/java/org/opensearch/search/relevance/SearchRelevanceTests.java create mode 100644 src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25ScorerTests.java create mode 100644 src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParserTests.java create mode 100644 src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitterTests.java create mode 100644 src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizerTests.java create mode 100644 src/test/resources/splitter/input.txt create mode 100644 src/yamlRestTest/java/org/opensearch/search/relevance/SearchRelevanceClientYamlTestSuiteIT.java create mode 100644 src/yamlRestTest/resources/rest-api-spec/test/10_basic.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index dfe732a..0880010 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -30,17 +30,6 @@ jobs: with: java-version: ${{ matrix.java }} -# - name: Install dependencies on ubuntu -## if: startsWith(matrix.os,'ubuntu') -# run: | -# sudo apt-get install libopenblas-dev gfortran -y - -# - name: Install dependencies on macos -# if: startsWith(matrix.os, 'macos') -# run: | -# brew reinstall gcc -# export FC=/usr/local/Cellar/gcc/12.2.0/bin/gfortran - - name: Run build run: | ./gradlew build @@ -68,68 +57,7 @@ jobs: with: java-version: ${{ matrix.java }} -# - name: Install MinGW Using Scoop -# run: | -# iex "& {$(irm get.scoop.sh)} -RunAsAdmin" -# scoop bucket add main -# scoop install mingw - -# - name: Add MinGW to PATH -# run: | -# echo "C:/Users/runneradmin/scoop/apps/mingw/current/bin" >> $env:GITHUB_PATH -# refreshenv - -# - name: Download OpenBLAS -# run: | -# curl -L -O https://github.com/xianyi/OpenBLAS/releases/download/v0.3.21/OpenBLAS-0.3.21-x64.zip -# mkdir OpenBLAS -# Expand-Archive -Path .\OpenBLAS-0.3.21-x64.zip -DestinationPath .\OpenBLAS\ -# mkdir ./src/main/resources/windowsDependencies -# cp ./OpenBLAS/bin/libopenblas.dll ./src/main/resources/windowsDependencies/ -# rm .\OpenBLAS-0.3.21-x64.zip -# rm -r .\OpenBLAS\ - - name: Run build run: | ./gradlew.bat build -# - name: Pull and Run Docker for security tests -# run: | -# plugin=`ls build/distributions/*.zip` -# version=`echo $plugin|awk -F- '{print $4}'| cut -d. -f 1-3` -# plugin_version=`echo $plugin|awk -F- '{print $4}'| cut -d. -f 1-4` -# echo $version -# cd .. -# if docker pull opendistroforelasticsearch/opendistroforelasticsearch:$version -# then -# echo "FROM opendistroforelasticsearch/opendistroforelasticsearch:$version" >> Dockerfile -# echo "RUN if [ -d /usr/share/elasticsearch/plugins/opendistro-knn ]; then /usr/share/elasticsearch/bin/elasticsearch-plugin remove opendistro-knn; fi" >> Dockerfile -# echo "RUN yum -y update \ && yum -y groupinstall "Development Tools" \ && yum install -y unzip glibc.x86_64 cmake \ && yum clean all" >> Dockerfile -# echo "RUN git clone --recursive --branch ${GITHUB_REF##*/} https://github.com/opendistro-for-elasticsearch/k-NN.git /usr/share/elasticsearch/k-NN \ " >> Dockerfile -# echo "&& cd /usr/share/elasticsearch/k-NN/jni \ && sed -i 's/-march=native/-march=x86-64/g' external/nmslib/similarity_search/CMakeLists.txt \ && cmake . \ && make \ " >> Dockerfile -# echo "&& mkdir /tmp/jni/ && cp release/*.so /tmp/jni/ && ls -ltr /tmp/jni/ \ && cp /tmp/jni/libKNNIndex*.so /usr/lib \ && rm -rf /usr/share/elasticsearch/k-NN" >> Dockerfile -# echo "RUN cd /usr/share/elasticsearch/" >> Dockerfile -# echo "ADD k-NN/build/distributions/opendistro-knn-$plugin_version.zip /tmp/" >> Dockerfile -# echo "RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch file:/tmp/opendistro-knn-$plugin_version.zip" >> Dockerfile -# docker build -t odfe-knn:test . -# echo "imagePresent=true" >> $GITHUB_ENV -# else -# echo "imagePresent=false" >> $GITHUB_ENV -# fi -# - name: Run Docker Image -# if: env.imagePresent == 'true' -# run: | -# cd .. -# docker run -p 9200:9200 -d -p 9600:9600 -e "discovery.type=single-node" odfe-knn:test -# sleep 90 -# - name: Run k-NN Test -# if: env.imagePresent == 'true' -# run: | -# security=`curl -XGET https://localhost:9200/_cat/plugins?v -u admin:admin --insecure |grep opendistro_security|wc -l` -# if [ $security -gt 0 ] -# then -# echo "Security plugin is available. Running tests in security mode" -# ./gradlew :integTest -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername="docker-cluster" -Dhttps=true -Duser=admin -Dpassword=admin -# else -# echo "Security plugin is NOT available. Skipping tests as they are already ran part of ./gradlew build" -# fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc343a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,89 @@ +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk \ No newline at end of file diff --git a/RELEVANCE.md b/RELEVANCE.md new file mode 100644 index 0000000..57429ed --- /dev/null +++ b/RELEVANCE.md @@ -0,0 +1,62 @@ +# Search Relevance + +## Overview + +In a search engine, the relevance is the measure of the relationship accuracy between the search query and the search result. Higher the relevance is, the higher is the quality of search result and the users are able to get more relevant content. This project aims to add plugins to OpenSearch to help users make their query results more accurate, contextual and relevant. + +## Relevancy and OpenSearch + +Today, OpenSearch provides results in the order of scores generated by algorithms matching the indexed document contents to the input query. The documents that are more relevant to the query are ranked higher than the ones that are less relevant. These rankings may make sense to one set of users/applications and for others it may be very irrelevant. For example, relevancy for an E-commerce company can mean more similar products in the same category of the search query. While for a document search relevancy may mean, searching the query across different topics/categories present in the document store. This is why, we need more ways to customize the results and its rankings as per the need of the user/business. + +## Relevancy Engineering + +Relevancy as a problem, can’t just be solved at the search layer. Improving relevancy should be envisioned holistically from understanding the ingested data and usage signals to extracting feature, adding re-writers and improving algorithms. Below is the architecture of OpenSearch Relevancy Engineering. + +

+ +

+[Initially presented at Haystack 2022 by @anirudha , @JohannesDaniel and @ps48]. +
+
+Overall the Relevancy Engineering can be divided into two tiers: + +1. **Ingestion Tier:** This tier handles getting the data from different sources to OpenSearch. This data may include: + 1. Search Data: + 1. Core search data, that needs to be queried on by OpenSearch + 2. Ingestion connectors to fetch the data from different data sources and sink in OpenSearch indices. + 2. Search Management Data: + 1. Adding rules and judgements to the rewriter indices. + 3. Observability Data: + 1. Adding customer usage signals to OpenSearch, these signals may include granular details like anonymized customer queries, clicks, orders and session details. +2. **Search & Relevancy Platform Tier:** This tier is responsible for analytics, re-wrtiers, model improvements and adding search configurations. + 1. Search Analytics & Discovery: + 1. Dashboards for analytics, metrics for search tests, search UIs and query profiling. + 2. Querqy based query Rewriting: + 1. Rewriters to customize queries with synonyms, word-breaks, spell corrections, query relaxation. + 3. Search Back Office: + 1. Manage business rules, ontologies and manual judgments. + 4. Relevancy workbench: + 1. Improve algorithms with automated testing, relevance model trainings, personalizations and custom re-rankers. +### [RFC Search Relevance](https://github.com/opensearch-project/search-relevance/issues/1) + +## Contributing + +See [developer guide](https://github.com/opensearch-project/opensearch-plugins/blob/main/BUILDING.md#developing-new-plugins-for-opensearch) and [how to contribute to this project](CONTRIBUTING.md). + +## Getting Help + +If you find a bug, or have a feature request, please don't hesitate to open an issue in this repository. + +For more information, see [project website](https://opensearch.org/) and [documentation](https://opensearch.org/docs). If you need help and are unsure where to open an issue, try [forums](https://forum.opensearch.org/). + +## Security + +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public GitHub issue. + +## License + +This project is licensed under the [Apache v2.0 License](LICENSE). + +## Copyright + +Copyright OpenSearch Contributors. See [NOTICE](NOTICE) for details. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..e3b7e7f --- /dev/null +++ b/build.gradle @@ -0,0 +1,131 @@ +import org.opensearch.gradle.test.RestIntegTestTask + +apply plugin: 'java' +apply plugin: 'idea' +apply plugin: 'opensearch.opensearchplugin' +apply plugin: 'opensearch.yaml-rest-test' +apply plugin: 'opensearch.pluginzip' + +group = 'org.opensearch' + +def pluginName = 'search-relevance' +def pluginDescription = 'Make Opensearch results more relevant.' +def projectPath = 'org.opensearch' +def pathToPlugin = 'search.relevance' +def pluginClassName = 'SearchRelevancePlugin' + +publishing { + publications { + pluginZip(MavenPublication) { publication -> + pom { + name = pluginName + description = pluginDescription + 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/search-relevance" + } + } + } + } + } +} +opensearchplugin { + name pluginName + description pluginDescription + classname "${projectPath}.${pathToPlugin}.${pluginClassName}" + licenseFile rootProject.file('LICENSE') + noticeFile rootProject.file('NOTICE') +} + +// This requires an additional Jar not published as part of build-tools +loggerUsageCheck.enabled = false + +// No need to validate pom, as we do not upload to maven/sonatype +validateNebulaPom.enabled = false + +buildscript { + ext { + opensearch_version = "2.1.0-SNAPSHOT" + } + + 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:${opensearch_version}" + } +} + +repositories { + mavenLocal() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } +} + +dependencies { + implementation 'com.ibm.icu:icu4j:57.2' + implementation 'org.apache.commons:commons-lang3:3.12.0' + implementation 'org.apache.httpcomponents:httpclient:4.5.13' + implementation 'org.apache.httpcomponents:httpcore:4.4.12' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2' + implementation 'com.fasterxml.jackson.core:jackson-core:2.13.2' + implementation 'com.fasterxml.jackson.core:jackson-annotations:2.13.2' + implementation 'commons-logging:commons-logging:1.2' + implementation 'com.amazonaws:aws-java-sdk-sts:1.12.300' + implementation 'com.amazonaws:aws-java-sdk-core:1.12.300' +} + +test { + include '**/*Tests.class' +} + +task integTest(type: RestIntegTestTask) { + description = "Run tests against a cluster" + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath +} +tasks.named("check").configure { dependsOn(integTest) } + +integTest { + // The --debug-jvm command-line option makes the cluster debuggable; this makes the tests debuggable + if (System.getProperty("test.debug") != null) { + jvmArgs '-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005' + } +} + +testClusters.integTest { + testDistribution = "INTEG_TEST" + + // This installs our plugin into the testClusters + plugin(project.tasks.bundlePlugin.archiveFile) +} + +run { + useCluster testClusters.integTest +} + +sourceSets { + main { + resources { + srcDirs = ["config"] + includes = ["**/*.yml"] + } + } +} + +// TODO: Enable these checks +dependencyLicenses.enabled = false +thirdPartyAudit.enabled = false +loggerUsageCheck.enabled = false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..aa991fc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..005bcde --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/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. +# + +############################################################################## +# +# 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/master/subprojects/plugins/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 + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"' + +# 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 + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + 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 + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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 new file mode 100644 index 0000000..6a68175 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@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 + +@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=. +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=-Dfile.encoding=UTF-8 "-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%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +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%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..f273f7d --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/6.5.1/userguide/multi_project_builds.html + */ + +rootProject.name = 'search-relevance' diff --git a/src/main/java/org/opensearch/search/relevance/SearchRelevancePlugin.java b/src/main/java/org/opensearch/search/relevance/SearchRelevancePlugin.java new file mode 100644 index 0000000..6be5a17 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/SearchRelevancePlugin.java @@ -0,0 +1,100 @@ +/* + * 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.search.relevance; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.opensearch.action.support.ActionFilter; +import org.opensearch.client.Client; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.io.stream.NamedWriteableRegistry; +import org.opensearch.common.settings.Setting; +import org.opensearch.common.xcontent.NamedXContentRegistry; +import org.opensearch.env.Environment; +import org.opensearch.env.NodeEnvironment; +import org.opensearch.plugins.ActionPlugin; +import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.SearchPlugin; +import org.opensearch.repositories.RepositoriesService; +import org.opensearch.script.ScriptService; +import org.opensearch.search.relevance.actionfilter.SearchActionFilter; +import org.opensearch.search.relevance.transformer.ResultTransformerType; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.client.KendraClientSettings; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.client.KendraHttpClient; +import org.opensearch.search.relevance.client.OpenSearchClient; +import org.opensearch.search.relevance.configuration.SearchConfigurationExtBuilder; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.KendraIntelligentRanker; +import org.opensearch.search.relevance.transformer.ResultTransformer; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.watcher.ResourceWatcherService; + +public class SearchRelevancePlugin extends Plugin implements ActionPlugin, SearchPlugin { + + private OpenSearchClient openSearchClient; + private KendraHttpClient kendraClient; + private KendraIntelligentRanker kendraIntelligentRanker; + + private Map getAllResultTransformers() { + // Initialize and add other transformers here + return Map.of(ResultTransformerType.KENDRA_INTELLIGENT_RANKING, this.kendraIntelligentRanker); + } + + @Override + public List getActionFilters() { + return Arrays.asList(new SearchActionFilter(getAllResultTransformers(), openSearchClient)); + } + + @Override + public List> getSettings() { + // NOTE: cannot use kendraIntelligentRanker.getTransformerSettings because the object is not yet created + List> allTransformerSettings = new ArrayList<>(); + allTransformerSettings.addAll(KendraIntelligentRankerSettings.getAllSettings()); + // Add settings for other transformers here + return allTransformerSettings; + } + + @Override + public Collection createComponents( + Client client, + ClusterService clusterService, + ThreadPool threadPool, + ResourceWatcherService resourceWatcherService, + ScriptService scriptService, + NamedXContentRegistry xContentRegistry, + Environment environment, + NodeEnvironment nodeEnvironment, + NamedWriteableRegistry namedWriteableRegistry, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier repositoriesServiceSupplier + ) { + this.openSearchClient = new OpenSearchClient(client); + this.kendraClient = new KendraHttpClient(KendraClientSettings.getClientSettings(environment.settings())); + this.kendraIntelligentRanker = new KendraIntelligentRanker(this.kendraClient); + + return Arrays.asList( + this.openSearchClient, + this.kendraClient, + this.kendraIntelligentRanker + ); + } + + @Override + public List> getSearchExts() { + return Collections.singletonList( + new SearchExtSpec<>(SearchConfigurationExtBuilder.NAME, SearchConfigurationExtBuilder::new, SearchConfigurationExtBuilder::parse)); + } + +} diff --git a/src/main/java/org/opensearch/search/relevance/actionfilter/SearchActionFilter.java b/src/main/java/org/opensearch/search/relevance/actionfilter/SearchActionFilter.java new file mode 100644 index 0000000..92ba5ac --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/actionfilter/SearchActionFilter.java @@ -0,0 +1,265 @@ +/* + * 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.search.relevance.actionfilter; + +import static org.opensearch.action.search.ShardSearchFailure.readShardSearchFailure; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.OpenSearchException; +import org.opensearch.action.ActionListener; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionResponse; +import org.opensearch.action.search.SearchAction; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.search.SearchResponseSections; +import org.opensearch.action.search.ShardSearchFailure; +import org.opensearch.action.support.ActionFilter; +import org.opensearch.action.support.ActionFilterChain; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.opensearch.common.io.stream.NamedWriteableRegistry; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.search.SearchHit; +import org.opensearch.search.SearchHits; +import org.opensearch.search.aggregations.InternalAggregations; +import org.opensearch.search.internal.InternalSearchResponse; +import org.opensearch.search.profile.SearchProfileShardResults; +import org.opensearch.search.relevance.configuration.ConfigurationUtils; +import org.opensearch.search.relevance.client.OpenSearchClient; +import org.opensearch.search.relevance.configuration.ResultTransformerConfiguration; +import org.opensearch.search.relevance.transformer.ResultTransformer; +import org.opensearch.search.relevance.transformer.ResultTransformerType; +import org.opensearch.search.suggest.Suggest; +import org.opensearch.tasks.Task; + +public class SearchActionFilter implements ActionFilter { + private static final Logger logger = LogManager.getLogger(SearchActionFilter.class); + + private final int order; + + private final NamedWriteableRegistry namedWriteableRegistry; + private final Map supportedResultTransformers; + private final OpenSearchClient openSearchClient; + + public SearchActionFilter(Map supportedResultTransformers, OpenSearchClient openSearchClient) { + order = 10; // TODO: Finalize this value + namedWriteableRegistry = new NamedWriteableRegistry(Collections.emptyList()); + this.supportedResultTransformers = supportedResultTransformers; + this.openSearchClient = openSearchClient; + } + + @Override + public int order() { + return order; + } + + @Override + public void apply( + final Task task, + final String action, + final Request request, + final ActionListener listener, + final ActionFilterChain chain) { + + final long startTime = System.nanoTime(); + + if (!SearchAction.INSTANCE.name().equals(action)) { + chain.proceed(task, action, request, listener); + return; + } + + final SearchRequest searchRequest = (SearchRequest) request; + + final String[] indices = searchRequest.indices(); + // Skip if no, or more than 1, index is specified. + if (indices == null || indices.length != 1) { + chain.proceed(task, action, request, listener); + return; + } + + List resultTransformerConfigurations = getResultTransformerConfigurations(indices[0], searchRequest); + + LinkedHashMap orderedTransformersAndConfigs = new LinkedHashMap<>(); + for (ResultTransformerConfiguration config : resultTransformerConfigurations) { + ResultTransformer resultTransformer = supportedResultTransformers.get(config.getType()); + if (resultTransformer.shouldTransform(searchRequest, config)) { + orderedTransformersAndConfigs.put(resultTransformer, config); + } + } + + if (!orderedTransformersAndConfigs.isEmpty()) { + // Source is returned in response hits by default. If disabled by the user, overwrite and enable + // in order to access document contents for reranking, then suppress at response time. + boolean suppressSourceOnResponse = false; + if (searchRequest.source().fetchSource() != null && !searchRequest.source().fetchSource().fetchSource()) { + searchRequest.source().fetchSource(true); + suppressSourceOnResponse = true; + } + + final ActionListener searchResponseListener = createSearchResponseListener( + listener, startTime, searchRequest, orderedTransformersAndConfigs, suppressSourceOnResponse); + chain.proceed(task, action, request, searchResponseListener); + return; + } + + chain.proceed(task, action, request, listener); + } + + /** + * Parse and return a list of result transformers from request and index level configurations + * Request level configuration takes precedence over index level + * @param indexName name of the OpenSearch index + * @param searchRequest input request + * @return ordered and validated list of result transformers, empty list if not specified at + * either request or index level + */ + private List getResultTransformerConfigurations( + final String indexName, + final SearchRequest searchRequest) { + + List configs = new ArrayList<>(); + + // Request level configuration takes precedence over index level + configs = ConfigurationUtils.getResultTransformersFromRequestConfiguration(searchRequest); + if (!configs.isEmpty()) { + return configs; + } + + // Fetch all index settings for this plugin + String[] settingNames = supportedResultTransformers.values() + .stream() + .map(t -> t.getTransformerSettings() + .stream() + .map(s -> s.getKey()) + .collect(Collectors.toList())) + .flatMap(Collection::stream) + .collect(Collectors.toList()) + .toArray(new String[0]); + + configs = ConfigurationUtils.getResultTransformersFromIndexConfiguration( + openSearchClient.getIndexSettings(indexName, settingNames)); + + return configs; + } + + /** + * Create a Listener that, during the OpenSearch response chain, + * calls external service Kendra Ranking to rerank OpenSearch hits + * @param listener default listened + * @param startTime time when request was received, used to calculate latency added by reranking + * @param searchRequest input search request + * @param orderedTransformersAndConfigs transformers to apply, with their corresponding configurations + * @param suppressSourceOnResponse boolean indicating whether to suppress the document source on response + * @param OpenSearch response type + * @return ActionListener with override for onResponse method + */ + private ActionListener createSearchResponseListener( + final ActionListener listener, + final long startTime, + final SearchRequest searchRequest, + final LinkedHashMap orderedTransformersAndConfigs, + final boolean suppressSourceOnResponse) { + return new ActionListener() { + + @Override + public void onResponse(final Response response) { + final SearchResponse searchResponse = (SearchResponse) response; + final long totalHits = searchResponse.getHits().getTotalHits().value; + if (totalHits == 0) { + logger.info("TotalHits = 0. Returning search response without re-ranking."); + listener.onResponse(response); + return; + } + + logger.debug("Starting re-ranking for search response: {}", searchResponse); + try { + final BytesStreamOutput out = new BytesStreamOutput(); + searchResponse.writeTo(out); + + final StreamInput in = new NamedWriteableAwareStreamInput(out.bytes().streamInput(), + namedWriteableRegistry); + + SearchHits hits = new SearchHits(in); + for (Map.Entry entry : orderedTransformersAndConfigs.entrySet()) { + hits = entry.getKey().transform(hits, searchRequest, entry.getValue()); + } + + if (suppressSourceOnResponse) { + List hitsWithModifiedSource = Arrays.stream(hits.getHits()) + .map(hit -> hit.sourceRef(null)) + .collect(Collectors.toList()); + hits = new SearchHits( + hitsWithModifiedSource.toArray(new SearchHit[hitsWithModifiedSource.size()]), + hits.getTotalHits(), + hits.getMaxScore()); + } + + final InternalAggregations aggregations = + in.readBoolean() ? InternalAggregations.readFrom(in) : null; + final Suggest suggest = in.readBoolean() ? new Suggest(in) : null; + final boolean timedOut = in.readBoolean(); + final Boolean terminatedEarly = in.readOptionalBoolean(); + final SearchProfileShardResults profileResults = in.readOptionalWriteable( + SearchProfileShardResults::new); + final int numReducePhases = in.readVInt(); + + final SearchResponseSections internalResponse = new InternalSearchResponse(hits, + aggregations, suggest, + profileResults, timedOut, terminatedEarly, numReducePhases); + + final int totalShards = in.readVInt(); + final int successfulShards = in.readVInt(); + final int shardSearchFailureSize = in.readVInt(); + final ShardSearchFailure[] shardFailures; + if (shardSearchFailureSize == 0) { + shardFailures = ShardSearchFailure.EMPTY_ARRAY; + } else { + shardFailures = new ShardSearchFailure[shardSearchFailureSize]; + for (int i = 0; i < shardFailures.length; i++) { + shardFailures[i] = readShardSearchFailure(in); + } + } + + final SearchResponse.Clusters clusters = new SearchResponse.Clusters(in.readVInt(), + in.readVInt(), in.readVInt()); + final String scrollId = in.readOptionalString(); + final int skippedShards = in.readVInt(); + + final long tookInMillis = (System.nanoTime() - startTime) / 1000000; + final SearchResponse newResponse = new SearchResponse(internalResponse, scrollId, + totalShards, successfulShards, + skippedShards, tookInMillis, shardFailures, clusters); + listener.onResponse((Response) newResponse); + + // TODO: Change this to a metric + logger.info("Re-ranking overhead time: {}ms", + tookInMillis - searchResponse.getTook().getMillis()); + } catch (final Exception e) { + logger.error("Failed to parse search response.", e); + throw new OpenSearchException("Failed to parse a search response.", e); + } + } + + @Override + public void onFailure(final Exception e) { + listener.onFailure(e); + } + }; + } +} diff --git a/src/main/java/org/opensearch/search/relevance/client/OpenSearchClient.java b/src/main/java/org/opensearch/search/relevance/client/OpenSearchClient.java new file mode 100644 index 0000000..1b306dc --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/client/OpenSearchClient.java @@ -0,0 +1,32 @@ +/* + * 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.search.relevance.client; + +import org.opensearch.action.admin.indices.settings.get.GetSettingsAction; +import org.opensearch.action.admin.indices.settings.get.GetSettingsRequest; +import org.opensearch.action.admin.indices.settings.get.GetSettingsResponse; +import org.opensearch.client.Client; +import org.opensearch.common.settings.Settings; + +public class OpenSearchClient { + private final Client client; + + public OpenSearchClient(Client client) { + this.client = client; + } + + public Settings getIndexSettings(String indexName, String[] settingNames) { + GetSettingsRequest getSettingsRequest = new GetSettingsRequest() + .indices(indexName); + if (settingNames != null && settingNames.length > 0) { + getSettingsRequest.names(settingNames); + } + GetSettingsResponse getSettingsResponse = client.execute(GetSettingsAction.INSTANCE, getSettingsRequest).actionGet(); + return getSettingsResponse.getIndexToSettings().get(indexName); + } +} diff --git a/src/main/java/org/opensearch/search/relevance/configuration/ConfigurationUtils.java b/src/main/java/org/opensearch/search/relevance/configuration/ConfigurationUtils.java new file mode 100644 index 0000000..b57cde5 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/configuration/ConfigurationUtils.java @@ -0,0 +1,95 @@ +/* + * 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.search.relevance.configuration; + +import static org.opensearch.search.relevance.configuration.Constants.RESULT_TRANSFORMER_SETTING_PREFIX; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.common.settings.Settings; +import org.opensearch.search.SearchExtBuilder; +import org.opensearch.search.relevance.transformer.ResultTransformerType; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankingConfiguration; + +public class ConfigurationUtils { + + /** + * Get result transformer configurations from Search Request + * @param settings all index settings configured for this plugin + * @return ordered and validated list of result transformers, empty list if not specified + */ + public static List getResultTransformersFromIndexConfiguration( + Settings settings) { + List indexLevelConfigs = new ArrayList<>(); + + if (settings != null) { + if (settings.getGroups(RESULT_TRANSFORMER_SETTING_PREFIX) != null) { + for (Map.Entry resultTransformer : settings.getGroups(RESULT_TRANSFORMER_SETTING_PREFIX).entrySet()) { + ResultTransformerType resultTransformerType = ResultTransformerType.fromString(resultTransformer.getKey()); + if (ResultTransformerType.KENDRA_INTELLIGENT_RANKING.equals(resultTransformerType)) { + indexLevelConfigs.add(new KendraIntelligentRankingConfiguration(resultTransformer.getValue())); + } + } + } + } + + return reorderAndValidateConfigs(indexLevelConfigs); + } + + /** + * Get result transformer configurations from Search Request + * @param searchRequest input request + * @return ordered and validated list of result transformers, empty list if not specified + */ + public static List getResultTransformersFromRequestConfiguration( + final SearchRequest searchRequest) { + + // Fetch result transformers specified in request + SearchConfigurationExtBuilder requestLevelSearchConfiguration = null; + if (searchRequest.source().ext() != null && !searchRequest.source().ext().isEmpty()) { + // Filter ext builders by name + List extBuilders = searchRequest.source().ext().stream() + .filter(searchExtBuilder -> SearchConfigurationExtBuilder.NAME.equals(searchExtBuilder.getWriteableName())) + .collect(Collectors.toList()); + if (!extBuilders.isEmpty()) { + requestLevelSearchConfiguration = (SearchConfigurationExtBuilder) extBuilders.get(0); + } + } + + List requestLevelConfigs = new ArrayList<>(); + if (requestLevelSearchConfiguration != null) { + requestLevelConfigs = reorderAndValidateConfigs(requestLevelSearchConfiguration.getResultTransformers()); + } + return requestLevelConfigs; + } + + /** + * Sort configurations in ascending order of invocation, and validate + * @param configs list of result transformer configurations + * @return ordered and validated list of result transformers + */ + public static List reorderAndValidateConfigs( + final List configs) throws IllegalArgumentException { + + // Sort + configs.sort(Comparator.comparingInt(ResultTransformerConfiguration::getOrder)); + + for (int i = 0; i < configs.size(); ++i) { + if (configs.get(i).getOrder() != (i + 1)) { + throw new IllegalArgumentException("Expected order [" + (i + 1) + "] for transformer [" + + configs.get(i).getType() + "], but found [" + configs.get(i).getOrder() + "]"); + } + } + + return configs; + } +} diff --git a/src/main/java/org/opensearch/search/relevance/configuration/Constants.java b/src/main/java/org/opensearch/search/relevance/configuration/Constants.java new file mode 100644 index 0000000..f3913a8 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/configuration/Constants.java @@ -0,0 +1,23 @@ +/* + * 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.search.relevance.configuration; + +import static org.opensearch.search.relevance.transformer.TransformerType.RESULT_TRANSFORMER; + +public class Constants { + public static final String PLUGIN_NAME = "searchrelevance"; + public static final String SEARCH_CONFIGURATION = "search_configuration"; + + public static final String PLUGIN_SETTING_PREFIX = + String.join(".", "index", "plugin", PLUGIN_NAME); + public static final String RESULT_TRANSFORMER_SETTING_PREFIX = + String.join(".", PLUGIN_SETTING_PREFIX, RESULT_TRANSFORMER.toString()); + + public static final String PROPERTIES = "properties"; + public static final String ORDER = "order"; +} diff --git a/src/main/java/org/opensearch/search/relevance/configuration/ResultTransformerConfiguration.java b/src/main/java/org/opensearch/search/relevance/configuration/ResultTransformerConfiguration.java new file mode 100644 index 0000000..f63ec46 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/configuration/ResultTransformerConfiguration.java @@ -0,0 +1,16 @@ +/* + * 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.search.relevance.configuration; + +import org.opensearch.search.relevance.transformer.ResultTransformerType; + +public abstract class ResultTransformerConfiguration extends TransformerConfiguration { + + public abstract ResultTransformerType getType(); + +} diff --git a/src/main/java/org/opensearch/search/relevance/configuration/SearchConfigurationExtBuilder.java b/src/main/java/org/opensearch/search/relevance/configuration/SearchConfigurationExtBuilder.java new file mode 100644 index 0000000..64590a9 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/configuration/SearchConfigurationExtBuilder.java @@ -0,0 +1,137 @@ +/* + * 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.search.relevance.configuration; + +import static org.opensearch.search.relevance.configuration.Constants.SEARCH_CONFIGURATION; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.common.ParseField; +import org.opensearch.common.ParsingException; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; +import org.opensearch.common.xcontent.XContentBuilder; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.search.SearchExtBuilder; +import org.opensearch.search.relevance.transformer.ResultTransformerType; +import org.opensearch.search.relevance.transformer.TransformerType; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankingConfiguration; + +public class SearchConfigurationExtBuilder extends SearchExtBuilder { + public static final String NAME = SEARCH_CONFIGURATION; + + private static final ParseField RESULT_TRANSFORMER = new ParseField(TransformerType.RESULT_TRANSFORMER.toString()); + private static final ParseField KENDRA_INTELLIGENT_RANKING = new ParseField(ResultTransformerType.KENDRA_INTELLIGENT_RANKING.toString()); + + private List resultTransformerConfigurations = new ArrayList<>(); + + public SearchConfigurationExtBuilder() {} + + public SearchConfigurationExtBuilder(StreamInput input) throws IOException { + ResultTransformerConfiguration cfg1 = input.readOptionalWriteable(KendraIntelligentRankingConfiguration::new); + resultTransformerConfigurations.add(cfg1); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + for (ResultTransformerConfiguration config : resultTransformerConfigurations) { + out.writeOptionalWriteable(config); + } + } + + @Override + public String getWriteableName() { + return NAME; + } + + public static SearchConfigurationExtBuilder parse(XContentParser parser) throws IOException { + SearchConfigurationExtBuilder extBuilder = new SearchConfigurationExtBuilder(); + XContentParser.Token token = parser.currentToken(); + String currentFieldName = null; + if (token != XContentParser.Token.START_OBJECT && (token = parser.nextToken()) != XContentParser.Token.START_OBJECT) { + throw new ParsingException( + parser.getTokenLocation(), + "Expected [" + XContentParser.Token.START_OBJECT + "] but found [" + token + "]", + parser.getTokenLocation() + ); + } + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_OBJECT) { + if (RESULT_TRANSFORMER.match(currentFieldName, parser.getDeprecationHandler())) { + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } + if (KENDRA_INTELLIGENT_RANKING.match(currentFieldName, + parser.getDeprecationHandler())) { + extBuilder.addResultTransformer( + KendraIntelligentRankingConfiguration.parse(parser, null)); + } else { + throw new IllegalArgumentException( + "Unrecognized Result Transformer type [" + currentFieldName + "]"); + } + } + } else { + throw new IllegalArgumentException("Unrecognized Transformer type [" + currentFieldName + "]"); + } + } else { + throw new ParsingException( + parser.getTokenLocation(), + "Unknown key for a " + token + " in [" + currentFieldName + "].", + parser.getTokenLocation() + ); + } + } + return extBuilder; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(NAME); + for (ResultTransformerConfiguration config : resultTransformerConfigurations) { + builder.field(config.getType().toString(), config); + } + return builder.endObject(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof SearchConfigurationExtBuilder)) { + return false; + } + SearchConfigurationExtBuilder o = (SearchConfigurationExtBuilder) obj; + return (this.resultTransformerConfigurations.size() == o.resultTransformerConfigurations.size() && + this.resultTransformerConfigurations.containsAll(o.resultTransformerConfigurations) && + o.resultTransformerConfigurations.containsAll(this.resultTransformerConfigurations)) ; + } + + @Override + public int hashCode() { + return Objects.hash(this.getClass(), this.resultTransformerConfigurations); + } + + public void setResultTransformers(final List resultTransformerConfigurations) { + this.resultTransformerConfigurations = resultTransformerConfigurations; } + + public List getResultTransformers() { + return this.resultTransformerConfigurations; + } + + public void addResultTransformer(final ResultTransformerConfiguration resultTransformerConfiguration) { + this.resultTransformerConfigurations.add(resultTransformerConfiguration); + } +} diff --git a/src/main/java/org/opensearch/search/relevance/configuration/TransformerConfiguration.java b/src/main/java/org/opensearch/search/relevance/configuration/TransformerConfiguration.java new file mode 100644 index 0000000..625ab02 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/configuration/TransformerConfiguration.java @@ -0,0 +1,30 @@ +/* + * 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.search.relevance.configuration; + +import static org.opensearch.search.relevance.configuration.Constants.ORDER; +import static org.opensearch.search.relevance.configuration.Constants.PROPERTIES; + +import org.opensearch.common.ParseField; +import org.opensearch.common.io.stream.Writeable; +import org.opensearch.common.xcontent.ToXContentObject; + +public abstract class TransformerConfiguration implements Writeable, ToXContentObject { + protected static final ParseField TRANSFORMER_ORDER = new ParseField(ORDER); + protected static final ParseField TRANSFORMER_PROPERTIES = new ParseField(PROPERTIES); + + protected int order; + + public int getOrder() { + return this.order; + } + + public void setOrder(final int order) { + this.order = order; + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/ResultTransformer.java b/src/main/java/org/opensearch/search/relevance/transformer/ResultTransformer.java new file mode 100644 index 0000000..c5b77d8 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/ResultTransformer.java @@ -0,0 +1,40 @@ +/* + * 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.search.relevance.transformer; + +import java.util.List; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.common.settings.Setting; +import org.opensearch.search.SearchHits; +import org.opensearch.search.relevance.configuration.ResultTransformerConfiguration; + +public interface ResultTransformer { + + /** + * Get the list of settings required / supported by the transformer + * @return list of transformer settings + */ + List> getTransformerSettings(); + + /** + * Decide whether to apply the transformer on the input request + * @param request input request + * @param configuration Configuration parameters for the transformer + * @return boolean decision on whether to apply the transformer + */ + boolean shouldTransform(final SearchRequest request, final ResultTransformerConfiguration configuration); + + /** + * Rank hits based on the provided query + * @param hits hits to be re-ranked + * @param request Search request + * @param configuration Configuration parameters for the transformer + * @return SearchHits ordered by score generated by ranker + */ + SearchHits transform(final SearchHits hits, final SearchRequest request, final ResultTransformerConfiguration configuration); +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/ResultTransformerType.java b/src/main/java/org/opensearch/search/relevance/transformer/ResultTransformerType.java new file mode 100644 index 0000000..81880fc --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/ResultTransformerType.java @@ -0,0 +1,32 @@ +/* + * 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.search.relevance.transformer; + +public enum ResultTransformerType { + KENDRA_INTELLIGENT_RANKING("kendra_intelligent_ranking"); + + private final String type; + + ResultTransformerType(String type) { + this.type = type; + } + + @Override + public String toString() { + return type; + } + + public static ResultTransformerType fromString(String type) { + for (ResultTransformerType resultTransformerType : values()) { + if (resultTransformerType.type.equalsIgnoreCase(type)) { + return resultTransformerType; + } + } + throw new IllegalArgumentException("Unrecognized Result Transformer type [" + type + "]"); + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/TransformerType.java b/src/main/java/org/opensearch/search/relevance/transformer/TransformerType.java new file mode 100644 index 0000000..aef6945 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/TransformerType.java @@ -0,0 +1,32 @@ +/* + * 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.search.relevance.transformer; + +public enum TransformerType { + RESULT_TRANSFORMER("result_transformer"); + + private final String type; + + TransformerType(String type) { + this.type = type; + } + + @Override + public String toString() { + return type; + } + + public static TransformerType fromString(String type) { + for (TransformerType transformerType : values()) { + if (transformerType.type.equalsIgnoreCase(type)) { + return transformerType; + } + } + throw new IllegalArgumentException("Unrecognized Transformer type [" + type + "]"); + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/KendraIntelligentRanker.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/KendraIntelligentRanker.java new file mode 100644 index 0000000..7b20c43 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/KendraIntelligentRanker.java @@ -0,0 +1,170 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.search.SearchRequest; +import org.opensearch.common.settings.Setting; +import org.opensearch.search.SearchHit; +import org.opensearch.search.SearchHits; +import org.opensearch.search.relevance.configuration.ResultTransformerConfiguration; +import org.opensearch.search.relevance.transformer.ResultTransformer; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.client.KendraHttpClient; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankingConfiguration; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.model.PassageScore; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.model.dto.Document; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.model.dto.RescoreRequest; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.model.dto.RescoreResult; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.model.dto.RescoreResultItem; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.preprocess.BM25Scorer; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.preprocess.QueryParser; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.preprocess.QueryParser.QueryParserResult; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.preprocess.SlidingWindowTextSplitter; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.preprocess.TextTokenizer; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings; + +public class KendraIntelligentRanker implements ResultTransformer { + + private static final int PASSAGE_SIZE_LIMIT = 600; + private static final int SLIDING_WINDOW_STEP = PASSAGE_SIZE_LIMIT - 50; + private static final int MAXIMUM_PASSAGES = 10; + private static final double BM25_B_VALUE = 0.75; + private static final double BM25_K1_VALUE = 1.6; + private static final int TOP_K_PASSAGES = 3; + + private static final Logger logger = LogManager.getLogger(KendraIntelligentRanker.class); + + private final KendraHttpClient kendraClient; + private final TextTokenizer textTokenizer; + private final QueryParser queryParser; + + public KendraIntelligentRanker(KendraHttpClient kendraClient) { + this.kendraClient = kendraClient; + this.textTokenizer = new TextTokenizer(); + this.queryParser = new QueryParser(); + } + + @Override + public List> getTransformerSettings() { + return KendraIntelligentRankerSettings.getAllSettings(); + } + + /** + * Check if search request is eligible for rescore + * @param request Search Request + * @return boolean decision on whether to re-rank + */ + @Override + public boolean shouldTransform(SearchRequest request, ResultTransformerConfiguration configuration) { + if (request.source() == null) { + return false; + } + + // Skip if there is scroll, pagination, or sorting. + if (request.scroll() != null || request.source().from() > 0 || + (request.source().sorts() != null && !request.source().sorts().isEmpty())) { + return false; + } + return true; + } + + /** + * + * @param hits Search hits to rerank with respect to query + * @param request Search request + * @return SearchHits reranked search hits + */ + @Override + public SearchHits transform(final SearchHits hits, + final SearchRequest request, + final ResultTransformerConfiguration configuration) { + KendraIntelligentRankingConfiguration kendraConfig = (KendraIntelligentRankingConfiguration) configuration; + QueryParserResult queryParserResult = queryParser.parse( + request.source().query(), + kendraConfig.getProperties().getBodyFields(), + kendraConfig.getProperties().getTitleFields()); + if (queryParserResult == null) { + return hits; + } + try { + List originalHits = new ArrayList<>(); + for (SearchHit searchHit : hits.getHits()) { + Map docSourceMap = searchHit.getSourceAsMap(); + SlidingWindowTextSplitter textSplitter = new SlidingWindowTextSplitter(PASSAGE_SIZE_LIMIT, SLIDING_WINDOW_STEP, MAXIMUM_PASSAGES); + List splitPassages = textSplitter.split(docSourceMap.get(queryParserResult.getBodyFieldName()).toString()); + List> topPassages = getTopPassages(queryParserResult.getQueryText(), splitPassages); + List tokenizedTitle = null; + if (queryParserResult.getTitleFieldName() != null) { + tokenizedTitle = textTokenizer.tokenize(docSourceMap.get(queryParserResult.getTitleFieldName()).toString()); + } + for (int i = 0; i < topPassages.size(); i++) { + originalHits.add( + new Document(searchHit.getId() + "@" + (i + 1), searchHit.getId(), tokenizedTitle, topPassages.get(i), searchHit.getScore()) + ); + } + } + + final RescoreRequest rescoreRequest = new RescoreRequest(queryParserResult.getQueryText(), originalHits); + final RescoreResult rescoreResult = kendraClient.rescore(rescoreRequest); + Map idToSearchHitMap = new HashMap<>(); + for (SearchHit searchHit : hits.getHits()) { + idToSearchHitMap.put(searchHit.getId(), searchHit); + } + List newSearchHits = new ArrayList<>(); + float maxScore = 0; + for (RescoreResultItem rescoreResultItem : rescoreResult.getResultItems()) { + SearchHit searchHit = idToSearchHitMap.get(rescoreResultItem.getDocumentId()); + if (searchHit == null) { + logger.warn("Response from external service references hit id {}, which does not exist in original results. Skipping.", + rescoreResultItem.getDocumentId()); + continue; + } + searchHit.score(rescoreResultItem.getScore()); + maxScore = Math.max(maxScore, rescoreResultItem.getScore()); + newSearchHits.add(searchHit); + } + return new SearchHits(newSearchHits.toArray(new SearchHit[newSearchHits.size()]), hits.getTotalHits(), maxScore); + } catch (Exception ex) { + logger.error("Failed to re-rank. Returning original search results without re-ranking.", ex); + return hits; + } + } + + private List> getTopPassages(final String queryText, final List splitPassages) { + List query = textTokenizer.tokenize(queryText); + List> passages = textTokenizer.tokenize(splitPassages); + BM25Scorer bm25Scorer = new BM25Scorer(BM25_B_VALUE, BM25_K1_VALUE, passages); + PriorityQueue pq = new PriorityQueue<>(Comparator.comparingDouble(x -> x.getScore())); + + for (int i = 0; i < passages.size(); i++) { + double score = bm25Scorer.score(query, passages.get(i)); + pq.offer(new PassageScore(score, i)); + if (pq.size() > TOP_K_PASSAGES) { + // Maintain heap of top K passages + pq.poll(); + } + } + + List> topPassages = new ArrayList<>(); + while (!pq.isEmpty()) { + topPassages.add(passages.get(pq.poll().getIndex())); + } + Collections.reverse(topPassages); // reverse to order from highest to lowest score + return topPassages; + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraClientSettings.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraClientSettings.java new file mode 100644 index 0000000..44fb113 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraClientSettings.java @@ -0,0 +1,117 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.client; + +import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings.ACCESS_KEY_SETTING; +import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings.ASSUME_ROLE_ARN_SETTING; +import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings.EXECUTION_PLAN_ID_SETTING; +import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings.SECRET_KEY_SETTING; +import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings.SERVICE_ENDPOINT_SETTING; +import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings.SERVICE_REGION_SETTING; +import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings.SESSION_TOKEN_SETTING; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.auth.BasicSessionCredentials; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.common.settings.SecureString; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.settings.SettingsException; + +/** + * A container for settings used to create a Kendra client. + */ +public final class KendraClientSettings { + + private static final Logger logger = LogManager.getLogger(KendraClientSettings.class); + + /** + * Credentials to authenticate with Kendra. + */ + private final AWSCredentials credentials; + private final String serviceEndpoint; + private final String serviceRegion; + private final String executionPlanId; + private final String assumeRoleArn; + + protected KendraClientSettings(AWSCredentials credentials, String serviceEndpoint, String serviceRegion, String executionPlanId, String assumeRoleArn) { + this.credentials = credentials; + this.serviceEndpoint = serviceEndpoint; + this.serviceRegion = serviceRegion; + this.executionPlanId = executionPlanId; + this.assumeRoleArn = assumeRoleArn; + } + + public AWSCredentials getCredentials() { + return credentials; + } + + public String getExecutionPlanId() { + return executionPlanId; + } + + public String getServiceEndpoint() { + return serviceEndpoint; + } + + public String getServiceRegion() { + return serviceRegion; + } + + public String getAssumeRoleArn() { + return assumeRoleArn; + } + + static AWSCredentials loadCredentials(Settings settings) { + try (SecureString key = ACCESS_KEY_SETTING.get(settings); + SecureString secret = SECRET_KEY_SETTING.get(settings); + SecureString sessionToken = SESSION_TOKEN_SETTING.get(settings)) { + if (key.length() == 0 && secret.length() == 0) { + if (sessionToken.length() > 0) { + throw new SettingsException("Setting [{}] is set but [{}] and [{}] are not", + SESSION_TOKEN_SETTING.getKey(), ACCESS_KEY_SETTING.getKey(), SECRET_KEY_SETTING.getKey()); + } + + logger.debug("Using either environment variables, system properties or instance profile credentials"); + return null; + } else if (key.length() == 0 || secret.length() == 0) { + throw new SettingsException("One of settings [{}] and [{}] is not set.", + ACCESS_KEY_SETTING.getKey(), SECRET_KEY_SETTING.getKey()); + } else { + final AWSCredentials credentials; + if (sessionToken.length() == 0) { + logger.debug("Using basic key/secret credentials"); + credentials = new BasicAWSCredentials(key.toString(), secret.toString()); + } else { + logger.debug("Using basic session credentials"); + credentials = new BasicSessionCredentials(key.toString(), secret.toString(), sessionToken.toString()); + } + return credentials; + } + } + } + + /** + * Parse settings for a single client. + * @param settings a {@link Settings} instance from which to derive the endpoint settings + * @return KendraClientSettings comprising credentials and endpoint settings + */ + public static KendraClientSettings getClientSettings(Settings settings) { + final AWSCredentials credentials = loadCredentials(settings); + return new KendraClientSettings( + credentials, + SERVICE_ENDPOINT_SETTING.get(settings), + SERVICE_REGION_SETTING.get(settings), + EXECUTION_PLAN_ID_SETTING.get(settings), + ASSUME_ROLE_ARN_SETTING.get(settings) + ); + } + +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraHttpClient.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraHttpClient.java new file mode 100644 index 0000000..d30fb27 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraHttpClient.java @@ -0,0 +1,122 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.client; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.ClientConfiguration; +import com.amazonaws.DefaultRequest; +import com.amazonaws.Request; +import com.amazonaws.Response; +import com.amazonaws.auth.AWS4Signer; +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.auth.STSAssumeRoleSessionCredentialsProvider; +import com.amazonaws.http.AmazonHttpClient; +import com.amazonaws.http.ExecutionContext; +import com.amazonaws.http.HttpMethodName; +import com.amazonaws.http.HttpResponseHandler; +import com.amazonaws.services.securitytoken.AWSSecurityTokenService; +import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import org.opensearch.search.relevance.transformer.kendraintelligentranking.model.dto.RescoreRequest; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.model.dto.RescoreResult; + +public class KendraHttpClient { + private static final String KENDRA_RANKING_SERVICE_NAME = "kendra-ranking"; + private static final String KENDRA_RESCORE_URI = "rescore"; + private static final String KENDRA_RESCORE_EXECUTION_PLANS = "rescore-execution-plans"; + private static final String ASSUME_ROLE_SESSION_NAME = "OpenSearchKendraIntelligentRankingPluginSession"; + + private final AmazonHttpClient amazonHttpClient; + private final HttpResponseHandler errorHandler; + private final HttpResponseHandler responseHandler; + private final AWSCredentialsProvider awsCredentialsProvider; + private final AWS4Signer aws4Signer; + private final String serviceEndpoint; + private final String executionPlanId; + private final ObjectMapper objectMapper; + + public KendraHttpClient(KendraClientSettings clientSettings) { + amazonHttpClient = AccessController.doPrivileged((PrivilegedAction) () -> new AmazonHttpClient(new ClientConfiguration())); + errorHandler = new SimpleAwsErrorHandler(); + responseHandler = new SimpleResponseHandler(); + aws4Signer = new AWS4Signer(); + aws4Signer.setServiceName(KENDRA_RANKING_SERVICE_NAME); + aws4Signer.setRegionName(clientSettings.getServiceRegion()); + objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + serviceEndpoint = clientSettings.getServiceEndpoint(); + executionPlanId = clientSettings.getExecutionPlanId(); + + final AWSCredentialsProvider credentialsProvider; + final AWSCredentials credentials = clientSettings.getCredentials(); + if (credentials == null) { + // Use environment variables, system properties or instance profile credentials. + credentialsProvider = DefaultAWSCredentialsProviderChain.getInstance(); + } else { + // Use keystore credentials. + credentialsProvider = new AWSStaticCredentialsProvider(credentials); + } + + final String assumeRoleArn = clientSettings.getAssumeRoleArn(); + if (assumeRoleArn != null && !assumeRoleArn.isBlank()) { + // If AssumeRoleArn was provided in config, use auto-refreshed role credentials. + awsCredentialsProvider = AccessController.doPrivileged( + (PrivilegedAction) () -> { + AWSSecurityTokenService awsSecurityTokenService = AWSSecurityTokenServiceClientBuilder.standard() + .withCredentials(credentialsProvider) + .withRegion(clientSettings.getServiceRegion()) + .build(); + + return new STSAssumeRoleSessionCredentialsProvider.Builder(clientSettings.getAssumeRoleArn(), ASSUME_ROLE_SESSION_NAME) + .withStsClient(awsSecurityTokenService) + .build(); + }); + } else { + awsCredentialsProvider = credentialsProvider; + } + } + + public RescoreResult rescore(RescoreRequest rescoreRequest) { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + Request request = new DefaultRequest<>(aws4Signer.getServiceName()); + request.setHttpMethod(HttpMethodName.POST); + request.setEndpoint(buildRescoreURI()); + request.setContent(new ByteArrayInputStream(objectMapper.writeValueAsString(rescoreRequest).getBytes(StandardCharsets.UTF_8))); + aws4Signer.sign(request, awsCredentialsProvider.getCredentials()); + + Response rsp = amazonHttpClient + .requestExecutionBuilder() + .executionContext(new ExecutionContext(true)) + .request(request) + .errorResponseHandler(errorHandler) + .execute(responseHandler); + + return objectMapper.readValue(rsp.getAwsResponse(), RescoreResult.class); + } catch (Exception ex) { + throw new RuntimeException("Exception executing request.", ex); + } + }); + } + + public URI buildRescoreURI() { + return URI.create(String.join("/", + serviceEndpoint, KENDRA_RESCORE_EXECUTION_PLANS, executionPlanId, KENDRA_RESCORE_URI)); + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleAwsErrorHandler.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleAwsErrorHandler.java new file mode 100644 index 0000000..5293357 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleAwsErrorHandler.java @@ -0,0 +1,28 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.client; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.http.HttpResponse; +import com.amazonaws.http.HttpResponseHandler; + +import java.nio.charset.StandardCharsets; + +public class SimpleAwsErrorHandler implements HttpResponseHandler { + @Override public AmazonServiceException handle(HttpResponse response) throws Exception { + AmazonServiceException ase = new AmazonServiceException(new String(response.getContent().readAllBytes(), StandardCharsets.UTF_8)); + ase.setStatusCode(response.getStatusCode()); + ase.setServiceName(response.getRequest().getServiceName()); + ase.setErrorCode(response.getStatusText()); + return ase; + } + + @Override public boolean needsConnectionLeftOpen() { + return false; + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleResponseHandler.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleResponseHandler.java new file mode 100644 index 0000000..6646a8c --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleResponseHandler.java @@ -0,0 +1,23 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.client; + +import com.amazonaws.http.HttpResponse; +import com.amazonaws.http.HttpResponseHandler; + +import java.nio.charset.StandardCharsets; + +public class SimpleResponseHandler implements HttpResponseHandler { + @Override public String handle(HttpResponse response) throws Exception { + return new String(response.getContent().readAllBytes(), StandardCharsets.UTF_8); + } + + @Override public boolean needsConnectionLeftOpen() { + return false; + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/Constants.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/Constants.java new file mode 100644 index 0000000..61e07cd --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/Constants.java @@ -0,0 +1,31 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.configuration; + +import static org.opensearch.search.relevance.configuration.Constants.ORDER; +import static org.opensearch.search.relevance.configuration.Constants.PROPERTIES; +import static org.opensearch.search.relevance.configuration.Constants.RESULT_TRANSFORMER_SETTING_PREFIX; + +public class Constants { + // Transformer name + public static final String KENDRA_INTELLIGENT_RANKING = "kendra_intelligent_ranking"; + // Transformer properties + public static final String BODY_FIELD = "body_field"; + public static final String TITLE_FIELD = "title_field"; + + public static final String KENDRA_SETTINGS_PREFIX = + String.join(".", RESULT_TRANSFORMER_SETTING_PREFIX, KENDRA_INTELLIGENT_RANKING); + + public static final String ORDER_SETTING_NAME = + String.join(".", KENDRA_SETTINGS_PREFIX, ORDER); + public static final String BODY_FIELD_SETTING_NAME = + String.join(".", KENDRA_SETTINGS_PREFIX, PROPERTIES, BODY_FIELD); + public static final String TITLE_FIELD_SETTING_NAME = + String.join(".", KENDRA_SETTINGS_PREFIX, PROPERTIES, TITLE_FIELD); + +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankerSettings.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankerSettings.java new file mode 100644 index 0000000..756b344 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankerSettings.java @@ -0,0 +1,95 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.configuration; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import org.opensearch.common.settings.SecureSetting; +import org.opensearch.common.settings.SecureString; +import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Setting.Property; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.Constants; + +public class KendraIntelligentRankerSettings { + + /** + * Flag controlling whether to invoke Kendra for rescoring. + */ + public static final Setting KENDRA_ORDER_SETTING = Setting.intSetting(Constants.ORDER_SETTING_NAME, 1, 1, + Property.Dynamic, Property.IndexScope); + + /** + * Document field to be considered as "body" when invoking Kendra. + */ + public static final Setting> KENDRA_BODY_FIELD_SETTING = Setting.listSetting(Constants.BODY_FIELD_SETTING_NAME, Collections.emptyList(), + Function.identity(), new FieldSettingValidator(Constants.BODY_FIELD_SETTING_NAME), + Property.Dynamic, Property.IndexScope); + + /** + * Document field to be considered as "title" when invoking Kendra. + */ + public static final Setting> KENDRA_TITLE_FIELD_SETTING = Setting.listSetting(Constants.TITLE_FIELD_SETTING_NAME, Collections.emptyList(), + Function.identity(), new FieldSettingValidator(Constants.TITLE_FIELD_SETTING_NAME), + Property.Dynamic, Property.IndexScope); + + static final class FieldSettingValidator implements Setting.Validator> { + + private String settingName; + + public FieldSettingValidator(final String name) { + this.settingName = name; + } + + @Override + public void validate(List value) { + if (value != null && value.size() > 1) { + throw new IllegalArgumentException("[" + this.settingName + "] can have at most 1 element"); + } + } + } + + /** + * The access key (ie login id) for connecting to Kendra. + */ + public static final Setting ACCESS_KEY_SETTING = SecureSetting.secureString("kendra_intelligent_ranking.aws.access_key", null); + + /** + * The secret key (ie password) for connecting to Kendra. + */ + public static final Setting SECRET_KEY_SETTING = SecureSetting.secureString("kendra_intelligent_ranking.aws.secret_key", null); + + /** + * The session token for connecting to Kendra. + */ + public static final Setting SESSION_TOKEN_SETTING = SecureSetting.secureString("kendra_intelligent_ranking.aws.session_token", null); + + public static final Setting SERVICE_ENDPOINT_SETTING = Setting.simpleString("kendra_intelligent_ranking.service.endpoint", Setting.Property.NodeScope); + + public static final Setting SERVICE_REGION_SETTING = Setting.simpleString("kendra_intelligent_ranking.service.region", Setting.Property.NodeScope); + + public static final Setting EXECUTION_PLAN_ID_SETTING = Setting.simpleString("kendra_intelligent_ranking.service.execution_plan_id", Setting.Property.NodeScope); + + public static final Setting ASSUME_ROLE_ARN_SETTING = Setting.simpleString("kendra_intelligent_ranking.service.assume_role_arn", Setting.Property.NodeScope); + + public static final List> getAllSettings() { + return Arrays.asList( + KENDRA_ORDER_SETTING, + KENDRA_BODY_FIELD_SETTING, + KENDRA_TITLE_FIELD_SETTING, + ACCESS_KEY_SETTING, + SECRET_KEY_SETTING, + SESSION_TOKEN_SETTING, + SERVICE_ENDPOINT_SETTING, + SERVICE_REGION_SETTING, + EXECUTION_PLAN_ID_SETTING, + ASSUME_ROLE_ARN_SETTING + ); + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankingConfiguration.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankingConfiguration.java new file mode 100644 index 0000000..e9dddc4 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankingConfiguration.java @@ -0,0 +1,209 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.configuration; + +import static org.opensearch.search.relevance.configuration.Constants.ORDER; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import org.opensearch.common.ParseField; +import org.opensearch.common.ParsingException; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; +import org.opensearch.common.io.stream.Writeable; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.ObjectParser; +import org.opensearch.common.xcontent.ToXContentObject; +import org.opensearch.common.xcontent.XContentBuilder; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.search.relevance.configuration.ResultTransformerConfiguration; +import org.opensearch.search.relevance.configuration.TransformerConfiguration; +import org.opensearch.search.relevance.transformer.ResultTransformerType; + +public class KendraIntelligentRankingConfiguration extends ResultTransformerConfiguration { + private static final ObjectParser PARSER; + + static { + PARSER = new ObjectParser("kendra_intelligent_ranking_configuration", KendraIntelligentRankingConfiguration::new); + PARSER.declareInt(TransformerConfiguration::setOrder, TRANSFORMER_ORDER); + PARSER.declareObject(KendraIntelligentRankingConfiguration::setProperties, + (p, c) -> KendraIntelligentRankingProperties.parse(p, c), + TRANSFORMER_PROPERTIES); + } + + private KendraIntelligentRankingProperties properties; + + public KendraIntelligentRankingConfiguration() {} + + public KendraIntelligentRankingConfiguration(final int order, final KendraIntelligentRankingProperties properties) { + this.order = order; + this.properties = properties; + } + + public KendraIntelligentRankingConfiguration(StreamInput input) throws IOException { + this.order = input.readInt(); + this.properties = new KendraIntelligentRankingProperties(input); + } + + public KendraIntelligentRankingConfiguration(Settings settings) { + this.order = settings.getAsInt(ORDER, 0); + this.properties = new KendraIntelligentRankingProperties( + settings.getAsList("properties.body_field"), + settings.getAsList("properties.title_field")); + } + + @Override + public ResultTransformerType getType() { + return ResultTransformerType.KENDRA_INTELLIGENT_RANKING; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeInt(this.order); + this.properties.writeTo(out); + } + + public static ResultTransformerConfiguration parse(XContentParser parser, Void context) throws IOException { + try { + KendraIntelligentRankingConfiguration configuration = PARSER.parse(parser, null); + if (configuration != null && configuration.getOrder() <= 0) { + throw new ParsingException(parser.getTokenLocation(), + "Failed to parse value [" + configuration.getOrder() + "] for Transformer order, must be >= 1"); + } + return configuration; + } catch (IllegalArgumentException iae) { + throw new ParsingException(parser.getTokenLocation(), iae.getMessage(), iae); + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(TRANSFORMER_ORDER.getPreferredName(), this.order); + builder.field(TRANSFORMER_PROPERTIES.getPreferredName(), this.properties); + return builder.endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + KendraIntelligentRankingConfiguration config = (KendraIntelligentRankingConfiguration) o; + + if (order != config.order) return false; + return properties.equals(config.properties); + } + + @Override + public int hashCode() { + return Objects.hash(order, properties); + } + + public void setProperties(KendraIntelligentRankingProperties properties) { + this.properties = properties; + } + + public KendraIntelligentRankingProperties getProperties() { + return this.properties; + } + + public static class KendraIntelligentRankingProperties implements Writeable, ToXContentObject { + protected static final ParseField BODY_FIELD = new ParseField(Constants.BODY_FIELD); + protected static final ParseField TITLE_FIELD = new ParseField(Constants.TITLE_FIELD); + + private static final ObjectParser PARSER; + + static { + PARSER = new ObjectParser<>("kendra_intelligent_ranking_configuration", KendraIntelligentRankingProperties::new); + PARSER.declareStringArray(KendraIntelligentRankingProperties::setBodyFields, BODY_FIELD); + PARSER.declareStringArray(KendraIntelligentRankingProperties::setTitleFields, TITLE_FIELD); + } + + private List bodyFields; + private List titleFields; + + public KendraIntelligentRankingProperties() {} + + public KendraIntelligentRankingProperties(final List bodyFields, final List titleFields) { + this.bodyFields = bodyFields; + this.titleFields = titleFields; + } + + public KendraIntelligentRankingProperties(StreamInput input) throws IOException { + this.bodyFields = input.readStringList(); + this.bodyFields = input.readStringList(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeStringCollection(this.bodyFields); + out.writeStringCollection(this.titleFields); + } + + public static KendraIntelligentRankingProperties parse(XContentParser parser, Void context) throws IOException { + try { + KendraIntelligentRankingProperties properties = PARSER.parse(parser, null); + if (properties != null) { + if (properties.getBodyFields() != null && !properties.getBodyFields().isEmpty() && properties.getBodyFields().size() > 1) { + throw new ParsingException(parser.getTokenLocation(), + "[" + BODY_FIELD + "] can have at most 1 element"); + } + if (properties.getTitleFields() != null && !properties.getTitleFields().isEmpty() && properties.getTitleFields().size() > 1) { + throw new ParsingException(parser.getTokenLocation(), + "[" + TITLE_FIELD + "] can have at most 1 element"); + } + } + return properties; + } catch (IllegalArgumentException iae) { + throw new ParsingException(parser.getTokenLocation(), iae.getMessage(), iae); + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(BODY_FIELD.getPreferredName(), this.bodyFields); + builder.field(TITLE_FIELD.getPreferredName(), this.titleFields); + return builder.endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + KendraIntelligentRankingProperties properties = (KendraIntelligentRankingProperties) o; + + if (bodyFields != properties.bodyFields) return false; + return (titleFields == properties.titleFields); + } + + @Override + public int hashCode() { + return Objects.hash(bodyFields, titleFields); + } + + public List getBodyFields() { + return this.bodyFields; + } + + public void setBodyFields(final List bodyFields) { + this.bodyFields = bodyFields; + } + + public List getTitleFields() { + return this.titleFields; + } + + public void setTitleFields(final List titleFields) { + this.titleFields = titleFields; + } + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/PassageScore.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/PassageScore.java new file mode 100644 index 0000000..a806853 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/PassageScore.java @@ -0,0 +1,26 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.model; + +public class PassageScore { + private double score; + private int index; + + public PassageScore(double score, int index) { + this.score = score; + this.index = index; + } + + public double getScore() { + return score; + } + + public int getIndex() { + return index; + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/Document.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/Document.java new file mode 100644 index 0000000..3328b7a --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/Document.java @@ -0,0 +1,72 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.model.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import java.util.List; + +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) +public class Document { + + private String id; + private String groupId; + private List tokenizedTitle; + private List tokenizedBody; + private Float originalScore; + + public Document(String id, String groupId, List tokenizedTitle, List tokenizedBody, + Float originalScore) { + this.id = id; + this.groupId = groupId; + this.tokenizedTitle = tokenizedTitle; + this.tokenizedBody = tokenizedBody; + this.originalScore = originalScore; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public List getTokenizedTitle() { + return tokenizedTitle; + } + + public void setTokenizedTitle(List tokenizedTitle) { + this.tokenizedTitle = tokenizedTitle; + } + + public List getTokenizedBody() { + return tokenizedBody; + } + + public void setTokenizedBody(List tokenizedBody) { + this.tokenizedBody = tokenizedBody; + } + + public Float getOriginalScore() { + return originalScore; + } + + public void setOriginalScore(Float originalScore) { + this.originalScore = originalScore; + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreRequest.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreRequest.java new file mode 100644 index 0000000..8a43c2b --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreRequest.java @@ -0,0 +1,40 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.model.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import java.util.List; + +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) +public class RescoreRequest { + private String searchQuery; + private List documents; + + public RescoreRequest(String searchQuery, List documents) { + this.searchQuery = searchQuery; + this.documents = documents; + } + + public String getSearchQuery() { + return searchQuery; + } + + public void setSearchQuery(String searchQuery) { + this.searchQuery = searchQuery; + } + + public List getDocuments() { + return documents; + } + + public void setDocuments(List documents) { + this.documents = documents; + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResult.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResult.java new file mode 100644 index 0000000..890ccc2 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResult.java @@ -0,0 +1,27 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.model.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import java.util.List; + +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) +public class RescoreResult { + private String rescoreId; + private List resultItems; + + public String getRescoreId() { + return rescoreId; + } + + public List getResultItems() { + return resultItems; + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResultItem.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResultItem.java new file mode 100644 index 0000000..8d815c9 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResultItem.java @@ -0,0 +1,25 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.model.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) +public class RescoreResultItem { + private String documentId; + private Float score; + + public String getDocumentId() { + return documentId; + } + + public Float getScore() { + return score; + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25Scorer.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25Scorer.java new file mode 100644 index 0000000..4601c2e --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25Scorer.java @@ -0,0 +1,84 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.preprocess; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class BM25Scorer { + private Map wordToDocumentCount = new HashMap<>(); + private double b; + private double k1; + private int totalNumberOfDocs; + private double averageDocumentLength; // avdl + + /** + * Initialize dataset. + * + * @param b free parameter for BM25 + * @param k1 free parameter for BM25 + * @param documents list of documents, each document is represented by a list of words + */ + public BM25Scorer(double b, double k1, List> documents) { + this.b = b; + this.k1 = k1; + this.totalNumberOfDocs = documents.size(); + + double totalDocumentLength = 0; + for (List document : documents) { + totalDocumentLength += document.size(); + + Set uniqueWordsInDocument = new HashSet<>(document); // add to set to remove duplicates + for (String term : uniqueWordsInDocument) { + wordToDocumentCount.put(term, wordToDocumentCount.getOrDefault(term, 0) + 1); + } + } + this.averageDocumentLength = totalDocumentLength / documents.size(); + } + + /** + * Calculate the BM25 score of a document given a query. + * + * @param query query represented as a list of words + * @param document document represented as a list of words + * @return the BM25 score + */ + public double score(List query, List document) { + double score = 0; + + Map documentWordCounts = new HashMap<>(); + for (String word : document) { + documentWordCounts.put(word, documentWordCounts.getOrDefault(word, 0) + 1); + } + + for (String queryWord : query) { + if (!documentWordCounts.containsKey(queryWord)) { + continue; + } + double termFrequency = (double) documentWordCounts.get(queryWord) / document.size(); + double denominator = termFrequency + k1 * (1 - b + b * document.size() / averageDocumentLength); + double idf = idf(queryWord); + double numerator = idf * termFrequency * (k1 + 1); + score += numerator / denominator; + } + return score; + } + + /** + * Calculate the idf (inverse document frequency) of a word in the dataset. + * + * @param word word to calculate idf on + * @return idf value + */ + private double idf(String word) { + return totalNumberOfDocs > 0 ? Math.log10((double) totalNumberOfDocs / wordToDocumentCount.get(word)) : 0; + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParser.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParser.java new file mode 100644 index 0000000..49dfb7c --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParser.java @@ -0,0 +1,128 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.preprocess; + +import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.Constants.BODY_FIELD; + +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.index.query.MatchQueryBuilder; +import org.opensearch.index.query.MultiMatchQueryBuilder; +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.Constants; + +public class QueryParser { + private static final Logger logger = LogManager.getLogger(QueryParser.class); + + private static final String FILED_MISMATCH_ERROR_MESSAGE = + "Mismatch: Field configured in %s property [%s] is not present in query fields [%s]. Will not apply Kendra Intelligent Ranking."; + + public QueryParserResult parse( + final QueryBuilder query, + final List bodyFieldSetting, + final List titleFieldSetting) { + final String bodyFieldFromSetting = bodyFieldSetting.isEmpty() ? null : bodyFieldSetting.get(0); + final String titleFieldFromSetting = titleFieldSetting.isEmpty() ? null : titleFieldSetting.get(0); + QueryParserResult result = null; + + if (query instanceof MatchQueryBuilder) { + result = parseMatchQuery((MatchQueryBuilder) query, bodyFieldFromSetting); + } else if (query instanceof MultiMatchQueryBuilder) { + result = parseMultiMatchQuery((MultiMatchQueryBuilder) query, bodyFieldFromSetting, titleFieldFromSetting); + } else { + logger.warn(Constants.KENDRA_INTELLIGENT_RANKING + " does not support query type [" + + query.queryName() + "]. Will not apply Kendra Intelligent Ranking."); + } + return result; + } + + private QueryParserResult parseMatchQuery(MatchQueryBuilder matchQuery, String bodyFieldFromSetting) { + QueryParserResult result = null; + if (!bodyFieldFromSetting.equals(matchQuery.fieldName())) { + logger.warn(String.format(Locale.ENGLISH, FILED_MISMATCH_ERROR_MESSAGE, BODY_FIELD, + bodyFieldFromSetting, matchQuery.fieldName())); + } else { + result = new QueryParserResult(matchQuery.value().toString(), bodyFieldFromSetting); + } + return result; + } + + private QueryParserResult parseMultiMatchQuery(MultiMatchQueryBuilder multiMatchQuery, + String bodyFieldFromSetting, String titleFieldFromSetting) { + + QueryParserResult result = null; + boolean configuredBodyFieldPresentInQuery = false; + boolean configuredTitleFieldPresentInQuery = false; + for (String field : multiMatchQuery.fields().keySet()) { + Pattern fieldNameRegex = Pattern.compile(createRegexFromGlob(field)); + if (fieldNameRegex.matcher(bodyFieldFromSetting).matches()) { + configuredBodyFieldPresentInQuery = true; + } + if (titleFieldFromSetting != null && fieldNameRegex.matcher(titleFieldFromSetting).matches()) { + configuredTitleFieldPresentInQuery = true; + } + } + if (!configuredBodyFieldPresentInQuery) { + logger.warn(String.format(Locale.ENGLISH, FILED_MISMATCH_ERROR_MESSAGE, BODY_FIELD, + bodyFieldFromSetting, multiMatchQuery.fields().keySet())); + } else { + final String titleFieldToUse = configuredTitleFieldPresentInQuery ? titleFieldFromSetting : null; + result = new QueryParserResult(multiMatchQuery.value().toString(), bodyFieldFromSetting, titleFieldToUse); + } + return result; + } + + private static String createRegexFromGlob(String glob) { + StringBuilder out = new StringBuilder("^"); + for(int i = 0; i < glob.length(); ++i) { + final char c = glob.charAt(i); + switch(c) { + case '*': out.append(".*"); break; + case '?': out.append('.'); break; + case '.': out.append("\\."); break; + case '\\': out.append("\\\\"); break; + default: out.append(c); + } + } + out.append('$'); + return out.toString(); + } + + public class QueryParserResult { + private String queryText; + private String bodyFieldName; + private String titleFieldName; + + public QueryParserResult(String queryText, String bodyFieldName) { + this.queryText = queryText; + this.bodyFieldName = bodyFieldName; + this.titleFieldName = null; + } + + public QueryParserResult(String queryText, String bodyFieldName, String titleFieldName) { + this.queryText = queryText; + this.bodyFieldName = bodyFieldName; + this.titleFieldName = titleFieldName; + } + + public String getQueryText() { + return queryText; + } + + public String getBodyFieldName() { + return bodyFieldName; + } + + public String getTitleFieldName() { + return titleFieldName; + } + } +} diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitter.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitter.java new file mode 100644 index 0000000..6ff8455 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitter.java @@ -0,0 +1,159 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.preprocess; + +import com.ibm.icu.text.BreakIterator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import org.apache.commons.lang3.tuple.Pair; + +/** + * Applies a sliding window to split input text into passages. However, splitting is aware of + * sentence boundaries and hence the size of splits may be larger than the configured window size. + */ +public class SlidingWindowTextSplitter { + + /** + * Minimum size of window of text to be selected in a split. Window size can be larger + * in practice because of respecting sentence boundaries. + */ + private int windowSize; + + /** + * The minimum step size by which to move the sliding window after a split. Step size can be larger + * in practice because of respecting sentence boundaries. + */ + private int stepSize; + + /** + * The maximum number of passages to be extracted from the input text + */ + private int maximumPassages; + + //local parameters to control sentence/token iteration + private Pair lastSentenceBoundary; + private boolean useTokenIterator; + + public SlidingWindowTextSplitter(int windowSize, int stepSize, int maximumPassages) { + setSlidingWindow(windowSize, stepSize); + this.maximumPassages = maximumPassages; + this.useTokenIterator = Boolean.FALSE; + this.lastSentenceBoundary = null; + } + + /** + * Set parameters of sliding window text splitter + * @param updatedWindowSize text window size + * @param updatedStepSize step size to move the window + */ + public void setSlidingWindow(int updatedWindowSize, int updatedStepSize) { + if (updatedStepSize > updatedWindowSize) { + throw new IllegalArgumentException("Step size " + updatedStepSize + " is larger than window size " + updatedWindowSize); + } + + this.windowSize = updatedWindowSize; + this.stepSize = updatedStepSize; + } + + /** + * Obtains a list of split passages, aware of sentence boundaries, from the input text. + * @param text Text to be split + * @return List of passages extracted from input text + */ + public List split(String text) { + if (text.isEmpty()) { + return new ArrayList<>(); + } + + if (text.length() <= windowSize) { + return Arrays.asList(text); + } + + List splitText = new LinkedList<>(); + + BreakIterator sentenceIterator = BreakIterator.getSentenceInstance(Locale.ENGLISH); + BreakIterator tokenIterator = BreakIterator.getLineInstance(Locale.ENGLISH); + sentenceIterator.setText(text); + + int passageCounter = 0; + int startBoundaryIndex = 0; + int endBoundaryIndex = 0; + + int nextStartBoundaryIndex = 0; + boolean nextStartBoundaryIndexUpdated = false; + + while ((passageCounter < this.maximumPassages) && (endBoundaryIndex != BreakIterator.DONE)) { + // If current passage length is already larger than the step size, + // use the end index as the start index for next window + if (!nextStartBoundaryIndexUpdated && (endBoundaryIndex - startBoundaryIndex + 1) >= stepSize) { + nextStartBoundaryIndexUpdated = true; + nextStartBoundaryIndex = endBoundaryIndex; + } + + if (endBoundaryIndex == text.length() ) { + // End of the input text. Add the passage, irrespective of its length. + splitText.add(text.substring(startBoundaryIndex, endBoundaryIndex)); + ++passageCounter; + } else if ((endBoundaryIndex - startBoundaryIndex + 1) >= windowSize && endBoundaryIndex > nextStartBoundaryIndex ) { + // If current passage length is greater than both step and window size, extend the window + // such that all passages have some overlap + splitText.add(text.substring(startBoundaryIndex, endBoundaryIndex)); + ++passageCounter; + + startBoundaryIndex = nextStartBoundaryIndex; + + // Check whether current end boundary index can be used as next start boundary index + if ((endBoundaryIndex - startBoundaryIndex + 1) >= stepSize) { + nextStartBoundaryIndex = endBoundaryIndex; + nextStartBoundaryIndexUpdated = true; + } else { + nextStartBoundaryIndexUpdated = false; + } + } + + endBoundaryIndex = getNextEndBoundaryIndex(sentenceIterator, tokenIterator, endBoundaryIndex, text); + } + + return splitText; + } + + /** + * Get the end boundary index of the next split. In common cases, move the iterator by a sentence. + * If a sentence is too long, move the iterator by tokens rather than sentences. + * @param sentenceIterator iterator respecting sentence boundaries + * @param tokenIterator iterator respecting tokens + * @param previousEndBoundaryIndex end boundary index of previous split + * @param text input text to split + */ + private int getNextEndBoundaryIndex(BreakIterator sentenceIterator, BreakIterator tokenIterator, int previousEndBoundaryIndex, String text) { + if (!useTokenIterator) { + int nextSentenceBoundary = sentenceIterator.next(); + int sentenceLength = nextSentenceBoundary - previousEndBoundaryIndex; + if (nextSentenceBoundary == BreakIterator.DONE || sentenceLength < windowSize) { + return nextSentenceBoundary; + } else { + // Sentence is too long, use tokenIterator + lastSentenceBoundary = Pair.of(previousEndBoundaryIndex, nextSentenceBoundary); + useTokenIterator = Boolean.TRUE; + tokenIterator.setText(text.substring(previousEndBoundaryIndex, nextSentenceBoundary)); + } + } + + // Always add the offset of previous sentence + int nextTokenBoundary = lastSentenceBoundary.getLeft() + tokenIterator.next(); + if (nextTokenBoundary == lastSentenceBoundary.getRight()) { + // Finished token iterations, unset booleans to start from next sentence. + useTokenIterator = Boolean.FALSE; + lastSentenceBoundary = null; + } + return nextTokenBoundary; + } +} \ No newline at end of file diff --git a/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizer.java b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizer.java new file mode 100644 index 0000000..daf89f3 --- /dev/null +++ b/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizer.java @@ -0,0 +1,51 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.preprocess; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class TextTokenizer { + private static final Set STOP_WORDS = new HashSet<>( + Arrays.asList("i", "me", "my", "myself", "we", "our", "ours", "ourselves", "you", "your", "yours", "yourself", "yourselves", "he", "him", "his", + "himself", "she", "her", "hers", "herself", "it", "its", "itself", "they", "them", "their", "theirs", "themselves", "what", "which", "who", + "whom", "this", "that", "these", "those", "am", "is", "are", "was", "were", "be", "been", "being", "have", "has", "had", "having", "do", + "does", "did", "doing", "a", "an", "the", "and", "but", "if", "or", "because", "as", "until", "while", "of", "at", "by", "for", "with", + "about", "against", "between", "into", "through", "during", "before", "after", "above", "below", "to", "from", "up", "down", "in", "out", + "on", "off", "over", "under", "again", "further", "then", "once", "here", "there", "when", "where", "why", "how", "all", "any", "both", + "each", "few", "more", "most", "other", "some", "such", "no", "nor", "not", "only", "own", "same", "so", "than", "too", "very", "s", "t", + "can", "will", "just", "don", "should", "now")); + private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\p{Punct}\\s]+"); + + public List> tokenize(List texts) { + return texts.stream() + .map(text -> tokenize(text)) + .collect(Collectors.toList()); + } + + public List tokenize(String text) { + String[] tokens = text.split(SPLIT_PATTERN.pattern()); + List validTokens = new ArrayList<>(); + for (String token : tokens) { + if (token.length() == 0) { + continue; + } + String lowerCased = token.toLowerCase(Locale.ENGLISH); + if (!STOP_WORDS.contains(lowerCased)) { + validTokens.add(lowerCased); + } + } + return validTokens; + } +} diff --git a/src/main/plugin-metadata/plugin-security.policy b/src/main/plugin-metadata/plugin-security.policy new file mode 100644 index 0000000..b16dfe0 --- /dev/null +++ b/src/main/plugin-metadata/plugin-security.policy @@ -0,0 +1,15 @@ +/* + * 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. + */ + +grant { + permission java.lang.RuntimePermission "accessDeclaredMembers"; + permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; + + permission java.net.SocketPermission "*", "connect,resolve"; + permission java.lang.RuntimePermission "getClassLoader"; +}; diff --git a/src/test/java/org/opensearch/search/relevance/SearchRelevancePluginIT.java b/src/test/java/org/opensearch/search/relevance/SearchRelevancePluginIT.java new file mode 100644 index 0000000..9f01dfa --- /dev/null +++ b/src/test/java/org/opensearch/search/relevance/SearchRelevancePluginIT.java @@ -0,0 +1,39 @@ +/* + * 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.search.relevance; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.util.EntityUtils; +import org.opensearch.client.Request; +import org.opensearch.client.Response; +import org.opensearch.plugins.Plugin; +import org.opensearch.test.OpenSearchIntegTestCase; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; + +import static org.hamcrest.Matchers.containsString; + +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +@OpenSearchIntegTestCase.ClusterScope(scope = OpenSearchIntegTestCase.Scope.SUITE) +public class SearchRelevancePluginIT extends OpenSearchIntegTestCase { + + @Override + protected Collection> nodePlugins() { + return Collections.singletonList(SearchRelevancePlugin.class); + } + + public void testPluginInstalled() throws IOException { + Response response = createRestClient().performRequest(new Request("GET", "/_cat/plugins")); + String body = EntityUtils.toString(response.getEntity()); + + logger.info("response body: {}", body); + assertThat(body, containsString("search-relevance")); + } +} diff --git a/src/test/java/org/opensearch/search/relevance/SearchRelevanceTests.java b/src/test/java/org/opensearch/search/relevance/SearchRelevanceTests.java new file mode 100644 index 0000000..e65b820 --- /dev/null +++ b/src/test/java/org/opensearch/search/relevance/SearchRelevanceTests.java @@ -0,0 +1,14 @@ +/* + * 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.search.relevance; + +import org.opensearch.test.OpenSearchTestCase; + +public class SearchRelevanceTests extends OpenSearchTestCase { + // Add unit tests for your plugin +} diff --git a/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25ScorerTests.java b/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25ScorerTests.java new file mode 100644 index 0000000..4436cb5 --- /dev/null +++ b/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25ScorerTests.java @@ -0,0 +1,48 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.preprocess; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Assert; +import org.opensearch.test.OpenSearchTestCase; + +public class BM25ScorerTests extends OpenSearchTestCase { + + public void testBM25Scorer() { + TextTokenizer textTokenizer = new TextTokenizer(); + List document1 = textTokenizer.tokenize( + "OpenSearch is a distributed, community-driven, Apache 2.0-licensed, 100% open-source search and analytics suite used for a broad set of use cases like real-time application monitoring, log analytics, and website search. OpenSearch provides a highly scalable system for providing fast access and response to large volumes of data with an integrated visualization tool, OpenSearch Dashboards, that makes it easy for users to explore their data. OpenSearch is powered by the Apache Lucene search library, and it supports a number of search and analytics capabilities such as k-nearest neighbors (KNN) search, SQL, Anomaly Detection, Machine Learning Commons, Trace Analytics, full-text search, and more." + ); + List document2 = textTokenizer.tokenize( + "OpenSearch enables you to easily ingest, secure, search, aggregate, view, and analyze data for a number of use cases such as log analytics, application search, enterprise search, and more. With OpenSearch, you benefit from having a 100% open source product you can use, modify, extend, monetize, and resell however you want. There are a growing number of OpenSearch Project partners that offer a variety of services such as professional support, enhanced features, and managed OpenSearch services. The OpenSearch Project continues to provide a secure, high-quality search and analytics suite with a rich roadmap of new and innovative functionality." + ); + List document3 = textTokenizer.tokenize( + "The sky is blue" + ); + + BM25Scorer bm25Scorer = new BM25Scorer(0.75, 1.6, Arrays.asList(document1, document2, document3)); + + List query1 = textTokenizer.tokenize("Apache Lucene search library"); + double doc1Score1 = bm25Scorer.score(query1, document1); + double doc2Score1 = bm25Scorer.score(query1, document2); + double doc3Score1 = bm25Scorer.score(query1, document3); + Assert.assertTrue(doc1Score1 > doc2Score1); + Assert.assertTrue(doc2Score1 > doc3Score1); + Assert.assertEquals(0, doc3Score1, 0); + + List query2 = textTokenizer.tokenize("sky color"); + double doc1Score2 = bm25Scorer.score(query2, document1); + double doc2Score2 = bm25Scorer.score(query2, document2); + double doc3Score2 = bm25Scorer.score(query2, document3); + Assert.assertTrue(doc3Score2 > doc1Score2); + Assert.assertEquals(0, doc1Score2, 0); + Assert.assertEquals(0, doc2Score2, 0); + } +} diff --git a/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParserTests.java b/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParserTests.java new file mode 100644 index 0000000..af27553 --- /dev/null +++ b/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParserTests.java @@ -0,0 +1,133 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.preprocess; + +import java.util.Arrays; +import java.util.List; +import org.junit.Assert; +import org.opensearch.index.query.MatchQueryBuilder; +import org.opensearch.index.query.MultiMatchQueryBuilder; +import org.opensearch.index.query.TermQueryBuilder; +import org.opensearch.search.relevance.transformer.kendraintelligentranking.preprocess.QueryParser.QueryParserResult; +import org.opensearch.test.OpenSearchTestCase; + +public class QueryParserTests extends OpenSearchTestCase { + + private static final String TEST_BODY_FIELD = "bodyField"; + private static final String TEST_TITLE_FIELD = "titleField"; + private static final String TEST_QUERY_TEXT = "queryText"; + private static final String INVALID_FIELD = "invalidField"; + private QueryParser queryParser = new QueryParser(); + + public void testParse_Match_RequestFieldMatchesSetting() { + QueryParser.QueryParserResult queryParserResult = queryParser.parse( + new MatchQueryBuilder(TEST_BODY_FIELD, TEST_QUERY_TEXT), + Arrays.asList(TEST_BODY_FIELD), + Arrays.asList(TEST_TITLE_FIELD)); + + performQueryParserResultAssertions(queryParserResult, TEST_QUERY_TEXT, TEST_BODY_FIELD, null); + } + + public void testParse_Match_RequestFieldDoesNotMatchSetting() { + QueryParser.QueryParserResult queryParserResult = queryParser.parse( + new MatchQueryBuilder(TEST_BODY_FIELD, TEST_QUERY_TEXT), + Arrays.asList(INVALID_FIELD), + Arrays.asList(TEST_TITLE_FIELD)); + + Assert.assertNull(queryParserResult); + } + + public void testParse_MultiMatch_RequestFieldMatchesSetting() { + QueryParser.QueryParserResult queryParserResult = queryParser.parse( + new MultiMatchQueryBuilder(TEST_QUERY_TEXT, TEST_TITLE_FIELD, TEST_BODY_FIELD), + Arrays.asList(TEST_BODY_FIELD), + Arrays.asList(TEST_TITLE_FIELD)); + + performQueryParserResultAssertions(queryParserResult, TEST_QUERY_TEXT, TEST_BODY_FIELD, TEST_TITLE_FIELD); + } + + public void testParse_MultiMatch_OnlyBodyFieldMatchesSetting() { + QueryParser.QueryParserResult queryParserResult = queryParser.parse( + new MultiMatchQueryBuilder(TEST_QUERY_TEXT, TEST_BODY_FIELD, INVALID_FIELD), + Arrays.asList(TEST_BODY_FIELD), + Arrays.asList(TEST_TITLE_FIELD)); + + performQueryParserResultAssertions(queryParserResult, TEST_QUERY_TEXT, TEST_BODY_FIELD, null); + } + + public void testParse_MultiMatch_RequestFieldMatchesSetting_WithFieldBoosting() { + MultiMatchQueryBuilder multiMatchQueryBuilder = new MultiMatchQueryBuilder(TEST_QUERY_TEXT, TEST_TITLE_FIELD, TEST_BODY_FIELD); + multiMatchQueryBuilder.field(TEST_BODY_FIELD, 1.5f); + multiMatchQueryBuilder.field(TEST_TITLE_FIELD, 3f); + QueryParser.QueryParserResult queryParserResult = queryParser.parse( + multiMatchQueryBuilder, + Arrays.asList(TEST_BODY_FIELD), + Arrays.asList(TEST_TITLE_FIELD)); + + performQueryParserResultAssertions(queryParserResult, TEST_QUERY_TEXT, TEST_BODY_FIELD, TEST_TITLE_FIELD); + } + + public void testParse_MultiMatch_RequestFieldMatchesSetting_WithWildcards() { + final List wildcardsIncludingTitleAndBody = Arrays.asList("*Field", "*Field*", "^*ld$"); + final List wildcardsIncludingBodyOnly = Arrays.asList("body*", "*dy*", "^bo*$"); + + for (String fieldWildcard : wildcardsIncludingTitleAndBody) { + QueryParser.QueryParserResult queryParserResult = queryParser.parse( + new MultiMatchQueryBuilder(TEST_QUERY_TEXT, fieldWildcard), + Arrays.asList(TEST_BODY_FIELD), + Arrays.asList(TEST_TITLE_FIELD)); + + performQueryParserResultAssertions(queryParserResult, TEST_QUERY_TEXT, TEST_BODY_FIELD, TEST_TITLE_FIELD); + } + + for (String fieldWildcard : wildcardsIncludingBodyOnly) { + QueryParser.QueryParserResult queryParserResult1 = queryParser.parse( + new MultiMatchQueryBuilder(TEST_QUERY_TEXT, fieldWildcard), + Arrays.asList(TEST_BODY_FIELD), + Arrays.asList(TEST_TITLE_FIELD)); + + performQueryParserResultAssertions(queryParserResult1, TEST_QUERY_TEXT, TEST_BODY_FIELD, null); + + QueryParser.QueryParserResult queryParserResult2 = queryParser.parse( + new MultiMatchQueryBuilder(TEST_QUERY_TEXT, fieldWildcard, TEST_TITLE_FIELD), + Arrays.asList(TEST_BODY_FIELD), + Arrays.asList(TEST_TITLE_FIELD)); + + performQueryParserResultAssertions(queryParserResult2, TEST_QUERY_TEXT, TEST_BODY_FIELD, TEST_TITLE_FIELD); + } + } + + public void testParse_MultiMatch_RequestFieldDoesNotMatchSetting() { + final List fieldConfigurationsExcludingBody = + Arrays.asList(TEST_TITLE_FIELD, "*title*", "^title*$", "title*^3"); + + for (String fieldConfig : fieldConfigurationsExcludingBody) { + QueryParser.QueryParserResult queryParserResult = queryParser.parse( + new MultiMatchQueryBuilder(TEST_QUERY_TEXT, fieldConfig), + Arrays.asList(TEST_BODY_FIELD), + Arrays.asList(TEST_TITLE_FIELD)); + + Assert.assertNull(queryParserResult); + } + } + + public void testParse_UnsupportedQueryType() { + QueryParser.QueryParserResult queryParserResult = queryParser.parse( + new TermQueryBuilder(TEST_BODY_FIELD, TEST_QUERY_TEXT), + Arrays.asList(TEST_BODY_FIELD), + Arrays.asList(TEST_TITLE_FIELD)); + Assert.assertNull(queryParserResult); + } + + private void performQueryParserResultAssertions(final QueryParserResult queryParserResult, + final String query, final String bodyField, final String titleField) { + Assert.assertEquals(query, queryParserResult.getQueryText()); + Assert.assertEquals(bodyField, queryParserResult.getBodyFieldName()); + Assert.assertEquals(titleField, queryParserResult.getTitleFieldName()); + } +} diff --git a/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitterTests.java b/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitterTests.java new file mode 100644 index 0000000..cf630de --- /dev/null +++ b/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitterTests.java @@ -0,0 +1,157 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.preprocess; + +import static org.opensearch.common.io.PathUtils.get; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Scanner; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.opensearch.test.OpenSearchTestCase; + +public class SlidingWindowTextSplitterTests extends OpenSearchTestCase { + private static final int MAXIMUM_PASSAGES = 10; + private static final String TEST_FILE_PATH = "splitter/input.txt"; + + public void testConstructWithInvalidInputs() { + // Step size cannot be larger than window size + assertThrows(IllegalArgumentException.class, () -> new SlidingWindowTextSplitter(3, 4, MAXIMUM_PASSAGES)); + } + + public void testSplitWithEmptyInput() { + SlidingWindowTextSplitter splitter = new SlidingWindowTextSplitter(10, 4, MAXIMUM_PASSAGES); + assertEquals(Collections.emptyList(), splitter.split("")); + } + + public void testSetSlidingWindowWithInvalidStepSize() { + SlidingWindowTextSplitter splitter = new SlidingWindowTextSplitter(10, 4, MAXIMUM_PASSAGES); + assertThrows(IllegalArgumentException.class, () -> splitter.setSlidingWindow(2, 4)); + } + + public void testSetSlidingWindowUpdatesSplitter() { + SlidingWindowTextSplitter splitter = new SlidingWindowTextSplitter(30, 10, MAXIMUM_PASSAGES); + final String testInput = generateTestInput(1, 5, false); + List splitText = splitter.split(testInput); + + assertEquals(4, splitText.size()); + assertEquals(Arrays.asList( + generateTestInput(1, 2, true), + generateTestInput(2, 3, true), + generateTestInput(3, 4, true), + generateTestInput(4, 5, false)), + splitText); + + // double window size, expect each passage to be loonger + splitter.setSlidingWindow(60, 10); + splitText = splitter.split(testInput); + + assertEquals(2, splitText.size()); + assertEquals(Arrays.asList( + generateTestInput(1, 4, true), + generateTestInput(2, 5, false)), + splitText); + + // double + splitter.setSlidingWindow(60, 30); + splitText = splitter.split(testInput); + + assertEquals(2, splitText.size()); + assertEquals(Arrays.asList( + generateTestInput(1, 4, true), + generateTestInput(3, 5, false)), + splitText); + } + + public void testSplitObeysMaximumPassagesLimit() { + final int maximumPassages = 3; + final String inputText = generateTestInput(1, 5, false); + + SlidingWindowTextSplitter splitter = new SlidingWindowTextSplitter(30, 10, maximumPassages); + List splitText = splitter.split(inputText); + + assertEquals(maximumPassages, splitText.size()); + assertEquals(Arrays.asList( + generateTestInput(1, 2, true), + generateTestInput(2, 3, true), + generateTestInput(3, 4, true)), + splitText); + } + + public void testSplitWhenLastSplitIsShorterThanWindow() { + final String shortText = "Short text"; + final String inputText = String.join(" ", generateTestInput(1, 5, false), shortText); + final String expectedFinalSplit = String.join(" ", "This is a test 5.", shortText); + + SlidingWindowTextSplitter splitter = new SlidingWindowTextSplitter(30, 10, MAXIMUM_PASSAGES); + List splitText = splitter.split(inputText); + + assertEquals(5, splitText.size()); + assertEquals(Arrays.asList( + generateTestInput(1, 2, true), + generateTestInput(2, 3, true), + generateTestInput(3, 4, true), + generateTestInput(4, 5, true), + expectedFinalSplit), + splitText); + } + + public void testSplitWithOverlap() throws IOException { + final int windowSize = 1500; + final int stepSize = 1300; + // Because of respecting sentence boundaries, actual overlap might be smaller. + // Provide a buffer of 20 characters + final int expectedOverlap = windowSize - stepSize - 20; + + SlidingWindowTextSplitter splitter = new SlidingWindowTextSplitter(windowSize, stepSize, MAXIMUM_PASSAGES); + final String input = loadTestInputFromFile(); + + List splitText = splitter.split(input); + + // Overlap means that we get more splits + assertTrue(splitText.size() >= input.length() / windowSize); + + // Verify overlap for every pair of splits + for(int i = 0; i < splitText.size() - 1; ++i) { + assertTrue(splitText.get(i).contains(splitText.get(i + 1).substring(0, expectedOverlap))); + } + } + + public void testSplitUsesLineBreakWhenSentenceIsTooLong() throws IOException { + final int windowSize = 1500; + final int stepSize = 1300; + + SlidingWindowTextSplitter splitter = new SlidingWindowTextSplitter(windowSize, stepSize, MAXIMUM_PASSAGES); + final String input = loadTestInputFromFile(); + String inputWithoutSentenceBoundaries = input.replace(".", " "); + + List splitText = splitter.split(inputWithoutSentenceBoundaries); + + // Ensure that text is split + assertTrue(splitText.size() > 1); + } + + private String generateTestInput(int start, int end, boolean addTerminalSpace) { + final String testInput = IntStream.range(start, end + 1).boxed().map( + i -> String.format(Locale.ENGLISH, "This is a test %s.", i) + ).collect(Collectors.joining(" ")); + return addTerminalSpace ? testInput + " " : testInput; + } + + private String loadTestInputFromFile() throws IOException { + final Scanner scanner = new Scanner(SlidingWindowTextSplitterTests.class.getClassLoader() + .getResourceAsStream(TEST_FILE_PATH), + StandardCharsets.UTF_8.name()).useDelimiter("\\A"); + return scanner.hasNext() ? scanner.next() : ""; + } +} diff --git a/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizerTests.java b/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizerTests.java new file mode 100644 index 0000000..e8c0a21 --- /dev/null +++ b/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizerTests.java @@ -0,0 +1,38 @@ +/* + * 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.search.relevance.transformer.kendraintelligentranking.preprocess; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Assert; +import org.opensearch.test.OpenSearchTestCase; + +public class TextTokenizerTests extends OpenSearchTestCase { + + public static final String TEXT_1 = "What is the capital of the United States?"; + public static final List EXPECTED_1 = Arrays.asList("capital", "united", "states"); + public static final String TEXT_2 = "OPENSEARCH IS OPEN SOURCE SEARCH AND ANALYTICS SUITE."; + public static final List EXPECTED_2 = Arrays.asList("opensearch", "open", "source", "search", "analytics", "suite"); + private TextTokenizer textTokenizer = new TextTokenizer(); + + public void testTokenize1() { + List actual = textTokenizer.tokenize(TEXT_1); + Assert.assertEquals(EXPECTED_1, actual); + } + + public void testTokenize2() { + List actual = textTokenizer.tokenize(TEXT_2); + Assert.assertEquals(EXPECTED_2, actual); + } + + public void testTokenizeMultiple() { + List> actual = textTokenizer.tokenize(Arrays.asList(TEXT_1, TEXT_2)); + Assert.assertEquals(Arrays.asList(EXPECTED_1, EXPECTED_2), actual); + } +} diff --git a/src/test/resources/splitter/input.txt b/src/test/resources/splitter/input.txt new file mode 100644 index 0000000..025aadd --- /dev/null +++ b/src/test/resources/splitter/input.txt @@ -0,0 +1,15 @@ +The history of natural language processing (NLP) generally started in the 1950s, although work can be found from earlier periods. In 1950, Alan Turing published an article titled "Computing Machinery and Intelligence" which proposed what is now called the Turing test as a criterion of intelligence[clarification needed]. + +The Georgetown experiment in 1954 involved fully automatic translation of more than sixty Russian sentences into English. The authors claimed that within three or five years, machine translation would be a solved problem.[2] However, real progress was much slower, and after the ALPAC report in 1966, which found that ten-year-long research had failed to fulfill the expectations, funding for machine translation was dramatically reduced. Little further research in machine translation was conducted until the late 1980s, when the first statistical machine translation systems were developed. + +Some notably successful natural language processing systems developed in the 1960s were SHRDLU, a natural language system working in restricted "blocks worlds" with restricted vocabularies, and ELIZA, a simulation of a Rogerian psychotherapist, written by Joseph Weizenbaum between 1964 and 1966. Using almost no information about human thought or emotion, ELIZA sometimes provided a startlingly human-like interaction. When the "patient" exceeded the very small knowledge base, ELIZA might provide a generic response, for example, responding to "My head hurts" with "Why do you say your head hurts?". + +During the 1970s, many programmers began to write "conceptual ontologies", which structured real-world information into computer-understandable data. Examples are MARGIE (Schank, 1975), SAM (Cullingford, 1978), PAM (Wilensky, 1978), TaleSpin (Meehan, 1976), QUALM (Lehnert, 1977), Politics (Carbonell, 1979), and Plot Units (Lehnert 1981). During this time, many chatterbots were written including PARRY, Racter, and Jabberwacky. + +Up to the 1980s, most natural language processing systems were based on complex sets of hand-written rules. Starting in the late 1980s, however, there was a revolution in natural language processing with the introduction of machine learning algorithms for language processing. This was due to both the steady increase in computational power (see Moore's law) and the gradual lessening of the dominance of Chomskyan theories of linguistics (e.g. transformational grammar), whose theoretical underpinnings discouraged the sort of corpus linguistics that underlies the machine-learning approach to language processing.[3] Some of the earliest-used machine learning algorithms, such as decision trees, produced systems of hard if-then rules similar to existing hand-written rules. However, part-of-speech tagging introduced the use of hidden Markov models to natural language processing, and increasingly, research has focused on statistical models, which make soft, probabilistic decisions based on attaching real-valued weights to the features making up the input data. The cache language models upon which many speech recognition systems now rely are examples of such statistical models. Such models are generally more robust when given unfamiliar input, especially input that contains errors (as is very common for real-world data), and produce more reliable results when integrated into a larger system comprising multiple subtasks. + +Many of the notable early successes occurred in the field of machine translation, due especially to work at IBM Research, where successively more complicated statistical models were developed. These systems were able to take advantage of existing multilingual textual corpora that had been produced by the Parliament of Canada and the European Union as a result of laws calling for the translation of all governmental proceedings into all official languages of the corresponding systems of government. However, most other systems depended on corpora specifically developed for the tasks implemented by these systems, which was (and often continues to be) a major limitation in the success of these systems. As a result, a great deal of research has gone into methods of more effectively learning from limited amounts of data. + +Recent research has increasingly focused on unsupervised and semi-supervised learning algorithms. Such algorithms are able to learn from data that has not been hand-annotated with the desired answers, or using a combination of annotated and non-annotated data. Generally, this task is much more difficult than supervised learning, and typically produces less accurate results for a given amount of input data. However, there is an enormous amount of non-annotated data available (including, among other things, the entire content of the World Wide Web), which can often make up for the inferior results if the algorithm used has a low enough time complexity to be practical. + +In the 2010s, representation learning and deep neural network-style machine learning methods became widespread in natural language processing, due in part to a flurry of results showing that such techniques[4][5] can achieve state-of-the-art results in many natural language tasks, for example in language modeling,[6] parsing,[7][8] and many others. Popular techniques include the use of word embeddings to capture semantic properties of words, and an increase in end-to-end learning of a higher-level task (e.g., question answering) instead of relying on a pipeline of separate intermediate tasks (e.g., part-of-speech tagging and dependency parsing). In some areas, this shift has entailed substantial changes in how NLP systems are designed, such that deep neural network-based approaches may be viewed as a new paradigm distinct from statistical natural language processing. For instance, the term neural machine translation (NMT) emphasizes the fact that deep learning-based approaches to machine translation directly learn sequence-to-sequence transformations, obviating the need for intermediate steps such as word alignment and language modeling that were used in statistical machine translation (SMT). \ No newline at end of file diff --git a/src/yamlRestTest/java/org/opensearch/search/relevance/SearchRelevanceClientYamlTestSuiteIT.java b/src/yamlRestTest/java/org/opensearch/search/relevance/SearchRelevanceClientYamlTestSuiteIT.java new file mode 100644 index 0000000..1f0d7dd --- /dev/null +++ b/src/yamlRestTest/java/org/opensearch/search/relevance/SearchRelevanceClientYamlTestSuiteIT.java @@ -0,0 +1,26 @@ +/* + * 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.search.relevance; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import org.opensearch.test.rest.yaml.ClientYamlTestCandidate; +import org.opensearch.test.rest.yaml.OpenSearchClientYamlSuiteTestCase; + + +public class SearchRelevanceClientYamlTestSuiteIT extends OpenSearchClientYamlSuiteTestCase { + + public SearchRelevanceClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { + super(testCandidate); + } + + @ParametersFactory + public static Iterable parameters() throws Exception { + return OpenSearchClientYamlSuiteTestCase.createParameters(); + } +} diff --git a/src/yamlRestTest/resources/rest-api-spec/test/10_basic.yml b/src/yamlRestTest/resources/rest-api-spec/test/10_basic.yml new file mode 100644 index 0000000..4e1f41e --- /dev/null +++ b/src/yamlRestTest/resources/rest-api-spec/test/10_basic.yml @@ -0,0 +1,8 @@ +"Test that the plugin is loaded in OpenSearch": + - do: + cat.plugins: + local: true + h: component + + - match: + $body: /^search-relevance\n$/ From 472a6b900e11ce8340cec3c1ff9aa06a501133f2 Mon Sep 17 00:00:00 2001 From: Mark Cohen Date: Sun, 6 Nov 2022 12:21:47 -0500 Subject: [PATCH 03/13] deleted semantic-reranker dir after moving to root Signed-off-by: Mark Cohen --- semantic-reranker/.github/workflows/CI.yml | 34 --- semantic-reranker/.github/workflows/dco.yml | 18 -- semantic-reranker/.gitignore | 58 ---- semantic-reranker/CONTRIBUTING.md | 115 -------- semantic-reranker/LICENSE.txt | 201 ------------- semantic-reranker/NOTICE.txt | 2 - semantic-reranker/README.md | 1 - semantic-reranker/build.gradle | 131 --------- .../gradle/wrapper/gradle-wrapper.jar | Bin 59821 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - semantic-reranker/gradlew | 234 ---------------- semantic-reranker/gradlew.bat | 89 ------ semantic-reranker/settings.gradle | 10 - .../relevance/SearchRelevancePlugin.java | 100 ------- .../actionfilter/SearchActionFilter.java | 265 ------------------ .../relevance/client/OpenSearchClient.java | 32 --- .../configuration/ConfigurationUtils.java | 95 ------- .../relevance/configuration/Constants.java | 23 -- .../ResultTransformerConfiguration.java | 16 -- .../SearchConfigurationExtBuilder.java | 137 --------- .../TransformerConfiguration.java | 30 -- .../transformer/ResultTransformer.java | 40 --- .../transformer/ResultTransformerType.java | 32 --- .../transformer/TransformerType.java | 32 --- .../KendraIntelligentRanker.java | 170 ----------- .../client/KendraClientSettings.java | 117 -------- .../client/KendraHttpClient.java | 122 -------- .../client/SimpleAwsErrorHandler.java | 28 -- .../client/SimpleResponseHandler.java | 23 -- .../configuration/Constants.java | 31 -- .../KendraIntelligentRankerSettings.java | 95 ------- ...KendraIntelligentRankingConfiguration.java | 209 -------------- .../model/PassageScore.java | 26 -- .../model/dto/Document.java | 72 ----- .../model/dto/RescoreRequest.java | 40 --- .../model/dto/RescoreResult.java | 27 -- .../model/dto/RescoreResultItem.java | 25 -- .../preprocess/BM25Scorer.java | 84 ------ .../preprocess/QueryParser.java | 128 --------- .../preprocess/SlidingWindowTextSplitter.java | 159 ----------- .../preprocess/TextTokenizer.java | 51 ---- .../plugin-metadata/plugin-security.policy | 15 - .../relevance/SearchRelevancePluginIT.java | 39 --- .../relevance/SearchRelevanceTests.java | 14 - .../preprocess/BM25ScorerTests.java | 48 ---- .../preprocess/QueryParserTests.java | 133 --------- .../SlidingWindowTextSplitterTests.java | 157 ----------- .../preprocess/TextTokenizerTests.java | 38 --- .../src/test/resources/splitter/input.txt | 15 - .../SearchRelevanceClientYamlTestSuiteIT.java | 26 -- .../resources/rest-api-spec/test/10_basic.yml | 8 - 51 files changed, 3600 deletions(-) delete mode 100644 semantic-reranker/.github/workflows/CI.yml delete mode 100644 semantic-reranker/.github/workflows/dco.yml delete mode 100644 semantic-reranker/.gitignore delete mode 100644 semantic-reranker/CONTRIBUTING.md delete mode 100644 semantic-reranker/LICENSE.txt delete mode 100644 semantic-reranker/NOTICE.txt delete mode 100644 semantic-reranker/README.md delete mode 100644 semantic-reranker/build.gradle delete mode 100644 semantic-reranker/gradle/wrapper/gradle-wrapper.jar delete mode 100644 semantic-reranker/gradle/wrapper/gradle-wrapper.properties delete mode 100755 semantic-reranker/gradlew delete mode 100644 semantic-reranker/gradlew.bat delete mode 100644 semantic-reranker/settings.gradle delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/SearchRelevancePlugin.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/actionfilter/SearchActionFilter.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/client/OpenSearchClient.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/ConfigurationUtils.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/Constants.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/ResultTransformerConfiguration.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/SearchConfigurationExtBuilder.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/TransformerConfiguration.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/ResultTransformer.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/ResultTransformerType.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/TransformerType.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/KendraIntelligentRanker.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraClientSettings.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraHttpClient.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleAwsErrorHandler.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleResponseHandler.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/Constants.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankerSettings.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankingConfiguration.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/PassageScore.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/Document.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreRequest.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResult.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResultItem.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25Scorer.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParser.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitter.java delete mode 100644 semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizer.java delete mode 100644 semantic-reranker/src/main/plugin-metadata/plugin-security.policy delete mode 100644 semantic-reranker/src/test/java/org/opensearch/search/relevance/SearchRelevancePluginIT.java delete mode 100644 semantic-reranker/src/test/java/org/opensearch/search/relevance/SearchRelevanceTests.java delete mode 100644 semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25ScorerTests.java delete mode 100644 semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParserTests.java delete mode 100644 semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitterTests.java delete mode 100644 semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizerTests.java delete mode 100644 semantic-reranker/src/test/resources/splitter/input.txt delete mode 100644 semantic-reranker/src/yamlRestTest/java/org/opensearch/search/relevance/SearchRelevanceClientYamlTestSuiteIT.java delete mode 100644 semantic-reranker/src/yamlRestTest/resources/rest-api-spec/test/10_basic.yml diff --git a/semantic-reranker/.github/workflows/CI.yml b/semantic-reranker/.github/workflows/CI.yml deleted file mode 100644 index a22d69c..0000000 --- a/semantic-reranker/.github/workflows/CI.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Build and Test Plugin Template - -on: - push: - branches: - - "*" - pull_request: - branches: - - "*" - -jobs: - build: - strategy: - matrix: - os: - - ubuntu-latest - - macOS-latest - java: - - 17 - name: Build and Test Plugin Template - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v1 - with: - java-version: ${{ matrix.java }} - - - name: Build and Run Tests - run: | - ./gradlew build - # Generated by 'opensearch.pluginzip' custom gradle plugin - ./gradlew publishPluginZipPublicationToZipStagingRepository diff --git a/semantic-reranker/.github/workflows/dco.yml b/semantic-reranker/.github/workflows/dco.yml deleted file mode 100644 index 53ed530..0000000 --- a/semantic-reranker/.github/workflows/dco.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Developer Certificate of Origin Check - -on: [pull_request] - -jobs: - check: - runs-on: ubuntu-latest - - steps: - - name: Get PR Commits - id: 'get-pr-commits' - uses: tim-actions/get-pr-commits@v1.1.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - name: DCO Check - uses: tim-actions/dco@v1.1.0 - with: - commits: ${{ steps.get-pr-commits.outputs.commits }} \ No newline at end of file diff --git a/semantic-reranker/.gitignore b/semantic-reranker/.gitignore deleted file mode 100644 index 3266af2..0000000 --- a/semantic-reranker/.gitignore +++ /dev/null @@ -1,58 +0,0 @@ - -# intellij files -.idea/ -*.iml -*.ipr -*.iws -build-idea/ -out/ - -# include shared intellij config -!.idea/inspectionProfiles/Project_Default.xml -!.idea/runConfigurations/Debug_OpenSearch.xml - -# These files are generated in the main tree by IntelliJ -benchmarks/src/main/generated/* - -# eclipse files -.project -.classpath -.settings -build-eclipse/ - -# netbeans files -nb-configuration.xml -nbactions.xml - -# gradle stuff -.gradle/ -build/ -bin/ - -# vscode stuff -.vscode/ - -# testing stuff -**/.local* -.vagrant/ -/logs/ - -# osx stuff -.DS_Store - -# default folders in which the create_bwc_index.py expects to find old es versions in -/backwards -/dev-tools/backwards - -# needed in case docs build is run...maybe we can configure doc build to generate files under build? -html_docs - -# random old stuff that we should look at the necessity of... -/tmp/ -eclipse-build - -# projects using testfixtures -testfixtures_shared/ - -# These are generated from .ci/jobs.t -.ci/jobs/ diff --git a/semantic-reranker/CONTRIBUTING.md b/semantic-reranker/CONTRIBUTING.md deleted file mode 100644 index 95baabd..0000000 --- a/semantic-reranker/CONTRIBUTING.md +++ /dev/null @@ -1,115 +0,0 @@ -# Contributing Guidelines - -Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional -documentation, we greatly value feedback and contributions from our community. - -Please read through this document before submitting any issues or pull requests to ensure we have all the necessary -information to effectively respond to your bug report or contribution. - - -## Reporting Bugs/Feature Requests - -We welcome you to use the GitHub issue tracker to report bugs or suggest features. - -When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already -reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - - -## Contributing via Pull Requests -Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: - -1. You are working against the latest source on the *main* branch. -2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. -3. You open an issue to discuss any significant work - we would hate for your time to be wasted. - -To send us a pull request, please: - -1. Fork the repository. -2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. -3. Ensure local tests pass. -4. Commit to your fork using clear commit messages. -5. Send us a pull request, answering any default questions in the pull request interface. -6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. - -GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and -[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - -## Developer Certificate of Origin - -OpenSearch is an open source product released under the Apache 2.0 license (see either [the Apache site](https://www.apache.org/licenses/LICENSE-2.0) or the [LICENSE.txt file](LICENSE.txt)). The Apache 2.0 license allows you to freely use, modify, distribute, and sell your own products that include Apache 2.0 licensed software. - -We respect intellectual property rights of others and we want to make sure all incoming contributions are correctly attributed and licensed. A Developer Certificate of Origin (DCO) is a lightweight mechanism to do that. - -The DCO is a declaration attached to every contribution made by every developer. In the commit message of the contribution, the developer simply adds a `Signed-off-by` statement and thereby agrees to the DCO, which you can find below or at [DeveloperCertificate.org](http://developercertificate.org/). - -``` -Developer's Certificate of Origin 1.1 - -By making a contribution to this project, I certify that: - -(a) The contribution was created in whole or in part by me and I - have the right to submit it under the open source license - indicated in the file; or - -(b) The contribution is based upon previous work that, to the - best of my knowledge, is covered under an appropriate open - source license and I have the right under that license to - submit that work with modifications, whether created in whole - or in part by me, under the same open source license (unless - I am permitted to submit under a different license), as - Indicated in the file; or - -(c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. - -(d) I understand and agree that this project and the contribution - are public and that a record of the contribution (including - all personal information I submit with it, including my - sign-off) is maintained indefinitely and may be redistributed - consistent with this project or the open source license(s) - involved. - ``` - -We require that every contribution to OpenSearch is signed with a Developer Certificate of Origin. Additionally, please use your real name. We do not accept anonymous contributors nor those utilizing pseudonyms. - -Each commit must include a DCO which looks like this - -``` -Signed-off-by: Jane Smith -``` - -You may type this line on your own when writing your commit messages. However, if your user.name and user.email are set in your git configs, you can use `-s` or `– – signoff` to add the `Signed-off-by` line to the end of the commit message. - -## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. - - -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. - - -## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. - -## License Headers - -New files in your code contributions should contain the following license header. If you are modifying existing files with license headers, or including new files that already have license headers, do not remove or modify them without guidance. - -``` -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -``` - -## Licensing - -See the [LICENSE](LICENSE.txt) file for our project's licensing. We will ask you to confirm the licensing of your contribution. diff --git a/semantic-reranker/LICENSE.txt b/semantic-reranker/LICENSE.txt deleted file mode 100644 index 261eeb9..0000000 --- a/semantic-reranker/LICENSE.txt +++ /dev/null @@ -1,201 +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. diff --git a/semantic-reranker/NOTICE.txt b/semantic-reranker/NOTICE.txt deleted file mode 100644 index 731cb60..0000000 --- a/semantic-reranker/NOTICE.txt +++ /dev/null @@ -1,2 +0,0 @@ -OpenSearch (https://opensearch.org/) -Copyright OpenSearch Contributors diff --git a/semantic-reranker/README.md b/semantic-reranker/README.md deleted file mode 100644 index e5d4892..0000000 --- a/semantic-reranker/README.md +++ /dev/null @@ -1 +0,0 @@ -# Kendra Intelligent Ranking \ No newline at end of file diff --git a/semantic-reranker/build.gradle b/semantic-reranker/build.gradle deleted file mode 100644 index 2c5080c..0000000 --- a/semantic-reranker/build.gradle +++ /dev/null @@ -1,131 +0,0 @@ -import org.opensearch.gradle.test.RestIntegTestTask - -apply plugin: 'java' -apply plugin: 'idea' -apply plugin: 'opensearch.opensearchplugin' -apply plugin: 'opensearch.yaml-rest-test' -apply plugin: 'opensearch.pluginzip' - -group = 'org.opensearch' - -def pluginName = 'search-relevance' -def pluginDescription = 'Make Opensearch results more relevant.' -def projectPath = 'org.opensearch' -def pathToPlugin = 'search.relevance' -def pluginClassName = 'SearchRelevancePlugin' - -publishing { - publications { - pluginZip(MavenPublication) { publication -> - pom { - name = pluginName - description = pluginDescription - 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/search-relevance" - } - } - } - } - } -} -opensearchplugin { - name pluginName - description pluginDescription - classname "${projectPath}.${pathToPlugin}.${pluginClassName}" - licenseFile rootProject.file('LICENSE.txt') - noticeFile rootProject.file('NOTICE.txt') -} - -// This requires an additional Jar not published as part of build-tools -loggerUsageCheck.enabled = false - -// No need to validate pom, as we do not upload to maven/sonatype -validateNebulaPom.enabled = false - -buildscript { - ext { - opensearch_version = "2.1.0-SNAPSHOT" - } - - 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:${opensearch_version}" - } -} - -repositories { - mavenLocal() - maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } - mavenCentral() - maven { url "https://plugins.gradle.org/m2/" } -} - -dependencies { - implementation 'com.ibm.icu:icu4j:57.2' - implementation 'org.apache.commons:commons-lang3:3.12.0' - implementation 'org.apache.httpcomponents:httpclient:4.5.13' - implementation 'org.apache.httpcomponents:httpcore:4.4.12' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2' - implementation 'com.fasterxml.jackson.core:jackson-core:2.13.2' - implementation 'com.fasterxml.jackson.core:jackson-annotations:2.13.2' - implementation 'commons-logging:commons-logging:1.2' - implementation 'com.amazonaws:aws-java-sdk-sts:1.12.300' - implementation 'com.amazonaws:aws-java-sdk-core:1.12.300' -} - -test { - include '**/*Tests.class' -} - -task integTest(type: RestIntegTestTask) { - description = "Run tests against a cluster" - testClassesDirs = sourceSets.test.output.classesDirs - classpath = sourceSets.test.runtimeClasspath -} -tasks.named("check").configure { dependsOn(integTest) } - -integTest { - // The --debug-jvm command-line option makes the cluster debuggable; this makes the tests debuggable - if (System.getProperty("test.debug") != null) { - jvmArgs '-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005' - } -} - -testClusters.integTest { - testDistribution = "INTEG_TEST" - - // This installs our plugin into the testClusters - plugin(project.tasks.bundlePlugin.archiveFile) -} - -run { - useCluster testClusters.integTest -} - -sourceSets { - main { - resources { - srcDirs = ["config"] - includes = ["**/*.yml"] - } - } -} - -// TODO: Enable these checks -dependencyLicenses.enabled = false -thirdPartyAudit.enabled = false -loggerUsageCheck.enabled = false \ No newline at end of file diff --git a/semantic-reranker/gradle/wrapper/gradle-wrapper.jar b/semantic-reranker/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 41d9927a4d4fb3f96a785543079b8df6723c946b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59821 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 diff --git a/semantic-reranker/gradle/wrapper/gradle-wrapper.properties b/semantic-reranker/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index aa991fc..0000000 --- a/semantic-reranker/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-7.4.2-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/semantic-reranker/gradlew b/semantic-reranker/gradlew deleted file mode 100755 index 005bcde..0000000 --- a/semantic-reranker/gradlew +++ /dev/null @@ -1,234 +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. -# - -############################################################################## -# -# 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/master/subprojects/plugins/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 - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"' - -# 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 - which java >/dev/null 2>&1 || 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 - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - 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 - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# 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/semantic-reranker/gradlew.bat b/semantic-reranker/gradlew.bat deleted file mode 100644 index 4cc84e5..0000000 --- a/semantic-reranker/gradlew.bat +++ /dev/null @@ -1,89 +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 - -@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=. -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=-Dfile.encoding=UTF-8 "-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%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -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%"=="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! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/semantic-reranker/settings.gradle b/semantic-reranker/settings.gradle deleted file mode 100644 index f273f7d..0000000 --- a/semantic-reranker/settings.gradle +++ /dev/null @@ -1,10 +0,0 @@ -/* - * This file was generated by the Gradle 'init' task. - * - * The settings file is used to specify which projects to include in your build. - * - * Detailed information about configuring a multi-project build in Gradle can be found - * in the user manual at https://docs.gradle.org/6.5.1/userguide/multi_project_builds.html - */ - -rootProject.name = 'search-relevance' diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/SearchRelevancePlugin.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/SearchRelevancePlugin.java deleted file mode 100644 index 6be5a17..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/SearchRelevancePlugin.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.search.relevance; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Supplier; - -import org.opensearch.action.support.ActionFilter; -import org.opensearch.client.Client; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.io.stream.NamedWriteableRegistry; -import org.opensearch.common.settings.Setting; -import org.opensearch.common.xcontent.NamedXContentRegistry; -import org.opensearch.env.Environment; -import org.opensearch.env.NodeEnvironment; -import org.opensearch.plugins.ActionPlugin; -import org.opensearch.plugins.Plugin; -import org.opensearch.plugins.SearchPlugin; -import org.opensearch.repositories.RepositoriesService; -import org.opensearch.script.ScriptService; -import org.opensearch.search.relevance.actionfilter.SearchActionFilter; -import org.opensearch.search.relevance.transformer.ResultTransformerType; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.client.KendraClientSettings; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.client.KendraHttpClient; -import org.opensearch.search.relevance.client.OpenSearchClient; -import org.opensearch.search.relevance.configuration.SearchConfigurationExtBuilder; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.KendraIntelligentRanker; -import org.opensearch.search.relevance.transformer.ResultTransformer; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.watcher.ResourceWatcherService; - -public class SearchRelevancePlugin extends Plugin implements ActionPlugin, SearchPlugin { - - private OpenSearchClient openSearchClient; - private KendraHttpClient kendraClient; - private KendraIntelligentRanker kendraIntelligentRanker; - - private Map getAllResultTransformers() { - // Initialize and add other transformers here - return Map.of(ResultTransformerType.KENDRA_INTELLIGENT_RANKING, this.kendraIntelligentRanker); - } - - @Override - public List getActionFilters() { - return Arrays.asList(new SearchActionFilter(getAllResultTransformers(), openSearchClient)); - } - - @Override - public List> getSettings() { - // NOTE: cannot use kendraIntelligentRanker.getTransformerSettings because the object is not yet created - List> allTransformerSettings = new ArrayList<>(); - allTransformerSettings.addAll(KendraIntelligentRankerSettings.getAllSettings()); - // Add settings for other transformers here - return allTransformerSettings; - } - - @Override - public Collection createComponents( - Client client, - ClusterService clusterService, - ThreadPool threadPool, - ResourceWatcherService resourceWatcherService, - ScriptService scriptService, - NamedXContentRegistry xContentRegistry, - Environment environment, - NodeEnvironment nodeEnvironment, - NamedWriteableRegistry namedWriteableRegistry, - IndexNameExpressionResolver indexNameExpressionResolver, - Supplier repositoriesServiceSupplier - ) { - this.openSearchClient = new OpenSearchClient(client); - this.kendraClient = new KendraHttpClient(KendraClientSettings.getClientSettings(environment.settings())); - this.kendraIntelligentRanker = new KendraIntelligentRanker(this.kendraClient); - - return Arrays.asList( - this.openSearchClient, - this.kendraClient, - this.kendraIntelligentRanker - ); - } - - @Override - public List> getSearchExts() { - return Collections.singletonList( - new SearchExtSpec<>(SearchConfigurationExtBuilder.NAME, SearchConfigurationExtBuilder::new, SearchConfigurationExtBuilder::parse)); - } - -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/actionfilter/SearchActionFilter.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/actionfilter/SearchActionFilter.java deleted file mode 100644 index 92ba5ac..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/actionfilter/SearchActionFilter.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * 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.search.relevance.actionfilter; - -import static org.opensearch.action.search.ShardSearchFailure.readShardSearchFailure; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.OpenSearchException; -import org.opensearch.action.ActionListener; -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionResponse; -import org.opensearch.action.search.SearchAction; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.action.search.SearchResponseSections; -import org.opensearch.action.search.ShardSearchFailure; -import org.opensearch.action.support.ActionFilter; -import org.opensearch.action.support.ActionFilterChain; -import org.opensearch.common.io.stream.BytesStreamOutput; -import org.opensearch.common.io.stream.NamedWriteableAwareStreamInput; -import org.opensearch.common.io.stream.NamedWriteableRegistry; -import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.search.SearchHit; -import org.opensearch.search.SearchHits; -import org.opensearch.search.aggregations.InternalAggregations; -import org.opensearch.search.internal.InternalSearchResponse; -import org.opensearch.search.profile.SearchProfileShardResults; -import org.opensearch.search.relevance.configuration.ConfigurationUtils; -import org.opensearch.search.relevance.client.OpenSearchClient; -import org.opensearch.search.relevance.configuration.ResultTransformerConfiguration; -import org.opensearch.search.relevance.transformer.ResultTransformer; -import org.opensearch.search.relevance.transformer.ResultTransformerType; -import org.opensearch.search.suggest.Suggest; -import org.opensearch.tasks.Task; - -public class SearchActionFilter implements ActionFilter { - private static final Logger logger = LogManager.getLogger(SearchActionFilter.class); - - private final int order; - - private final NamedWriteableRegistry namedWriteableRegistry; - private final Map supportedResultTransformers; - private final OpenSearchClient openSearchClient; - - public SearchActionFilter(Map supportedResultTransformers, OpenSearchClient openSearchClient) { - order = 10; // TODO: Finalize this value - namedWriteableRegistry = new NamedWriteableRegistry(Collections.emptyList()); - this.supportedResultTransformers = supportedResultTransformers; - this.openSearchClient = openSearchClient; - } - - @Override - public int order() { - return order; - } - - @Override - public void apply( - final Task task, - final String action, - final Request request, - final ActionListener listener, - final ActionFilterChain chain) { - - final long startTime = System.nanoTime(); - - if (!SearchAction.INSTANCE.name().equals(action)) { - chain.proceed(task, action, request, listener); - return; - } - - final SearchRequest searchRequest = (SearchRequest) request; - - final String[] indices = searchRequest.indices(); - // Skip if no, or more than 1, index is specified. - if (indices == null || indices.length != 1) { - chain.proceed(task, action, request, listener); - return; - } - - List resultTransformerConfigurations = getResultTransformerConfigurations(indices[0], searchRequest); - - LinkedHashMap orderedTransformersAndConfigs = new LinkedHashMap<>(); - for (ResultTransformerConfiguration config : resultTransformerConfigurations) { - ResultTransformer resultTransformer = supportedResultTransformers.get(config.getType()); - if (resultTransformer.shouldTransform(searchRequest, config)) { - orderedTransformersAndConfigs.put(resultTransformer, config); - } - } - - if (!orderedTransformersAndConfigs.isEmpty()) { - // Source is returned in response hits by default. If disabled by the user, overwrite and enable - // in order to access document contents for reranking, then suppress at response time. - boolean suppressSourceOnResponse = false; - if (searchRequest.source().fetchSource() != null && !searchRequest.source().fetchSource().fetchSource()) { - searchRequest.source().fetchSource(true); - suppressSourceOnResponse = true; - } - - final ActionListener searchResponseListener = createSearchResponseListener( - listener, startTime, searchRequest, orderedTransformersAndConfigs, suppressSourceOnResponse); - chain.proceed(task, action, request, searchResponseListener); - return; - } - - chain.proceed(task, action, request, listener); - } - - /** - * Parse and return a list of result transformers from request and index level configurations - * Request level configuration takes precedence over index level - * @param indexName name of the OpenSearch index - * @param searchRequest input request - * @return ordered and validated list of result transformers, empty list if not specified at - * either request or index level - */ - private List getResultTransformerConfigurations( - final String indexName, - final SearchRequest searchRequest) { - - List configs = new ArrayList<>(); - - // Request level configuration takes precedence over index level - configs = ConfigurationUtils.getResultTransformersFromRequestConfiguration(searchRequest); - if (!configs.isEmpty()) { - return configs; - } - - // Fetch all index settings for this plugin - String[] settingNames = supportedResultTransformers.values() - .stream() - .map(t -> t.getTransformerSettings() - .stream() - .map(s -> s.getKey()) - .collect(Collectors.toList())) - .flatMap(Collection::stream) - .collect(Collectors.toList()) - .toArray(new String[0]); - - configs = ConfigurationUtils.getResultTransformersFromIndexConfiguration( - openSearchClient.getIndexSettings(indexName, settingNames)); - - return configs; - } - - /** - * Create a Listener that, during the OpenSearch response chain, - * calls external service Kendra Ranking to rerank OpenSearch hits - * @param listener default listened - * @param startTime time when request was received, used to calculate latency added by reranking - * @param searchRequest input search request - * @param orderedTransformersAndConfigs transformers to apply, with their corresponding configurations - * @param suppressSourceOnResponse boolean indicating whether to suppress the document source on response - * @param OpenSearch response type - * @return ActionListener with override for onResponse method - */ - private ActionListener createSearchResponseListener( - final ActionListener listener, - final long startTime, - final SearchRequest searchRequest, - final LinkedHashMap orderedTransformersAndConfigs, - final boolean suppressSourceOnResponse) { - return new ActionListener() { - - @Override - public void onResponse(final Response response) { - final SearchResponse searchResponse = (SearchResponse) response; - final long totalHits = searchResponse.getHits().getTotalHits().value; - if (totalHits == 0) { - logger.info("TotalHits = 0. Returning search response without re-ranking."); - listener.onResponse(response); - return; - } - - logger.debug("Starting re-ranking for search response: {}", searchResponse); - try { - final BytesStreamOutput out = new BytesStreamOutput(); - searchResponse.writeTo(out); - - final StreamInput in = new NamedWriteableAwareStreamInput(out.bytes().streamInput(), - namedWriteableRegistry); - - SearchHits hits = new SearchHits(in); - for (Map.Entry entry : orderedTransformersAndConfigs.entrySet()) { - hits = entry.getKey().transform(hits, searchRequest, entry.getValue()); - } - - if (suppressSourceOnResponse) { - List hitsWithModifiedSource = Arrays.stream(hits.getHits()) - .map(hit -> hit.sourceRef(null)) - .collect(Collectors.toList()); - hits = new SearchHits( - hitsWithModifiedSource.toArray(new SearchHit[hitsWithModifiedSource.size()]), - hits.getTotalHits(), - hits.getMaxScore()); - } - - final InternalAggregations aggregations = - in.readBoolean() ? InternalAggregations.readFrom(in) : null; - final Suggest suggest = in.readBoolean() ? new Suggest(in) : null; - final boolean timedOut = in.readBoolean(); - final Boolean terminatedEarly = in.readOptionalBoolean(); - final SearchProfileShardResults profileResults = in.readOptionalWriteable( - SearchProfileShardResults::new); - final int numReducePhases = in.readVInt(); - - final SearchResponseSections internalResponse = new InternalSearchResponse(hits, - aggregations, suggest, - profileResults, timedOut, terminatedEarly, numReducePhases); - - final int totalShards = in.readVInt(); - final int successfulShards = in.readVInt(); - final int shardSearchFailureSize = in.readVInt(); - final ShardSearchFailure[] shardFailures; - if (shardSearchFailureSize == 0) { - shardFailures = ShardSearchFailure.EMPTY_ARRAY; - } else { - shardFailures = new ShardSearchFailure[shardSearchFailureSize]; - for (int i = 0; i < shardFailures.length; i++) { - shardFailures[i] = readShardSearchFailure(in); - } - } - - final SearchResponse.Clusters clusters = new SearchResponse.Clusters(in.readVInt(), - in.readVInt(), in.readVInt()); - final String scrollId = in.readOptionalString(); - final int skippedShards = in.readVInt(); - - final long tookInMillis = (System.nanoTime() - startTime) / 1000000; - final SearchResponse newResponse = new SearchResponse(internalResponse, scrollId, - totalShards, successfulShards, - skippedShards, tookInMillis, shardFailures, clusters); - listener.onResponse((Response) newResponse); - - // TODO: Change this to a metric - logger.info("Re-ranking overhead time: {}ms", - tookInMillis - searchResponse.getTook().getMillis()); - } catch (final Exception e) { - logger.error("Failed to parse search response.", e); - throw new OpenSearchException("Failed to parse a search response.", e); - } - } - - @Override - public void onFailure(final Exception e) { - listener.onFailure(e); - } - }; - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/client/OpenSearchClient.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/client/OpenSearchClient.java deleted file mode 100644 index 1b306dc..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/client/OpenSearchClient.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.search.relevance.client; - -import org.opensearch.action.admin.indices.settings.get.GetSettingsAction; -import org.opensearch.action.admin.indices.settings.get.GetSettingsRequest; -import org.opensearch.action.admin.indices.settings.get.GetSettingsResponse; -import org.opensearch.client.Client; -import org.opensearch.common.settings.Settings; - -public class OpenSearchClient { - private final Client client; - - public OpenSearchClient(Client client) { - this.client = client; - } - - public Settings getIndexSettings(String indexName, String[] settingNames) { - GetSettingsRequest getSettingsRequest = new GetSettingsRequest() - .indices(indexName); - if (settingNames != null && settingNames.length > 0) { - getSettingsRequest.names(settingNames); - } - GetSettingsResponse getSettingsResponse = client.execute(GetSettingsAction.INSTANCE, getSettingsRequest).actionGet(); - return getSettingsResponse.getIndexToSettings().get(indexName); - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/ConfigurationUtils.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/ConfigurationUtils.java deleted file mode 100644 index b57cde5..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/ConfigurationUtils.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.search.relevance.configuration; - -import static org.opensearch.search.relevance.configuration.Constants.RESULT_TRANSFORMER_SETTING_PREFIX; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.common.settings.Settings; -import org.opensearch.search.SearchExtBuilder; -import org.opensearch.search.relevance.transformer.ResultTransformerType; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankingConfiguration; - -public class ConfigurationUtils { - - /** - * Get result transformer configurations from Search Request - * @param settings all index settings configured for this plugin - * @return ordered and validated list of result transformers, empty list if not specified - */ - public static List getResultTransformersFromIndexConfiguration( - Settings settings) { - List indexLevelConfigs = new ArrayList<>(); - - if (settings != null) { - if (settings.getGroups(RESULT_TRANSFORMER_SETTING_PREFIX) != null) { - for (Map.Entry resultTransformer : settings.getGroups(RESULT_TRANSFORMER_SETTING_PREFIX).entrySet()) { - ResultTransformerType resultTransformerType = ResultTransformerType.fromString(resultTransformer.getKey()); - if (ResultTransformerType.KENDRA_INTELLIGENT_RANKING.equals(resultTransformerType)) { - indexLevelConfigs.add(new KendraIntelligentRankingConfiguration(resultTransformer.getValue())); - } - } - } - } - - return reorderAndValidateConfigs(indexLevelConfigs); - } - - /** - * Get result transformer configurations from Search Request - * @param searchRequest input request - * @return ordered and validated list of result transformers, empty list if not specified - */ - public static List getResultTransformersFromRequestConfiguration( - final SearchRequest searchRequest) { - - // Fetch result transformers specified in request - SearchConfigurationExtBuilder requestLevelSearchConfiguration = null; - if (searchRequest.source().ext() != null && !searchRequest.source().ext().isEmpty()) { - // Filter ext builders by name - List extBuilders = searchRequest.source().ext().stream() - .filter(searchExtBuilder -> SearchConfigurationExtBuilder.NAME.equals(searchExtBuilder.getWriteableName())) - .collect(Collectors.toList()); - if (!extBuilders.isEmpty()) { - requestLevelSearchConfiguration = (SearchConfigurationExtBuilder) extBuilders.get(0); - } - } - - List requestLevelConfigs = new ArrayList<>(); - if (requestLevelSearchConfiguration != null) { - requestLevelConfigs = reorderAndValidateConfigs(requestLevelSearchConfiguration.getResultTransformers()); - } - return requestLevelConfigs; - } - - /** - * Sort configurations in ascending order of invocation, and validate - * @param configs list of result transformer configurations - * @return ordered and validated list of result transformers - */ - public static List reorderAndValidateConfigs( - final List configs) throws IllegalArgumentException { - - // Sort - configs.sort(Comparator.comparingInt(ResultTransformerConfiguration::getOrder)); - - for (int i = 0; i < configs.size(); ++i) { - if (configs.get(i).getOrder() != (i + 1)) { - throw new IllegalArgumentException("Expected order [" + (i + 1) + "] for transformer [" + - configs.get(i).getType() + "], but found [" + configs.get(i).getOrder() + "]"); - } - } - - return configs; - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/Constants.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/Constants.java deleted file mode 100644 index f3913a8..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/Constants.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.search.relevance.configuration; - -import static org.opensearch.search.relevance.transformer.TransformerType.RESULT_TRANSFORMER; - -public class Constants { - public static final String PLUGIN_NAME = "searchrelevance"; - public static final String SEARCH_CONFIGURATION = "search_configuration"; - - public static final String PLUGIN_SETTING_PREFIX = - String.join(".", "index", "plugin", PLUGIN_NAME); - public static final String RESULT_TRANSFORMER_SETTING_PREFIX = - String.join(".", PLUGIN_SETTING_PREFIX, RESULT_TRANSFORMER.toString()); - - public static final String PROPERTIES = "properties"; - public static final String ORDER = "order"; -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/ResultTransformerConfiguration.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/ResultTransformerConfiguration.java deleted file mode 100644 index f63ec46..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/ResultTransformerConfiguration.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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.search.relevance.configuration; - -import org.opensearch.search.relevance.transformer.ResultTransformerType; - -public abstract class ResultTransformerConfiguration extends TransformerConfiguration { - - public abstract ResultTransformerType getType(); - -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/SearchConfigurationExtBuilder.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/SearchConfigurationExtBuilder.java deleted file mode 100644 index 64590a9..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/SearchConfigurationExtBuilder.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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.search.relevance.configuration; - -import static org.opensearch.search.relevance.configuration.Constants.SEARCH_CONFIGURATION; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.common.ParseField; -import org.opensearch.common.ParsingException; -import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.common.io.stream.StreamOutput; -import org.opensearch.common.xcontent.XContentBuilder; -import org.opensearch.common.xcontent.XContentParser; -import org.opensearch.search.SearchExtBuilder; -import org.opensearch.search.relevance.transformer.ResultTransformerType; -import org.opensearch.search.relevance.transformer.TransformerType; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankingConfiguration; - -public class SearchConfigurationExtBuilder extends SearchExtBuilder { - public static final String NAME = SEARCH_CONFIGURATION; - - private static final ParseField RESULT_TRANSFORMER = new ParseField(TransformerType.RESULT_TRANSFORMER.toString()); - private static final ParseField KENDRA_INTELLIGENT_RANKING = new ParseField(ResultTransformerType.KENDRA_INTELLIGENT_RANKING.toString()); - - private List resultTransformerConfigurations = new ArrayList<>(); - - public SearchConfigurationExtBuilder() {} - - public SearchConfigurationExtBuilder(StreamInput input) throws IOException { - ResultTransformerConfiguration cfg1 = input.readOptionalWriteable(KendraIntelligentRankingConfiguration::new); - resultTransformerConfigurations.add(cfg1); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - for (ResultTransformerConfiguration config : resultTransformerConfigurations) { - out.writeOptionalWriteable(config); - } - } - - @Override - public String getWriteableName() { - return NAME; - } - - public static SearchConfigurationExtBuilder parse(XContentParser parser) throws IOException { - SearchConfigurationExtBuilder extBuilder = new SearchConfigurationExtBuilder(); - XContentParser.Token token = parser.currentToken(); - String currentFieldName = null; - if (token != XContentParser.Token.START_OBJECT && (token = parser.nextToken()) != XContentParser.Token.START_OBJECT) { - throw new ParsingException( - parser.getTokenLocation(), - "Expected [" + XContentParser.Token.START_OBJECT + "] but found [" + token + "]", - parser.getTokenLocation() - ); - } - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - } else if (token == XContentParser.Token.START_OBJECT) { - if (RESULT_TRANSFORMER.match(currentFieldName, parser.getDeprecationHandler())) { - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - } - if (KENDRA_INTELLIGENT_RANKING.match(currentFieldName, - parser.getDeprecationHandler())) { - extBuilder.addResultTransformer( - KendraIntelligentRankingConfiguration.parse(parser, null)); - } else { - throw new IllegalArgumentException( - "Unrecognized Result Transformer type [" + currentFieldName + "]"); - } - } - } else { - throw new IllegalArgumentException("Unrecognized Transformer type [" + currentFieldName + "]"); - } - } else { - throw new ParsingException( - parser.getTokenLocation(), - "Unknown key for a " + token + " in [" + currentFieldName + "].", - parser.getTokenLocation() - ); - } - } - return extBuilder; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(NAME); - for (ResultTransformerConfiguration config : resultTransformerConfigurations) { - builder.field(config.getType().toString(), config); - } - return builder.endObject(); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (!(obj instanceof SearchConfigurationExtBuilder)) { - return false; - } - SearchConfigurationExtBuilder o = (SearchConfigurationExtBuilder) obj; - return (this.resultTransformerConfigurations.size() == o.resultTransformerConfigurations.size() && - this.resultTransformerConfigurations.containsAll(o.resultTransformerConfigurations) && - o.resultTransformerConfigurations.containsAll(this.resultTransformerConfigurations)) ; - } - - @Override - public int hashCode() { - return Objects.hash(this.getClass(), this.resultTransformerConfigurations); - } - - public void setResultTransformers(final List resultTransformerConfigurations) { - this.resultTransformerConfigurations = resultTransformerConfigurations; } - - public List getResultTransformers() { - return this.resultTransformerConfigurations; - } - - public void addResultTransformer(final ResultTransformerConfiguration resultTransformerConfiguration) { - this.resultTransformerConfigurations.add(resultTransformerConfiguration); - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/TransformerConfiguration.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/TransformerConfiguration.java deleted file mode 100644 index 625ab02..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/configuration/TransformerConfiguration.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.search.relevance.configuration; - -import static org.opensearch.search.relevance.configuration.Constants.ORDER; -import static org.opensearch.search.relevance.configuration.Constants.PROPERTIES; - -import org.opensearch.common.ParseField; -import org.opensearch.common.io.stream.Writeable; -import org.opensearch.common.xcontent.ToXContentObject; - -public abstract class TransformerConfiguration implements Writeable, ToXContentObject { - protected static final ParseField TRANSFORMER_ORDER = new ParseField(ORDER); - protected static final ParseField TRANSFORMER_PROPERTIES = new ParseField(PROPERTIES); - - protected int order; - - public int getOrder() { - return this.order; - } - - public void setOrder(final int order) { - this.order = order; - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/ResultTransformer.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/ResultTransformer.java deleted file mode 100644 index c5b77d8..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/ResultTransformer.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.search.relevance.transformer; - -import java.util.List; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.common.settings.Setting; -import org.opensearch.search.SearchHits; -import org.opensearch.search.relevance.configuration.ResultTransformerConfiguration; - -public interface ResultTransformer { - - /** - * Get the list of settings required / supported by the transformer - * @return list of transformer settings - */ - List> getTransformerSettings(); - - /** - * Decide whether to apply the transformer on the input request - * @param request input request - * @param configuration Configuration parameters for the transformer - * @return boolean decision on whether to apply the transformer - */ - boolean shouldTransform(final SearchRequest request, final ResultTransformerConfiguration configuration); - - /** - * Rank hits based on the provided query - * @param hits hits to be re-ranked - * @param request Search request - * @param configuration Configuration parameters for the transformer - * @return SearchHits ordered by score generated by ranker - */ - SearchHits transform(final SearchHits hits, final SearchRequest request, final ResultTransformerConfiguration configuration); -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/ResultTransformerType.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/ResultTransformerType.java deleted file mode 100644 index 81880fc..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/ResultTransformerType.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.search.relevance.transformer; - -public enum ResultTransformerType { - KENDRA_INTELLIGENT_RANKING("kendra_intelligent_ranking"); - - private final String type; - - ResultTransformerType(String type) { - this.type = type; - } - - @Override - public String toString() { - return type; - } - - public static ResultTransformerType fromString(String type) { - for (ResultTransformerType resultTransformerType : values()) { - if (resultTransformerType.type.equalsIgnoreCase(type)) { - return resultTransformerType; - } - } - throw new IllegalArgumentException("Unrecognized Result Transformer type [" + type + "]"); - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/TransformerType.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/TransformerType.java deleted file mode 100644 index aef6945..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/TransformerType.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.search.relevance.transformer; - -public enum TransformerType { - RESULT_TRANSFORMER("result_transformer"); - - private final String type; - - TransformerType(String type) { - this.type = type; - } - - @Override - public String toString() { - return type; - } - - public static TransformerType fromString(String type) { - for (TransformerType transformerType : values()) { - if (transformerType.type.equalsIgnoreCase(type)) { - return transformerType; - } - } - throw new IllegalArgumentException("Unrecognized Transformer type [" + type + "]"); - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/KendraIntelligentRanker.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/KendraIntelligentRanker.java deleted file mode 100644 index 7b20c43..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/KendraIntelligentRanker.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.PriorityQueue; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.action.search.SearchRequest; -import org.opensearch.common.settings.Setting; -import org.opensearch.search.SearchHit; -import org.opensearch.search.SearchHits; -import org.opensearch.search.relevance.configuration.ResultTransformerConfiguration; -import org.opensearch.search.relevance.transformer.ResultTransformer; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.client.KendraHttpClient; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankingConfiguration; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.model.PassageScore; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.model.dto.Document; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.model.dto.RescoreRequest; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.model.dto.RescoreResult; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.model.dto.RescoreResultItem; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.preprocess.BM25Scorer; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.preprocess.QueryParser; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.preprocess.QueryParser.QueryParserResult; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.preprocess.SlidingWindowTextSplitter; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.preprocess.TextTokenizer; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings; - -public class KendraIntelligentRanker implements ResultTransformer { - - private static final int PASSAGE_SIZE_LIMIT = 600; - private static final int SLIDING_WINDOW_STEP = PASSAGE_SIZE_LIMIT - 50; - private static final int MAXIMUM_PASSAGES = 10; - private static final double BM25_B_VALUE = 0.75; - private static final double BM25_K1_VALUE = 1.6; - private static final int TOP_K_PASSAGES = 3; - - private static final Logger logger = LogManager.getLogger(KendraIntelligentRanker.class); - - private final KendraHttpClient kendraClient; - private final TextTokenizer textTokenizer; - private final QueryParser queryParser; - - public KendraIntelligentRanker(KendraHttpClient kendraClient) { - this.kendraClient = kendraClient; - this.textTokenizer = new TextTokenizer(); - this.queryParser = new QueryParser(); - } - - @Override - public List> getTransformerSettings() { - return KendraIntelligentRankerSettings.getAllSettings(); - } - - /** - * Check if search request is eligible for rescore - * @param request Search Request - * @return boolean decision on whether to re-rank - */ - @Override - public boolean shouldTransform(SearchRequest request, ResultTransformerConfiguration configuration) { - if (request.source() == null) { - return false; - } - - // Skip if there is scroll, pagination, or sorting. - if (request.scroll() != null || request.source().from() > 0 || - (request.source().sorts() != null && !request.source().sorts().isEmpty())) { - return false; - } - return true; - } - - /** - * - * @param hits Search hits to rerank with respect to query - * @param request Search request - * @return SearchHits reranked search hits - */ - @Override - public SearchHits transform(final SearchHits hits, - final SearchRequest request, - final ResultTransformerConfiguration configuration) { - KendraIntelligentRankingConfiguration kendraConfig = (KendraIntelligentRankingConfiguration) configuration; - QueryParserResult queryParserResult = queryParser.parse( - request.source().query(), - kendraConfig.getProperties().getBodyFields(), - kendraConfig.getProperties().getTitleFields()); - if (queryParserResult == null) { - return hits; - } - try { - List originalHits = new ArrayList<>(); - for (SearchHit searchHit : hits.getHits()) { - Map docSourceMap = searchHit.getSourceAsMap(); - SlidingWindowTextSplitter textSplitter = new SlidingWindowTextSplitter(PASSAGE_SIZE_LIMIT, SLIDING_WINDOW_STEP, MAXIMUM_PASSAGES); - List splitPassages = textSplitter.split(docSourceMap.get(queryParserResult.getBodyFieldName()).toString()); - List> topPassages = getTopPassages(queryParserResult.getQueryText(), splitPassages); - List tokenizedTitle = null; - if (queryParserResult.getTitleFieldName() != null) { - tokenizedTitle = textTokenizer.tokenize(docSourceMap.get(queryParserResult.getTitleFieldName()).toString()); - } - for (int i = 0; i < topPassages.size(); i++) { - originalHits.add( - new Document(searchHit.getId() + "@" + (i + 1), searchHit.getId(), tokenizedTitle, topPassages.get(i), searchHit.getScore()) - ); - } - } - - final RescoreRequest rescoreRequest = new RescoreRequest(queryParserResult.getQueryText(), originalHits); - final RescoreResult rescoreResult = kendraClient.rescore(rescoreRequest); - Map idToSearchHitMap = new HashMap<>(); - for (SearchHit searchHit : hits.getHits()) { - idToSearchHitMap.put(searchHit.getId(), searchHit); - } - List newSearchHits = new ArrayList<>(); - float maxScore = 0; - for (RescoreResultItem rescoreResultItem : rescoreResult.getResultItems()) { - SearchHit searchHit = idToSearchHitMap.get(rescoreResultItem.getDocumentId()); - if (searchHit == null) { - logger.warn("Response from external service references hit id {}, which does not exist in original results. Skipping.", - rescoreResultItem.getDocumentId()); - continue; - } - searchHit.score(rescoreResultItem.getScore()); - maxScore = Math.max(maxScore, rescoreResultItem.getScore()); - newSearchHits.add(searchHit); - } - return new SearchHits(newSearchHits.toArray(new SearchHit[newSearchHits.size()]), hits.getTotalHits(), maxScore); - } catch (Exception ex) { - logger.error("Failed to re-rank. Returning original search results without re-ranking.", ex); - return hits; - } - } - - private List> getTopPassages(final String queryText, final List splitPassages) { - List query = textTokenizer.tokenize(queryText); - List> passages = textTokenizer.tokenize(splitPassages); - BM25Scorer bm25Scorer = new BM25Scorer(BM25_B_VALUE, BM25_K1_VALUE, passages); - PriorityQueue pq = new PriorityQueue<>(Comparator.comparingDouble(x -> x.getScore())); - - for (int i = 0; i < passages.size(); i++) { - double score = bm25Scorer.score(query, passages.get(i)); - pq.offer(new PassageScore(score, i)); - if (pq.size() > TOP_K_PASSAGES) { - // Maintain heap of top K passages - pq.poll(); - } - } - - List> topPassages = new ArrayList<>(); - while (!pq.isEmpty()) { - topPassages.add(passages.get(pq.poll().getIndex())); - } - Collections.reverse(topPassages); // reverse to order from highest to lowest score - return topPassages; - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraClientSettings.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraClientSettings.java deleted file mode 100644 index 44fb113..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraClientSettings.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.client; - -import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings.ACCESS_KEY_SETTING; -import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings.ASSUME_ROLE_ARN_SETTING; -import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings.EXECUTION_PLAN_ID_SETTING; -import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings.SECRET_KEY_SETTING; -import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings.SERVICE_ENDPOINT_SETTING; -import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings.SERVICE_REGION_SETTING; -import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.KendraIntelligentRankerSettings.SESSION_TOKEN_SETTING; - -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.auth.BasicSessionCredentials; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.common.settings.SecureString; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.settings.SettingsException; - -/** - * A container for settings used to create a Kendra client. - */ -public final class KendraClientSettings { - - private static final Logger logger = LogManager.getLogger(KendraClientSettings.class); - - /** - * Credentials to authenticate with Kendra. - */ - private final AWSCredentials credentials; - private final String serviceEndpoint; - private final String serviceRegion; - private final String executionPlanId; - private final String assumeRoleArn; - - protected KendraClientSettings(AWSCredentials credentials, String serviceEndpoint, String serviceRegion, String executionPlanId, String assumeRoleArn) { - this.credentials = credentials; - this.serviceEndpoint = serviceEndpoint; - this.serviceRegion = serviceRegion; - this.executionPlanId = executionPlanId; - this.assumeRoleArn = assumeRoleArn; - } - - public AWSCredentials getCredentials() { - return credentials; - } - - public String getExecutionPlanId() { - return executionPlanId; - } - - public String getServiceEndpoint() { - return serviceEndpoint; - } - - public String getServiceRegion() { - return serviceRegion; - } - - public String getAssumeRoleArn() { - return assumeRoleArn; - } - - static AWSCredentials loadCredentials(Settings settings) { - try (SecureString key = ACCESS_KEY_SETTING.get(settings); - SecureString secret = SECRET_KEY_SETTING.get(settings); - SecureString sessionToken = SESSION_TOKEN_SETTING.get(settings)) { - if (key.length() == 0 && secret.length() == 0) { - if (sessionToken.length() > 0) { - throw new SettingsException("Setting [{}] is set but [{}] and [{}] are not", - SESSION_TOKEN_SETTING.getKey(), ACCESS_KEY_SETTING.getKey(), SECRET_KEY_SETTING.getKey()); - } - - logger.debug("Using either environment variables, system properties or instance profile credentials"); - return null; - } else if (key.length() == 0 || secret.length() == 0) { - throw new SettingsException("One of settings [{}] and [{}] is not set.", - ACCESS_KEY_SETTING.getKey(), SECRET_KEY_SETTING.getKey()); - } else { - final AWSCredentials credentials; - if (sessionToken.length() == 0) { - logger.debug("Using basic key/secret credentials"); - credentials = new BasicAWSCredentials(key.toString(), secret.toString()); - } else { - logger.debug("Using basic session credentials"); - credentials = new BasicSessionCredentials(key.toString(), secret.toString(), sessionToken.toString()); - } - return credentials; - } - } - } - - /** - * Parse settings for a single client. - * @param settings a {@link Settings} instance from which to derive the endpoint settings - * @return KendraClientSettings comprising credentials and endpoint settings - */ - public static KendraClientSettings getClientSettings(Settings settings) { - final AWSCredentials credentials = loadCredentials(settings); - return new KendraClientSettings( - credentials, - SERVICE_ENDPOINT_SETTING.get(settings), - SERVICE_REGION_SETTING.get(settings), - EXECUTION_PLAN_ID_SETTING.get(settings), - ASSUME_ROLE_ARN_SETTING.get(settings) - ); - } - -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraHttpClient.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraHttpClient.java deleted file mode 100644 index d30fb27..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/KendraHttpClient.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.client; - -import com.amazonaws.AmazonServiceException; -import com.amazonaws.ClientConfiguration; -import com.amazonaws.DefaultRequest; -import com.amazonaws.Request; -import com.amazonaws.Response; -import com.amazonaws.auth.AWS4Signer; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.auth.STSAssumeRoleSessionCredentialsProvider; -import com.amazonaws.http.AmazonHttpClient; -import com.amazonaws.http.ExecutionContext; -import com.amazonaws.http.HttpMethodName; -import com.amazonaws.http.HttpResponseHandler; -import com.amazonaws.services.securitytoken.AWSSecurityTokenService; -import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.io.ByteArrayInputStream; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.security.AccessController; -import java.security.PrivilegedAction; - -import org.opensearch.search.relevance.transformer.kendraintelligentranking.model.dto.RescoreRequest; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.model.dto.RescoreResult; - -public class KendraHttpClient { - private static final String KENDRA_RANKING_SERVICE_NAME = "kendra-ranking"; - private static final String KENDRA_RESCORE_URI = "rescore"; - private static final String KENDRA_RESCORE_EXECUTION_PLANS = "rescore-execution-plans"; - private static final String ASSUME_ROLE_SESSION_NAME = "OpenSearchKendraIntelligentRankingPluginSession"; - - private final AmazonHttpClient amazonHttpClient; - private final HttpResponseHandler errorHandler; - private final HttpResponseHandler responseHandler; - private final AWSCredentialsProvider awsCredentialsProvider; - private final AWS4Signer aws4Signer; - private final String serviceEndpoint; - private final String executionPlanId; - private final ObjectMapper objectMapper; - - public KendraHttpClient(KendraClientSettings clientSettings) { - amazonHttpClient = AccessController.doPrivileged((PrivilegedAction) () -> new AmazonHttpClient(new ClientConfiguration())); - errorHandler = new SimpleAwsErrorHandler(); - responseHandler = new SimpleResponseHandler(); - aws4Signer = new AWS4Signer(); - aws4Signer.setServiceName(KENDRA_RANKING_SERVICE_NAME); - aws4Signer.setRegionName(clientSettings.getServiceRegion()); - objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - serviceEndpoint = clientSettings.getServiceEndpoint(); - executionPlanId = clientSettings.getExecutionPlanId(); - - final AWSCredentialsProvider credentialsProvider; - final AWSCredentials credentials = clientSettings.getCredentials(); - if (credentials == null) { - // Use environment variables, system properties or instance profile credentials. - credentialsProvider = DefaultAWSCredentialsProviderChain.getInstance(); - } else { - // Use keystore credentials. - credentialsProvider = new AWSStaticCredentialsProvider(credentials); - } - - final String assumeRoleArn = clientSettings.getAssumeRoleArn(); - if (assumeRoleArn != null && !assumeRoleArn.isBlank()) { - // If AssumeRoleArn was provided in config, use auto-refreshed role credentials. - awsCredentialsProvider = AccessController.doPrivileged( - (PrivilegedAction) () -> { - AWSSecurityTokenService awsSecurityTokenService = AWSSecurityTokenServiceClientBuilder.standard() - .withCredentials(credentialsProvider) - .withRegion(clientSettings.getServiceRegion()) - .build(); - - return new STSAssumeRoleSessionCredentialsProvider.Builder(clientSettings.getAssumeRoleArn(), ASSUME_ROLE_SESSION_NAME) - .withStsClient(awsSecurityTokenService) - .build(); - }); - } else { - awsCredentialsProvider = credentialsProvider; - } - } - - public RescoreResult rescore(RescoreRequest rescoreRequest) { - return AccessController.doPrivileged((PrivilegedAction) () -> { - try { - Request request = new DefaultRequest<>(aws4Signer.getServiceName()); - request.setHttpMethod(HttpMethodName.POST); - request.setEndpoint(buildRescoreURI()); - request.setContent(new ByteArrayInputStream(objectMapper.writeValueAsString(rescoreRequest).getBytes(StandardCharsets.UTF_8))); - aws4Signer.sign(request, awsCredentialsProvider.getCredentials()); - - Response rsp = amazonHttpClient - .requestExecutionBuilder() - .executionContext(new ExecutionContext(true)) - .request(request) - .errorResponseHandler(errorHandler) - .execute(responseHandler); - - return objectMapper.readValue(rsp.getAwsResponse(), RescoreResult.class); - } catch (Exception ex) { - throw new RuntimeException("Exception executing request.", ex); - } - }); - } - - public URI buildRescoreURI() { - return URI.create(String.join("/", - serviceEndpoint, KENDRA_RESCORE_EXECUTION_PLANS, executionPlanId, KENDRA_RESCORE_URI)); - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleAwsErrorHandler.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleAwsErrorHandler.java deleted file mode 100644 index 5293357..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleAwsErrorHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.client; - -import com.amazonaws.AmazonServiceException; -import com.amazonaws.http.HttpResponse; -import com.amazonaws.http.HttpResponseHandler; - -import java.nio.charset.StandardCharsets; - -public class SimpleAwsErrorHandler implements HttpResponseHandler { - @Override public AmazonServiceException handle(HttpResponse response) throws Exception { - AmazonServiceException ase = new AmazonServiceException(new String(response.getContent().readAllBytes(), StandardCharsets.UTF_8)); - ase.setStatusCode(response.getStatusCode()); - ase.setServiceName(response.getRequest().getServiceName()); - ase.setErrorCode(response.getStatusText()); - return ase; - } - - @Override public boolean needsConnectionLeftOpen() { - return false; - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleResponseHandler.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleResponseHandler.java deleted file mode 100644 index 6646a8c..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/client/SimpleResponseHandler.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.client; - -import com.amazonaws.http.HttpResponse; -import com.amazonaws.http.HttpResponseHandler; - -import java.nio.charset.StandardCharsets; - -public class SimpleResponseHandler implements HttpResponseHandler { - @Override public String handle(HttpResponse response) throws Exception { - return new String(response.getContent().readAllBytes(), StandardCharsets.UTF_8); - } - - @Override public boolean needsConnectionLeftOpen() { - return false; - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/Constants.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/Constants.java deleted file mode 100644 index 61e07cd..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/Constants.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.configuration; - -import static org.opensearch.search.relevance.configuration.Constants.ORDER; -import static org.opensearch.search.relevance.configuration.Constants.PROPERTIES; -import static org.opensearch.search.relevance.configuration.Constants.RESULT_TRANSFORMER_SETTING_PREFIX; - -public class Constants { - // Transformer name - public static final String KENDRA_INTELLIGENT_RANKING = "kendra_intelligent_ranking"; - // Transformer properties - public static final String BODY_FIELD = "body_field"; - public static final String TITLE_FIELD = "title_field"; - - public static final String KENDRA_SETTINGS_PREFIX = - String.join(".", RESULT_TRANSFORMER_SETTING_PREFIX, KENDRA_INTELLIGENT_RANKING); - - public static final String ORDER_SETTING_NAME = - String.join(".", KENDRA_SETTINGS_PREFIX, ORDER); - public static final String BODY_FIELD_SETTING_NAME = - String.join(".", KENDRA_SETTINGS_PREFIX, PROPERTIES, BODY_FIELD); - public static final String TITLE_FIELD_SETTING_NAME = - String.join(".", KENDRA_SETTINGS_PREFIX, PROPERTIES, TITLE_FIELD); - -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankerSettings.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankerSettings.java deleted file mode 100644 index 756b344..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankerSettings.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.configuration; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.function.Function; -import org.opensearch.common.settings.SecureSetting; -import org.opensearch.common.settings.SecureString; -import org.opensearch.common.settings.Setting; -import org.opensearch.common.settings.Setting.Property; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.Constants; - -public class KendraIntelligentRankerSettings { - - /** - * Flag controlling whether to invoke Kendra for rescoring. - */ - public static final Setting KENDRA_ORDER_SETTING = Setting.intSetting(Constants.ORDER_SETTING_NAME, 1, 1, - Property.Dynamic, Property.IndexScope); - - /** - * Document field to be considered as "body" when invoking Kendra. - */ - public static final Setting> KENDRA_BODY_FIELD_SETTING = Setting.listSetting(Constants.BODY_FIELD_SETTING_NAME, Collections.emptyList(), - Function.identity(), new FieldSettingValidator(Constants.BODY_FIELD_SETTING_NAME), - Property.Dynamic, Property.IndexScope); - - /** - * Document field to be considered as "title" when invoking Kendra. - */ - public static final Setting> KENDRA_TITLE_FIELD_SETTING = Setting.listSetting(Constants.TITLE_FIELD_SETTING_NAME, Collections.emptyList(), - Function.identity(), new FieldSettingValidator(Constants.TITLE_FIELD_SETTING_NAME), - Property.Dynamic, Property.IndexScope); - - static final class FieldSettingValidator implements Setting.Validator> { - - private String settingName; - - public FieldSettingValidator(final String name) { - this.settingName = name; - } - - @Override - public void validate(List value) { - if (value != null && value.size() > 1) { - throw new IllegalArgumentException("[" + this.settingName + "] can have at most 1 element"); - } - } - } - - /** - * The access key (ie login id) for connecting to Kendra. - */ - public static final Setting ACCESS_KEY_SETTING = SecureSetting.secureString("kendra_intelligent_ranking.aws.access_key", null); - - /** - * The secret key (ie password) for connecting to Kendra. - */ - public static final Setting SECRET_KEY_SETTING = SecureSetting.secureString("kendra_intelligent_ranking.aws.secret_key", null); - - /** - * The session token for connecting to Kendra. - */ - public static final Setting SESSION_TOKEN_SETTING = SecureSetting.secureString("kendra_intelligent_ranking.aws.session_token", null); - - public static final Setting SERVICE_ENDPOINT_SETTING = Setting.simpleString("kendra_intelligent_ranking.service.endpoint", Setting.Property.NodeScope); - - public static final Setting SERVICE_REGION_SETTING = Setting.simpleString("kendra_intelligent_ranking.service.region", Setting.Property.NodeScope); - - public static final Setting EXECUTION_PLAN_ID_SETTING = Setting.simpleString("kendra_intelligent_ranking.service.execution_plan_id", Setting.Property.NodeScope); - - public static final Setting ASSUME_ROLE_ARN_SETTING = Setting.simpleString("kendra_intelligent_ranking.service.assume_role_arn", Setting.Property.NodeScope); - - public static final List> getAllSettings() { - return Arrays.asList( - KENDRA_ORDER_SETTING, - KENDRA_BODY_FIELD_SETTING, - KENDRA_TITLE_FIELD_SETTING, - ACCESS_KEY_SETTING, - SECRET_KEY_SETTING, - SESSION_TOKEN_SETTING, - SERVICE_ENDPOINT_SETTING, - SERVICE_REGION_SETTING, - EXECUTION_PLAN_ID_SETTING, - ASSUME_ROLE_ARN_SETTING - ); - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankingConfiguration.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankingConfiguration.java deleted file mode 100644 index e9dddc4..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/configuration/KendraIntelligentRankingConfiguration.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.configuration; - -import static org.opensearch.search.relevance.configuration.Constants.ORDER; - -import java.io.IOException; -import java.util.List; -import java.util.Objects; -import org.opensearch.common.ParseField; -import org.opensearch.common.ParsingException; -import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.common.io.stream.StreamOutput; -import org.opensearch.common.io.stream.Writeable; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.xcontent.ObjectParser; -import org.opensearch.common.xcontent.ToXContentObject; -import org.opensearch.common.xcontent.XContentBuilder; -import org.opensearch.common.xcontent.XContentParser; -import org.opensearch.search.relevance.configuration.ResultTransformerConfiguration; -import org.opensearch.search.relevance.configuration.TransformerConfiguration; -import org.opensearch.search.relevance.transformer.ResultTransformerType; - -public class KendraIntelligentRankingConfiguration extends ResultTransformerConfiguration { - private static final ObjectParser PARSER; - - static { - PARSER = new ObjectParser("kendra_intelligent_ranking_configuration", KendraIntelligentRankingConfiguration::new); - PARSER.declareInt(TransformerConfiguration::setOrder, TRANSFORMER_ORDER); - PARSER.declareObject(KendraIntelligentRankingConfiguration::setProperties, - (p, c) -> KendraIntelligentRankingProperties.parse(p, c), - TRANSFORMER_PROPERTIES); - } - - private KendraIntelligentRankingProperties properties; - - public KendraIntelligentRankingConfiguration() {} - - public KendraIntelligentRankingConfiguration(final int order, final KendraIntelligentRankingProperties properties) { - this.order = order; - this.properties = properties; - } - - public KendraIntelligentRankingConfiguration(StreamInput input) throws IOException { - this.order = input.readInt(); - this.properties = new KendraIntelligentRankingProperties(input); - } - - public KendraIntelligentRankingConfiguration(Settings settings) { - this.order = settings.getAsInt(ORDER, 0); - this.properties = new KendraIntelligentRankingProperties( - settings.getAsList("properties.body_field"), - settings.getAsList("properties.title_field")); - } - - @Override - public ResultTransformerType getType() { - return ResultTransformerType.KENDRA_INTELLIGENT_RANKING; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeInt(this.order); - this.properties.writeTo(out); - } - - public static ResultTransformerConfiguration parse(XContentParser parser, Void context) throws IOException { - try { - KendraIntelligentRankingConfiguration configuration = PARSER.parse(parser, null); - if (configuration != null && configuration.getOrder() <= 0) { - throw new ParsingException(parser.getTokenLocation(), - "Failed to parse value [" + configuration.getOrder() + "] for Transformer order, must be >= 1"); - } - return configuration; - } catch (IllegalArgumentException iae) { - throw new ParsingException(parser.getTokenLocation(), iae.getMessage(), iae); - } - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field(TRANSFORMER_ORDER.getPreferredName(), this.order); - builder.field(TRANSFORMER_PROPERTIES.getPreferredName(), this.properties); - return builder.endObject(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - KendraIntelligentRankingConfiguration config = (KendraIntelligentRankingConfiguration) o; - - if (order != config.order) return false; - return properties.equals(config.properties); - } - - @Override - public int hashCode() { - return Objects.hash(order, properties); - } - - public void setProperties(KendraIntelligentRankingProperties properties) { - this.properties = properties; - } - - public KendraIntelligentRankingProperties getProperties() { - return this.properties; - } - - public static class KendraIntelligentRankingProperties implements Writeable, ToXContentObject { - protected static final ParseField BODY_FIELD = new ParseField(Constants.BODY_FIELD); - protected static final ParseField TITLE_FIELD = new ParseField(Constants.TITLE_FIELD); - - private static final ObjectParser PARSER; - - static { - PARSER = new ObjectParser<>("kendra_intelligent_ranking_configuration", KendraIntelligentRankingProperties::new); - PARSER.declareStringArray(KendraIntelligentRankingProperties::setBodyFields, BODY_FIELD); - PARSER.declareStringArray(KendraIntelligentRankingProperties::setTitleFields, TITLE_FIELD); - } - - private List bodyFields; - private List titleFields; - - public KendraIntelligentRankingProperties() {} - - public KendraIntelligentRankingProperties(final List bodyFields, final List titleFields) { - this.bodyFields = bodyFields; - this.titleFields = titleFields; - } - - public KendraIntelligentRankingProperties(StreamInput input) throws IOException { - this.bodyFields = input.readStringList(); - this.bodyFields = input.readStringList(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeStringCollection(this.bodyFields); - out.writeStringCollection(this.titleFields); - } - - public static KendraIntelligentRankingProperties parse(XContentParser parser, Void context) throws IOException { - try { - KendraIntelligentRankingProperties properties = PARSER.parse(parser, null); - if (properties != null) { - if (properties.getBodyFields() != null && !properties.getBodyFields().isEmpty() && properties.getBodyFields().size() > 1) { - throw new ParsingException(parser.getTokenLocation(), - "[" + BODY_FIELD + "] can have at most 1 element"); - } - if (properties.getTitleFields() != null && !properties.getTitleFields().isEmpty() && properties.getTitleFields().size() > 1) { - throw new ParsingException(parser.getTokenLocation(), - "[" + TITLE_FIELD + "] can have at most 1 element"); - } - } - return properties; - } catch (IllegalArgumentException iae) { - throw new ParsingException(parser.getTokenLocation(), iae.getMessage(), iae); - } - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field(BODY_FIELD.getPreferredName(), this.bodyFields); - builder.field(TITLE_FIELD.getPreferredName(), this.titleFields); - return builder.endObject(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - KendraIntelligentRankingProperties properties = (KendraIntelligentRankingProperties) o; - - if (bodyFields != properties.bodyFields) return false; - return (titleFields == properties.titleFields); - } - - @Override - public int hashCode() { - return Objects.hash(bodyFields, titleFields); - } - - public List getBodyFields() { - return this.bodyFields; - } - - public void setBodyFields(final List bodyFields) { - this.bodyFields = bodyFields; - } - - public List getTitleFields() { - return this.titleFields; - } - - public void setTitleFields(final List titleFields) { - this.titleFields = titleFields; - } - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/PassageScore.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/PassageScore.java deleted file mode 100644 index a806853..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/PassageScore.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.model; - -public class PassageScore { - private double score; - private int index; - - public PassageScore(double score, int index) { - this.score = score; - this.index = index; - } - - public double getScore() { - return score; - } - - public int getIndex() { - return index; - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/Document.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/Document.java deleted file mode 100644 index 3328b7a..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/Document.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.model.dto; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import java.util.List; - -@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) -public class Document { - - private String id; - private String groupId; - private List tokenizedTitle; - private List tokenizedBody; - private Float originalScore; - - public Document(String id, String groupId, List tokenizedTitle, List tokenizedBody, - Float originalScore) { - this.id = id; - this.groupId = groupId; - this.tokenizedTitle = tokenizedTitle; - this.tokenizedBody = tokenizedBody; - this.originalScore = originalScore; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getGroupId() { - return groupId; - } - - public void setGroupId(String groupId) { - this.groupId = groupId; - } - - public List getTokenizedTitle() { - return tokenizedTitle; - } - - public void setTokenizedTitle(List tokenizedTitle) { - this.tokenizedTitle = tokenizedTitle; - } - - public List getTokenizedBody() { - return tokenizedBody; - } - - public void setTokenizedBody(List tokenizedBody) { - this.tokenizedBody = tokenizedBody; - } - - public Float getOriginalScore() { - return originalScore; - } - - public void setOriginalScore(Float originalScore) { - this.originalScore = originalScore; - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreRequest.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreRequest.java deleted file mode 100644 index 8a43c2b..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.model.dto; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import java.util.List; - -@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) -public class RescoreRequest { - private String searchQuery; - private List documents; - - public RescoreRequest(String searchQuery, List documents) { - this.searchQuery = searchQuery; - this.documents = documents; - } - - public String getSearchQuery() { - return searchQuery; - } - - public void setSearchQuery(String searchQuery) { - this.searchQuery = searchQuery; - } - - public List getDocuments() { - return documents; - } - - public void setDocuments(List documents) { - this.documents = documents; - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResult.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResult.java deleted file mode 100644 index 890ccc2..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResult.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.model.dto; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import java.util.List; - -@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) -public class RescoreResult { - private String rescoreId; - private List resultItems; - - public String getRescoreId() { - return rescoreId; - } - - public List getResultItems() { - return resultItems; - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResultItem.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResultItem.java deleted file mode 100644 index 8d815c9..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/model/dto/RescoreResultItem.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.model.dto; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) -public class RescoreResultItem { - private String documentId; - private Float score; - - public String getDocumentId() { - return documentId; - } - - public Float getScore() { - return score; - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25Scorer.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25Scorer.java deleted file mode 100644 index 4601c2e..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25Scorer.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.preprocess; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class BM25Scorer { - private Map wordToDocumentCount = new HashMap<>(); - private double b; - private double k1; - private int totalNumberOfDocs; - private double averageDocumentLength; // avdl - - /** - * Initialize dataset. - * - * @param b free parameter for BM25 - * @param k1 free parameter for BM25 - * @param documents list of documents, each document is represented by a list of words - */ - public BM25Scorer(double b, double k1, List> documents) { - this.b = b; - this.k1 = k1; - this.totalNumberOfDocs = documents.size(); - - double totalDocumentLength = 0; - for (List document : documents) { - totalDocumentLength += document.size(); - - Set uniqueWordsInDocument = new HashSet<>(document); // add to set to remove duplicates - for (String term : uniqueWordsInDocument) { - wordToDocumentCount.put(term, wordToDocumentCount.getOrDefault(term, 0) + 1); - } - } - this.averageDocumentLength = totalDocumentLength / documents.size(); - } - - /** - * Calculate the BM25 score of a document given a query. - * - * @param query query represented as a list of words - * @param document document represented as a list of words - * @return the BM25 score - */ - public double score(List query, List document) { - double score = 0; - - Map documentWordCounts = new HashMap<>(); - for (String word : document) { - documentWordCounts.put(word, documentWordCounts.getOrDefault(word, 0) + 1); - } - - for (String queryWord : query) { - if (!documentWordCounts.containsKey(queryWord)) { - continue; - } - double termFrequency = (double) documentWordCounts.get(queryWord) / document.size(); - double denominator = termFrequency + k1 * (1 - b + b * document.size() / averageDocumentLength); - double idf = idf(queryWord); - double numerator = idf * termFrequency * (k1 + 1); - score += numerator / denominator; - } - return score; - } - - /** - * Calculate the idf (inverse document frequency) of a word in the dataset. - * - * @param word word to calculate idf on - * @return idf value - */ - private double idf(String word) { - return totalNumberOfDocs > 0 ? Math.log10((double) totalNumberOfDocs / wordToDocumentCount.get(word)) : 0; - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParser.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParser.java deleted file mode 100644 index 49dfb7c..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParser.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.preprocess; - -import static org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.Constants.BODY_FIELD; - -import java.util.List; -import java.util.Locale; -import java.util.regex.Pattern; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.index.query.MatchQueryBuilder; -import org.opensearch.index.query.MultiMatchQueryBuilder; -import org.opensearch.index.query.QueryBuilder; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.configuration.Constants; - -public class QueryParser { - private static final Logger logger = LogManager.getLogger(QueryParser.class); - - private static final String FILED_MISMATCH_ERROR_MESSAGE = - "Mismatch: Field configured in %s property [%s] is not present in query fields [%s]. Will not apply Kendra Intelligent Ranking."; - - public QueryParserResult parse( - final QueryBuilder query, - final List bodyFieldSetting, - final List titleFieldSetting) { - final String bodyFieldFromSetting = bodyFieldSetting.isEmpty() ? null : bodyFieldSetting.get(0); - final String titleFieldFromSetting = titleFieldSetting.isEmpty() ? null : titleFieldSetting.get(0); - QueryParserResult result = null; - - if (query instanceof MatchQueryBuilder) { - result = parseMatchQuery((MatchQueryBuilder) query, bodyFieldFromSetting); - } else if (query instanceof MultiMatchQueryBuilder) { - result = parseMultiMatchQuery((MultiMatchQueryBuilder) query, bodyFieldFromSetting, titleFieldFromSetting); - } else { - logger.warn(Constants.KENDRA_INTELLIGENT_RANKING + " does not support query type [" + - query.queryName() + "]. Will not apply Kendra Intelligent Ranking."); - } - return result; - } - - private QueryParserResult parseMatchQuery(MatchQueryBuilder matchQuery, String bodyFieldFromSetting) { - QueryParserResult result = null; - if (!bodyFieldFromSetting.equals(matchQuery.fieldName())) { - logger.warn(String.format(Locale.ENGLISH, FILED_MISMATCH_ERROR_MESSAGE, BODY_FIELD, - bodyFieldFromSetting, matchQuery.fieldName())); - } else { - result = new QueryParserResult(matchQuery.value().toString(), bodyFieldFromSetting); - } - return result; - } - - private QueryParserResult parseMultiMatchQuery(MultiMatchQueryBuilder multiMatchQuery, - String bodyFieldFromSetting, String titleFieldFromSetting) { - - QueryParserResult result = null; - boolean configuredBodyFieldPresentInQuery = false; - boolean configuredTitleFieldPresentInQuery = false; - for (String field : multiMatchQuery.fields().keySet()) { - Pattern fieldNameRegex = Pattern.compile(createRegexFromGlob(field)); - if (fieldNameRegex.matcher(bodyFieldFromSetting).matches()) { - configuredBodyFieldPresentInQuery = true; - } - if (titleFieldFromSetting != null && fieldNameRegex.matcher(titleFieldFromSetting).matches()) { - configuredTitleFieldPresentInQuery = true; - } - } - if (!configuredBodyFieldPresentInQuery) { - logger.warn(String.format(Locale.ENGLISH, FILED_MISMATCH_ERROR_MESSAGE, BODY_FIELD, - bodyFieldFromSetting, multiMatchQuery.fields().keySet())); - } else { - final String titleFieldToUse = configuredTitleFieldPresentInQuery ? titleFieldFromSetting : null; - result = new QueryParserResult(multiMatchQuery.value().toString(), bodyFieldFromSetting, titleFieldToUse); - } - return result; - } - - private static String createRegexFromGlob(String glob) { - StringBuilder out = new StringBuilder("^"); - for(int i = 0; i < glob.length(); ++i) { - final char c = glob.charAt(i); - switch(c) { - case '*': out.append(".*"); break; - case '?': out.append('.'); break; - case '.': out.append("\\."); break; - case '\\': out.append("\\\\"); break; - default: out.append(c); - } - } - out.append('$'); - return out.toString(); - } - - public class QueryParserResult { - private String queryText; - private String bodyFieldName; - private String titleFieldName; - - public QueryParserResult(String queryText, String bodyFieldName) { - this.queryText = queryText; - this.bodyFieldName = bodyFieldName; - this.titleFieldName = null; - } - - public QueryParserResult(String queryText, String bodyFieldName, String titleFieldName) { - this.queryText = queryText; - this.bodyFieldName = bodyFieldName; - this.titleFieldName = titleFieldName; - } - - public String getQueryText() { - return queryText; - } - - public String getBodyFieldName() { - return bodyFieldName; - } - - public String getTitleFieldName() { - return titleFieldName; - } - } -} diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitter.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitter.java deleted file mode 100644 index 6ff8455..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitter.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.preprocess; - -import com.ibm.icu.text.BreakIterator; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import org.apache.commons.lang3.tuple.Pair; - -/** - * Applies a sliding window to split input text into passages. However, splitting is aware of - * sentence boundaries and hence the size of splits may be larger than the configured window size. - */ -public class SlidingWindowTextSplitter { - - /** - * Minimum size of window of text to be selected in a split. Window size can be larger - * in practice because of respecting sentence boundaries. - */ - private int windowSize; - - /** - * The minimum step size by which to move the sliding window after a split. Step size can be larger - * in practice because of respecting sentence boundaries. - */ - private int stepSize; - - /** - * The maximum number of passages to be extracted from the input text - */ - private int maximumPassages; - - //local parameters to control sentence/token iteration - private Pair lastSentenceBoundary; - private boolean useTokenIterator; - - public SlidingWindowTextSplitter(int windowSize, int stepSize, int maximumPassages) { - setSlidingWindow(windowSize, stepSize); - this.maximumPassages = maximumPassages; - this.useTokenIterator = Boolean.FALSE; - this.lastSentenceBoundary = null; - } - - /** - * Set parameters of sliding window text splitter - * @param updatedWindowSize text window size - * @param updatedStepSize step size to move the window - */ - public void setSlidingWindow(int updatedWindowSize, int updatedStepSize) { - if (updatedStepSize > updatedWindowSize) { - throw new IllegalArgumentException("Step size " + updatedStepSize + " is larger than window size " + updatedWindowSize); - } - - this.windowSize = updatedWindowSize; - this.stepSize = updatedStepSize; - } - - /** - * Obtains a list of split passages, aware of sentence boundaries, from the input text. - * @param text Text to be split - * @return List of passages extracted from input text - */ - public List split(String text) { - if (text.isEmpty()) { - return new ArrayList<>(); - } - - if (text.length() <= windowSize) { - return Arrays.asList(text); - } - - List splitText = new LinkedList<>(); - - BreakIterator sentenceIterator = BreakIterator.getSentenceInstance(Locale.ENGLISH); - BreakIterator tokenIterator = BreakIterator.getLineInstance(Locale.ENGLISH); - sentenceIterator.setText(text); - - int passageCounter = 0; - int startBoundaryIndex = 0; - int endBoundaryIndex = 0; - - int nextStartBoundaryIndex = 0; - boolean nextStartBoundaryIndexUpdated = false; - - while ((passageCounter < this.maximumPassages) && (endBoundaryIndex != BreakIterator.DONE)) { - // If current passage length is already larger than the step size, - // use the end index as the start index for next window - if (!nextStartBoundaryIndexUpdated && (endBoundaryIndex - startBoundaryIndex + 1) >= stepSize) { - nextStartBoundaryIndexUpdated = true; - nextStartBoundaryIndex = endBoundaryIndex; - } - - if (endBoundaryIndex == text.length() ) { - // End of the input text. Add the passage, irrespective of its length. - splitText.add(text.substring(startBoundaryIndex, endBoundaryIndex)); - ++passageCounter; - } else if ((endBoundaryIndex - startBoundaryIndex + 1) >= windowSize && endBoundaryIndex > nextStartBoundaryIndex ) { - // If current passage length is greater than both step and window size, extend the window - // such that all passages have some overlap - splitText.add(text.substring(startBoundaryIndex, endBoundaryIndex)); - ++passageCounter; - - startBoundaryIndex = nextStartBoundaryIndex; - - // Check whether current end boundary index can be used as next start boundary index - if ((endBoundaryIndex - startBoundaryIndex + 1) >= stepSize) { - nextStartBoundaryIndex = endBoundaryIndex; - nextStartBoundaryIndexUpdated = true; - } else { - nextStartBoundaryIndexUpdated = false; - } - } - - endBoundaryIndex = getNextEndBoundaryIndex(sentenceIterator, tokenIterator, endBoundaryIndex, text); - } - - return splitText; - } - - /** - * Get the end boundary index of the next split. In common cases, move the iterator by a sentence. - * If a sentence is too long, move the iterator by tokens rather than sentences. - * @param sentenceIterator iterator respecting sentence boundaries - * @param tokenIterator iterator respecting tokens - * @param previousEndBoundaryIndex end boundary index of previous split - * @param text input text to split - */ - private int getNextEndBoundaryIndex(BreakIterator sentenceIterator, BreakIterator tokenIterator, int previousEndBoundaryIndex, String text) { - if (!useTokenIterator) { - int nextSentenceBoundary = sentenceIterator.next(); - int sentenceLength = nextSentenceBoundary - previousEndBoundaryIndex; - if (nextSentenceBoundary == BreakIterator.DONE || sentenceLength < windowSize) { - return nextSentenceBoundary; - } else { - // Sentence is too long, use tokenIterator - lastSentenceBoundary = Pair.of(previousEndBoundaryIndex, nextSentenceBoundary); - useTokenIterator = Boolean.TRUE; - tokenIterator.setText(text.substring(previousEndBoundaryIndex, nextSentenceBoundary)); - } - } - - // Always add the offset of previous sentence - int nextTokenBoundary = lastSentenceBoundary.getLeft() + tokenIterator.next(); - if (nextTokenBoundary == lastSentenceBoundary.getRight()) { - // Finished token iterations, unset booleans to start from next sentence. - useTokenIterator = Boolean.FALSE; - lastSentenceBoundary = null; - } - return nextTokenBoundary; - } -} \ No newline at end of file diff --git a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizer.java b/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizer.java deleted file mode 100644 index daf89f3..0000000 --- a/semantic-reranker/src/main/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizer.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.preprocess; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -public class TextTokenizer { - private static final Set STOP_WORDS = new HashSet<>( - Arrays.asList("i", "me", "my", "myself", "we", "our", "ours", "ourselves", "you", "your", "yours", "yourself", "yourselves", "he", "him", "his", - "himself", "she", "her", "hers", "herself", "it", "its", "itself", "they", "them", "their", "theirs", "themselves", "what", "which", "who", - "whom", "this", "that", "these", "those", "am", "is", "are", "was", "were", "be", "been", "being", "have", "has", "had", "having", "do", - "does", "did", "doing", "a", "an", "the", "and", "but", "if", "or", "because", "as", "until", "while", "of", "at", "by", "for", "with", - "about", "against", "between", "into", "through", "during", "before", "after", "above", "below", "to", "from", "up", "down", "in", "out", - "on", "off", "over", "under", "again", "further", "then", "once", "here", "there", "when", "where", "why", "how", "all", "any", "both", - "each", "few", "more", "most", "other", "some", "such", "no", "nor", "not", "only", "own", "same", "so", "than", "too", "very", "s", "t", - "can", "will", "just", "don", "should", "now")); - private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\p{Punct}\\s]+"); - - public List> tokenize(List texts) { - return texts.stream() - .map(text -> tokenize(text)) - .collect(Collectors.toList()); - } - - public List tokenize(String text) { - String[] tokens = text.split(SPLIT_PATTERN.pattern()); - List validTokens = new ArrayList<>(); - for (String token : tokens) { - if (token.length() == 0) { - continue; - } - String lowerCased = token.toLowerCase(Locale.ENGLISH); - if (!STOP_WORDS.contains(lowerCased)) { - validTokens.add(lowerCased); - } - } - return validTokens; - } -} diff --git a/semantic-reranker/src/main/plugin-metadata/plugin-security.policy b/semantic-reranker/src/main/plugin-metadata/plugin-security.policy deleted file mode 100644 index b16dfe0..0000000 --- a/semantic-reranker/src/main/plugin-metadata/plugin-security.policy +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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. - */ - -grant { - permission java.lang.RuntimePermission "accessDeclaredMembers"; - permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; - - permission java.net.SocketPermission "*", "connect,resolve"; - permission java.lang.RuntimePermission "getClassLoader"; -}; diff --git a/semantic-reranker/src/test/java/org/opensearch/search/relevance/SearchRelevancePluginIT.java b/semantic-reranker/src/test/java/org/opensearch/search/relevance/SearchRelevancePluginIT.java deleted file mode 100644 index 9f01dfa..0000000 --- a/semantic-reranker/src/test/java/org/opensearch/search/relevance/SearchRelevancePluginIT.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.search.relevance; - -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import org.apache.http.util.EntityUtils; -import org.opensearch.client.Request; -import org.opensearch.client.Response; -import org.opensearch.plugins.Plugin; -import org.opensearch.test.OpenSearchIntegTestCase; - -import java.io.IOException; -import java.util.Collection; -import java.util.Collections; - -import static org.hamcrest.Matchers.containsString; - -@ThreadLeakScope(ThreadLeakScope.Scope.NONE) -@OpenSearchIntegTestCase.ClusterScope(scope = OpenSearchIntegTestCase.Scope.SUITE) -public class SearchRelevancePluginIT extends OpenSearchIntegTestCase { - - @Override - protected Collection> nodePlugins() { - return Collections.singletonList(SearchRelevancePlugin.class); - } - - public void testPluginInstalled() throws IOException { - Response response = createRestClient().performRequest(new Request("GET", "/_cat/plugins")); - String body = EntityUtils.toString(response.getEntity()); - - logger.info("response body: {}", body); - assertThat(body, containsString("search-relevance")); - } -} diff --git a/semantic-reranker/src/test/java/org/opensearch/search/relevance/SearchRelevanceTests.java b/semantic-reranker/src/test/java/org/opensearch/search/relevance/SearchRelevanceTests.java deleted file mode 100644 index e65b820..0000000 --- a/semantic-reranker/src/test/java/org/opensearch/search/relevance/SearchRelevanceTests.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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.search.relevance; - -import org.opensearch.test.OpenSearchTestCase; - -public class SearchRelevanceTests extends OpenSearchTestCase { - // Add unit tests for your plugin -} diff --git a/semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25ScorerTests.java b/semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25ScorerTests.java deleted file mode 100644 index 4436cb5..0000000 --- a/semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/BM25ScorerTests.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.preprocess; - -import java.util.Arrays; -import java.util.List; - -import org.junit.Assert; -import org.opensearch.test.OpenSearchTestCase; - -public class BM25ScorerTests extends OpenSearchTestCase { - - public void testBM25Scorer() { - TextTokenizer textTokenizer = new TextTokenizer(); - List document1 = textTokenizer.tokenize( - "OpenSearch is a distributed, community-driven, Apache 2.0-licensed, 100% open-source search and analytics suite used for a broad set of use cases like real-time application monitoring, log analytics, and website search. OpenSearch provides a highly scalable system for providing fast access and response to large volumes of data with an integrated visualization tool, OpenSearch Dashboards, that makes it easy for users to explore their data. OpenSearch is powered by the Apache Lucene search library, and it supports a number of search and analytics capabilities such as k-nearest neighbors (KNN) search, SQL, Anomaly Detection, Machine Learning Commons, Trace Analytics, full-text search, and more." - ); - List document2 = textTokenizer.tokenize( - "OpenSearch enables you to easily ingest, secure, search, aggregate, view, and analyze data for a number of use cases such as log analytics, application search, enterprise search, and more. With OpenSearch, you benefit from having a 100% open source product you can use, modify, extend, monetize, and resell however you want. There are a growing number of OpenSearch Project partners that offer a variety of services such as professional support, enhanced features, and managed OpenSearch services. The OpenSearch Project continues to provide a secure, high-quality search and analytics suite with a rich roadmap of new and innovative functionality." - ); - List document3 = textTokenizer.tokenize( - "The sky is blue" - ); - - BM25Scorer bm25Scorer = new BM25Scorer(0.75, 1.6, Arrays.asList(document1, document2, document3)); - - List query1 = textTokenizer.tokenize("Apache Lucene search library"); - double doc1Score1 = bm25Scorer.score(query1, document1); - double doc2Score1 = bm25Scorer.score(query1, document2); - double doc3Score1 = bm25Scorer.score(query1, document3); - Assert.assertTrue(doc1Score1 > doc2Score1); - Assert.assertTrue(doc2Score1 > doc3Score1); - Assert.assertEquals(0, doc3Score1, 0); - - List query2 = textTokenizer.tokenize("sky color"); - double doc1Score2 = bm25Scorer.score(query2, document1); - double doc2Score2 = bm25Scorer.score(query2, document2); - double doc3Score2 = bm25Scorer.score(query2, document3); - Assert.assertTrue(doc3Score2 > doc1Score2); - Assert.assertEquals(0, doc1Score2, 0); - Assert.assertEquals(0, doc2Score2, 0); - } -} diff --git a/semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParserTests.java b/semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParserTests.java deleted file mode 100644 index af27553..0000000 --- a/semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/QueryParserTests.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.preprocess; - -import java.util.Arrays; -import java.util.List; -import org.junit.Assert; -import org.opensearch.index.query.MatchQueryBuilder; -import org.opensearch.index.query.MultiMatchQueryBuilder; -import org.opensearch.index.query.TermQueryBuilder; -import org.opensearch.search.relevance.transformer.kendraintelligentranking.preprocess.QueryParser.QueryParserResult; -import org.opensearch.test.OpenSearchTestCase; - -public class QueryParserTests extends OpenSearchTestCase { - - private static final String TEST_BODY_FIELD = "bodyField"; - private static final String TEST_TITLE_FIELD = "titleField"; - private static final String TEST_QUERY_TEXT = "queryText"; - private static final String INVALID_FIELD = "invalidField"; - private QueryParser queryParser = new QueryParser(); - - public void testParse_Match_RequestFieldMatchesSetting() { - QueryParser.QueryParserResult queryParserResult = queryParser.parse( - new MatchQueryBuilder(TEST_BODY_FIELD, TEST_QUERY_TEXT), - Arrays.asList(TEST_BODY_FIELD), - Arrays.asList(TEST_TITLE_FIELD)); - - performQueryParserResultAssertions(queryParserResult, TEST_QUERY_TEXT, TEST_BODY_FIELD, null); - } - - public void testParse_Match_RequestFieldDoesNotMatchSetting() { - QueryParser.QueryParserResult queryParserResult = queryParser.parse( - new MatchQueryBuilder(TEST_BODY_FIELD, TEST_QUERY_TEXT), - Arrays.asList(INVALID_FIELD), - Arrays.asList(TEST_TITLE_FIELD)); - - Assert.assertNull(queryParserResult); - } - - public void testParse_MultiMatch_RequestFieldMatchesSetting() { - QueryParser.QueryParserResult queryParserResult = queryParser.parse( - new MultiMatchQueryBuilder(TEST_QUERY_TEXT, TEST_TITLE_FIELD, TEST_BODY_FIELD), - Arrays.asList(TEST_BODY_FIELD), - Arrays.asList(TEST_TITLE_FIELD)); - - performQueryParserResultAssertions(queryParserResult, TEST_QUERY_TEXT, TEST_BODY_FIELD, TEST_TITLE_FIELD); - } - - public void testParse_MultiMatch_OnlyBodyFieldMatchesSetting() { - QueryParser.QueryParserResult queryParserResult = queryParser.parse( - new MultiMatchQueryBuilder(TEST_QUERY_TEXT, TEST_BODY_FIELD, INVALID_FIELD), - Arrays.asList(TEST_BODY_FIELD), - Arrays.asList(TEST_TITLE_FIELD)); - - performQueryParserResultAssertions(queryParserResult, TEST_QUERY_TEXT, TEST_BODY_FIELD, null); - } - - public void testParse_MultiMatch_RequestFieldMatchesSetting_WithFieldBoosting() { - MultiMatchQueryBuilder multiMatchQueryBuilder = new MultiMatchQueryBuilder(TEST_QUERY_TEXT, TEST_TITLE_FIELD, TEST_BODY_FIELD); - multiMatchQueryBuilder.field(TEST_BODY_FIELD, 1.5f); - multiMatchQueryBuilder.field(TEST_TITLE_FIELD, 3f); - QueryParser.QueryParserResult queryParserResult = queryParser.parse( - multiMatchQueryBuilder, - Arrays.asList(TEST_BODY_FIELD), - Arrays.asList(TEST_TITLE_FIELD)); - - performQueryParserResultAssertions(queryParserResult, TEST_QUERY_TEXT, TEST_BODY_FIELD, TEST_TITLE_FIELD); - } - - public void testParse_MultiMatch_RequestFieldMatchesSetting_WithWildcards() { - final List wildcardsIncludingTitleAndBody = Arrays.asList("*Field", "*Field*", "^*ld$"); - final List wildcardsIncludingBodyOnly = Arrays.asList("body*", "*dy*", "^bo*$"); - - for (String fieldWildcard : wildcardsIncludingTitleAndBody) { - QueryParser.QueryParserResult queryParserResult = queryParser.parse( - new MultiMatchQueryBuilder(TEST_QUERY_TEXT, fieldWildcard), - Arrays.asList(TEST_BODY_FIELD), - Arrays.asList(TEST_TITLE_FIELD)); - - performQueryParserResultAssertions(queryParserResult, TEST_QUERY_TEXT, TEST_BODY_FIELD, TEST_TITLE_FIELD); - } - - for (String fieldWildcard : wildcardsIncludingBodyOnly) { - QueryParser.QueryParserResult queryParserResult1 = queryParser.parse( - new MultiMatchQueryBuilder(TEST_QUERY_TEXT, fieldWildcard), - Arrays.asList(TEST_BODY_FIELD), - Arrays.asList(TEST_TITLE_FIELD)); - - performQueryParserResultAssertions(queryParserResult1, TEST_QUERY_TEXT, TEST_BODY_FIELD, null); - - QueryParser.QueryParserResult queryParserResult2 = queryParser.parse( - new MultiMatchQueryBuilder(TEST_QUERY_TEXT, fieldWildcard, TEST_TITLE_FIELD), - Arrays.asList(TEST_BODY_FIELD), - Arrays.asList(TEST_TITLE_FIELD)); - - performQueryParserResultAssertions(queryParserResult2, TEST_QUERY_TEXT, TEST_BODY_FIELD, TEST_TITLE_FIELD); - } - } - - public void testParse_MultiMatch_RequestFieldDoesNotMatchSetting() { - final List fieldConfigurationsExcludingBody = - Arrays.asList(TEST_TITLE_FIELD, "*title*", "^title*$", "title*^3"); - - for (String fieldConfig : fieldConfigurationsExcludingBody) { - QueryParser.QueryParserResult queryParserResult = queryParser.parse( - new MultiMatchQueryBuilder(TEST_QUERY_TEXT, fieldConfig), - Arrays.asList(TEST_BODY_FIELD), - Arrays.asList(TEST_TITLE_FIELD)); - - Assert.assertNull(queryParserResult); - } - } - - public void testParse_UnsupportedQueryType() { - QueryParser.QueryParserResult queryParserResult = queryParser.parse( - new TermQueryBuilder(TEST_BODY_FIELD, TEST_QUERY_TEXT), - Arrays.asList(TEST_BODY_FIELD), - Arrays.asList(TEST_TITLE_FIELD)); - Assert.assertNull(queryParserResult); - } - - private void performQueryParserResultAssertions(final QueryParserResult queryParserResult, - final String query, final String bodyField, final String titleField) { - Assert.assertEquals(query, queryParserResult.getQueryText()); - Assert.assertEquals(bodyField, queryParserResult.getBodyFieldName()); - Assert.assertEquals(titleField, queryParserResult.getTitleFieldName()); - } -} diff --git a/semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitterTests.java b/semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitterTests.java deleted file mode 100644 index cf630de..0000000 --- a/semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/SlidingWindowTextSplitterTests.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.preprocess; - -import static org.opensearch.common.io.PathUtils.get; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Scanner; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import org.opensearch.test.OpenSearchTestCase; - -public class SlidingWindowTextSplitterTests extends OpenSearchTestCase { - private static final int MAXIMUM_PASSAGES = 10; - private static final String TEST_FILE_PATH = "splitter/input.txt"; - - public void testConstructWithInvalidInputs() { - // Step size cannot be larger than window size - assertThrows(IllegalArgumentException.class, () -> new SlidingWindowTextSplitter(3, 4, MAXIMUM_PASSAGES)); - } - - public void testSplitWithEmptyInput() { - SlidingWindowTextSplitter splitter = new SlidingWindowTextSplitter(10, 4, MAXIMUM_PASSAGES); - assertEquals(Collections.emptyList(), splitter.split("")); - } - - public void testSetSlidingWindowWithInvalidStepSize() { - SlidingWindowTextSplitter splitter = new SlidingWindowTextSplitter(10, 4, MAXIMUM_PASSAGES); - assertThrows(IllegalArgumentException.class, () -> splitter.setSlidingWindow(2, 4)); - } - - public void testSetSlidingWindowUpdatesSplitter() { - SlidingWindowTextSplitter splitter = new SlidingWindowTextSplitter(30, 10, MAXIMUM_PASSAGES); - final String testInput = generateTestInput(1, 5, false); - List splitText = splitter.split(testInput); - - assertEquals(4, splitText.size()); - assertEquals(Arrays.asList( - generateTestInput(1, 2, true), - generateTestInput(2, 3, true), - generateTestInput(3, 4, true), - generateTestInput(4, 5, false)), - splitText); - - // double window size, expect each passage to be loonger - splitter.setSlidingWindow(60, 10); - splitText = splitter.split(testInput); - - assertEquals(2, splitText.size()); - assertEquals(Arrays.asList( - generateTestInput(1, 4, true), - generateTestInput(2, 5, false)), - splitText); - - // double - splitter.setSlidingWindow(60, 30); - splitText = splitter.split(testInput); - - assertEquals(2, splitText.size()); - assertEquals(Arrays.asList( - generateTestInput(1, 4, true), - generateTestInput(3, 5, false)), - splitText); - } - - public void testSplitObeysMaximumPassagesLimit() { - final int maximumPassages = 3; - final String inputText = generateTestInput(1, 5, false); - - SlidingWindowTextSplitter splitter = new SlidingWindowTextSplitter(30, 10, maximumPassages); - List splitText = splitter.split(inputText); - - assertEquals(maximumPassages, splitText.size()); - assertEquals(Arrays.asList( - generateTestInput(1, 2, true), - generateTestInput(2, 3, true), - generateTestInput(3, 4, true)), - splitText); - } - - public void testSplitWhenLastSplitIsShorterThanWindow() { - final String shortText = "Short text"; - final String inputText = String.join(" ", generateTestInput(1, 5, false), shortText); - final String expectedFinalSplit = String.join(" ", "This is a test 5.", shortText); - - SlidingWindowTextSplitter splitter = new SlidingWindowTextSplitter(30, 10, MAXIMUM_PASSAGES); - List splitText = splitter.split(inputText); - - assertEquals(5, splitText.size()); - assertEquals(Arrays.asList( - generateTestInput(1, 2, true), - generateTestInput(2, 3, true), - generateTestInput(3, 4, true), - generateTestInput(4, 5, true), - expectedFinalSplit), - splitText); - } - - public void testSplitWithOverlap() throws IOException { - final int windowSize = 1500; - final int stepSize = 1300; - // Because of respecting sentence boundaries, actual overlap might be smaller. - // Provide a buffer of 20 characters - final int expectedOverlap = windowSize - stepSize - 20; - - SlidingWindowTextSplitter splitter = new SlidingWindowTextSplitter(windowSize, stepSize, MAXIMUM_PASSAGES); - final String input = loadTestInputFromFile(); - - List splitText = splitter.split(input); - - // Overlap means that we get more splits - assertTrue(splitText.size() >= input.length() / windowSize); - - // Verify overlap for every pair of splits - for(int i = 0; i < splitText.size() - 1; ++i) { - assertTrue(splitText.get(i).contains(splitText.get(i + 1).substring(0, expectedOverlap))); - } - } - - public void testSplitUsesLineBreakWhenSentenceIsTooLong() throws IOException { - final int windowSize = 1500; - final int stepSize = 1300; - - SlidingWindowTextSplitter splitter = new SlidingWindowTextSplitter(windowSize, stepSize, MAXIMUM_PASSAGES); - final String input = loadTestInputFromFile(); - String inputWithoutSentenceBoundaries = input.replace(".", " "); - - List splitText = splitter.split(inputWithoutSentenceBoundaries); - - // Ensure that text is split - assertTrue(splitText.size() > 1); - } - - private String generateTestInput(int start, int end, boolean addTerminalSpace) { - final String testInput = IntStream.range(start, end + 1).boxed().map( - i -> String.format(Locale.ENGLISH, "This is a test %s.", i) - ).collect(Collectors.joining(" ")); - return addTerminalSpace ? testInput + " " : testInput; - } - - private String loadTestInputFromFile() throws IOException { - final Scanner scanner = new Scanner(SlidingWindowTextSplitterTests.class.getClassLoader() - .getResourceAsStream(TEST_FILE_PATH), - StandardCharsets.UTF_8.name()).useDelimiter("\\A"); - return scanner.hasNext() ? scanner.next() : ""; - } -} diff --git a/semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizerTests.java b/semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizerTests.java deleted file mode 100644 index e8c0a21..0000000 --- a/semantic-reranker/src/test/java/org/opensearch/search/relevance/transformer/kendraintelligentranking/preprocess/TextTokenizerTests.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.search.relevance.transformer.kendraintelligentranking.preprocess; - -import java.util.Arrays; -import java.util.List; - -import org.junit.Assert; -import org.opensearch.test.OpenSearchTestCase; - -public class TextTokenizerTests extends OpenSearchTestCase { - - public static final String TEXT_1 = "What is the capital of the United States?"; - public static final List EXPECTED_1 = Arrays.asList("capital", "united", "states"); - public static final String TEXT_2 = "OPENSEARCH IS OPEN SOURCE SEARCH AND ANALYTICS SUITE."; - public static final List EXPECTED_2 = Arrays.asList("opensearch", "open", "source", "search", "analytics", "suite"); - private TextTokenizer textTokenizer = new TextTokenizer(); - - public void testTokenize1() { - List actual = textTokenizer.tokenize(TEXT_1); - Assert.assertEquals(EXPECTED_1, actual); - } - - public void testTokenize2() { - List actual = textTokenizer.tokenize(TEXT_2); - Assert.assertEquals(EXPECTED_2, actual); - } - - public void testTokenizeMultiple() { - List> actual = textTokenizer.tokenize(Arrays.asList(TEXT_1, TEXT_2)); - Assert.assertEquals(Arrays.asList(EXPECTED_1, EXPECTED_2), actual); - } -} diff --git a/semantic-reranker/src/test/resources/splitter/input.txt b/semantic-reranker/src/test/resources/splitter/input.txt deleted file mode 100644 index 025aadd..0000000 --- a/semantic-reranker/src/test/resources/splitter/input.txt +++ /dev/null @@ -1,15 +0,0 @@ -The history of natural language processing (NLP) generally started in the 1950s, although work can be found from earlier periods. In 1950, Alan Turing published an article titled "Computing Machinery and Intelligence" which proposed what is now called the Turing test as a criterion of intelligence[clarification needed]. - -The Georgetown experiment in 1954 involved fully automatic translation of more than sixty Russian sentences into English. The authors claimed that within three or five years, machine translation would be a solved problem.[2] However, real progress was much slower, and after the ALPAC report in 1966, which found that ten-year-long research had failed to fulfill the expectations, funding for machine translation was dramatically reduced. Little further research in machine translation was conducted until the late 1980s, when the first statistical machine translation systems were developed. - -Some notably successful natural language processing systems developed in the 1960s were SHRDLU, a natural language system working in restricted "blocks worlds" with restricted vocabularies, and ELIZA, a simulation of a Rogerian psychotherapist, written by Joseph Weizenbaum between 1964 and 1966. Using almost no information about human thought or emotion, ELIZA sometimes provided a startlingly human-like interaction. When the "patient" exceeded the very small knowledge base, ELIZA might provide a generic response, for example, responding to "My head hurts" with "Why do you say your head hurts?". - -During the 1970s, many programmers began to write "conceptual ontologies", which structured real-world information into computer-understandable data. Examples are MARGIE (Schank, 1975), SAM (Cullingford, 1978), PAM (Wilensky, 1978), TaleSpin (Meehan, 1976), QUALM (Lehnert, 1977), Politics (Carbonell, 1979), and Plot Units (Lehnert 1981). During this time, many chatterbots were written including PARRY, Racter, and Jabberwacky. - -Up to the 1980s, most natural language processing systems were based on complex sets of hand-written rules. Starting in the late 1980s, however, there was a revolution in natural language processing with the introduction of machine learning algorithms for language processing. This was due to both the steady increase in computational power (see Moore's law) and the gradual lessening of the dominance of Chomskyan theories of linguistics (e.g. transformational grammar), whose theoretical underpinnings discouraged the sort of corpus linguistics that underlies the machine-learning approach to language processing.[3] Some of the earliest-used machine learning algorithms, such as decision trees, produced systems of hard if-then rules similar to existing hand-written rules. However, part-of-speech tagging introduced the use of hidden Markov models to natural language processing, and increasingly, research has focused on statistical models, which make soft, probabilistic decisions based on attaching real-valued weights to the features making up the input data. The cache language models upon which many speech recognition systems now rely are examples of such statistical models. Such models are generally more robust when given unfamiliar input, especially input that contains errors (as is very common for real-world data), and produce more reliable results when integrated into a larger system comprising multiple subtasks. - -Many of the notable early successes occurred in the field of machine translation, due especially to work at IBM Research, where successively more complicated statistical models were developed. These systems were able to take advantage of existing multilingual textual corpora that had been produced by the Parliament of Canada and the European Union as a result of laws calling for the translation of all governmental proceedings into all official languages of the corresponding systems of government. However, most other systems depended on corpora specifically developed for the tasks implemented by these systems, which was (and often continues to be) a major limitation in the success of these systems. As a result, a great deal of research has gone into methods of more effectively learning from limited amounts of data. - -Recent research has increasingly focused on unsupervised and semi-supervised learning algorithms. Such algorithms are able to learn from data that has not been hand-annotated with the desired answers, or using a combination of annotated and non-annotated data. Generally, this task is much more difficult than supervised learning, and typically produces less accurate results for a given amount of input data. However, there is an enormous amount of non-annotated data available (including, among other things, the entire content of the World Wide Web), which can often make up for the inferior results if the algorithm used has a low enough time complexity to be practical. - -In the 2010s, representation learning and deep neural network-style machine learning methods became widespread in natural language processing, due in part to a flurry of results showing that such techniques[4][5] can achieve state-of-the-art results in many natural language tasks, for example in language modeling,[6] parsing,[7][8] and many others. Popular techniques include the use of word embeddings to capture semantic properties of words, and an increase in end-to-end learning of a higher-level task (e.g., question answering) instead of relying on a pipeline of separate intermediate tasks (e.g., part-of-speech tagging and dependency parsing). In some areas, this shift has entailed substantial changes in how NLP systems are designed, such that deep neural network-based approaches may be viewed as a new paradigm distinct from statistical natural language processing. For instance, the term neural machine translation (NMT) emphasizes the fact that deep learning-based approaches to machine translation directly learn sequence-to-sequence transformations, obviating the need for intermediate steps such as word alignment and language modeling that were used in statistical machine translation (SMT). \ No newline at end of file diff --git a/semantic-reranker/src/yamlRestTest/java/org/opensearch/search/relevance/SearchRelevanceClientYamlTestSuiteIT.java b/semantic-reranker/src/yamlRestTest/java/org/opensearch/search/relevance/SearchRelevanceClientYamlTestSuiteIT.java deleted file mode 100644 index 1f0d7dd..0000000 --- a/semantic-reranker/src/yamlRestTest/java/org/opensearch/search/relevance/SearchRelevanceClientYamlTestSuiteIT.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.search.relevance; - -import com.carrotsearch.randomizedtesting.annotations.Name; -import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; -import org.opensearch.test.rest.yaml.ClientYamlTestCandidate; -import org.opensearch.test.rest.yaml.OpenSearchClientYamlSuiteTestCase; - - -public class SearchRelevanceClientYamlTestSuiteIT extends OpenSearchClientYamlSuiteTestCase { - - public SearchRelevanceClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { - super(testCandidate); - } - - @ParametersFactory - public static Iterable parameters() throws Exception { - return OpenSearchClientYamlSuiteTestCase.createParameters(); - } -} diff --git a/semantic-reranker/src/yamlRestTest/resources/rest-api-spec/test/10_basic.yml b/semantic-reranker/src/yamlRestTest/resources/rest-api-spec/test/10_basic.yml deleted file mode 100644 index 4e1f41e..0000000 --- a/semantic-reranker/src/yamlRestTest/resources/rest-api-spec/test/10_basic.yml +++ /dev/null @@ -1,8 +0,0 @@ -"Test that the plugin is loaded in OpenSearch": - - do: - cat.plugins: - local: true - h: component - - - match: - $body: /^search-relevance\n$/ From 311ca6bce7f7465c371030562a77e13e84734a4f Mon Sep 17 00:00:00 2001 From: Mark Cohen Date: Sun, 6 Nov 2022 12:22:40 -0500 Subject: [PATCH 04/13] added msfroh, ps48, yang-db, and macohen to maintainers Signed-off-by: Mark Cohen --- MAINTAINERS.md | 4 ++++ README.md | 63 +------------------------------------------------- 2 files changed, 5 insertions(+), 62 deletions(-) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 39b921e..157fbf6 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -32,6 +32,10 @@ This document explains who the maintainers are (see below), what they do in this | Maintainer | GitHub ID | Affiliation | |-------------------------|-----------------------------------------| ----------- | | Anirudha Jadhav | [anirudha](https://github.com/anirudha) | Amazon | +| Michael Froh | [msfroh](https://github.com/msfroh) | Amazon | +| Mark Cohen | [macohen](https://github.com/macohen) | Amazon | +| Lior Perry | [yang-db](https://github.com/yang-db) | Amazon | +| Shenoy Pratik | [ps48](https://github.com/ps48) | Amazon | ## Maintainer Responsibilities diff --git a/README.md b/README.md index 57429ed..accd773 100644 --- a/README.md +++ b/README.md @@ -1,62 +1 @@ -# Search Relevance - -## Overview - -In a search engine, the relevance is the measure of the relationship accuracy between the search query and the search result. Higher the relevance is, the higher is the quality of search result and the users are able to get more relevant content. This project aims to add plugins to OpenSearch to help users make their query results more accurate, contextual and relevant. - -## Relevancy and OpenSearch - -Today, OpenSearch provides results in the order of scores generated by algorithms matching the indexed document contents to the input query. The documents that are more relevant to the query are ranked higher than the ones that are less relevant. These rankings may make sense to one set of users/applications and for others it may be very irrelevant. For example, relevancy for an E-commerce company can mean more similar products in the same category of the search query. While for a document search relevancy may mean, searching the query across different topics/categories present in the document store. This is why, we need more ways to customize the results and its rankings as per the need of the user/business. - -## Relevancy Engineering - -Relevancy as a problem, can’t just be solved at the search layer. Improving relevancy should be envisioned holistically from understanding the ingested data and usage signals to extracting feature, adding re-writers and improving algorithms. Below is the architecture of OpenSearch Relevancy Engineering. - -

- -

-[Initially presented at Haystack 2022 by @anirudha , @JohannesDaniel and @ps48]. -
-
-Overall the Relevancy Engineering can be divided into two tiers: - -1. **Ingestion Tier:** This tier handles getting the data from different sources to OpenSearch. This data may include: - 1. Search Data: - 1. Core search data, that needs to be queried on by OpenSearch - 2. Ingestion connectors to fetch the data from different data sources and sink in OpenSearch indices. - 2. Search Management Data: - 1. Adding rules and judgements to the rewriter indices. - 3. Observability Data: - 1. Adding customer usage signals to OpenSearch, these signals may include granular details like anonymized customer queries, clicks, orders and session details. -2. **Search & Relevancy Platform Tier:** This tier is responsible for analytics, re-wrtiers, model improvements and adding search configurations. - 1. Search Analytics & Discovery: - 1. Dashboards for analytics, metrics for search tests, search UIs and query profiling. - 2. Querqy based query Rewriting: - 1. Rewriters to customize queries with synonyms, word-breaks, spell corrections, query relaxation. - 3. Search Back Office: - 1. Manage business rules, ontologies and manual judgments. - 4. Relevancy workbench: - 1. Improve algorithms with automated testing, relevance model trainings, personalizations and custom re-rankers. -### [RFC Search Relevance](https://github.com/opensearch-project/search-relevance/issues/1) - -## Contributing - -See [developer guide](https://github.com/opensearch-project/opensearch-plugins/blob/main/BUILDING.md#developing-new-plugins-for-opensearch) and [how to contribute to this project](CONTRIBUTING.md). - -## Getting Help - -If you find a bug, or have a feature request, please don't hesitate to open an issue in this repository. - -For more information, see [project website](https://opensearch.org/) and [documentation](https://opensearch.org/docs). If you need help and are unsure where to open an issue, try [forums](https://forum.opensearch.org/). - -## Security - -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public GitHub issue. - -## License - -This project is licensed under the [Apache v2.0 License](LICENSE). - -## Copyright - -Copyright OpenSearch Contributors. See [NOTICE](NOTICE) for details. +# Search Request \ No newline at end of file From 9daf278705a92708188c08641b24c239f3d3c2b0 Mon Sep 17 00:00:00 2001 From: Mark Cohen Date: Sun, 6 Nov 2022 12:32:40 -0500 Subject: [PATCH 05/13] commented qa workflow build; force added gradle-wrapper.jar after gitignore ignored it Signed-off-by: Mark Cohen --- .../backwards_compatibility_tests_workflow.yml | 12 ++++-------- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59821 bytes 2 files changed, 4 insertions(+), 8 deletions(-) create mode 100644 gradle/wrapper/gradle-wrapper.jar diff --git a/.github/workflows/backwards_compatibility_tests_workflow.yml b/.github/workflows/backwards_compatibility_tests_workflow.yml index eac70c8..ac4f40b 100644 --- a/.github/workflows/backwards_compatibility_tests_workflow.yml +++ b/.github/workflows/backwards_compatibility_tests_workflow.yml @@ -31,15 +31,11 @@ jobs: with: java-version: ${{ matrix.java }} -# - name: Install dependencies +# Comments intentional; these workflow scripts are copied from k-NN and we are reverse engineering them; missing qa directory fails the workflow here +# - name: Run SRP Restart-Upgrade BWC Tests from BWCVersion-${{ matrix.bwc_version }} to OpenSearch Version-${{ matrix.opensearch_version }} # run: | -# sudo apt-get install libopenblas-dev gfortran -y - - - - name: Run SRP Restart-Upgrade BWC Tests from BWCVersion-${{ matrix.bwc_version }} to OpenSearch Version-${{ matrix.opensearch_version }} - run: | - echo "Running restart-upgrade backwards compatibility tests ..." - ./gradlew :qa:restart-upgrade:testRestartUpgrade -Dtests.bwc.version=$BWC_VERSION_RESTART_UPGRADE +# echo "Running restart-upgrade backwards compatibility tests ..." +# ./gradlew :qa:restart-upgrade:testRestartUpgrade -Dtests.bwc.version=$BWC_VERSION_RESTART_UPGRADE Rolling-Upgrade-BWCTests-SRP: diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..41d9927a4d4fb3f96a785543079b8df6723c946b GIT binary patch literal 59821 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 literal 0 HcmV?d00001 From e212a5040be2b9757cfda21be9f755e0b09b3bf8 Mon Sep 17 00:00:00 2001 From: Mark Cohen Date: Sun, 6 Nov 2022 12:35:48 -0500 Subject: [PATCH 06/13] adding release notes folder Signed-off-by: Mark Cohen --- release_notes | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 release_notes diff --git a/release_notes b/release_notes new file mode 100644 index 0000000..e69de29 From 36ceb467840bb149c9189f5584fe2ed3cec98ce0 Mon Sep 17 00:00:00 2001 From: Mark Cohen Date: Mon, 7 Nov 2022 09:41:13 -0500 Subject: [PATCH 07/13] commented windows build to get the changes into a PR; filing windows build issue #19 Signed-off-by: Mark Cohen --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 0880010..104e3c9 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: java: [11, 17] - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest] # want to get the build running first; fixing windows build after- windows-latest] name: Build and Test Search Request Processor Plugin runs-on: ${{ matrix.os }} From 747e94d8383f5eea4fd4fc3aaaa6212eb7fd3c57 Mon Sep 17 00:00:00 2001 From: Mark Cohen Date: Mon, 7 Nov 2022 09:46:36 -0500 Subject: [PATCH 08/13] commented qa gradle build for BWC; need to add it back and work on missing qa folder and what should be in there Signed-off-by: Mark Cohen --- .../workflows/backwards_compatibility_tests_workflow.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backwards_compatibility_tests_workflow.yml b/.github/workflows/backwards_compatibility_tests_workflow.yml index ac4f40b..750c8b5 100644 --- a/.github/workflows/backwards_compatibility_tests_workflow.yml +++ b/.github/workflows/backwards_compatibility_tests_workflow.yml @@ -63,7 +63,7 @@ jobs: # run: | # sudo apt-get install libopenblas-dev gfortran -y - - name: Run SRP Rolling-Upgrade BWC Tests from BWCVersion-${{ matrix.bwc_version }} to OpenSearch Version-${{ matrix.opensearch_version }} - run: | - echo "Running rolling-upgrade backwards compatibility tests ..." - ./gradlew :qa:rolling-upgrade:testRollingUpgrade -Dtests.bwc.version=$BWC_VERSION_ROLLING_UPGRADE +# - name: Run SRP Rolling-Upgrade BWC Tests from BWCVersion-${{ matrix.bwc_version }} to OpenSearch Version-${{ matrix.opensearch_version }} +# run: | +# echo "Running rolling-upgrade backwards compatibility tests ..." +# ./gradlew :qa:rolling-upgrade:testRollingUpgrade -Dtests.bwc.version=$BWC_VERSION_ROLLING_UPGRADE From dab511e9ab516350ebb3e5446354b40b228a9c12 Mon Sep 17 00:00:00 2001 From: Mark Cohen Date: Mon, 7 Nov 2022 10:06:52 -0500 Subject: [PATCH 09/13] Modified README to reflect current state of plugin hosted in the repo. Signed-off-by: Mark Cohen --- README.md | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index accd773..2733402 100644 --- a/README.md +++ b/README.md @@ -1 +1,43 @@ -# Search Request \ No newline at end of file +[![Build and Test Search Reranker](https://github.com/opensearch-project/search-relevance/actions/workflows/CI.yml/badge.svg)](https://github.com/opensearch-project/search-relevance/actions/workflows/CI.yml) +[![codecov](https://codecov.io/gh/opensearch-project/search-relevance/branch/main/graph/badge.svg?token=PYQO2GW39S)](https://codecov.io/gh/opensearch-project/search-relevance) +![PRs welcome!](https://img.shields.io/badge/PRs-welcome!-success) + +# OpenSearch Search Relevance - Reranker +- [Welcome!](#welcome) +- [Project Resources](#project-resources) +- [Credits and Acknowledgments](#credits-and-acknowledgments) +- [Code of Conduct](#code-of-conduct) +- [License](#license) +- [Copyright](#copyright) + +## Welcome! +This repository currently hosts a plugin to rerank search results before returning them to the client inline. The current plan is to expand this functionality to allow users to add rewriters, rerankers, and log what those components do. + +# History +This repository has also been used for discussion and ideas around search relevance. These discussions still exist here, however due to the relatively new standard of having one repo per plugin in OpenSearch and our implementations beginning to make it into the OpenSearch build, we have two repositories now. This repository will develop into a plugin that will allow OpenSearch users to rewrite search queries, rerank results, and log data about those actions. The other repository, [dashboards-search-relevance](https://www.github.com/opensearch-projects/dashboards-search-relevance), is where we will build front-end tooling to help relevance engineers and business users tune results. + + +## Project Resources + +* [Project Website](https://opensearch.org/) +* [Downloads](https://opensearch.org/downloads.html). +* [Project Principles](https://opensearch.org/#principles) +* [Contributing to OpenSearch Search Request Processor](CONTRIBUTING.md) +* [Search Relevance](RELEVANCE.md) +* [Maintainer Responsibilities](MAINTAINERS.md) +* [Release Management](RELEASING.md) +* [Admin Responsibilities](ADMINS.md) +* [Security](SECURITY.md) + + +## Code of Conduct + +This project has adopted the [Amazon Open Source Code of Conduct](CODE_OF_CONDUCT.md). For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq), or contact [opensource-codeofconduct@amazon.com](mailto:opensource-codeofconduct@amazon.com) with any additional questions or comments. + +## License + +This project is licensed under the [Apache v2.0 License](LICENSE). + +## Copyright + +Copyright OpenSearch Contributors. See [NOTICE](NOTICE) for details. From 8334ed17237ae3c60052731a51109a17066a2e2d Mon Sep 17 00:00:00 2001 From: Mark Cohen Date: Mon, 7 Nov 2022 10:13:09 -0500 Subject: [PATCH 10/13] commented out more windows build just to get macos and ubuntu workflows working; replaced k-NN reference Signed-off-by: Mark Cohen --- .github/workflows/CI.yml | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 104e3c9..3337411 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,4 +1,4 @@ -name: Build and Test Search Request Processor +name: Build & Test Search Request Processor on: schedule: - cron: '0 0 * * *' # every night @@ -12,7 +12,7 @@ on: - "feature/**" jobs: - Build-k-NN: + Build-search-request-processor: strategy: matrix: java: [11, 17] @@ -40,24 +40,24 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} - Build-search-request-processor-Windows: - strategy: - matrix: +# Build-search-request-processor-Windows: +# strategy: +# matrix: java: [ 11, 17 ] - - name: Build and Test Search Request Processor Plugin on Windows - runs-on: windows-latest - - steps: - - name: Checkout Search Request Processor - uses: actions/checkout@v1 - - - name: Setup Java ${{ matrix.java }} - uses: actions/setup-java@v1 - with: - java-version: ${{ matrix.java }} - - - name: Run build - run: | - ./gradlew.bat build +# +# name: Build and Test Search Request Processor Plugin on Windows +# runs-on: windows-latest + +# steps: +# - name: Checkout Search Request Processor +# uses: actions/checkout@v1 + +# - name: Setup Java ${{ matrix.java }} +# uses: actions/setup-java@v1 +# with: +# java-version: ${{ matrix.java }} +# +# - name: Run build +# run: | +# ./gradlew.bat build From 04c06509391b21f4bba3049458b960c3577dc88e Mon Sep 17 00:00:00 2001 From: Mark Cohen Date: Mon, 7 Nov 2022 10:14:48 -0500 Subject: [PATCH 11/13] missed a line to comment Signed-off-by: Mark Cohen --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3337411..10fc7d7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -43,7 +43,7 @@ jobs: # Build-search-request-processor-Windows: # strategy: # matrix: - java: [ 11, 17 ] +# java: [ 11, 17 ] # # name: Build and Test Search Request Processor Plugin on Windows # runs-on: windows-latest From 29f2ca1838ac5d9da9da712016c8f1b1afd84f1f Mon Sep 17 00:00:00 2001 From: Mark Cohen Date: Mon, 7 Nov 2022 10:28:02 -0500 Subject: [PATCH 12/13] cleaning up documentation Signed-off-by: Mark Cohen --- ADMINS.md | 30 ++++++++++++++++++++++++++++++ RELEASING.md | 1 + SECURITY.md | 3 +++ 3 files changed, 34 insertions(+) create mode 100644 ADMINS.md create mode 100644 RELEASING.md create mode 100644 SECURITY.md diff --git a/ADMINS.md b/ADMINS.md new file mode 100644 index 0000000..a4ffa8c --- /dev/null +++ b/ADMINS.md @@ -0,0 +1,30 @@ +## Overview + +This document explains who the admins are (see below), what they do in this repo, and how they should be doing it. If you're interested in becoming a maintainer, see [MAINTAINERS](MAINTAINERS.md). If you're interested in contributing, see [CONTRIBUTING](CONTRIBUTING.md). + +## Current Admins + +| Admin | GitHub ID | Affiliation | +| --------------- | --------------------------------------- | ----------- | +| Charlotte Henkle| [CEHENKLE](https://github.com/CEHENKLE) | Amazon | +| Henri Yandell | [hyandell](https://github.com/hyandell) | Amazon | +| Anirudha Jadhav | [anirudha](https://github.com/anirudha) | Amazon | +| Mark Cohen | [macohen](https://github.com/macohen) | Amazon | + +## Admin Responsibilities + +As an admin you own stewardship of the repository and its settings. Admins have [admin-level permissions on a repository](https://docs.github.com/en/organizations/managing-access-to-your-organizations-repositories/repository-permission-levels-for-an-organization). Use those privileges to serve the community and protect the repository as follows. + +### Prioritize Security + +Security is your number one priority. Manage security keys and safeguard access to the repository. + +Note that this repository is monitored and supported 24/7 by Amazon Security, see [Reporting a Vulnerability](SECURITY.md) for details. + +### Enforce Code of Conduct + +Act on [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) violations by revoking access, and blocking malicious actors. + +### Adopt Organizational Best Practices + +Adopt organizational best practices, work in the open, and collaborate with other admins by opening issues before making process changes. Prefer consistency, and avoid diverging from practices in the opensearch-project organization. \ No newline at end of file diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..6903e71 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1 @@ +This project follows the [OpenSearch release process](https://github.com/opensearch-project/.github/blob/main/RELEASING.md). \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..0b85ca0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +## Reporting a Vulnerability + +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) or directly via email to aws-security@amazon.com. Please do **not** create a public GitHub issue. \ No newline at end of file From cfe221b40936fe7710dd5da486bc25422bb1cd51 Mon Sep 17 00:00:00 2001 From: Mark Cohen Date: Mon, 7 Nov 2022 12:26:09 -0500 Subject: [PATCH 13/13] deleting editor swap files Signed-off-by: Mark Cohen --- .github/CODEOWNERS~ | 2 - .github/workflows/CI.yml~ | 135 ------------------ ...ackwards_compatibility_tests_workflow.yml~ | 73 ---------- .github/workflows/delete_backport_branch.yml~ | 15 -- 4 files changed, 225 deletions(-) delete mode 100644 .github/CODEOWNERS~ delete mode 100644 .github/workflows/CI.yml~ delete mode 100644 .github/workflows/backwards_compatibility_tests_workflow.yml~ delete mode 100644 .github/workflows/delete_backport_branch.yml~ diff --git a/.github/CODEOWNERS~ b/.github/CODEOWNERS~ deleted file mode 100644 index 2c0e889..0000000 --- a/.github/CODEOWNERS~ +++ /dev/null @@ -1,2 +0,0 @@ -# This should match the owning team set up in https://github.com/orgs/opensearch-project/teams -* @opensearch-project/k-nn \ No newline at end of file diff --git a/.github/workflows/CI.yml~ b/.github/workflows/CI.yml~ deleted file mode 100644 index 31d16f2..0000000 --- a/.github/workflows/CI.yml~ +++ /dev/null @@ -1,135 +0,0 @@ -name: Build and Test k-NN -on: - schedule: - - cron: '0 0 * * *' # every night - push: - branches: - - "*" - - "feature/**" - pull_request: - branches: - - "*" - - "feature/**" - -jobs: - Build-k-NN: - strategy: - matrix: - java: [11, 17] - os: [ubuntu-latest, macos-latest] - - name: Build and Test k-NN Plugin - runs-on: ${{ matrix.os }} - - steps: - - name: Checkout k-NN - uses: actions/checkout@v1 - - - name: Setup Java ${{ matrix.java }} - uses: actions/setup-java@v1 - with: - java-version: ${{ matrix.java }} - - - name: Install dependencies on ubuntu - if: startsWith(matrix.os,'ubuntu') - run: | - sudo apt-get install libopenblas-dev gfortran -y - - - name: Install dependencies on macos - if: startsWith(matrix.os, 'macos') - run: | - brew reinstall gcc - export FC=/usr/local/Cellar/gcc/12.2.0/bin/gfortran - - - name: Run build - run: | - ./gradlew build - - - name: Upload Coverage Report - if: startsWith(matrix.os,'ubuntu') - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - - Build-k-NN-Windows: - strategy: - matrix: - java: [ 11, 17 ] - - name: Build and Test k-NN Plugin on Windows - runs-on: windows-latest - - steps: - - name: Checkout k-NN - uses: actions/checkout@v1 - - - name: Setup Java ${{ matrix.java }} - uses: actions/setup-java@v1 - with: - java-version: ${{ matrix.java }} - - - name: Install MinGW Using Scoop - run: | - iex "& {$(irm get.scoop.sh)} -RunAsAdmin" - scoop bucket add main - scoop install mingw - - - name: Add MinGW to PATH - run: | - echo "C:/Users/runneradmin/scoop/apps/mingw/current/bin" >> $env:GITHUB_PATH - refreshenv - - - name: Download OpenBLAS - run: | - curl -L -O https://github.com/xianyi/OpenBLAS/releases/download/v0.3.21/OpenBLAS-0.3.21-x64.zip - mkdir OpenBLAS - Expand-Archive -Path .\OpenBLAS-0.3.21-x64.zip -DestinationPath .\OpenBLAS\ - mkdir ./src/main/resources/windowsDependencies - cp ./OpenBLAS/bin/libopenblas.dll ./src/main/resources/windowsDependencies/ - rm .\OpenBLAS-0.3.21-x64.zip - rm -r .\OpenBLAS\ - - - name: Run build - run: | - ./gradlew.bat build - -# - name: Pull and Run Docker for security tests -# run: | -# plugin=`ls build/distributions/*.zip` -# version=`echo $plugin|awk -F- '{print $4}'| cut -d. -f 1-3` -# plugin_version=`echo $plugin|awk -F- '{print $4}'| cut -d. -f 1-4` -# echo $version -# cd .. -# if docker pull opendistroforelasticsearch/opendistroforelasticsearch:$version -# then -# echo "FROM opendistroforelasticsearch/opendistroforelasticsearch:$version" >> Dockerfile -# echo "RUN if [ -d /usr/share/elasticsearch/plugins/opendistro-knn ]; then /usr/share/elasticsearch/bin/elasticsearch-plugin remove opendistro-knn; fi" >> Dockerfile -# echo "RUN yum -y update \ && yum -y groupinstall "Development Tools" \ && yum install -y unzip glibc.x86_64 cmake \ && yum clean all" >> Dockerfile -# echo "RUN git clone --recursive --branch ${GITHUB_REF##*/} https://github.com/opendistro-for-elasticsearch/k-NN.git /usr/share/elasticsearch/k-NN \ " >> Dockerfile -# echo "&& cd /usr/share/elasticsearch/k-NN/jni \ && sed -i 's/-march=native/-march=x86-64/g' external/nmslib/similarity_search/CMakeLists.txt \ && cmake . \ && make \ " >> Dockerfile -# echo "&& mkdir /tmp/jni/ && cp release/*.so /tmp/jni/ && ls -ltr /tmp/jni/ \ && cp /tmp/jni/libKNNIndex*.so /usr/lib \ && rm -rf /usr/share/elasticsearch/k-NN" >> Dockerfile -# echo "RUN cd /usr/share/elasticsearch/" >> Dockerfile -# echo "ADD k-NN/build/distributions/opendistro-knn-$plugin_version.zip /tmp/" >> Dockerfile -# echo "RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch file:/tmp/opendistro-knn-$plugin_version.zip" >> Dockerfile -# docker build -t odfe-knn:test . -# echo "imagePresent=true" >> $GITHUB_ENV -# else -# echo "imagePresent=false" >> $GITHUB_ENV -# fi -# - name: Run Docker Image -# if: env.imagePresent == 'true' -# run: | -# cd .. -# docker run -p 9200:9200 -d -p 9600:9600 -e "discovery.type=single-node" odfe-knn:test -# sleep 90 -# - name: Run k-NN Test -# if: env.imagePresent == 'true' -# run: | -# security=`curl -XGET https://localhost:9200/_cat/plugins?v -u admin:admin --insecure |grep opendistro_security|wc -l` -# if [ $security -gt 0 ] -# then -# echo "Security plugin is available. Running tests in security mode" -# ./gradlew :integTest -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername="docker-cluster" -Dhttps=true -Duser=admin -Dpassword=admin -# else -# echo "Security plugin is NOT available. Skipping tests as they are already ran part of ./gradlew build" -# fi diff --git a/.github/workflows/backwards_compatibility_tests_workflow.yml~ b/.github/workflows/backwards_compatibility_tests_workflow.yml~ deleted file mode 100644 index b77adaf..0000000 --- a/.github/workflows/backwards_compatibility_tests_workflow.yml~ +++ /dev/null @@ -1,73 +0,0 @@ -name: Backwards Compatibility Tests k-NN -on: - push: - branches: - - "*" - - "feature/**" - pull_request: - branches: - - "*" - - "feature/**" - -jobs: - Restart-Upgrade-BWCTests-k-NN: - strategy: - matrix: - java: [ 11, 17 ] - bwc_version : [ "2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0" ] - opensearch_version : [ "3.0.0-SNAPSHOT" ] - - name: k-NN Restart-Upgrade BWC Tests - runs-on: ubuntu-latest - env: - BWC_VERSION_RESTART_UPGRADE: ${{ matrix.bwc_version }} - - steps: - - name: Checkout k-NN - uses: actions/checkout@v1 - - - name: Setup Java ${{ matrix.java }} - uses: actions/setup-java@v1 - with: - java-version: ${{ matrix.java }} - - - name: Install dependencies - run: | - sudo apt-get install libopenblas-dev gfortran -y - - - - name: Run k-NN Restart-Upgrade BWC Tests from BWCVersion-${{ matrix.bwc_version }} to OpenSearch Version-${{ matrix.opensearch_version }} - run: | - echo "Running restart-upgrade backwards compatibility tests ..." - ./gradlew :qa:restart-upgrade:testRestartUpgrade -Dtests.bwc.version=$BWC_VERSION_RESTART_UPGRADE - - - Rolling-Upgrade-BWCTests-k-NN: - strategy: - matrix: - java: [ 11, 17 ] - bwc_version: [ "2.4.0" ] - opensearch_version: [ "3.0.0-SNAPSHOT" ] - - name: k-NN Rolling-Upgrade BWC Tests - runs-on: ubuntu-latest - env: - BWC_VERSION_ROLLING_UPGRADE: ${{ matrix.bwc_version }} - - steps: - - name: Checkout k-NN - uses: actions/checkout@v1 - - - name: Setup Java ${{ matrix.java }} - uses: actions/setup-java@v1 - with: - java-version: ${{ matrix.java }} - - - name: Install dependencies - run: | - sudo apt-get install libopenblas-dev gfortran -y - - - name: Run k-NN Rolling-Upgrade BWC Tests from BWCVersion-${{ matrix.bwc_version }} to OpenSearch Version-${{ matrix.opensearch_version }} - run: | - echo "Running rolling-upgrade backwards compatibility tests ..." - ./gradlew :qa:rolling-upgrade:testRollingUpgrade -Dtests.bwc.version=$BWC_VERSION_ROLLING_UPGRADE diff --git a/.github/workflows/delete_backport_branch.yml~ b/.github/workflows/delete_backport_branch.yml~ deleted file mode 100644 index d654df6..0000000 --- a/.github/workflows/delete_backport_branch.yml~ +++ /dev/null @@ -1,15 +0,0 @@ -name: Delete merged branch of the backport PRs -on: - pull_request: - types: - - closed - -jobs: - delete-branch: - runs-on: ubuntu-latest - if: startsWith(github.event.pull_request.head.ref,'backport/') - steps: - - name: Delete merged branch - uses: SvanBoxel/delete-merged-branch@main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file